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.

SentDeliveredReadRead by many
Message Delivery State Sent
Message Delivery State Delivered
Message Delivery State Read
Message Delivery State Read Group

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.

class CustomAppFactory: ViewFactory {
    @Injected(\.chatClient) public var chatClient

    private init() {}

	public func makeMessageReadIndicatorView(
		channel: ChatChannel,
		message: ChatMessage
	) -> some View {
		return EmptyView()
	}
}
BeforeAfter
Hide Message Delivery State Before
Hide Message Delivery State 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:

BeforeAfter
Customize Message Delivery State Read Before
Customize Message Delivery State Read 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:

Message Delivery State Read and Delivered

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.

© Getstream.io, Inc. All Rights Reserved.