Skip to content

Commit 5a927f9

Browse files
authored
Introduce NotificationActionsFactory (#6037)
* Introduce NotificationActionsFactory. * Update CHANGELOG.md.
1 parent 8e39f76 commit 5a927f9

File tree

6 files changed

+295
-17
lines changed

6 files changed

+295
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Add `ChatClient.markThreadUnread(String, String, String)` for marking a thread as unread. [#6027](https://github.com/GetStream/stream-chat-android/pull/6027)
2222
- Add `ChannelClient.markUnread(Date)` for marking a channel as unread from a given timestamp. [#6027](https://github.com/GetStream/stream-chat-android/pull/6027)
2323
- Add `ChannelClient.markThreadUnread(String)` for marking a thread as unread. [#6027](https://github.com/GetStream/stream-chat-android/pull/6027)
24+
- Add `NotificationActionsFactory` for building and customizing the default notification actions. [#6037](https://github.com/GetStream/stream-chat-android/pull/6037)
2425

2526
### ⚠️ Changed
2627
- Deprecate `ChatClient.markThreadUnread(String, String, String, String)` because marking a thread as unread from a given message is currently not supported. [#6027](https://github.com/GetStream/stream-chat-android/pull/6027)

stream-chat-android-client/api/stream-chat-android-client.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2874,6 +2874,14 @@ public final class io/getstream/chat/android/client/notifications/handler/ChatNo
28742874
public fun toString ()Ljava/lang/String;
28752875
}
28762876

2877+
public final class io/getstream/chat/android/client/notifications/handler/NotificationActionsFactory {
2878+
public static final field INSTANCE Lio/getstream/chat/android/client/notifications/handler/NotificationActionsFactory;
2879+
public final fun createMarkReadAction (Landroid/content/Context;ILio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/Message;ILjava/lang/String;Landroid/app/PendingIntent;)Landroidx/core/app/NotificationCompat$Action;
2880+
public static synthetic fun createMarkReadAction$default (Lio/getstream/chat/android/client/notifications/handler/NotificationActionsFactory;Landroid/content/Context;ILio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/Message;ILjava/lang/String;Landroid/app/PendingIntent;ILjava/lang/Object;)Landroidx/core/app/NotificationCompat$Action;
2881+
public final fun createReplyAction (Landroid/content/Context;ILio/getstream/chat/android/models/Channel;ILjava/lang/String;Ljava/lang/String;Landroid/app/PendingIntent;)Landroidx/core/app/NotificationCompat$Action;
2882+
public static synthetic fun createReplyAction$default (Lio/getstream/chat/android/client/notifications/handler/NotificationActionsFactory;Landroid/content/Context;ILio/getstream/chat/android/models/Channel;ILjava/lang/String;Ljava/lang/String;Landroid/app/PendingIntent;ILjava/lang/Object;)Landroidx/core/app/NotificationCompat$Action;
2883+
}
2884+
28772885
public final class io/getstream/chat/android/client/notifications/handler/NotificationConfig {
28782886
public fun <init> ()V
28792887
public fun <init> (Z)V
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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.handler
18+
19+
import android.app.PendingIntent
20+
import android.content.Context
21+
import androidx.annotation.DrawableRes
22+
import androidx.core.app.NotificationCompat
23+
import io.getstream.chat.android.client.R
24+
import io.getstream.chat.android.client.receivers.NotificationMessageReceiver
25+
import io.getstream.chat.android.models.Channel
26+
import io.getstream.chat.android.models.Message
27+
28+
/**
29+
* Factory for creating [NotificationCompat.Action] instances used in chat push notifications.
30+
*
31+
* This factory provides convenient methods to create common notification actions such as
32+
* "Mark as Read" and "Reply" with sensible defaults, while allowing full customization.
33+
*
34+
* @see NotificationCompat.Action
35+
*/
36+
public object NotificationActionsFactory {
37+
38+
/**
39+
* Creates a "Mark as Read" notification action.
40+
*
41+
* When triggered, this action marks the specified message as read in the channel.
42+
*
43+
* @param context The [Context] used to access resources and create the pending intent.
44+
* @param notificationId The unique identifier for the notification. Used for the pending intent request code.
45+
* @param channel The [Channel] containing the message to mark as read.
46+
* @param message The [Message] to mark as read when the action is triggered.
47+
* @param icon The drawable resource ID for the action icon. Defaults to [android.R.drawable.ic_menu_view].
48+
* @param title The label text displayed on the action button. Defaults to the localized "Mark as read" string.
49+
* @param pendingIntent The [PendingIntent] to execute when the action is triggered. Defaults to a broadcast intent
50+
* which will mark the channel as read.
51+
* @return A [NotificationCompat.Action] configured for marking messages as read.
52+
*/
53+
public fun createMarkReadAction(
54+
context: Context,
55+
notificationId: Int,
56+
channel: Channel,
57+
message: Message,
58+
@DrawableRes icon: Int = android.R.drawable.ic_menu_view,
59+
title: String = context.getString(R.string.stream_chat_notification_read),
60+
pendingIntent: PendingIntent = createMarkReadPendingIntent(context, notificationId, channel, message),
61+
): NotificationCompat.Action {
62+
return NotificationMessageReceiver.createMarkReadAction(
63+
context = context,
64+
notificationId = notificationId,
65+
channel = channel,
66+
message = message,
67+
icon = icon,
68+
title = title,
69+
pendingIntent = pendingIntent,
70+
)
71+
}
72+
73+
/**
74+
* Creates a "Reply" notification action with inline reply support.
75+
*
76+
* This action displays an inline text input field (on supported devices) allowing users
77+
* to reply directly from the notification without opening the app.
78+
*
79+
* @param context The [Context] used to access resources and create the pending intent.
80+
* @param notificationId The unique identifier for the notification. Used for the pending intent request code.
81+
* @param channel The [Channel] to send the reply message to.
82+
* @param icon The drawable resource ID for the action icon. Defaults to [android.R.drawable.ic_menu_send].
83+
* @param title The label text displayed on the action button. Defaults to the localized "Reply" string.
84+
* @param hint The placeholder text shown in the inline reply input field. Defaults to the localized
85+
* "Type a message" string.
86+
* @param pendingIntent The [PendingIntent] to execute when the reply is submitted. Defaults to a broadcast intent
87+
* which sends a message in the channel.
88+
* @return A [NotificationCompat.Action] configured for inline reply.
89+
*/
90+
public fun createReplyAction(
91+
context: Context,
92+
notificationId: Int,
93+
channel: Channel,
94+
@DrawableRes icon: Int = android.R.drawable.ic_menu_send,
95+
title: String = context.getString(R.string.stream_chat_notification_reply),
96+
hint: String = context.getString(R.string.stream_chat_notification_type_hint),
97+
pendingIntent: PendingIntent = createReplyPendingIntent(context, notificationId, channel),
98+
): NotificationCompat.Action {
99+
return NotificationMessageReceiver.createReplyAction(
100+
context = context,
101+
notificationId = notificationId,
102+
channel = channel,
103+
icon = icon,
104+
title = title,
105+
hint = hint,
106+
pendingIntent = pendingIntent,
107+
)
108+
}
109+
110+
private fun createMarkReadPendingIntent(
111+
context: Context,
112+
notificationId: Int,
113+
channel: Channel,
114+
message: Message,
115+
): PendingIntent {
116+
return NotificationMessageReceiver.createMarkReadPendingIntent(
117+
context = context,
118+
notificationId = notificationId,
119+
channel = channel,
120+
message = message,
121+
)
122+
}
123+
124+
private fun createReplyPendingIntent(
125+
context: Context,
126+
notificationId: Int,
127+
channel: Channel,
128+
): PendingIntent {
129+
return NotificationMessageReceiver.createReplyPendingIntent(
130+
context = context,
131+
notificationId = notificationId,
132+
channel = channel,
133+
)
134+
}
135+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import androidx.core.graphics.drawable.IconCompat
2929
import io.getstream.android.push.permissions.DefaultNotificationPermissionHandler
3030
import io.getstream.android.push.permissions.NotificationPermissionHandler
3131
import io.getstream.chat.android.client.R
32-
import io.getstream.chat.android.client.receivers.NotificationMessageReceiver
3332
import io.getstream.chat.android.models.Channel
3433
import io.getstream.chat.android.models.Message
3534
import io.getstream.chat.android.models.PushMessage
@@ -54,6 +53,7 @@ public object NotificationHandlerFactory {
5453
* @param permissionHandler Handles [android.Manifest.permission.POST_NOTIFICATIONS] permission lifecycle.
5554
* @param notificationTextFormatter Lambda expression used to formats the text of the notification.
5655
* @param actionsProvider Lambda expression used to provide actions for the notification.
56+
* See [NotificationActionsFactory] for customizing the default actions.
5757
* @param notificationBuilderTransformer Lambda expression used to transform the [NotificationCompat.Builder]
5858
* before building the notification.
5959
* @param onPushMessage Lambda expression called when a new push message is received. Return true if the
@@ -108,8 +108,8 @@ public object NotificationHandlerFactory {
108108
): (notificationId: Int, channel: Channel, message: Message) -> List<NotificationCompat.Action> =
109109
{ notificationId, channel, message ->
110110
listOf(
111-
NotificationMessageReceiver.createReadAction(context, notificationId, channel, message),
112-
NotificationMessageReceiver.createReplyAction(context, notificationId, channel),
111+
NotificationActionsFactory.createMarkReadAction(context, notificationId, channel, message),
112+
NotificationActionsFactory.createReplyAction(context, notificationId, channel),
113113
)
114114
}
115115

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import android.content.BroadcastReceiver
2121
import android.content.Context
2222
import android.content.Intent
2323
import android.os.Build
24+
import androidx.annotation.DrawableRes
2425
import androidx.core.app.NotificationCompat
2526
import androidx.core.app.RemoteInput
2627
import io.getstream.chat.android.client.ChatClient
@@ -55,7 +56,7 @@ internal class NotificationMessageReceiver : BroadcastReceiver() {
5556
PendingIntent.FLAG_UPDATE_CURRENT
5657
}
5758

58-
private fun createReplyPendingIntent(
59+
internal fun createReplyPendingIntent(
5960
context: Context,
6061
notificationId: Int,
6162
channel: Channel,
@@ -67,7 +68,7 @@ internal class NotificationMessageReceiver : BroadcastReceiver() {
6768
MUTABLE_PENDING_INTENT_FLAGS,
6869
)
6970

70-
private fun createReadPendingIntent(
71+
internal fun createMarkReadPendingIntent(
7172
context: Context,
7273
notificationId: Int,
7374
channel: Channel,
@@ -92,33 +93,32 @@ internal class NotificationMessageReceiver : BroadcastReceiver() {
9293
IMMUTABLE_PENDING_INTENT_FLAGS,
9394
)
9495

95-
internal fun createReadAction(
96+
internal fun createMarkReadAction(
9697
context: Context,
9798
notificationId: Int,
9899
channel: Channel,
99100
message: Message,
101+
@DrawableRes icon: Int = android.R.drawable.ic_menu_view,
102+
title: String = context.getString(R.string.stream_chat_notification_read),
103+
pendingIntent: PendingIntent = createMarkReadPendingIntent(context, notificationId, channel, message),
100104
): NotificationCompat.Action {
101-
return NotificationCompat.Action.Builder(
102-
android.R.drawable.ic_menu_view,
103-
context.getString(R.string.stream_chat_notification_read),
104-
createReadPendingIntent(context, notificationId, channel, message),
105-
).build()
105+
return NotificationCompat.Action.Builder(icon, title, pendingIntent).build()
106106
}
107107

108108
internal fun createReplyAction(
109109
context: Context,
110110
notificationId: Int,
111111
channel: Channel,
112+
@DrawableRes icon: Int = android.R.drawable.ic_menu_send,
113+
title: String = context.getString(R.string.stream_chat_notification_reply),
114+
hint: String = context.getString(R.string.stream_chat_notification_type_hint),
115+
pendingIntent: PendingIntent = createReplyPendingIntent(context, notificationId, channel),
112116
): NotificationCompat.Action {
113117
val remoteInput =
114118
RemoteInput.Builder(KEY_TEXT_REPLY)
115-
.setLabel(context.getString(R.string.stream_chat_notification_type_hint))
119+
.setLabel(hint)
116120
.build()
117-
return NotificationCompat.Action.Builder(
118-
android.R.drawable.ic_menu_send,
119-
context.getString(R.string.stream_chat_notification_reply),
120-
createReplyPendingIntent(context, notificationId, channel),
121-
)
121+
return NotificationCompat.Action.Builder(icon, title, pendingIntent)
122122
.addRemoteInput(remoteInput)
123123
.setAllowGeneratedReplies(true)
124124
.build()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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.handler
18+
19+
import android.app.PendingIntent
20+
import android.content.Context
21+
import androidx.test.core.app.ApplicationProvider
22+
import androidx.test.ext.junit.runners.AndroidJUnit4
23+
import io.getstream.chat.android.client.R
24+
import io.getstream.chat.android.randomChannel
25+
import io.getstream.chat.android.randomInt
26+
import io.getstream.chat.android.randomMessage
27+
import io.getstream.chat.android.randomString
28+
import org.junit.Assert.assertEquals
29+
import org.junit.Assert.assertNotNull
30+
import org.junit.Assert.assertTrue
31+
import org.junit.Before
32+
import org.junit.Test
33+
import org.junit.runner.RunWith
34+
import org.mockito.kotlin.mock
35+
import org.robolectric.annotation.Config
36+
37+
@RunWith(AndroidJUnit4::class)
38+
@Config(sdk = [33])
39+
internal class NotificationActionsFactoryTest {
40+
41+
private lateinit var context: Context
42+
43+
@Before
44+
fun setUp() {
45+
context = ApplicationProvider.getApplicationContext()
46+
}
47+
48+
@Test
49+
fun `createMarkReadAction should return action with default values`() {
50+
val notificationId = randomInt()
51+
val channel = randomChannel()
52+
val message = randomMessage()
53+
54+
val action = NotificationActionsFactory.createMarkReadAction(
55+
context = context,
56+
notificationId = notificationId,
57+
channel = channel,
58+
message = message,
59+
)
60+
61+
assertNotNull(action)
62+
assertEquals(android.R.drawable.ic_menu_view, action.iconCompat?.resId)
63+
assertEquals(context.getString(R.string.stream_chat_notification_read), action.title.toString())
64+
assertNotNull(action.actionIntent)
65+
}
66+
67+
@Test
68+
fun `createMarkReadAction should return action with custom values`() {
69+
val notificationId = randomInt()
70+
val channel = randomChannel()
71+
val message = randomMessage()
72+
val customIcon = android.R.drawable.ic_menu_close_clear_cancel
73+
val customTitle = randomString()
74+
val customPendingIntent: PendingIntent = mock()
75+
76+
val action = NotificationActionsFactory.createMarkReadAction(
77+
context = context,
78+
notificationId = notificationId,
79+
channel = channel,
80+
message = message,
81+
icon = customIcon,
82+
title = customTitle,
83+
pendingIntent = customPendingIntent,
84+
)
85+
86+
assertNotNull(action)
87+
assertEquals(customIcon, action.iconCompat?.resId)
88+
assertEquals(customTitle, action.title.toString())
89+
assertEquals(customPendingIntent, action.actionIntent)
90+
}
91+
92+
@Test
93+
fun `createReplyAction should return action with default values`() {
94+
val notificationId = randomInt()
95+
val channel = randomChannel()
96+
97+
val action = NotificationActionsFactory.createReplyAction(
98+
context = context,
99+
notificationId = notificationId,
100+
channel = channel,
101+
)
102+
103+
assertNotNull(action)
104+
assertEquals(android.R.drawable.ic_menu_send, action.iconCompat?.resId)
105+
assertEquals(context.getString(R.string.stream_chat_notification_reply), action.title.toString())
106+
assertNotNull(action.actionIntent)
107+
assertNotNull(action.remoteInputs)
108+
assertTrue(action.remoteInputs!!.isNotEmpty())
109+
assertTrue(action.allowGeneratedReplies)
110+
}
111+
112+
@Test
113+
fun `createReplyAction should return action with custom values`() {
114+
val notificationId = randomInt()
115+
val channel = randomChannel()
116+
val customIcon = android.R.drawable.ic_menu_edit
117+
val customTitle = randomString()
118+
val customPendingIntent: PendingIntent = mock()
119+
120+
val action = NotificationActionsFactory.createReplyAction(
121+
context = context,
122+
notificationId = notificationId,
123+
channel = channel,
124+
icon = customIcon,
125+
title = customTitle,
126+
pendingIntent = customPendingIntent,
127+
)
128+
129+
assertNotNull(action)
130+
assertEquals(customIcon, action.iconCompat?.resId)
131+
assertEquals(customTitle, action.title.toString())
132+
assertEquals(customPendingIntent, action.actionIntent)
133+
}
134+
}

0 commit comments

Comments
 (0)