# Messages Send, update, delete, and receive messages in a chat room with any number of participants. Users subscribe to messages by registering a listener, and send messages to all users that are subscribed to receive them. A user can also update or delete a message, all users that are subscribed to the room will be notified of the changes. ## Subscribe to messages Subscribe to receive messages in a room by registering a listener. Use the [`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#subscribe)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/subscribe%28%29-360z1)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/subscribe.html) method in a room to receive all messages that are sent to it: You can use [`messages.asFlow()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/as-flow.html) to receive new messages: Subscribe to messages with the [`useMessages`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useMessages.html) hook. Supply a listener and the hook will automatically subscribe to message events sent to the room. As long as a defined value is provided, the subscription will persist across renders. If the listener value is undefined, the subscription will be removed until it becomes defined again. Providing a listener will also enable you to retrieve messages that have been [previously sent to the room](https://ably.com/docs/chat/rooms/history). ```javascript const {unsubscribe} = room.messages.subscribe((event) => { console.log(event.message); }); ``` ```react const MyComponent = () => { useMessages({ listener: (event) => { console.log('Received message: ', event.message); }, }); return
...
; }; ``` ```swift let messagesSubscription = try await room.messages.subscribe() for await message in messagesSubscription { print("Message received: \(message)") } ``` ```kotlin val subscription = room.messages.subscribe { messageEvent: ChatMessageEvent -> println(messageEvent.message.toString()) } ``` ```jetpack const MyComponent = () => { const { sendMessage } = useMessages(); const handleMessageSend = () => { sendMessage({ text: 'Hello, World!' }); }; return (
); }; ``` ```swift let message = try await room.messages.send(params: .init(text: "hello")) ``` ```kotlin room.messages.send(text = "hello") ``` ```jetpack const MyComponent = () => { const { getMessage } = useMessages(); const handleMessageGet = () => { getMessage('01726232498871-001@abcdefghij:001'); }; return (
); }; ``` ```swift let message = try await room.messages.get(withSerial: "01726232498871-001@abcdefghij:001") ``` ```kotlin val message = room.messages.get("01726232498871-001@abcdefghij:001") ``` ```jetpack val message = room.messages.get("01726232498871-001@abcdefghij:001") ```
## Update a message Use the [`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#update)[`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/update%28withserial:params:details:%29)[`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/update.html) method to update a message in a chat room. All users that are [subscribed](#subscribe) to messages on that room will receive the update: Use the [`updateMessage()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-react.UseMessagesResponse.html#updatemessage) method available from the response of the `useMessages` hook to update a message in the room: ```javascript const message: Message const updatedMessage = message.copy({text: "my updated text"}) await room.messages.update(updatedMessage.serial, updatedMessage, { description: "Message update by user" }); ``` ```react const MyComponent = () => { const { updateMessage } = useMessages(); const [message, setMessage] = useState(); const handleMessageUpdate = (msg: Message) => { updateMessage(msg.serial, msg.copy({ text: "my updated text" }), { description: "Message update by user" }) .then((updatedMsg: Message) => { console.log('Message updated:', updatedMsg); }) .catch((error) => { console.error('Error updating message: ', error); }); }; return (
); }; ``` ```swift let originalMessage: Message let updatedMessage = try await room.messages.update( forSerial: originalMessage.serial, params: .init(text: "my updated text"), details: .init(description: "Message update by user") ) ``` ```kotlin val originalMessage: Message val updatedMessage = room.messages.update( originalMessage.copy(text = "my updated text"), operationDescription = "Message update by user", ) ``` ```jetpack const {unsubscribe} = room.messages.subscribe((event) => { switch (event.type) { case ChatMessageEventType.Created: console.log('Received message: ', event.message); break; case ChatMessageEventType.Updated: const existing = myMessageList.find(msg => msg.serial === event.message.serial); if (existing && event.message.version.serial <= existing.version.serial) { // We've already received a more recent update, so this one can be discarded. return; } console.log('Message updated: ', event.message); break; default: break; } }); ``` ```react const MyComponent = () => { useMessages({ listener: (event) => { switch (event.type) { case ChatMessageEventType.Created: console.log('Received message: ', event.message); break; case ChatMessageEventType.Updated: const existing = myMessageList.find(msg => msg.serial === event.message.serial); if (existing && event.message.version.serial <= existing.version.serial) { // We've already received a more recent update, so this one can be discarded. return; } console.log('Message updated: ', event.message); break; default: break; } }, }); return
...
; }; ``` ```swift let messagesList: [Message] let messagesSubscription = try await room.messages.subscribe() for await message in messagesSubscription { switch message.action { case .messageCreate: messagesList.append(message) case .messageUpdate: // compare versions to ensure you are only updating with a newer message if let index = messagesList.firstIndex(where: { $0.serial == message.serial && message.version > $0.version }) { messagesList[index] = message } default: break } } ``` ```kotlin val myMessageList: List val messagesSubscription = room.messages.subscribe { event -> when (event.type) { ChatMessageEventType.Created -> println("Received message: ${event.message}") ChatMessageEventType.Updated -> myMessageList.find { event.message.serial == it.serial && event.message.version.serial > it.version.serial }?.let { println("Message updated: ${event.message}") } else -> {} } } ``` ```jetpack const messageToDelete: Message await room.messages.delete(messageToDelete.serial, { description: 'Message deleted by user' }); ``` ```react const MyComponent = () => { const { deleteMessage } = useMessages(); const [message, setMessage] = useState(); const handleMessageDelete = (msg: Message) => { deleteMessage(msg.serial, { description: 'Message deleted by user' }) .then((deletedMessage: Message) => { console.log('Message deleted:', deletedMessage); }) .catch((error) => { console.error('Error deleting message: ', error); }); }; return (
); }; ``` ```swift let messageToDelete: Message let deletedMessage = try await room.messages.delete( forSerial: messageToDelete.serial, params: .init(description: "Message deleted by user") ) ``` ```kotlin val messageToDelete: Message val deletedMessage = room.messages.delete( messageToDelete, operationDescription = "Message deleted by user", ) ``` ```jetpack const {unsubscribe} = room.messages.subscribe((event) => { switch (event.type) { case ChatMessageEventType.Created: console.log('Received message: ', event.message); break; case ChatMessageEventType.Deleted: const existing = myMessageList.find(msg => msg.serial === event.message.serial); if (existing && event.message.version.serial <= existing.version.serial) { // We've already received a more recent update, so this one can be discarded. return; } console.log('Message deleted: ', event.message); break; default: break; } }); ``` ```react const MyComponent = () => { useMessages({ listener: (event) => { switch (event.type) { case ChatMessageEventType.Created: console.log('Received message: ', event.message); break; case ChatMessageEventType.Deleted: const existing = myMessageList.find(msg => msg.serial === event.message.serial); if (existing && event.message.version.serial <= existing.version.serial) { // We've already received a more recent update, so this one can be discarded. return; } console.log('Message deleted: ', event.message); break; default: break; } }, }); return
...
; }; ``` ```swift let messagesList: [Message] let messagesSubscription = try await room.messages.subscribe() for await message in messagesSubscription { switch message.action { case .messageCreate: messagesList.append(message) case .messageDelete: // version check ensures the message you are deleting is older if let index = messagesList.firstIndex(where: { $0.serial == message.serial && message.version > $0.version }) { messagesList.remove(at: index) } default: break } } ``` ```kotlin val myMessageList: List val messagesSubscription = room.messages.subscribe { event -> when (event.type) { ChatMessageEventType.Created -> println("Received message: ${event.message}") ChatMessageEventType.Deleted -> myMessageList.find { event.message.serial == it.serial && event.message.version.serial > it.version.serial }?.let { println("Message deleted: ${event.message}") } else -> {} } } ``` ```jetpack const messageA: Message const messageB: Message if (messageA.serial < messageB.serial) { console.log('messageA occurred before messageB'); } else if (messageA.serial > messageB.serial) { console.log('messageA occurred after messageB'); } else { console.log('messageA and messageB are concurrent (the same message)'); } ```
### Ordering updates and deletes Applying an action to a message produces a new version, which is uniquely identified by the `version.serial` property. When two message instances share the same `serial` they represent the same chat message, but they can represent different versions. Lexicographically sorting the two message instances by the `version.serial` property gives the global order of the message versions: the message instance with a greater `version.serial` is newer, the message instance with a lower `version.serial` is older, and if their `version.serial` is equal then they are the same version. Update and Delete events provide the message payload without message reactions. To correctly use message reactions, always use the [`with()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Message.html#with) method to apply the event to the message instance. ## Keep messages updated using with() The [`Message`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Message.html) object has a method [`with`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Message.html#with) that takes a [`MessageEvent`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.MessageEvent.html), automatically compares version serials, and returns the newest `Message` instance. For updates and deletes, if `message.with(event)` is called with an `event` that has an older `version.serial` than the `message`, then the `message` is returned unchanged. If it is called with a newer event (greater `version.serial`), then the message from the event is returned. For message reaction events, the reactions will be correctly applied to the returned message. `Message.with()` also ensures that reactions from existing messages are copied over to the new message instance in the case of UPDATEs or DELETEs. Example usage to keep a list of messages updated: ```javascript let myMessageList: Message[]; // For messages (create, update, delete) room.messages.subscribe((event) => { switch (event.type) { case ChatMessageEventType.Created: myMessageList.push(event.message); break; case ChatMessageEventType.Updated: case ChatMessageEventType.Deleted: const idx = myMessageList.findIndex((msg) => msg.serial === event.message.serial); if (idx !== -1) { myMessageList[idx] = myMessageList[idx].with(event); } break; default: break; } }); // And for message reactions room.messages.reactions.subscribe((event) => { const idx = myMessageList.findIndex((msg) => msg.serial === event.messageSerial); if (idx !== -1) { myMessageList[idx] = myMessageList[idx].with(event); } }); ``` ```react const MyComponent = () => { // we use {list: []} to avoid copying the full array with every change // but still take advantage of React's state change detection const [ messages, setMessages ] = useState<{list: Message[]}>({list: []}); useMessages({ listener: (event) => { switch (event.type) { case ChatMessageEventType.Created: setMessages((prev) => { // append new message prev.list.push(event.message); // update reference without copying whole array return { list: prev.list }; }); break; case ChatMessageEventType.Updated: case ChatMessageEventType.Deleted: setMyMessageList((prev) => { // find existing message to apply update or delete to const existing = prev.list.findIndex((msg) => msg.serial === event.message.serial); if (existing === -1) { return prev; // no change if not found } const newMsg = existing.with(event); if (newMsg === existing) { // with() returns the same object if the event is older, // so in this case no change is needed return prev; } // set new message and update reference without copying whole array prev.list[existing] = newMsg; return { list: prev.list }; }); break; } }, }); return
...
; }; ```