Skip to content

Commit 189b4c0

Browse files
committed
Refactor: Improve message delivery receipts logic
This commit refactors the `MessageReceiptManager` to enhance the logic for marking messages as delivered. Key changes include: - Renaming `markMessagesAsDelivered(messages: List<Message>)` to `markMessageAsDelivered(message: Message)` and making it the public API. The old function is now private and handles batch processing. - Before marking a message as delivered, the manager now fetches the corresponding channel from the local repository or the API to ensure all conditions are met. - The responsibility of filtering messages that can be marked as delivered is moved into a new private function `canMarkMessageAsDelivered`. - Test cases have been updated to reflect these changes and improve coverage.
1 parent 0ec4d07 commit 189b4c0

File tree

5 files changed

+242
-180
lines changed

5 files changed

+242
-180
lines changed

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,9 @@ internal constructor(
338338
scope = userScope,
339339
now = now,
340340
getCurrentUser = ::getCurrentUser,
341+
channelRepository = repositoryFacade,
341342
messageReceiptRepository = repository,
343+
api = api,
342344
)
343345

344346
private var pushNotificationReceivedListener: PushNotificationReceivedListener =
@@ -463,7 +465,7 @@ internal constructor(
463465

464466
is NewMessageEvent -> {
465467
notifications.onChatEvent(event)
466-
messageReceiptManager.markMessagesAsDelivered(messages = listOf(event.message))
468+
messageReceiptManager.markMessageAsDelivered(event.message)
467469
}
468470

469471
is NotificationReminderDueEvent -> notifications.onChatEvent(event)

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/notifications/ChatNotifications.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ internal class ChatNotificationsImpl(
114114
id = pushMessage.messageId,
115115
cid = "${pushMessage.channelType}:${pushMessage.channelId}",
116116
)
117-
chatClient.messageReceiptManager.markMessagesAsDelivered(messages = listOf(message))
117+
chatClient.messageReceiptManager.markMessageAsDelivered(message)
118118

119119
if (notificationConfig.shouldShowNotificationOnPush() && !handler.onPushMessage(pushMessage)) {
120120
handlePushMessage(pushMessage)

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/receipts/MessageReceiptManager.kt

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@
1616

1717
package io.getstream.chat.android.client.receipts
1818

19+
import io.getstream.chat.android.client.api.ChatApi
20+
import io.getstream.chat.android.client.api.models.QueryChannelRequest
21+
import io.getstream.chat.android.client.extensions.cidToTypeAndId
1922
import io.getstream.chat.android.client.extensions.getCreatedAtOrThrow
2023
import io.getstream.chat.android.client.extensions.internal.NEVER
2124
import io.getstream.chat.android.client.extensions.internal.lastMessage
2225
import io.getstream.chat.android.client.extensions.userRead
26+
import io.getstream.chat.android.client.persistance.repository.ChannelRepository
2327
import io.getstream.chat.android.client.persistence.repository.MessageReceiptRepository
2428
import io.getstream.chat.android.client.utils.message.isDeleted
2529
import io.getstream.chat.android.models.Channel
2630
import io.getstream.chat.android.models.Message
2731
import io.getstream.chat.android.models.MessageType
2832
import io.getstream.chat.android.models.User
29-
import io.getstream.chat.android.models.UserId
3033
import io.getstream.log.taggedLogger
3134
import kotlinx.coroutines.CoroutineScope
3235
import kotlinx.coroutines.launch
@@ -40,111 +43,143 @@ internal class MessageReceiptManager(
4043
private val scope: CoroutineScope,
4144
private val now: () -> Date,
4245
private val getCurrentUser: () -> User?,
46+
private val channelRepository: ChannelRepository,
4347
private val messageReceiptRepository: MessageReceiptRepository,
48+
private val api: ChatApi,
4449
) {
4550

4651
private val logger by taggedLogger("Chat:MessageReceiptManager")
4752

4853
/**
49-
* Request to mark the last undelivered messages in the given channels as delivered.
54+
* Request to mark the given channels as delivered
55+
* if delivery receipts are enabled for the current user.
5056
*
51-
* A delivery message candidate is the last non-deleted message in the channel that:
57+
* A channel can have a message marked as delivered only if:
58+
* - Delivery events are enabled in the channel config
5259
*
60+
* A delivery message candidate is the last non-deleted message in the channel that:
61+
* - It was not sent by the current user
62+
* - It is not a system message
63+
* - It is not deleted
5364
* - Is not yet marked as read by the current user
5465
* - Is not yet marked as delivered by the current user
5566
*/
5667
fun markChannelsAsDelivered(channels: List<Channel>) {
57-
val deliveredMessageCandidates = channels.mapNotNull(::getUndeliveredMessage)
68+
val currentUser = getCurrentUser() ?: run {
69+
logger.w { "[markChannelsAsDelivered] Current user is null" }
70+
return
71+
}
72+
73+
if (!currentUser.isDeliveryReceiptsEnabled) return
74+
75+
val deliveredMessageCandidates = channels.mapNotNull { channel ->
76+
channel.lastMessage?.takeIf { lastNonDeletedMessage ->
77+
canMarkMessageAsDelivered(currentUser, channel, lastNonDeletedMessage)
78+
}
79+
}
5880
markMessagesAsDelivered(messages = deliveredMessageCandidates)
5981
}
6082

6183
/**
62-
* Request to mark the given messages as delivered if delivery receipts are enabled
63-
* in the current user privacy settings.
64-
*
65-
* A message can be marked as delivered only if:
84+
* Request to mark the given message as delivered
85+
* if delivery receipts are enabled for the current user.
6686
*
67-
* - It was not sent by the current user
68-
* - It is not a system message
69-
* - It is not deleted
87+
* @see [markChannelsAsDelivered] for the conditions to mark a message as delivered.
7088
*/
71-
fun markMessagesAsDelivered(messages: List<Message>) {
72-
if (messages.isEmpty()) {
73-
logger.w { "[markMessagesAsDelivered] No receipts to send" }
89+
fun markMessageAsDelivered(message: Message) {
90+
val currentUser = getCurrentUser() ?: run {
91+
logger.w { "[markMessageAsDelivered] Current user is null" }
7492
return
7593
}
7694

77-
logger.d { "[markMessagesAsDelivered] Processing delivery receipts for ${messages.size} messages…" }
95+
if (!currentUser.isDeliveryReceiptsEnabled) return
7896

79-
val currentUser = getCurrentUser() ?: run {
80-
logger.w { "[markMessagesAsDelivered] Cannot send delivery receipts: current user is null" }
81-
return
82-
}
97+
scope.launch {
98+
val channel = getChannel(message.cid) ?: run {
99+
logger.w { "[markMessageAsDelivered] Channel ${message.cid} not found" }
100+
return@launch
101+
}
83102

84-
// Check if delivery receipts are enabled for the current user
85-
if (!currentUser.isDeliveryReceiptsEnabled) {
86-
logger.w { "[markMessagesAsDelivered] Delivery receipts disabled for user ${currentUser.id}" }
87-
return
103+
if (canMarkMessageAsDelivered(currentUser, channel, message)) {
104+
markMessagesAsDelivered(messages = listOf(message))
105+
}
88106
}
107+
}
89108

90-
// Filter out messages that shouldn't have delivery receipts sent
91-
val filteredMessages = messages.filter { message ->
92-
shouldSendDeliveryReceipt(currentUserId = currentUser.id, message = message)
93-
}
94-
if (filteredMessages.size != messages.size) {
95-
logger.d {
96-
"[markMessagesAsDelivered] " +
97-
"Skipping delivery receipts for ${messages.size - filteredMessages.size} messages"
109+
private suspend fun getChannel(cid: String): Channel? =
110+
channelRepository.selectChannel(cid)
111+
?: run {
112+
val (channelType, channelId) = cid.cidToTypeAndId()
113+
val request = QueryChannelRequest()
114+
api.queryChannel(channelType, channelId, request)
115+
.await().getOrNull()
98116
}
99-
}
100117

101-
if (filteredMessages.isEmpty()) {
118+
private fun markMessagesAsDelivered(messages: List<Message>) {
119+
if (messages.isEmpty()) {
102120
logger.w { "[markMessagesAsDelivered] No receipts to send" }
103121
return
104122
}
105123

124+
logger.d { "[markMessagesAsDelivered] Processing delivery receipts for ${messages.size} messages…" }
125+
106126
scope.launch {
107-
val receipts = filteredMessages.map { message -> message.toDeliveryReceipt() }
127+
val receipts = messages.map { message -> message.toDeliveryReceipt() }
108128
messageReceiptRepository.upsertMessageReceipts(receipts)
109129

110-
logger.d { "[markMessagesAsDelivered] ${filteredMessages.size} delivery receipts upserted" }
130+
logger.d { "[markMessagesAsDelivered] ${messages.size} delivery receipts upserted" }
111131
}
112132
}
113133

114-
private fun getUndeliveredMessage(channel: Channel): Message? {
134+
private fun canMarkMessageAsDelivered(
135+
currentUser: User,
136+
channel: Channel,
137+
message: Message,
138+
): Boolean {
139+
// Check if delivery events are enabled for the channel
115140
if (!channel.config.deliveryEventsEnabled) {
116-
logger.w { "[getUndeliveredMessage] Delivery events disabled for channel ${channel.cid}" }
117-
return null
118-
}
119-
val currentUser = getCurrentUser() ?: run {
120-
logger.w { "[getUndeliveredMessage] Cannot get undelivered message: current user is null" }
121-
return null
141+
logger.w { "[canMarkMessageAsDelivered] Delivery events disabled for channel ${channel.cid}" }
142+
return false
122143
}
123-
val userRead = channel.userRead(currentUser.id) ?: return null
124-
// Get the last non-deleted message in the channel
125-
val lastMessage = channel.lastMessage ?: return null
126-
val createdAt = lastMessage.getCreatedAtOrThrow()
127-
// Check if the last message is already marked as read
128-
if (createdAt <= userRead.lastRead) return null
129-
// Check if the last message is already marked as delivered
130-
if (createdAt <= (userRead.lastDeliveredAt ?: NEVER)) return null
131144

132-
return lastMessage
133-
}
134-
135-
private fun shouldSendDeliveryReceipt(currentUserId: UserId, message: Message): Boolean {
136-
// Don't send delivery receipts for messages sent by the current user
137-
if (message.user.id == currentUserId) {
145+
// Do not send delivery receipts for messages sent by the current user
146+
if (message.user.id == currentUser.id) {
147+
logger.w {
148+
"[canMarkMessageAsDelivered] Message ${message.id} was sent by the current user ${currentUser.id}"
149+
}
138150
return false
139151
}
140152

141-
// Don't send delivery receipts for system messages
153+
// Do not send delivery receipts for system messages
142154
if (message.type == MessageType.SYSTEM) {
155+
logger.w { "[canMarkMessageAsDelivered] Message ${message.id} is a system message" }
143156
return false
144157
}
145158

146-
// Don't send delivery receipts for deleted messages
159+
// Do not send delivery receipts for deleted messages
147160
if (message.isDeleted()) {
161+
logger.w { "[canMarkMessageAsDelivered] Message ${message.id} is deleted" }
162+
return false
163+
}
164+
165+
val userRead = channel.userRead(currentUser.id) ?: run {
166+
logger.w {
167+
"[canMarkMessageAsDelivered] No read state for user ${currentUser.id} in channel ${channel.cid}"
168+
}
169+
return false
170+
}
171+
172+
val createdAt = message.getCreatedAtOrThrow()
173+
174+
// Check if the last message is already marked as read
175+
if (createdAt <= userRead.lastRead) {
176+
logger.w { "[canMarkMessageAsDelivered] Message ${message.id} is already marked as read" }
177+
return false
178+
}
179+
180+
// Check if the last message is already marked as delivered
181+
if (createdAt <= (userRead.lastDeliveredAt ?: NEVER)) {
182+
logger.w { "[canMarkMessageAsDelivered] Message ${message.id} is already marked as delivered" }
148183
return false
149184
}
150185

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/notifications/ChatNotificationsImplTest.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,11 @@ internal class ChatNotificationsImplTest {
6666

6767
sut.onPushMessage(pushMessage)
6868

69-
val messages = listOf(
70-
Message(
71-
id = pushMessage.messageId,
72-
cid = "${pushMessage.channelType}:${pushMessage.channelId}",
73-
),
69+
val message = Message(
70+
id = pushMessage.messageId,
71+
cid = "${pushMessage.channelType}:${pushMessage.channelId}",
7472
)
75-
fixture.verifyMarkMessagesAsDeliveredCalled(messages)
73+
fixture.verifyMarkMessageAsDeliveredCalled(message)
7674
}
7775

7876
@Test
@@ -131,8 +129,8 @@ internal class ChatNotificationsImplTest {
131129
notificationConfig = config
132130
}
133131

134-
fun verifyMarkMessagesAsDeliveredCalled(messages: List<Message>) {
135-
verify(mockMessageReceiptManager).markMessagesAsDelivered(messages)
132+
fun verifyMarkMessageAsDeliveredCalled(message: Message) {
133+
verify(mockMessageReceiptManager).markMessageAsDelivered(message)
136134
}
137135

138136
fun get(): ChatNotificationsImpl {

0 commit comments

Comments
 (0)