Part 6 - Send Messages
In this part of the tutorial, we will add the ability to send messages to the chat channel on behalf of the current user. Since the focus of this tutorial is the message list itself, we will use a button rather than a text input to simulate sending messages. But first let's cover some of the specifics of the feature.
Sending Messages and Scroll Position
Unlike receiving messages, sending messages should always scroll the list to the bottom. This is because the user is sending a message, and they should see it immediately. The autoScrollToBottom
callback will let us handle this.
Optimistic Updates
When sending a message, we want to show it immediately in the list, even before the server acknowledges it. Our ChatChannel
class exposes a sendOwnMessage
method which will return a "temporary" message with the delivered
field set to false. After a short delay (simulating the network round trip), we are going to receive the "real" message back through the onNewMessages
event handler.
Send Message Button
Add the following button after the 'receive message' button from the previous part:
<button
onClick={() => {
const tempMessage = channel.sendOwnMessage()
messageListRef.current?.data.append([tempMessage], ({ scrollInProgress, atBottom }) => {
if (atBottom || scrollInProgress) {
return 'smooth'
} else {
return 'auto'
}
})
}}
>
Send own message
</button>
After a short delay, the ChatChannel
will call the onNewMessages
event handler, and we will replace the temporary message with the real one. Modify the onNewMessages
event handler to handle the delivery of the message:
channel.onNewMessages = (messages) => {
const updatingMessageIds: number[] = []
messageListRef.current?.data.map((item) => {
const updatedItem = !item.delivered && messages.find((m) => m.localId === item.localId)
if (updatedItem) {
updatingMessageIds.push(updatedItem.id!)
return updatedItem
} else {
return item
}
})
const nonUpdatingMessages = messages.filter((m) => !updatingMessageIds.includes(m.id!))
messageListRef.current?.data.append(
nonUpdatingMessages,
({ atBottom, scrollInProgress }) => {
if (atBottom || scrollInProgress) {
return 'smooth'
} else {
setUnseenMessages((val) => val + 1)
return false
}
})
}
The snippet above uses data.map
to replace any temporary messages with the ones received from the server using the localId
field to match them. Just like before, the append
method is then used to add the remaining messages to the list.
The approach above lets us handle concurrent updates, as the "acknowledgement" of the message can come batched with other new messages from the server.
If everything works as expected, pressing the button will display a new message from the current user with a smooth scroll animation. After a short delay, the Delivering...
indicator below the message should disappear.