class CustomAppFactory: ViewFactory {
@Injected(\.chatClient) public var chatClient
private init() {}
public func makeMessageReadIndicatorView(
channel: ChatChannel,
message: ChatMessage
) -> some View {
return EmptyView()
}
}Message Read Indicators
For messages sent by the current user, the read indicators represent the read state of the message. It is shown below the message bubble, by default.
Read indicators states
The possible states of the read indicators are:
- sent: The message has been sent to the server successfully. It is shown as a gray checkmark.
- delivered: The message has been delivered to at least one of the channel members devices. It is shown as double gray checkmark.
- read: The message has been read by at least one of the channel members. It is shown as double blue checkmark.
The delivered state is only available since version 4.92.0 and it needs to be enabled in the Dashboard for each channel type.
| Sent | Delivered | Read | Read by many |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
If read_events are turned OFF for the channel, read indicators are hidden.
Basic Customization
Hide read indicators
In case your app does not need to show the read indicators, you can hide them by returning an empty view from the makeMessageReadIndicatorView view factory method.
| Before | After |
|---|---|
![]() | ![]() |
Advanced Customization
Display last read members avatars
The read indicators can be customized to display the last read members avatars by returning a custom view from the makeMessageReadIndicatorView view factory method.
Here is an example implementation:
class CustomAppFactory: ViewFactory {
@Injected(\.chatClient) public var chatClient
private init() {}
public func makeMessageReadIndicatorView(
channel: ChatChannel,
message: ChatMessage
) -> some View {
return CustomMessageReadIndicatorView(
channel: channel,
message: message
)
}
}
struct CustomMessageReadIndicatorView: View {
@Injected(\.chatClient) private var chatClient
@Injected(\.utils) private var utils
let channel: ChatChannel
let message: ChatMessage
private let avatarSize: CGFloat = 16
private let avatarOverlap: CGFloat = 8
private let maxAvatarsToShow = 3
var body: some View {
HStack(spacing: 4) {
defaultMessageReadIndicatorView
if !lastReadUsers.isEmpty {
readAvatarsView
}
}
}
var defaultMessageReadIndicatorView: some View {
DefaultViewFactory.shared.makeMessageReadIndicatorView(
channel: channel,
message: message
)
}
var readAvatarsView: some View {
HStack(spacing: -avatarOverlap) {
ForEach(Array(lastReadUsers.enumerated()), id: \.element.id) { index, user in
MessageAvatarView(
avatarURL: user.imageURL,
size: CGSize(width: avatarSize, height: avatarSize),
showOnlineIndicator: false
)
.overlay(
Circle()
.stroke(Color.white, lineWidth: 1)
)
.zIndex(Double(maxAvatarsToShow - index))
}
}
}
/// Last 3 users who read the message (most recent first)
private var lastReadUsers: [ChatUser] {
let reads = channel.reads(for: message)
.sorted { $0.lastReadAt > $1.lastReadAt }
return Array(reads.prefix(maxAvatarsToShow)).map(\.user)
}Below you can see a comparison of the default read indicator view and the custom one:
| Before | After |
|---|---|
![]() | ![]() |
Show all read and delivered members
To show all the read and delivered members, we can create a custom screen that shows the members that have read the message and the ones that were delivered but not read yet.
For this, we need to add a tap gesture to our custom read indicator view and present this custom view.
/// Our custom read indicator view from the previous example.
struct CustomMessageReadIndicatorView: View {
@Injected(\.chatClient) private var chatClient
@Injected(\.utils) private var utils
let channel: ChatChannel
let message: ChatMessage
private let avatarSize: CGFloat = 16
private let avatarOverlap: CGFloat = 8
private let maxAvatarsToShow = 3
@State private var showMessageReadsInfo = false
var body: some View {
HStack(spacing: 4) {
defaultMessageReadIndicatorView
if !lastReadUsers.isEmpty {
readAvatarsView
}
}
.onTapGesture {
showMessageReadsInfo = true
}
.sheet(isPresented: $showMessageReadsInfo) {
MessageReadsInfoView(
message: message,
channelController: chatClient.channelController(for: channel.cid)
)
}
}You can check an example implementation of the MessageReadsInfoView in our demo app here. It looks something like this:

At the moment, showing the timestamps of the read and delivered members is not recommended since the timestamp is always related to the latest message in the channel, and not for each message individually.







