diff --git a/src/pages/docs/chat/rooms/message-reactions.mdx b/src/pages/docs/chat/rooms/message-reactions.mdx index 2fbddb207d..63a13210dc 100644 --- a/src/pages/docs/chat/rooms/message-reactions.mdx +++ b/src/pages/docs/chat/rooms/message-reactions.mdx @@ -13,11 +13,89 @@ The reaction `name` represents the reaction itself, for example an emoji. Reacti 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 adding and removing them. + +| Type | Description | Example | Similar to | +| ---- | ----------- | ------- | --------- | +| `Unique` | Users can add a single reaction per message. If they react again, their previous reaction is replaced with the new one. | A user can add a 👍, but adding a ❤️ will replace the 👍. | iMessage, WhatsApp, Facebook Messenger | +| `Distinct` | Users can add each type of reaction once per message. Multiple different reactions are allowed, but duplicates are not. | A user can add both 👍 and ❤️, but cannot add a second 👍. | Slack | +| `Multiple` | Users can add unlimited reactions, including duplicates. A count parameter specifies how many reactions to add at once. Each new reaction adds to the total count. | A user can add 10 👍 reactions and 100 ❤️ reactions to the same message. | Claps on Medium | + +Note that if adding 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`. + + + +The default reaction type can be configured at room-level by passing `RoomOptions` to the `ChatRoomProvider`. If nothing is set, the default is `Distinct`. + + + +```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( + name: "room1", + options: .init( + messages: .init(defaultMessageReactionType: .unique) + ) +) +``` + +```kotlin +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 ( + + + + ); +}; +``` + + ## Adding a message reaction + To add a message reaction use `room.messages.reactions.send(message, params)`. This method takes the following parameters: -* `message` - The message to add the reaction to. It can also be an object of format `{serial: "message serial"}`. +* `message` - The message to add the reaction to. Can be either a Message object or a string containing the message serial. * `params` - Set the `name`, and optionally override the `type` or set a `count`. + + + +Use the [`sendReaction()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-react.UseMessagesResponse.html#sendReaction) method available from the response of the `useMessages` hook to add a reaction to a message. This method takes the following parameters: +* `message` - The message to add the reaction to. Can be either a Message object or a string containing the message serial. +* `params` - Set the `name`, and optionally override the `type` or set a `count`. + ```javascript @@ -87,52 +165,121 @@ room.messages.reactions.send(message, count = 100, ) ``` + +```react +import { MessageReactionType } from '@ably/chat'; +import { useMessages } from '@ably/chat/react'; + +const MyComponent = () => { + const { sendReaction } = useMessages(); + + const handleAddReaction = async (message) => { + try { + // Add 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' }); + + // Add a :love: reaction using the Unique type + await sendReaction(message, { + name: ':love:', + type: MessageReactionType.Unique, + }); + + // Add a ❤️ reaction with count 100 using the Multiple type + await sendReaction(message, { + name: '❤️', + type: MessageReactionType.Multiple, + count: 100, + }); + } catch (error) { + console.error('Error adding reaction:', error); + } + }; + + return ( +
+ +
+ ); +}; +```
+ -## 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 adding and removing them. - -| Type | Description | Example | Similar to | -| ---- | ----------- | ------- | --------- | -| `Unique`, `reaction:unique.v1` | A user can react to a message only once, with a reaction of their choice. When a user reacts a second time their reaction is changed. | Can 👍 or ❤️ but not both or more than once. | iMessage, WhatsApp, Facebook Messenger | -| `Distinct`, `reaction:distinct.v1` | A user can react to a message with each reaction at most once. | Can 👍 and ❤️ but each reaction only once. No 👍👍. | Slack | -| `Multiple`, `reaction:multiple.v1` | A user can react to a message with any reactions as many times as they like. Optionally a `count` parameter can be set when reacting. Reacting again adds to the existing count.| Can 👍 10 times and ❤️ 100 times. | Claps on Medium | +## Removing a message reaction -Note that if adding 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 + +To remove a message reaction use `room.messages.reactions.delete(message, params)`. This method takes the following parameters: +* `message` - The message to add the reaction to. This can be a Message object, or just the string serial. +* `params` - Set the `name`, and optionally override the `type` or set a `count`. + -The default reaction type can be configured at room-level using the Room Options. If nothing is set, the default is `Distinct`. + +Use the [`deleteReaction()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-react.UseMessagesResponse.html#deleteReaction) method available from the response of the `useMessages` hook to remove a reaction from a message. This method takes the following parameters: +* `message` - The message to add the reaction to. This can be a Message object, or just the string serial. +* `params` - Set the `name`, and optionally override the `type` or set a `count`. + ```javascript -import { MessageReactionType } from '@ably/chat'; +// Remove a 👍 reaction using the default type +await room.messages.reactions.delete(message, { name: '👍' }); -const room = await ablyChatClient.rooms.get('room1', { - messages: { - defaultMessageReactionType: MessageReactionType.Unique, - }, +// Remove a :love: reaction using the Unique type +await room.messages.reactions.delete(message, { + name: ':love:', + type: MessageReactionType.Unique, }); -``` -```swift -let room = try await ablyChatClient.rooms.get( - name: "room1", - options: .init( - messages: .init(defaultMessageReactionType: .unique) - ) -) +// Remove a ❤️ reaction with count 50 using the Multiple type +await room.messages.reactions.delete(message, { + name: '❤️', + type: MessageReactionType.Multiple, + count: 50, +}); ``` -```kotlin -val room = ablyChatClient.rooms.get("room1") { - messages { - defaultMessageReactionType = MessageReactionType.Unique +```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 ( +
+ +
+ ); +}; ```
@@ -151,6 +298,36 @@ interface Message { } } +// 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 @@ -181,7 +358,13 @@ Always call `Message.with(event)` when applying message events and reaction even ## 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. + + + +Subscribe to message reactions with the [`useMessages`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useMessages.html) hook. Supply a `reactionsListener` to receive message reaction summary events. 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. + ```javascript @@ -189,6 +372,7 @@ 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)") @@ -200,6 +384,20 @@ room.messages.reactions.subscribe { 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.summary` is the received reactions summary and contains the following properties: @@ -259,6 +457,47 @@ room.messages.reactions.subscribe { event -> } } ``` + +```react +import { useState, useEffect } from 'react'; +import { useMessages, Message } from '@ably/chat/react'; + +const MyComponent = () => { + const [messages, setMessages] = useState([]); + + const { get } = 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.summary.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(() => { + const initMessages = async () => { + try { + const result = await get({ limit: 50 }); + setMessages(result.items); + } catch (error) { + console.error('Error fetching messages:', error); + } + }; + initMessages(); + }, [get]); + + return
...
; +}; +``` ### Summary events are sent efficiently at scale
@@ -271,7 +510,7 @@ If multiple reactions are added in a short period of time, multiple reactions ma 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 configure them via a room option which, in turn, sets the appropriate channel options to enable receiving individual annotations and 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: ```javascript @@ -298,9 +537,35 @@ val room = ablyChatClient.rooms.get("room1") { } } ``` + +```react +import { ChatRoomProvider } from '@ably/chat/react'; + +const roomOptions = { + messages: { + messages: { + rawMessageReactions: true, + }, + }, +}; + +const MyComponent = () => { + return ( + + + + ); +}; +``` + Then you can receive raw reactions using the `room.messages.reactions.subscribeRaw()` method: + + + +Then you can receive raw reactions using the `rawReactionsListener` parameter in the `useMessages` hook: + ```javascript @@ -332,9 +597,29 @@ room.messages.reactions.subscribeRaw { event -> } } ``` + +```react +import { MessageReactionEvents, useMessages } from '@ably/chat/react'; + +const MyComponent = () => { + useMessages({ + rawReactionsListener: (event) => { + if (event.type === MessageReactionEvents.Create) { + console.log("new reaction", event.reaction); + } else if (event.type === MessageReactionEvents.Delete) { + console.log("reaction removed", event.reaction); + } + }, + }); + + return
...
; +}; +```
+ You should be aware of the following limitations: