Skip to content

Commit 60372f9

Browse files
Merge pull request #9506 from rafaeltonholo/feat/9245/add-android-notification-action-creator
feat(notification): add notification action intent creator
2 parents 25cbad6 + 1867045 commit 60372f9

File tree

17 files changed

+508
-13
lines changed

17 files changed

+508
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package net.thunderbird.feature.notification.api.ui.action.icon
2+
3+
import net.thunderbird.feature.notification.api.R
4+
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
5+
6+
internal actual val NotificationActionIcons.Reply: NotificationIcon
7+
get() = NotificationIcon(
8+
systemNotificationIcon = R.drawable.ic_reply,
9+
)
10+
11+
internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon
12+
get() = NotificationIcon(
13+
systemNotificationIcon = R.drawable.ic_mark_email_read,
14+
)
15+
16+
internal actual val NotificationActionIcons.Delete: NotificationIcon
17+
get() = NotificationIcon(
18+
systemNotificationIcon = R.drawable.ic_delete,
19+
)
20+
21+
internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon
22+
get() = NotificationIcon(
23+
systemNotificationIcon = R.drawable.ic_report,
24+
)
25+
26+
internal actual val NotificationActionIcons.Archive: NotificationIcon
27+
get() = NotificationIcon(
28+
systemNotificationIcon = R.drawable.ic_archive,
29+
)
30+
31+
internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon
32+
get() = NotificationIcon(
33+
systemNotificationIcon = R.drawable.ic_settings,
34+
)
35+
36+
internal actual val NotificationActionIcons.Retry: NotificationIcon
37+
get() = NotificationIcon(
38+
systemNotificationIcon = R.drawable.ic_refresh,
39+
)
40+
41+
internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon
42+
get() = NotificationIcon(
43+
systemNotificationIcon = R.drawable.ic_settings,
44+
)

feature/notification/api/src/commonMain/composeResources/values/strings.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,15 @@
4545

4646
<string name="push_info_disable_push_action">Disable Push</string>
4747

48+
<string name="notification_action_reply">Reply</string>
49+
<string name="notification_action_mark_as_read">Mark Read</string>
50+
<string name="notification_action_mark_all_as_read">Mark All Read</string>
51+
<string name="notification_action_delete">Delete</string>
52+
<string name="notification_action_delete_all">Delete All</string>
53+
<string name="notification_action_archive">Archive</string>
54+
<string name="notification_action_archive_all">Archive All</string>
55+
<string name="notification_action_spam">Spam</string>
56+
<string name="notification_action_retry">Retry</string>
57+
<string name="notification_action_update_server_settings">Update Server Settings</string>
58+
4859
</resources>

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package net.thunderbird.feature.notification.api.content
33
import net.thunderbird.feature.notification.api.NotificationChannel
44
import net.thunderbird.feature.notification.api.NotificationSeverity
55
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
6+
import net.thunderbird.feature.notification.api.ui.action.icon.DisablePushAction
7+
import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons
68
import net.thunderbird.feature.notification.api.ui.icon.AlarmPermissionMissing
79
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
810
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons
@@ -168,7 +170,9 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
168170
* @return A set of [NotificationAction] instances.
169171
*/
170172
private suspend fun buildNotificationActions(): Set<NotificationAction> = setOf(
173+
NotificationAction.Tap,
171174
NotificationAction.CustomAction(
172-
message = getString(resource = Res.string.push_info_disable_push_action),
175+
title = getString(resource = Res.string.push_info_disable_push_action),
176+
icon = NotificationActionIcons.DisablePushAction,
173177
),
174178
)
Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,125 @@
11
package net.thunderbird.feature.notification.api.ui.action
22

3+
import net.thunderbird.feature.notification.api.content.SystemNotification
4+
import net.thunderbird.feature.notification.api.ui.action.icon.Archive
5+
import net.thunderbird.feature.notification.api.ui.action.icon.Delete
6+
import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsRead
7+
import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsSpam
8+
import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons
9+
import net.thunderbird.feature.notification.api.ui.action.icon.Reply
10+
import net.thunderbird.feature.notification.api.ui.action.icon.Retry
11+
import net.thunderbird.feature.notification.api.ui.action.icon.UpdateServerSettings
12+
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
13+
import net.thunderbird.feature.notification.resources.api.Res
14+
import net.thunderbird.feature.notification.resources.api.notification_action_archive
15+
import net.thunderbird.feature.notification.resources.api.notification_action_delete
16+
import net.thunderbird.feature.notification.resources.api.notification_action_mark_as_read
17+
import net.thunderbird.feature.notification.resources.api.notification_action_reply
18+
import net.thunderbird.feature.notification.resources.api.notification_action_retry
19+
import net.thunderbird.feature.notification.resources.api.notification_action_spam
20+
import net.thunderbird.feature.notification.resources.api.notification_action_update_server_settings
21+
import org.jetbrains.compose.resources.StringResource
22+
import org.jetbrains.compose.resources.getString
23+
324
/**
425
* Represents the various actions that can be performed on a notification.
526
*/
6-
sealed interface NotificationAction {
27+
sealed class NotificationAction {
28+
abstract val icon: NotificationIcon?
29+
protected abstract val titleResource: StringResource?
30+
31+
open suspend fun resolveTitle(): String? = titleResource?.let { getString(it) }
32+
33+
/**
34+
* Action to open the notification. This is the default action when a notification is tapped.
35+
*
36+
* This action typically does not have an icon or title displayed on the notification itself,
37+
* as it's implied by tapping the notification content.
38+
*
39+
* All [SystemNotification] will have this action implicitly, even if not specified in the
40+
* [SystemNotification.actions] set.
41+
*/
42+
data object Tap : NotificationAction() {
43+
override val icon: NotificationIcon? = null
44+
override val titleResource: StringResource? = null
45+
}
46+
747
/**
848
* Action to reply to the email message associated with the notification.
949
*/
10-
data object Reply : NotificationAction
50+
data object Reply : NotificationAction() {
51+
override val icon: NotificationIcon = NotificationActionIcons.Reply
52+
53+
override val titleResource: StringResource = Res.string.notification_action_reply
54+
}
1155

1256
/**
1357
* Action to mark the email message associated with the notification as read.
1458
*/
15-
data object MarkAsRead : NotificationAction
59+
data object MarkAsRead : NotificationAction() {
60+
override val icon: NotificationIcon = NotificationActionIcons.MarkAsRead
61+
62+
override val titleResource: StringResource = Res.string.notification_action_mark_as_read
63+
}
1664

1765
/**
1866
* Action to delete the email message associated with the notification.
1967
*/
20-
data object Delete : NotificationAction
68+
data object Delete : NotificationAction() {
69+
override val icon: NotificationIcon = NotificationActionIcons.Delete
70+
71+
override val titleResource: StringResource = Res.string.notification_action_delete
72+
}
2173

2274
/**
2375
* Action to mark the email message associated with the notification as spam.
2476
*/
25-
data object MarkAsSpam : NotificationAction
77+
data object MarkAsSpam : NotificationAction() {
78+
override val icon: NotificationIcon = NotificationActionIcons.MarkAsSpam
79+
80+
override val titleResource: StringResource = Res.string.notification_action_spam
81+
}
2682

2783
/**
2884
* Action to archive the email message associated with the notification.
2985
*/
30-
data object Archive : NotificationAction
86+
data object Archive : NotificationAction() {
87+
override val icon: NotificationIcon = NotificationActionIcons.Archive
88+
89+
override val titleResource: StringResource = Res.string.notification_action_archive
90+
}
3191

3292
/**
3393
* Action to prompt the user to update server settings, typically when authentication fails.
3494
*/
35-
data object UpdateServerSettings : NotificationAction
95+
data object UpdateServerSettings : NotificationAction() {
96+
override val icon: NotificationIcon = NotificationActionIcons.UpdateServerSettings
97+
98+
override val titleResource: StringResource = Res.string.notification_action_update_server_settings
99+
}
36100

37101
/**
38102
* Action to retry a failed operation, such as sending a message or fetching new messages.
39103
*/
40-
data object Retry : NotificationAction
104+
data object Retry : NotificationAction() {
105+
override val icon: NotificationIcon = NotificationActionIcons.Retry
106+
107+
override val titleResource: StringResource = Res.string.notification_action_retry
108+
}
41109

42110
/**
43111
* Represents a custom notification action.
44112
*
45113
* This can be used for actions that are not predefined and require a specific message.
46114
*
47-
* @property message The text to be displayed for this custom action.
115+
* @property title The text to be displayed for this custom action.
48116
*/
49-
data class CustomAction(val message: String) : NotificationAction
117+
data class CustomAction(
118+
val title: String,
119+
override val icon: NotificationIcon? = null,
120+
) : NotificationAction() {
121+
override val titleResource: StringResource get() = error("Custom Action must not supply a title resource")
122+
123+
override suspend fun resolveTitle(): String = title
124+
}
50125
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package net.thunderbird.feature.notification.api.ui.action.icon
2+
3+
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
4+
5+
internal object NotificationActionIcons
6+
7+
internal expect val NotificationActionIcons.Reply: NotificationIcon
8+
internal expect val NotificationActionIcons.MarkAsRead: NotificationIcon
9+
internal expect val NotificationActionIcons.Delete: NotificationIcon
10+
internal expect val NotificationActionIcons.MarkAsSpam: NotificationIcon
11+
internal expect val NotificationActionIcons.Archive: NotificationIcon
12+
internal expect val NotificationActionIcons.UpdateServerSettings: NotificationIcon
13+
internal expect val NotificationActionIcons.Retry: NotificationIcon
14+
internal expect val NotificationActionIcons.DisablePushAction: NotificationIcon

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
1212
* @property inAppNotificationIcon The icon to be used for in-app notifications.
1313
*/
1414
data class NotificationIcon(
15-
private val systemNotificationIcon: SystemNotificationIcon? = null,
16-
private val inAppNotificationIcon: ImageVector? = null,
15+
val systemNotificationIcon: SystemNotificationIcon? = null,
16+
val inAppNotificationIcon: ImageVector? = null,
1717
) {
1818

1919
init {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package net.thunderbird.feature.notification.api.ui.action.icon
2+
3+
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
4+
5+
private const val ERROR_MESSAGE = "Can't send notifications from a jvm library. Use android library or app instead."
6+
7+
internal actual val NotificationActionIcons.Reply: NotificationIcon get() = error(ERROR_MESSAGE)
8+
internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon get() = error(ERROR_MESSAGE)
9+
internal actual val NotificationActionIcons.Delete: NotificationIcon get() = error(ERROR_MESSAGE)
10+
internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon get() = error(ERROR_MESSAGE)
11+
internal actual val NotificationActionIcons.Archive: NotificationIcon get() = error(ERROR_MESSAGE)
12+
internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon get() = error(ERROR_MESSAGE)
13+
internal actual val NotificationActionIcons.Retry: NotificationIcon get() = error(ERROR_MESSAGE)
14+
internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon get() = error(ERROR_MESSAGE)

feature/notification/impl/build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ kotlin {
1010
implementation(projects.core.logging.api)
1111
implementation(projects.feature.notification.api)
1212
}
13+
commonTest.dependencies {
14+
implementation(projects.core.logging.testing)
15+
}
16+
androidUnitTest.dependencies {
17+
implementation(libs.androidx.test.core)
18+
implementation(libs.mockito.core)
19+
implementation(libs.mockito.kotlin)
20+
implementation(libs.robolectric)
21+
}
1322
}
1423
}
1524

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package net.thunderbird.feature.notification.impl.inject
2+
3+
import net.thunderbird.feature.notification.api.content.SystemNotification
4+
import net.thunderbird.feature.notification.impl.intent.action.DefaultNotificationActionIntentCreator
5+
import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator
6+
import net.thunderbird.feature.notification.impl.ui.action.DefaultSystemNotificationActionCreator
7+
import net.thunderbird.feature.notification.impl.ui.action.NotificationActionCreator
8+
import org.koin.android.ext.koin.androidApplication
9+
import org.koin.core.module.Module
10+
import org.koin.core.qualifier.named
11+
import org.koin.dsl.module
12+
13+
internal actual val platformFeatureNotificationModule: Module = module {
14+
single<List<NotificationActionIntentCreator<*>>>(named<NotificationActionIntentCreator.TypeQualifier>()) {
15+
listOf(
16+
DefaultNotificationActionIntentCreator(
17+
logger = get(),
18+
applicationContext = androidApplication(),
19+
),
20+
)
21+
}
22+
23+
single<NotificationActionCreator<SystemNotification>>(named(NotificationActionCreator.TypeQualifier.System)) {
24+
DefaultSystemNotificationActionCreator(
25+
logger = get(),
26+
actionIntentCreators = get(named<NotificationActionIntentCreator.TypeQualifier>()),
27+
)
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package net.thunderbird.feature.notification.impl.intent.action
2+
3+
import android.app.PendingIntent
4+
import android.content.Context
5+
import androidx.core.app.PendingIntentCompat
6+
import net.thunderbird.core.logging.Logger
7+
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
8+
9+
private const val TAG = "DefaultNotificationActionIntentCreator"
10+
11+
/**
12+
* A default implementation of [NotificationActionIntentCreator] that creates a [PendingIntent]
13+
* to launch the application when a notification action is triggered.
14+
*
15+
* This creator accepts any [NotificationAction] and always attempts to create a launch intent
16+
* for the current application.
17+
*
18+
* @property logger The logger instance for logging debug messages.
19+
* @property applicationContext The application context used to access system services like PackageManager.
20+
*/
21+
internal class DefaultNotificationActionIntentCreator(
22+
private val logger: Logger,
23+
private val applicationContext: Context,
24+
) : NotificationActionIntentCreator<NotificationAction> {
25+
override fun accept(action: NotificationAction): Boolean = true
26+
27+
override fun create(action: NotificationAction): PendingIntent? {
28+
logger.debug(TAG) { "create() called with: action = $action" }
29+
val packageManager = applicationContext.packageManager
30+
val launchIntent = requireNotNull(
31+
packageManager.getLaunchIntentForPackage(applicationContext.packageName),
32+
) {
33+
"Could not retrieve the launch intent from ${applicationContext.packageName}"
34+
}
35+
36+
return PendingIntentCompat.getActivity(
37+
/* context = */
38+
applicationContext,
39+
/* requestCode = */
40+
1,
41+
/* intent = */
42+
launchIntent,
43+
/* flags = */
44+
0,
45+
/* isMutable = */
46+
false,
47+
)
48+
}
49+
}

0 commit comments

Comments
 (0)