```javascript
import { MessageReactionType } from '@ably/chat';
const room = await ablyChatClient.rooms.get('room1', {
messages: {
defaultMessageReactionType: MessageReactionType.Unique,
},
});
```
```swift
let room = try await ablyChatClient.rooms.get(
named: "room1",
options: .init(
messages: .init(defaultMessageReactionType: .unique)
)
)
```
```kotlin
val room = ablyChatClient.rooms.get("room1") {
messages {
defaultMessageReactionType = MessageReactionType.Unique
}
}
```
```jetpack
val room = ablyChatClient.rooms.get("room1") {
messages {
defaultMessageReactionType = MessageReactionType.Unique
}
}
```
```react
import { MessageReactionType } from '@ably/chat';
import { ChatRoomProvider } from '@ably/chat/react';
const roomOptions = {
messages: {
defaultMessageReactionType: MessageReactionType.Unique,
},
};
const MyComponent = () => {
return (
);
};
```
## Sending a message reaction
```javascript
import { MessageReactionType } from '@ably/chat';
// Send a 👍 reaction using the default type
await room.messages.reactions.send(message, { name: '👍' });
// The reaction can be anything, not just UTF-8 emojis:
await room.messages.reactions.send(message, { name: ':like:' });
await room.messages.reactions.send(message, { name: '+1' });
// Send a :love: reaction using the Unique type
await room.messages.reactions.send(message, {
name: ':love:',
type: MessageReactionType.Unique,
});
// Send a ❤️ reaction with count 100 using the Multiple type
await room.messages.reactions.send(message, {
name: '❤️',
type: MessageReactionType.Multiple,
count: 100,
});
```
```swift
// Send a 👍 reaction using the default type
try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: "👍"))
// The reaction can be anything, not just UTF-8 emojis:
try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: ":like:"))
try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: "+1"))
// Send a :love: reaction using the Unique type
try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(
name: ":love:",
type: .unique
))
// Send a ❤️ reaction with count 100 using the Multiple type
try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(
name: "❤️",
type: .multiple,
count: 100
))
```
```kotlin
// Send a 👍 reaction using the default type
room.messages.reactions.send(message, name = "👍")
// The reaction can be anything, not just UTF-8 emojis:
room.messages.reactions.send(message, name = ":like:")
room.messages.reactions.send(message, name = "+1")
// Send a :love: reaction using the Unique type
room.messages.reactions.send(message,
name = ":love:",
type = MessageReactionType.Unique,
)
// Send a ❤️ reaction with count 100 using the Multiple type
room.messages.reactions.send(message,
name = "❤️",
type = MessageReactionType.Multiple,
count = 100,
)
```
```jetpack
import androidx.compose.material.*
import androidx.compose.runtime.*
import com.ably.chat.Message
import com.ably.chat.MessageReactionType
import com.ably.chat.Room
import kotlinx.coroutines.launch
@Composable
fun SendMessageReactionComponent(room: Room, message: Message) {
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
coroutineScope.launch {
// Send a 👍 reaction using the default type
room.messages.reactions.send(message, name = "👍")
}
}) {
Text("Send 👍")
}
Button(onClick = {
coroutineScope.launch {
// Send a ❤️ reaction with count 100 using the Multiple type
room.messages.reactions.send(
message,
name = "❤️",
type = MessageReactionType.Multiple,
count = 100,
)
}
}) {
Text("Send ❤️ x100")
}
}
```
```react
import { MessageReactionType } from '@ably/chat';
import { useMessages } from '@ably/chat/react';
const MyComponent = () => {
const { sendReaction } = useMessages();
const handleSendReaction = async (message) => {
try {
// Send a 👍 reaction using the default type
await sendReaction(message, { name: '👍' });
// The reaction can be anything, not just UTF-8 emojis:
await sendReaction(message, { name: ':like:' });
await sendReaction(message, { name: '+1' });
// Send a :love: reaction using the Unique type
await sendReaction(message, {
name: ':love:',
type: MessageReactionType.Unique,
});
// Send a ❤️ reaction with count 100 using the Multiple type
await sendReaction(message, {
name: '❤️',
type: MessageReactionType.Multiple,
count: 100,
});
} catch (error) {
console.error('Error sending reaction:', error);
}
};
return (
);
};
```
## Removing a message reaction
```javascript
// Remove a 👍 reaction using the default type
await room.messages.reactions.delete(message, { name: '👍' });
// Remove a :love: reaction using the Unique type
await room.messages.reactions.delete(message, {
name: ':love:',
type: MessageReactionType.Unique,
});
// Remove a ❤️ reaction with count 50 using the Multiple type
await room.messages.reactions.delete(message, {
name: '❤️',
type: MessageReactionType.Multiple,
count: 50,
});
```
```react
import { MessageReactionType } from '@ably/chat';
import { useMessages } from '@ably/chat/react';
const MyComponent = () => {
const { deleteReaction } = useMessages();
const handleRemoveReaction = async (message) => {
try {
// Remove a 👍 reaction using the default type
await deleteReaction(message, { name: '👍' });
// Remove a :love: reaction using the Unique type
await deleteReaction(message, {
name: ':love:',
type: MessageReactionType.Unique,
});
// Remove a ❤️ reaction with count 50 using the Multiple type
await deleteReaction(message, {
name: '❤️',
type: MessageReactionType.Multiple,
count: 50,
});
} catch (error) {
console.error('Error removing reaction:', error);
}
};
return (
);
};
```
```swift
// Remove a 👍 reaction using the default type
try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init(name: "👍"))
// Remove a :love: reaction using the Unique type
try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init(
name: ":love:",
type: .unique
))
// Remove a ❤️ reaction using the Multiple type
try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init(
name: "❤️",
type: .multiple
))
```
```kotlin
// Remove a 👍 reaction using the default type
room.messages.reactions.delete(message, name = "👍")
// Remove a :love: reaction using the Unique type
room.messages.reactions.delete(message,
name = ":love:",
type = MessageReactionType.Unique,
)
// Remove a ❤️ reaction using the Multiple type
room.messages.reactions.delete(message,
name = "❤️",
type = MessageReactionType.Multiple,
)
```
```jetpack
import androidx.compose.material.*
import androidx.compose.runtime.*
import com.ably.chat.Message
import com.ably.chat.MessageReactionType
import com.ably.chat.Room
import kotlinx.coroutines.launch
@Composable
fun RemoveMessageReactionComponent(room: Room, message: Message) {
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
coroutineScope.launch {
// Remove a 👍 reaction using the default type
room.messages.reactions.delete(message, name = "👍")
}
}) {
Text("Remove 👍")
}
Button(onClick = {
coroutineScope.launch {
// Remove a ❤️ reaction using the Multiple type
room.messages.reactions.delete(
message,
name = "❤️",
type = MessageReactionType.Multiple,
)
}
}) {
Text("Remove ❤️")
}
}
```
## Messages and reactions
The `Message` object contains a `reactions` property which is an object that looks like this:
```javascript
interface Message {
// ... (other fields omitted)
reactions: {
unique: Ably.SummaryUniqueValues,
distinct: Ably.SummaryDistinctValues,
multiple: Ably.SummaryMultipleValues,
}
}
// example (in real use, it is unlikely that all reaction types are present):
{
// ... other message fields omitted
reactions: {
unique: {
'👍': { total: 2, clientIds: ['clientA', 'clientB'] },
'❤️': { total: 1, clientIds: ['clientC'] },
},
distinct: {
'👍': { total: 2, clientIds: ['clientA', 'clientB'] },
'❤️': { total: 1, clientIds: ['clientA'] },
},
multiple: {
'👍': { total: 10, clientIds: {'clientA': 7, 'clientB': 3} },
'❤️': { total: 100, clientIds: {'clientA': 100} },
},
}
}
```
```react
interface Message {
// ... (other fields omitted)
reactions: {
unique: Ably.SummaryUniqueValues,
distinct: Ably.SummaryDistinctValues,
multiple: Ably.SummaryMultipleValues,
}
}
// example (in real use, it is unlikely that all reaction types are present):
{
// ... other message fields omitted
reactions: {
unique: {
'👍': { total: 2, clientIds: ['clientA', 'clientB'] },
'❤️': { total: 1, clientIds: ['clientC'] },
},
distinct: {
'👍': { total: 2, clientIds: ['clientA', 'clientB'] },
'❤️': { total: 1, clientIds: ['clientA'] },
},
multiple: {
'👍': { total: 10, clientIds: {'clientA': 7, 'clientB': 3} },
'❤️': { total: 100, clientIds: {'clientA': 100} },
},
}
}
```
```swift
struct Message {
// ... (other fields omitted)
var reactions: MessageReactionSummary
}
struct MessageReactionSummary {
var unique: [String: ClientIDList]
var distinct: [String: ClientIDList]
var multiple: [String: ClientIDCounts]
}
// example (in real use, it is unlikely that all reaction types are present):
// ... other message fields omitted
reactions: MessageReactionSummary(
unique: [
"👍": ClientIDList(total: 2, clientIDs: ["clientA", "clientB"], clipped: false),
"❤️": ClientIDList(total: 1, clientIDs: ["clientC"], clipped: false),
],
distinct: [
"👍": ClientIDList(total: 2, clientIDs: ["clientA", "clientB"], clipped: false),
"❤️": ClientIDList(total: 1, clientIDs: ["clientA"], clipped: false),
],
multiple: [
"👍": ClientIDCounts(total: 10, clientIDs: ["clientA": 7, "clientB": 3], totalUnidentified: 0, clipped: false, totalClientIDs: 2),
"❤️": ClientIDCounts(total: 100, clientIDs: ["clientA": 100], totalUnidentified: 0, clipped: false, totalClientIDs: 1),
]
)
```
```kotlin
interface Message {
// ... (other fields omitted)
val reactions: MessageReactionSummary
}
interface MessageReactionSummary {
val unique: Map
val distinct: Map
val multiple: Map
}
// example (in real use, it is unlikely that all reaction types are present):
// ... other message fields omitted
reactions = MessageReactionSummary(
unique = mapOf(
"👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false),
"❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientC"), clipped = false),
),
distinct = mapOf(
"👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false),
"❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientA"), clipped = false),
),
multiple = mapOf(
"👍" to SummaryClientIdCounts(total = 10, clientIds = mapOf("clientA" to 7, "clientB" to 3), totalUnidentified = 0, clipped = false, totalClientIds = 2),
"❤️" to SummaryClientIdCounts(total = 100, clientIds = mapOf("clientA" to 100), totalUnidentified = 0, clipped = false, totalClientIds = 1),
)
)
```
```jetpack
interface Message {
// ... (other fields omitted)
val reactions: MessageReactionSummary
}
interface MessageReactionSummary {
val unique: Map
val distinct: Map
val multiple: Map
}
// example (in real use, it is unlikely that all reaction types are present):
// ... other message fields omitted
reactions = MessageReactionSummary(
unique = mapOf(
"👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false),
"❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientC"), clipped = false),
),
distinct = mapOf(
"👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false),
"❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientA"), clipped = false),
),
multiple = mapOf(
"👍" to SummaryClientIdCounts(total = 10, clientIds = mapOf("clientA" to 7, "clientB" to 3), totalUnidentified = 0, clipped = false, totalClientIds = 2),
"❤️" to SummaryClientIdCounts(total = 100, clientIds = mapOf("clientA" to 100), totalUnidentified = 0, clipped = false, totalClientIds = 1),
)
)
```
All reaction types are always available via `Message.reactions`, regardless of the default reaction type configured via room options.
The `Message.reactions` property is populated when fetching messages from history through `historyBeforeSubscribe()` or `room.messages.history()`. It is **not** populated when receiving message events such as `ChatMessageEventType.Created`, `ChatMessageEventType.Updated`, or `ChatMessageEventType.Deleted` from the realtime channel.
Always call `Message.with(event)` when applying message events and reaction events to existing messages to ensure that reactions are correctly copied or updated. **Do not** replace existing messages with messages received from events as reactions will be lost.
## Subscribing to message reactions
```javascript
room.messages.reactions.subscribe((event) => {
console.log("received reactions summary event", event);
});
```
```swift
room.messages.reactions.subscribe { event in
print("received reactions summary event: \(event)")
}
```
```kotlin
room.messages.reactions.subscribe { event ->
println("received reactions summary event: $event")
}
```
```jetpack
import androidx.compose.runtime.*
import com.ably.chat.Room
@Composable
fun SubscribeToReactionsComponent(room: Room) {
LaunchedEffect(room) {
room.messages.reactions.asFlow().collect { event ->
println("received reactions summary event: $event")
}
}
}
```
```react
import { useMessages } from '@ably/chat/react';
const MyComponent = () => {
useMessages({
reactionsListener: (event) => {
console.log("received reactions summary event", event);
},
});
return ...;
};
```
The event is of type `reaction.summary`. `event.reactions` is the received reactions summary and contains the following properties:
| Property | Description | Example |
| -------- | ----------- | ------- |
| `messageSerial` | Serial of the message this summary is for. | `01826232498871-001@abcdefghij:001` |
| `unique` | Unique reactions summary. | `{ "👍": { total: 2, clientIds: ["a", "b"]} }` |
| `distinct` | Distinct reactions summary. | `{ "👍": { total: 2, clientIds: ["a", "b"]} }` |
| `multiple` | Multiple reactions summary. | `{ "👍": { total: 5, clientIds: {"a": 2, "b": 3} }` |
Message reaction summary events can be used with `Message.with(event)` to get an updated message object, with the reactions applied correctly. Similarly, when calling `Message.with()` with other message events (for example `ChatMessageEventType.Updated`), the reactions summary will be correctly preserved in the resulting message object.
Example usage:
```javascript
// init messages, in practice this should be updated with a message subscription
let messages = await room.messages.history({limit: 50});
// subscribe to message reactions summary events
room.messages.reactions.subscribe((event) => {
// find the relevant message (in practice: use binary search or a map for lookups)
const idx = messages.findLastIndex((msg) => msg.serial === event.messageSerial);
if (idx === -1) {
// not found
return;
}
// update message
messages[idx] = messages[idx].with(event);
});
```
```swift
// init messages, in practice this should be updated with a message subscription
var messages = (try await room.messages.history(withParams: .init(limit: 50))).items
// subscribe to message reactions summary events
room.messages.reactions.subscribe { event in
if let idx = messages.lastIndex(where: { $0.serial == event.messageSerial }) {
messages[idx] = messages[idx].with(event)
}
}
```
```kotlin
// init messages, in practice this should be updated with a message subscription
val messages = room.messages.history(limit = 50).items.toMutableList()
// subscribe to message reactions summary events
room.messages.reactions.subscribe { event ->
// find the relevant message (in practice: use binary search or a map for lookups)
val idx = messages.indexOfLast { msg -> msg.serial == event.messageSerial }
if (idx != -1) {
// update message
messages[idx] = messages[idx].with(event)
}
}
```
```jetpack
import androidx.compose.runtime.*
import com.ably.chat.Message
import com.ably.chat.Room
@Composable
fun ReactionsWithMessagesComponent(room: Room) {
var messages by remember { mutableStateOf>(emptyList()) }
LaunchedEffect(room) {
// init messages
messages = room.messages.history(limit = 50).items
}
LaunchedEffect(room) {
// subscribe to message reactions summary events
room.messages.reactions.asFlow().collect { event ->
// find the relevant message (in practice: use binary search or a map for lookups)
val idx = messages.indexOfLast { msg -> msg.serial == event.messageSerial }
if (idx != -1) {
// update message
messages = messages.toMutableList().apply {
this[idx] = this[idx].with(event)
}
}
}
}
}
```
```react
import { useState, useEffect } from 'react';
import { useMessages, Message } from '@ably/chat/react';
const MyComponent = () => {
const [messages, setMessages] = useState([]);
const { historyBeforeSubscribe } = useMessages({
reactionsListener: (event) => {
// find the relevant message (in practice: use binary search or a map for lookups)
setMessages((prevMessages) => {
const idx = prevMessages.findLastIndex((msg) => msg.serial === event.messageSerial);
if (idx === -1) {
// not found
return prevMessages;
}
// update message
const updatedMessages = [...prevMessages];
updatedMessages[idx] = updatedMessages[idx].with(event);
return updatedMessages;
});
},
});
// Initialize messages on component mount
useEffect(() => {
if (!historyBeforeSubscribe) return
const initMessages = async () => {
try {
const result = await historyBeforeSubscribe({ limit: 50 });
setMessages(result.items);
} catch (error) {
console.error('Error fetching messages:', error);
}
};
initMessages();
}, [historyBeforeSubscribe]);
return ...;
};
```
### Summary events are sent efficiently at scale
Summary events are typically created and published immediately after a reaction is sent or removed. If the reaction is a no-op (for example, when removing a reaction that didn't exist), then there will be no summary event.
If multiple reactions are sent in a short period of time, multiple reactions may be rolled up and only a single summary event will be published that contains the aggregated results of all reactions. This reduces the number of outbound messages and thus your costs in busy rooms.
```javascript
const room = await ablyChatClient.rooms.get('room1', {
messages: {
rawMessageReactions: true,
},
});
```
```swift
let room = try await ablyChatClient.rooms.get(
named: "room1",
options: .init(
messages: .init(rawMessageReactions: true)
)
)
```
```kotlin
val room = ablyChatClient.rooms.get("room1") {
messages {
rawMessageReactions = true
}
}
```
```jetpack
val room = ablyChatClient.rooms.get("room1") {
messages {
rawMessageReactions = true
}
}
```
```react
import { ChatRoomProvider } from '@ably/chat/react';
const roomOptions = {
messages: {
rawMessageReactions: true,
},
};
const MyComponent = () => {
return (
);
};
```
```javascript
room.messages.reactions.subscribeRaw((event) => {
if (event.type === MessageReactionEventType.Create) {
console.log("new reaction", event.reaction);
} else if (event.type === MessageReactionEventType.Delete) {
console.log("reaction removed", event.reaction);
}
});
```
```swift
room.messages.reactions.subscribeRaw { event in
if (event.type == .create) {
print("new reaction: \(event.reaction)")
} else if (event.type == .delete) {
print("reaction removed: \(event.reaction)")
}
}
```
```kotlin
room.messages.reactions.subscribeRaw { event ->
if (event.type == MessageReactionEventType.Create) {
println("new reaction: ${event.reaction}")
} else if (event.type == MessageReactionEventType.Delete) {
println("reaction removed: ${event.reaction}")
}
}
```
```jetpack
import androidx.compose.runtime.*
import com.ably.chat.MessageReactionEventType
import com.ably.chat.Room
@Composable
fun SubscribeToRawReactionsComponent(room: Room) {
DisposableEffect(room) {
val (unsubscribe) = room.messages.reactions.subscribeRaw { event ->
if (event.type == MessageReactionEventType.Create) {
println("new reaction: ${event.reaction}")
} else if (event.type == MessageReactionEventType.Delete) {
println("reaction removed: ${event.reaction}")
}
}
onDispose {
unsubscribe()
}
}
}
```
```react
import { useMessages } from '@ably/chat/react';
import { MessageReactionEventType } from '@ably/chat';
const MyComponent = () => {
useMessages({
rawReactionsListener: (event) => {
if (event.type === MessageReactionEventType.Create) {
console.log("new reaction", event.reaction);
} else if (event.type === MessageReactionEventType.Delete) {
console.log("reaction removed", event.reaction);
}
},
});
return ...;
};
```
You should be aware of the following limitations:
* Deleting a reaction succeeds even if it did not initially exist. It is a no-op in regards to the summary but the delete event is still broadcast.
* Sending a reaction succeeds and is broadcast, even if it has no effect on the summary (for example, when double-sending a reaction with the same name of type `Distinct` or `Unique`).
* Sending a reaction of type `Unique` may remove another reaction, but no delete event will be broadcast.
* It is not recommended to use raw reactions for displaying counts, instead use the summary events.
* Keeping a local summary updated based on raw reactions is not recommended as it may become out-of-sync with server-generated summaries.
## Mapping of message reactions to annotations
Chat message reactions are powered by the PubSub annotations feature. Chat uses the `reaction:` prefix for annotation types. Here is the mapping from message reaction types to annotation types:
| Reaction type | Annotation type |
| ------------- | --------------- |
| `unique` | `reaction:unique.v1` |
| `distinct` | `reaction:distinct.v1` |
| `multiple` | `reaction:multiple.v1` |
Messages arriving from the realtime channel will show the annotation type. The Chat SDKs automatically map the annotation type to the message reaction type.