Skip to content

Commit d1d9a5c

Browse files
committed
Introduce a initial DeliveryReceiptsManager to handle message delivery receipts
1 parent 36431fd commit d1d9a5c

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.getstream.chat.android.client.api2.model.dto.AttachmentDto
2424
import io.getstream.chat.android.client.api2.model.dto.ChannelInfoDto
2525
import io.getstream.chat.android.client.api2.model.dto.CommandDto
2626
import io.getstream.chat.android.client.api2.model.dto.ConfigDto
27+
import io.getstream.chat.android.client.api2.model.dto.DeliveryReceiptsDto
2728
import io.getstream.chat.android.client.api2.model.dto.DeviceDto
2829
import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelDto
2930
import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelMuteDto
@@ -526,6 +527,8 @@ internal class DomainMapping(
526527
lastRead = last_read,
527528
unreadMessages = unread_messages,
528529
lastReadMessageId = last_read_message_id,
530+
lastDeliveredAt = last_delivered_at,
531+
lastDeliveredMessageId = last_delivered_message_id,
529532
)
530533

531534
/**
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.client.receipts
18+
19+
import io.getstream.chat.android.client.ChatClient
20+
import io.getstream.chat.android.client.utils.message.isDeleted
21+
import io.getstream.chat.android.models.Message
22+
import io.getstream.chat.android.models.MessageType
23+
import io.getstream.chat.android.models.User
24+
import io.getstream.chat.android.models.UserId
25+
import io.getstream.log.taggedLogger
26+
27+
internal class DeliveryReceiptsManager(
28+
private val chatClient: ChatClient,
29+
private val getCurrentUser: () -> User?,
30+
) {
31+
32+
private val logger by taggedLogger("MessageDeliveryReceiptsManager")
33+
34+
fun markMessagesAsDelivered(messages: List<Message>) {
35+
logger.d { "[markMessagesAsDelivered] Preparing to send delivery receipts for ${messages.size} messages" }
36+
37+
val currentUser = requireNotNull(getCurrentUser()) {
38+
"Cannot send delivery receipts: current user is null"
39+
}
40+
41+
// Check if delivery receipts are enabled for the current user
42+
if (!currentUser.isDeliveryReceiptsEnabled()) {
43+
logger.w { "[markMessagesAsDelivered] Delivery receipts disabled for user ${currentUser.id}" }
44+
return
45+
}
46+
47+
val messagesToMark = messages.filter { message ->
48+
shouldSendDeliveryReceipt(currentUserId = currentUser.id, message = message)
49+
}
50+
if (messagesToMark.size != messages.size) {
51+
logger.d {
52+
"[markMessagesAsDelivered] " +
53+
"Skipping delivery receipts for ${messages.size - messagesToMark.size} messages"
54+
}
55+
}
56+
57+
if (messagesToMark.isEmpty()) {
58+
logger.w { "[markMessagesAsDelivered] No receipts to send" }
59+
return
60+
}
61+
62+
logger.d { "[markMessagesAsDelivered] Sending ${messages.size} delivery receipts" }
63+
chatClient.markMessagesAsDelivered(messages)
64+
.execute()
65+
}
66+
67+
private fun shouldSendDeliveryReceipt(currentUserId: UserId, message: Message): Boolean {
68+
// Don't send delivery receipts for messages sent by the current user
69+
if (message.user.id == currentUserId) {
70+
return false
71+
}
72+
73+
// Don't send delivery receipts for system messages
74+
if (message.type == MessageType.SYSTEM) {
75+
return false
76+
}
77+
78+
// Don't send delivery receipts for deleted messages
79+
if (message.isDeleted()) {
80+
return false
81+
}
82+
83+
return true
84+
}
85+
}
86+
87+
private fun User.isDeliveryReceiptsEnabled(): Boolean =
88+
privacySettings?.deliveryReceipts?.enabled ?: false
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.client.receipts
18+
19+
import io.getstream.chat.android.DeliveryReceipts
20+
import io.getstream.chat.android.PrivacySettings
21+
import io.getstream.chat.android.client.ChatClient
22+
import io.getstream.chat.android.models.Message
23+
import io.getstream.chat.android.models.User
24+
import io.getstream.chat.android.randomMessage
25+
import io.getstream.chat.android.randomMessageList
26+
import io.getstream.chat.android.randomUser
27+
import io.getstream.chat.android.test.asCall
28+
import io.getstream.result.Error
29+
import org.junit.Test
30+
import org.junit.jupiter.api.assertThrows
31+
import org.mockito.kotlin.doReturn
32+
import org.mockito.kotlin.mock
33+
import org.mockito.kotlin.verify
34+
import org.mockito.kotlin.verifyNoInteractions
35+
import org.mockito.kotlin.whenever
36+
import java.util.Date
37+
38+
internal class DeliveryReceiptsManagerTest {
39+
40+
@Test
41+
fun `mark messages as delivered`() {
42+
val messages = randomMessageList(10) { randomMessage() }
43+
val fixture = Fixture().givenMarkMessagesAsDeliveredResult(messages)
44+
val sut = fixture.get()
45+
46+
sut.markMessagesAsDelivered(messages)
47+
48+
fixture.verifyMarkMessagesAsDelivered(messages)
49+
}
50+
51+
@Test
52+
fun `should not mark messages as delivered when current user is null`() {
53+
val messages = randomMessageList(10) { randomMessage() }
54+
val fixture = Fixture().givenCurrentUser(user = null)
55+
val sut = fixture.get()
56+
57+
assertThrows<IllegalArgumentException>(message = "Cannot send delivery receipts: current user is null") {
58+
sut.markMessagesAsDelivered(messages)
59+
}
60+
}
61+
62+
@Test
63+
fun `should skip mark messages as delivered when current user privacy settings are undefined`() {
64+
val currentUser = randomUser(privacySettings = null)
65+
val messages = randomMessageList(10) { randomMessage() }
66+
val fixture = Fixture().givenCurrentUser(currentUser)
67+
val sut = fixture.get()
68+
69+
sut.markMessagesAsDelivered(messages)
70+
71+
fixture.verifyNoInteractions()
72+
}
73+
74+
@Test
75+
fun `should skip mark messages as delivered when delivery receipts are disabled`() {
76+
val currentUser = randomUser(
77+
privacySettings = PrivacySettings(
78+
deliveryReceipts = DeliveryReceipts(enabled = false),
79+
),
80+
)
81+
val messages = randomMessageList(10) { randomMessage() }
82+
val fixture = Fixture().givenCurrentUser(currentUser)
83+
val sut = fixture.get()
84+
85+
sut.markMessagesAsDelivered(messages)
86+
87+
fixture.verifyNoInteractions()
88+
}
89+
90+
@Test
91+
fun `should skip mark messages as delivered with empty list`() {
92+
val messages = emptyList<Message>()
93+
val fixture = Fixture()
94+
val sut = fixture.get()
95+
96+
sut.markMessagesAsDelivered(messages)
97+
98+
fixture.verifyNoInteractions()
99+
}
100+
101+
@Test
102+
fun `should skip mark messages from the current user as delivered`() {
103+
val messages = randomMessageList(10) { randomMessage(user = CurrentUser) }
104+
val fixture = Fixture()
105+
val sut = fixture.get()
106+
107+
sut.markMessagesAsDelivered(messages)
108+
109+
fixture.verifyNoInteractions()
110+
}
111+
112+
@Test
113+
fun `should skip mark system messages as delivered`() {
114+
val messages = randomMessageList(10) { randomMessage(type = "system") }
115+
val fixture = Fixture()
116+
val sut = fixture.get()
117+
118+
sut.markMessagesAsDelivered(messages)
119+
120+
fixture.verifyNoInteractions()
121+
}
122+
123+
@Test
124+
fun `should skip mark deleted messages as delivered`() {
125+
val messages = randomMessageList(10) { randomMessage(deletedAt = Date()) }
126+
val fixture = Fixture()
127+
val sut = fixture.get()
128+
129+
sut.markMessagesAsDelivered(messages)
130+
131+
fixture.verifyNoInteractions()
132+
}
133+
134+
private class Fixture {
135+
private val mockChatClient = mock<ChatClient>()
136+
private var getCurrentUser: () -> User? = { CurrentUser }
137+
138+
fun givenCurrentUser(user: User?) = apply {
139+
getCurrentUser = { user }
140+
}
141+
142+
fun givenMarkMessagesAsDeliveredResult(messages: List<Message>, error: Error? = null) = apply {
143+
whenever(mockChatClient.markMessagesAsDelivered(messages)) doReturn
144+
(error?.asCall() ?: Unit.asCall())
145+
}
146+
147+
fun verifyNoInteractions() {
148+
verifyNoInteractions(mockChatClient)
149+
}
150+
151+
fun verifyMarkMessagesAsDelivered(messages: List<Message>) {
152+
verify(mockChatClient).markMessagesAsDelivered(messages)
153+
}
154+
155+
fun get() = DeliveryReceiptsManager(
156+
chatClient = mockChatClient,
157+
getCurrentUser = getCurrentUser,
158+
)
159+
}
160+
}
161+
162+
private val CurrentUser = randomUser(
163+
privacySettings = PrivacySettings(
164+
deliveryReceipts = DeliveryReceipts(enabled = true),
165+
),
166+
)

0 commit comments

Comments
 (0)