Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import io.getstream.chat.android.models.Option
import io.getstream.chat.android.models.Poll
import io.getstream.chat.android.models.PollOption
import io.getstream.chat.android.models.Reaction
import io.getstream.chat.android.models.SyncStatus
import io.getstream.chat.android.models.User
import io.getstream.chat.android.models.Vote
import io.getstream.chat.android.state.extensions.awaitRepliesAsState
Expand Down Expand Up @@ -1719,10 +1720,21 @@ public class MessageListController(
val itemState = messagesState.messageItems.lastOrNull { messageItem ->
messageItem is HasMessageListItemState
} as? HasMessageListItemState
val messageId = itemState?.message?.id
val messageText = itemState?.message?.text
val message = itemState?.message
val messageId = message?.id
val messageText = message?.text
logger.d { "[markLastMessageRead] cid: $cid, msgId($isInThread): $messageId, msgText: \"$messageText\"" }

// Skip when our own message is at the bottom and hasn't been confirmed by the server.
// Without this, marking read on an empty channel (only an in-flight optimistic message
// exists) causes the server to persist last_read_message_id = "" because its view of
// the channel is empty.
val currentUserId = clientState.user.value?.id
if (message != null && message.user.id == currentUserId && message.syncStatus != SyncStatus.COMPLETED) {
logger.v { "[markLastMessageRead] cid: $cid; rejected[$isInThread] (own unsynced): $messageId" }
return
}

val lastSeenMessageId = this.lastSeenMessageId
if (lastSeenMessageId == messageId) {
logger.v { "[markLastMessageRead] cid: $cid; rejected[$isInThread] (already seen msgId): $messageId" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.getstream.chat.android.models.Message
import io.getstream.chat.android.models.MessageType
import io.getstream.chat.android.models.MessagesState
import io.getstream.chat.android.models.Reaction
import io.getstream.chat.android.models.SyncStatus
import io.getstream.chat.android.models.TypingEvent
import io.getstream.chat.android.models.User
import io.getstream.chat.android.models.Vote
Expand Down Expand Up @@ -348,9 +349,9 @@ internal class MessageListControllerTests {
fun `When repetitive markLastMessageRead calls appear only single API call should be sent`() = runTest {
val chatClient: ChatClient = mock()
val messages = arrayListOf(
randomMessage(id = "1"),
randomMessage(id = "2"),
randomMessage(id = "3"),
randomMessage(id = "1", syncStatus = SyncStatus.COMPLETED),
randomMessage(id = "2", syncStatus = SyncStatus.COMPLETED),
randomMessage(id = "3", syncStatus = SyncStatus.COMPLETED),
)
val messagesState = MutableStateFlow(messages)
val controller = Fixture(chatClient = chatClient)
Expand All @@ -374,6 +375,68 @@ internal class MessageListControllerTests {
verify(chatClient, times(1)).markRead(any(), any())
}

@Test
fun `When current user's last message is COMPLETED markLastMessageRead should invoke markRead`() = runTest {
val chatClient: ChatClient = mock()
val messagesState = MutableStateFlow(
listOf(randomMessage(id = "1", user = user1, syncStatus = SyncStatus.COMPLETED)),
)
val controller = Fixture(chatClient = chatClient)
.givenCurrentUser()
.givenChannelQuery()
.givenMarkRead()
.givenChannelState(messagesState = messagesState)
.get()

controller.markLastMessageRead()
delay(1000)

verify(chatClient, times(1)).markRead(eq(CHANNEL_TYPE), eq(CHANNEL_ID))
controller.lastSeenMessageId `should be equal to` "1"
}

@Test
fun `When current user's last message is not COMPLETED markLastMessageRead should not invoke markRead`() = runTest {
val chatClient: ChatClient = mock()
val messagesState = MutableStateFlow(
listOf(randomMessage(id = "1", user = user1, syncStatus = SyncStatus.IN_PROGRESS)),
)
val controller = Fixture(chatClient = chatClient)
.givenCurrentUser()
.givenChannelQuery()
.givenMarkRead()
.givenChannelState(messagesState = messagesState)
.get()

controller.markLastMessageRead()
delay(1000)

verify(chatClient, times(0)).markRead(any(), any())
controller.lastSeenMessageId.shouldBeNull()
}

@Test
fun `When peer's last message is not COMPLETED markLastMessageRead should still invoke markRead`() = runTest {
// syncStatus is local-only and not on the wire. Peer messages inherit the data
// class default — the gate must not block them on that.
val chatClient: ChatClient = mock()
val messagesState = MutableStateFlow(
listOf(randomMessage(id = "1", user = user2, syncStatus = SyncStatus.IN_PROGRESS)),
)
val controller = Fixture(chatClient = chatClient)
.givenCurrentUser()
.givenChannelQuery()
.givenMarkRead()
.givenChannelState(messagesState = messagesState)
.get()

controller.markLastMessageRead()
delay(1000)

verify(chatClient, times(1)).markRead(eq(CHANNEL_TYPE), eq(CHANNEL_ID))
controller.lastSeenMessageId `should be equal to` "1"
}

@Test
fun `When channelData changes the updated Channel instance must be emitted`() = runTest {
val chatClient: ChatClient = mock()
Expand Down Expand Up @@ -1268,12 +1331,18 @@ internal class MessageListControllerTests {

private fun nowDate() = Date(testCoroutines.dispatcher.scheduler.currentTime)

private fun nowMessage(author: User, type: String, text: String = randomString()): Message {
private fun nowMessage(
author: User,
type: String,
text: String = randomString(),
syncStatus: SyncStatus = SyncStatus.COMPLETED,
): Message {
val nowDate = nowDate()
return randomMessage(
user = author,
type = type,
text = text,
syncStatus = syncStatus,
createdAt = nowDate,
updatedAt = nowDate,
deletedAt = null,
Expand Down
Loading