Message reactions
Send, remove and display message reactions in a chat room. Users can react to messages, typically with emojis but can be any string, and others can see the reactions to the message. Message reactions can be sent and removed and a summary of the reactions is persisted with the message.
The reaction name
represents the reaction itself, for example an emoji. Reactions are aggregated by name
and the aggregation method including how many reactions a user can place for a message is controlled by the reaction type
. The count
is an optional parameter that can be set when sending a reaction of type Multiple
.
The reaction name
can be any string. Summaries are aggregated based on unique name
values. UTF-8 emojis are a common use case, but any string can be used as long as they are consistent across all front-ends of your app. Examples of common reaction names are 👍
, ❤️
, :like:
, like
, +1
, and so on. How those are presented to the user is entirely up to the app.
Types of message reactions
Ably Chat supports three types of message reactions. They differ in how they are aggregated and what are the rules for sending and removing them.
Type | Description | Example | Similar to |
---|---|---|---|
Unique | Users can send a single reaction per message. If they react again, their previous reaction is replaced with the new one. | A user can send a 👍, but sending a ❤️ will replace the 👍. | iMessage, WhatsApp, Facebook Messenger |
Distinct | Users can send each type of reaction once per message. Multiple different reactions are allowed, but duplicates are not. | A user can send both 👍 and ❤️, but cannot send a second 👍. | Slack |
Multiple | Users can send unlimited reactions, including duplicates. A count parameter specifies how many reactions to send at once. Each new reaction adds to the total count. | A user can send 10 👍 reactions and 100 ❤️ reactions to the same message. | Claps on Medium |
Note that if sending two identical reactions of type Distinct
, the second one will be accepted and broadcast as a raw reaction, but it will be ignored in the summary (aggregate). Similarly, when removing a reaction that doesn't exist (of any type), the operation will be accepted and broadcast as a raw reaction, but it will have no effect on the summary.
Configure the default reaction type
The default reaction type can be configured at room-level by passing RoomOptions
when calling rooms.get
. If nothing is set, the default is Distinct
.
1
2
3
4
5
6
7
import { MessageReactionType } from '@ably/chat';
const room = await ablyChatClient.rooms.get('room1', {
messages: {
defaultMessageReactionType: MessageReactionType.Unique,
},
});
Sending a message reaction
To send a message reaction use room.messages.reactions.send(message, params)
. This method takes the following parameters:
message
- The message to send the reaction to. Can be either a Message object or a string containing the message serial.params
- Set thename
, and optionally override thetype
or set acount
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,
});
Removing a message reaction
To remove a message reaction use room.messages.reactions.delete(message, params)
. This method takes the following parameters:
message
- The message to remove the reaction from. This can be a Message object, or just the string serial.params
- Set thename
, and optionally override thetype
or set acount
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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,
});
Messages and reactions
The Message
object contains a reactions
property which is an object that looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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} },
},
}
}
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
Ably generates a summary (aggregate) of the reactions for each message and for each reaction type. For displaying accurate counts for message reactions, subscribe to changes in the message summary.
1
2
3
room.messages.reactions.subscribe((event) => {
console.log("received reactions summary event", event);
});
The event is of type reaction.summary
. event.summary
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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.summary.messageSerial);
if (idx === -1) {
// not found
return;
}
// update message
messages[idx] = messages[idx].with(event);
});
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.
Subscribing to raw reactions
Raw individual reactions are published for every reaction, unlike summaries which can be rolled up. Raw reactions are useful for receiving all reaction events, but they are not suitable for the purpose of displaying message reaction counts as their effect on the reactions summary depends on the previous reactions.
Individual reactions are not received by default to save bandwidth and to reduce the number of messages and cost. If you want to receive them, you can enable them via the rawMessageReactions
room option:
1
2
3
4
5
const room = await ablyChatClient.rooms.get('room1', {
messages: {
rawMessageReactions: true,
},
});
Then you can receive raw reactions using the room.messages.reactions.subscribeRaw()
method:
1
2
3
4
5
6
7
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);
}
});
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
orUnique
). - 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.