Skip to content

Commit 0ef7cb3

Browse files
committed
Mark messages as delivered on push notification
1 parent 8656682 commit 0ef7cb3

File tree

4 files changed

+197
-7
lines changed

4 files changed

+197
-7
lines changed

stream-chat-android-client/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ dependencies {
115115
testImplementation(libs.stream.result)
116116
testImplementation(libs.androidx.test.junit)
117117
testImplementation(libs.androidx.lifecycle.runtime.testing)
118+
testImplementation(libs.androidx.work.testing)
118119
testImplementation(libs.junit.jupiter.api)
119120
testImplementation(libs.junit.jupiter.params)
120121
testImplementation(libs.turbine)

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
2828
import io.getstream.chat.android.client.notifications.handler.NotificationHandler
2929
import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider
3030
import io.getstream.chat.android.models.Device
31+
import io.getstream.chat.android.models.Message
3132
import io.getstream.chat.android.models.PushMessage
3233
import io.getstream.chat.android.models.User
3334
import io.getstream.log.taggedLogger
@@ -38,7 +39,12 @@ internal interface ChatNotifications {
3839
fun onSetUser(user: User)
3940
fun setDevice(device: Device)
4041
suspend fun deleteDevice()
41-
fun onPushMessage(message: PushMessage, pushNotificationReceivedListener: PushNotificationReceivedListener)
42+
fun onPushMessage(
43+
pushMessage: PushMessage,
44+
pushNotificationReceivedListener: PushNotificationReceivedListener =
45+
PushNotificationReceivedListener { _, _ -> },
46+
)
47+
4248
fun onChatEvent(event: ChatEvent)
4349
suspend fun onLogout()
4450
fun displayNotification(notification: ChatNotification)
@@ -51,6 +57,7 @@ internal class ChatNotificationsImpl(
5157
private val notificationConfig: NotificationConfig,
5258
private val context: Context,
5359
private val scope: CoroutineScope = CoroutineScope(DispatcherProvider.IO),
60+
private val chatClient: ChatClient = ChatClient.instance(),
5461
) : ChatNotifications {
5562
private val logger by taggedLogger("Chat:Notifications")
5663

@@ -95,15 +102,21 @@ internal class ChatNotificationsImpl(
95102
}
96103

97104
override fun onPushMessage(
98-
message: PushMessage,
105+
pushMessage: PushMessage,
99106
pushNotificationReceivedListener: PushNotificationReceivedListener,
100107
) {
101-
logger.i { "[onReceivePushMessage] message: $message" }
108+
logger.i { "[onPushMessage] message: $pushMessage" }
109+
110+
pushNotificationReceivedListener.onPushNotificationReceived(pushMessage.channelType, pushMessage.channelId)
102111

103-
pushNotificationReceivedListener.onPushNotificationReceived(message.channelType, message.channelId)
112+
val message = Message(
113+
id = pushMessage.messageId,
114+
cid = "${pushMessage.channelType}:${pushMessage.channelId}",
115+
)
116+
chatClient.messageReceiptManager.markMessagesAsDelivered(messages = listOf(message))
104117

105-
if (notificationConfig.shouldShowNotificationOnPush() && !handler.onPushMessage(message)) {
106-
handlePushMessage(message)
118+
if (notificationConfig.shouldShowNotificationOnPush() && !handler.onPushMessage(pushMessage)) {
119+
handlePushMessage(pushMessage)
107120
}
108121
}
109122

@@ -193,6 +206,7 @@ internal class ChatNotificationsImpl(
193206
handler.showNotification(notification)
194207
}
195208
}
209+
196210
else -> handler.showNotification(notification)
197211
}
198212
}
@@ -210,7 +224,7 @@ internal object NoOpChatNotifications : ChatNotifications {
210224
override fun setDevice(device: Device) = Unit
211225
override suspend fun deleteDevice() = Unit
212226
override fun onPushMessage(
213-
message: PushMessage,
227+
pushMessage: PushMessage,
214228
pushNotificationReceivedListener: PushNotificationReceivedListener,
215229
) = Unit
216230

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import io.getstream.chat.android.models.ConnectionState
9191
import io.getstream.chat.android.models.FilterObject
9292
import io.getstream.chat.android.models.Filters
9393
import io.getstream.chat.android.models.InitializationState
94+
import io.getstream.chat.android.models.PushMessage
9495
import io.getstream.chat.android.models.User
9596
import io.getstream.chat.android.models.querysort.QuerySortByField
9697
import io.getstream.chat.android.models.querysort.QuerySorter
@@ -1279,6 +1280,22 @@ internal object Mother {
12791280
)
12801281
}
12811282

1283+
internal fun randomPushMessage(
1284+
messageId: String = randomString(),
1285+
channelId: String = randomString(),
1286+
channelType: String = randomString(),
1287+
getstream: Map<String, Any?> = emptyMap(),
1288+
extraData: Map<String, Any?> = emptyMap(),
1289+
metadata: Map<String, Any?> = emptyMap(),
1290+
) = PushMessage(
1291+
messageId = messageId,
1292+
channelId = channelId,
1293+
channelType = channelType,
1294+
getstream = getstream,
1295+
extraData = extraData,
1296+
metadata = metadata,
1297+
)
1298+
12821299
internal fun randomMessageReceipt(
12831300
messageId: String = randomString(),
12841301
type: String = randomString(),
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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.notifications
18+
19+
import android.content.Context
20+
import androidx.test.core.app.ApplicationProvider
21+
import androidx.test.ext.junit.runners.AndroidJUnit4
22+
import androidx.work.WorkInfo
23+
import androidx.work.WorkManager
24+
import androidx.work.testing.WorkManagerTestInitHelper
25+
import io.getstream.chat.android.client.ChatClient
26+
import io.getstream.chat.android.client.notifications.handler.NotificationConfig
27+
import io.getstream.chat.android.client.notifications.handler.NotificationHandler
28+
import io.getstream.chat.android.client.randomPushMessage
29+
import io.getstream.chat.android.client.receipts.MessageReceiptManager
30+
import io.getstream.chat.android.models.Message
31+
import io.getstream.chat.android.models.PushMessage
32+
import org.junit.Test
33+
import org.junit.jupiter.api.Assertions.assertNotNull
34+
import org.junit.jupiter.api.Assertions.assertNull
35+
import org.junit.runner.RunWith
36+
import org.mockito.kotlin.doReturn
37+
import org.mockito.kotlin.mock
38+
import org.mockito.kotlin.verify
39+
import org.mockito.kotlin.whenever
40+
import org.robolectric.annotation.Config
41+
42+
@RunWith(AndroidJUnit4::class)
43+
@Config(sdk = [33])
44+
internal class ChatNotificationsImplTest {
45+
46+
@Test
47+
fun `onPushMessage calls onPushNotificationReceived`() {
48+
val pushMessage = randomPushMessage()
49+
val mockListener = mock<PushNotificationReceivedListener>()
50+
val fixture = Fixture()
51+
val sut = fixture.get()
52+
53+
sut.onPushMessage(pushMessage, mockListener)
54+
55+
verify(mockListener).onPushNotificationReceived(
56+
channelType = pushMessage.channelType,
57+
channelId = pushMessage.channelId,
58+
)
59+
}
60+
61+
@Test
62+
fun `onPushMessage marks message as delivered`() {
63+
val pushMessage = randomPushMessage()
64+
val fixture = Fixture()
65+
val sut = fixture.get()
66+
67+
sut.onPushMessage(pushMessage)
68+
69+
val messages = listOf(
70+
Message(
71+
id = pushMessage.messageId,
72+
cid = "${pushMessage.channelType}:${pushMessage.channelId}",
73+
),
74+
)
75+
fixture.verifyMarkMessagesAsDeliveredCalled(messages)
76+
}
77+
78+
@Test
79+
fun `onPushMessage schedules work when shouldShowNotificationOnPush is true and handler does not handle message`() {
80+
val pushMessage = randomPushMessage()
81+
val fixture = Fixture()
82+
val sut = fixture.get()
83+
84+
sut.onPushMessage(pushMessage)
85+
86+
val workInfos = getLoadNotificationDataWorkerInfos()
87+
assertNotNull(workInfos.firstOrNull())
88+
}
89+
90+
@Test
91+
fun `onPushMessage does not schedule work when shouldShowNotificationOnPush is false`() {
92+
val pushMessage = randomPushMessage()
93+
val fixture = Fixture()
94+
.givenNotificationConfig(config = NotificationConfig(shouldShowNotificationOnPush = { false }))
95+
val sut = fixture.get()
96+
97+
sut.onPushMessage(pushMessage)
98+
99+
val workInfos = getLoadNotificationDataWorkerInfos()
100+
assertNull(workInfos.firstOrNull())
101+
}
102+
103+
@Test
104+
fun `onPushMessage does not schedule work when handler handles message`() {
105+
val pushMessage = randomPushMessage()
106+
val fixture = Fixture()
107+
.givenOnPushMessageHandled(pushMessage = pushMessage, handled = true)
108+
val sut = fixture.get()
109+
110+
sut.onPushMessage(pushMessage)
111+
112+
val workInfos = getLoadNotificationDataWorkerInfos()
113+
assertNull(workInfos.firstOrNull())
114+
}
115+
116+
private class Fixture {
117+
118+
private var mockNotificationHandler = mock<NotificationHandler>()
119+
private var notificationConfig = NotificationConfig()
120+
private val mockMessageReceiptManager = mock<MessageReceiptManager>()
121+
122+
private val mockChatClient = mock<ChatClient> {
123+
on { messageReceiptManager } doReturn mockMessageReceiptManager
124+
}
125+
126+
fun givenOnPushMessageHandled(pushMessage: PushMessage, handled: Boolean) = apply {
127+
whenever(mockNotificationHandler.onPushMessage(pushMessage)) doReturn handled
128+
}
129+
130+
fun givenNotificationConfig(config: NotificationConfig) = apply {
131+
notificationConfig = config
132+
}
133+
134+
fun verifyMarkMessagesAsDeliveredCalled(messages: List<Message>) {
135+
verify(mockMessageReceiptManager).markMessagesAsDelivered(messages)
136+
}
137+
138+
fun get(): ChatNotificationsImpl {
139+
val context = ApplicationProvider.getApplicationContext<Context>()
140+
WorkManagerTestInitHelper.initializeTestWorkManager(context)
141+
142+
return ChatNotificationsImpl(
143+
handler = mockNotificationHandler,
144+
notificationConfig = notificationConfig,
145+
context = context,
146+
scope = mock(),
147+
chatClient = mockChatClient,
148+
)
149+
}
150+
}
151+
}
152+
153+
private fun getLoadNotificationDataWorkerInfos(): List<WorkInfo> {
154+
val workInfos = WorkManager
155+
.getInstance(ApplicationProvider.getApplicationContext())
156+
.getWorkInfosByTag(LoadNotificationDataWorker::class.qualifiedName!!).get()
157+
return workInfos
158+
}

0 commit comments

Comments
 (0)