Working with Messages
Message Object
A message is represented by ChatMessage
model.
Depending on combination of its properties messages appear differently, a screenshot on the left showcases the most common types of messages. A screenshot on the right shows how some of ChatMessage
properties are reflected in views:
Optimistic Updates
Optimistic updates model is applied to messages, meaning that when there is a change to local messages state it is propagated to chat components so it is displayed for users right away and then it's synchronized with backend. In case of synchronization failure users may be prompted to retry the failed action.
This makes LocalMessageState
one of the most important properties in message's lifecycle, because it's used for keeping messages in sync with backend.
Get a Message by its ID
You can get a single message by its ID:
import StreamChat
/// Use the `ChatClient` to create a
/// `ChatMessageController` with the `ChannelId`.
let messageController = chatClient.messageController(
cid: ChannelId(type: .messaging, id: "general"),
messageId: "message-id"
)
/// Get the message
messageController.synchronize { error in
// handle possible errors / access message
print(error ?? messageController.message!)
}
Create a Message
ComposerVC
is a UI component that handles messages creation:
If you are using your own component for a message composer you can use ChatChannelController
to create messages:
let controller = ChatChannelController(
channelQuery: ChannelQuery(cid: ChannelId(type: .messaging, id: "general")),
client: client
)
controller.createNewMessage(
text: "Hello World!",
pinning: .noExpiration,
attachments: [image],
quotedMessageId: quotedMessage.id,
completion: { result in
switch result {
case .success(let messageId):
print(messageId)
case .failure(let error):
print(error)
}
}
)
More info on Pinning and Attachments can be found in corresponding guides.
More on Quoted messages could be found in this guide below.
How Sending a Message Works
When createNewMessage
is called, ChatChannelController
creates a new message locally and schedules it for send.
Uploading is handled by an internal entity called MessageSender
. It automatically starts
uploading when it detects locally cached messages with .pendingSend
state.
There is no need to take care of MessageSender
, it is created and added to the list of background workers by ChatClient
.
Sending of the message has the following phases:
- When a message with
.pendingSend
state local state appears in the db, the sender queues it in the sending queue for the channel the message belongs to. - The pending messages are send one by one order by their
locallyCreatedAt
value ascending. - When the message is being sent, its local state is changed to
.sending
- If the operation is successful, the local state of the message is changed to
nil
. If the operation fails, the local state of is changed tosendingFailed
.
┌──▶ `nil` if success
`pendingSend` ──────▶ `sending` ──┤
└─▶ `sendingFailed`
This behavior makes it possible to update your UI with the new message immediately without blocking the UI:
class MyChannelViewController: UIViewController {
let controller = ChannelController(cid: <#ChannelId#>)
func sendMessage(text: String) {
// This method creates a new message locally,
// initially with `localState == .pendingSend`
controller.createNewMessage(text: text)
}
// Example handling for Message local state:
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
...
let message = controller.messages[indexPath.row]
if message.localState == .pendingSend {
// show message as pending send
} else if message.localState == .sendingFailed {
// show retry button for the message
}
...
}
}
When a message is created ChannelController
sends stop typing event for this channel
Edit a Message
There is an action for editing messages:
When a user is editing a message ComposerVC
takes the following appearance:
If you use your own implementation for composer view, the same could be done with ChatMessageController
:
let messageController = chatClient.messageController(
cid: channelId,
messageId: messageId
)
messageController.editMessage(text: "World Hello!") { error in
if let error = error {
print(error)
}
}
Editing a message has several phases:
MessageModel.localState
states when editing a message:
┌──▶ `nil` if success
`pendingSync` ──────▶ `syncing` ──┤
└─▶ `syncingFailed`
This behavior makes it possible to update your UI with the updated message immediately without blocking the UI:
class MyChannelViewController: UIViewController {
let controller = ChannelController(cid: <#ChannelId#>)
func editMessage(message: ChatMessage, text: String) {
let messageController = controller.client.messageController(
cid: <#ChannelId#>,
messageId: message.id
)
// This method updates a message locally
// with the `localState == .pendingSync`
messageController.editMessage(text: text)
}
// Example handling for Message local state:
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
...
let message = controller.messages[indexPath.row]
if message.localState == .pendingSync {
// show message as not being synced with the servers
} else if message.localState == .syncingFailed {
// show retry button
}
...
}
}
Delete a Message
A message can be deleted with the corresponding action:
When a user deletes a message it will be hidden for all the rest users in conversation, but it will appear for the user who deleted it like this:
In an upcoming version it will become customizable, so it will be possible to hide deleted messages for all participants in a conversation.
Message deletion is handled by ChatMessageController
:
let messageController = chatClient.messageController(
cid: channelId,
messageId: messageId
)
messageController.deleteMessage { error in
if let error = error {
print(error)
}
}
If the message has .pendingSend
or .sendingFailed
state it will be removed locally as it hasn't been sent yet.
If the message has some other local state it should be removed on the backend.
Before the delete
network call happens the local state is set to deleting
and based on
the response it becomes either nil
if request succeeds or deletingFailed
if request fails.
┌──▶ `nil` if success
`deleting` ──┤
└─▶ `deletingFailed`
This behavior makes it possible to update your UI with the updated message immediately without blocking the UI:
class MyChannelViewController: UIViewController {
let controller = ChannelController(cid: <#ChannelId#>)
func deleteMessage(message: Message) {
// Create a `MessageController` for the message you want to delete
let messageController = controller.client.messageController(
cid: channelController.channelQuery.cid,
messageId: message.id
)
// Delete the message
messageController.deleteMessage()
}
// Example handling for Message local state:
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
...
let message = controller.messages[indexPath.row]
if message.localState == .deleting {
// show message as being deleted
} else if message.localState == .deletingFailed {
// show retry button for deleting the message
}
...
}
}
Reply a Message
There are two ways of replying a message:
Quoted reply.
In case if
ComposerVC
is used it looks like this during composing, and the resulting message will show both quoted message and the reply itself.Thread reply
Initiating a thread reply takes a user into thread details screen and the resulting message will look like a normal message that is placed inside the thread. It is also possible to duplicate it to the parent channel.
A message with thread replies appears like this:
If you use your own implementation for message composer you can create a thread reply for a message with MessageController
:
let messageController = chatClient.messageController(
cid: channelId,
messageId: "message-id"
)
messageController.createNewReply(
text: "Thread reply",
pinning: nil,
attachments: [],
showReplyInChannel: true,
quotedMessageId: nil
)
A quoted reply can be created like this:
let controller = ChatChannelController(
channelQuery: ChannelQuery(cid: ChannelId(type: .messaging, id: "general")),
client: client
)
channelController.createNewMessage(
text: "Quoted reply",
pinning: nil,
attachments: [],
quotedMessageId: "quoted-message-id"
)