diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt
new file mode 100644
index 00000000000..bf54d97db7c
--- /dev/null
+++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt
@@ -0,0 +1,44 @@
+package net.thunderbird.feature.notification.api.ui.action.icon
+
+import net.thunderbird.feature.notification.api.R
+import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
+
+internal actual val NotificationActionIcons.Reply: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_reply,
+ )
+
+internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_mark_email_read,
+ )
+
+internal actual val NotificationActionIcons.Delete: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_delete,
+ )
+
+internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_report,
+ )
+
+internal actual val NotificationActionIcons.Archive: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_archive,
+ )
+
+internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_settings,
+ )
+
+internal actual val NotificationActionIcons.Retry: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_refresh,
+ )
+
+internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon
+ get() = NotificationIcon(
+ systemNotificationIcon = R.drawable.ic_settings,
+ )
diff --git a/feature/notification/api/src/commonMain/composeResources/values/strings.xml b/feature/notification/api/src/commonMain/composeResources/values/strings.xml
index 2320de026e7..07881ac7ee9 100644
--- a/feature/notification/api/src/commonMain/composeResources/values/strings.xml
+++ b/feature/notification/api/src/commonMain/composeResources/values/strings.xml
@@ -45,4 +45,15 @@
Disable Push
+ Reply
+ Mark Read
+ Mark All Read
+ Delete
+ Delete All
+ Archive
+ Archive All
+ Spam
+ Retry
+ Update Server Settings
+
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt
index d663fc29bd6..60196f54890 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt
@@ -3,6 +3,8 @@ package net.thunderbird.feature.notification.api.content
import net.thunderbird.feature.notification.api.NotificationChannel
import net.thunderbird.feature.notification.api.NotificationSeverity
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+import net.thunderbird.feature.notification.api.ui.action.icon.DisablePushAction
+import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons
import net.thunderbird.feature.notification.api.ui.icon.AlarmPermissionMissing
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons
@@ -168,7 +170,9 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
* @return A set of [NotificationAction] instances.
*/
private suspend fun buildNotificationActions(): Set = setOf(
+ NotificationAction.Tap,
NotificationAction.CustomAction(
- message = getString(resource = Res.string.push_info_disable_push_action),
+ title = getString(resource = Res.string.push_info_disable_push_action),
+ icon = NotificationActionIcons.DisablePushAction,
),
)
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt
index d1183716826..adb1836fc84 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt
@@ -1,50 +1,125 @@
package net.thunderbird.feature.notification.api.ui.action
+import net.thunderbird.feature.notification.api.content.SystemNotification
+import net.thunderbird.feature.notification.api.ui.action.icon.Archive
+import net.thunderbird.feature.notification.api.ui.action.icon.Delete
+import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsRead
+import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsSpam
+import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons
+import net.thunderbird.feature.notification.api.ui.action.icon.Reply
+import net.thunderbird.feature.notification.api.ui.action.icon.Retry
+import net.thunderbird.feature.notification.api.ui.action.icon.UpdateServerSettings
+import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
+import net.thunderbird.feature.notification.resources.api.Res
+import net.thunderbird.feature.notification.resources.api.notification_action_archive
+import net.thunderbird.feature.notification.resources.api.notification_action_delete
+import net.thunderbird.feature.notification.resources.api.notification_action_mark_as_read
+import net.thunderbird.feature.notification.resources.api.notification_action_reply
+import net.thunderbird.feature.notification.resources.api.notification_action_retry
+import net.thunderbird.feature.notification.resources.api.notification_action_spam
+import net.thunderbird.feature.notification.resources.api.notification_action_update_server_settings
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.getString
+
/**
* Represents the various actions that can be performed on a notification.
*/
-sealed interface NotificationAction {
+sealed class NotificationAction {
+ abstract val icon: NotificationIcon?
+ protected abstract val titleResource: StringResource?
+
+ open suspend fun resolveTitle(): String? = titleResource?.let { getString(it) }
+
+ /**
+ * Action to open the notification. This is the default action when a notification is tapped.
+ *
+ * This action typically does not have an icon or title displayed on the notification itself,
+ * as it's implied by tapping the notification content.
+ *
+ * All [SystemNotification] will have this action implicitly, even if not specified in the
+ * [SystemNotification.actions] set.
+ */
+ data object Tap : NotificationAction() {
+ override val icon: NotificationIcon? = null
+ override val titleResource: StringResource? = null
+ }
+
/**
* Action to reply to the email message associated with the notification.
*/
- data object Reply : NotificationAction
+ data object Reply : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.Reply
+
+ override val titleResource: StringResource = Res.string.notification_action_reply
+ }
/**
* Action to mark the email message associated with the notification as read.
*/
- data object MarkAsRead : NotificationAction
+ data object MarkAsRead : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.MarkAsRead
+
+ override val titleResource: StringResource = Res.string.notification_action_mark_as_read
+ }
/**
* Action to delete the email message associated with the notification.
*/
- data object Delete : NotificationAction
+ data object Delete : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.Delete
+
+ override val titleResource: StringResource = Res.string.notification_action_delete
+ }
/**
* Action to mark the email message associated with the notification as spam.
*/
- data object MarkAsSpam : NotificationAction
+ data object MarkAsSpam : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.MarkAsSpam
+
+ override val titleResource: StringResource = Res.string.notification_action_spam
+ }
/**
* Action to archive the email message associated with the notification.
*/
- data object Archive : NotificationAction
+ data object Archive : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.Archive
+
+ override val titleResource: StringResource = Res.string.notification_action_archive
+ }
/**
* Action to prompt the user to update server settings, typically when authentication fails.
*/
- data object UpdateServerSettings : NotificationAction
+ data object UpdateServerSettings : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.UpdateServerSettings
+
+ override val titleResource: StringResource = Res.string.notification_action_update_server_settings
+ }
/**
* Action to retry a failed operation, such as sending a message or fetching new messages.
*/
- data object Retry : NotificationAction
+ data object Retry : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.Retry
+
+ override val titleResource: StringResource = Res.string.notification_action_retry
+ }
/**
* Represents a custom notification action.
*
* This can be used for actions that are not predefined and require a specific message.
*
- * @property message The text to be displayed for this custom action.
+ * @property title The text to be displayed for this custom action.
*/
- data class CustomAction(val message: String) : NotificationAction
+ data class CustomAction(
+ val title: String,
+ override val icon: NotificationIcon? = null,
+ ) : NotificationAction() {
+ override val titleResource: StringResource get() = error("Custom Action must not supply a title resource")
+
+ override suspend fun resolveTitle(): String = title
+ }
}
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt
new file mode 100644
index 00000000000..259d37d6bc8
--- /dev/null
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt
@@ -0,0 +1,14 @@
+package net.thunderbird.feature.notification.api.ui.action.icon
+
+import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
+
+internal object NotificationActionIcons
+
+internal expect val NotificationActionIcons.Reply: NotificationIcon
+internal expect val NotificationActionIcons.MarkAsRead: NotificationIcon
+internal expect val NotificationActionIcons.Delete: NotificationIcon
+internal expect val NotificationActionIcons.MarkAsSpam: NotificationIcon
+internal expect val NotificationActionIcons.Archive: NotificationIcon
+internal expect val NotificationActionIcons.UpdateServerSettings: NotificationIcon
+internal expect val NotificationActionIcons.Retry: NotificationIcon
+internal expect val NotificationActionIcons.DisablePushAction: NotificationIcon
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt
index 6280242d575..a53d234f841 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt
@@ -12,8 +12,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
* @property inAppNotificationIcon The icon to be used for in-app notifications.
*/
data class NotificationIcon(
- private val systemNotificationIcon: SystemNotificationIcon? = null,
- private val inAppNotificationIcon: ImageVector? = null,
+ val systemNotificationIcon: SystemNotificationIcon? = null,
+ val inAppNotificationIcon: ImageVector? = null,
) {
init {
diff --git a/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt
new file mode 100644
index 00000000000..821368370a2
--- /dev/null
+++ b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt
@@ -0,0 +1,14 @@
+package net.thunderbird.feature.notification.api.ui.action.icon
+
+import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
+
+private const val ERROR_MESSAGE = "Can't send notifications from a jvm library. Use android library or app instead."
+
+internal actual val NotificationActionIcons.Reply: NotificationIcon get() = error(ERROR_MESSAGE)
+internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon get() = error(ERROR_MESSAGE)
+internal actual val NotificationActionIcons.Delete: NotificationIcon get() = error(ERROR_MESSAGE)
+internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon get() = error(ERROR_MESSAGE)
+internal actual val NotificationActionIcons.Archive: NotificationIcon get() = error(ERROR_MESSAGE)
+internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon get() = error(ERROR_MESSAGE)
+internal actual val NotificationActionIcons.Retry: NotificationIcon get() = error(ERROR_MESSAGE)
+internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon get() = error(ERROR_MESSAGE)
diff --git a/feature/notification/impl/build.gradle.kts b/feature/notification/impl/build.gradle.kts
index 3ef9207f1ac..bac4e026572 100644
--- a/feature/notification/impl/build.gradle.kts
+++ b/feature/notification/impl/build.gradle.kts
@@ -10,6 +10,15 @@ kotlin {
implementation(projects.core.logging.api)
implementation(projects.feature.notification.api)
}
+ commonTest.dependencies {
+ implementation(projects.core.logging.testing)
+ }
+ androidUnitTest.dependencies {
+ implementation(libs.androidx.test.core)
+ implementation(libs.mockito.core)
+ implementation(libs.mockito.kotlin)
+ implementation(libs.robolectric)
+ }
}
}
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.android.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.android.kt
new file mode 100644
index 00000000000..cef362deedf
--- /dev/null
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.android.kt
@@ -0,0 +1,29 @@
+package net.thunderbird.feature.notification.impl.inject
+
+import net.thunderbird.feature.notification.api.content.SystemNotification
+import net.thunderbird.feature.notification.impl.intent.action.DefaultNotificationActionIntentCreator
+import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator
+import net.thunderbird.feature.notification.impl.ui.action.DefaultSystemNotificationActionCreator
+import net.thunderbird.feature.notification.impl.ui.action.NotificationActionCreator
+import org.koin.android.ext.koin.androidApplication
+import org.koin.core.module.Module
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+internal actual val platformFeatureNotificationModule: Module = module {
+ single>>(named()) {
+ listOf(
+ DefaultNotificationActionIntentCreator(
+ logger = get(),
+ applicationContext = androidApplication(),
+ ),
+ )
+ }
+
+ single>(named(NotificationActionCreator.TypeQualifier.System)) {
+ DefaultSystemNotificationActionCreator(
+ logger = get(),
+ actionIntentCreators = get(named()),
+ )
+ }
+}
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreator.kt
new file mode 100644
index 00000000000..ebabf08ff48
--- /dev/null
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreator.kt
@@ -0,0 +1,49 @@
+package net.thunderbird.feature.notification.impl.intent.action
+
+import android.app.PendingIntent
+import android.content.Context
+import androidx.core.app.PendingIntentCompat
+import net.thunderbird.core.logging.Logger
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+
+private const val TAG = "DefaultNotificationActionIntentCreator"
+
+/**
+ * A default implementation of [NotificationActionIntentCreator] that creates a [PendingIntent]
+ * to launch the application when a notification action is triggered.
+ *
+ * This creator accepts any [NotificationAction] and always attempts to create a launch intent
+ * for the current application.
+ *
+ * @property logger The logger instance for logging debug messages.
+ * @property applicationContext The application context used to access system services like PackageManager.
+ */
+internal class DefaultNotificationActionIntentCreator(
+ private val logger: Logger,
+ private val applicationContext: Context,
+) : NotificationActionIntentCreator {
+ override fun accept(action: NotificationAction): Boolean = true
+
+ override fun create(action: NotificationAction): PendingIntent? {
+ logger.debug(TAG) { "create() called with: action = $action" }
+ val packageManager = applicationContext.packageManager
+ val launchIntent = requireNotNull(
+ packageManager.getLaunchIntentForPackage(applicationContext.packageName),
+ ) {
+ "Could not retrieve the launch intent from ${applicationContext.packageName}"
+ }
+
+ return PendingIntentCompat.getActivity(
+ /* context = */
+ applicationContext,
+ /* requestCode = */
+ 1,
+ /* intent = */
+ launchIntent,
+ /* flags = */
+ 0,
+ /* isMutable = */
+ false,
+ )
+ }
+}
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt
new file mode 100644
index 00000000000..7c0299f2fe0
--- /dev/null
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt
@@ -0,0 +1,32 @@
+package net.thunderbird.feature.notification.impl.intent.action
+
+import android.app.PendingIntent
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+
+/**
+ * Interface for creating a [PendingIntent] for a given [NotificationAction].
+ *
+ * This interface is used to decouple the creation of [PendingIntent]s from the notification creation logic.
+ * Implementations of this interface should be registered in the Koin graph using the [TypeQualifier].
+ *
+ * @param TNotificationAction The type of [NotificationAction] this creator can handle.
+ */
+internal interface NotificationActionIntentCreator {
+ /**
+ * Determines whether this [NotificationActionIntentCreator] can create an intent for the given [action].
+ *
+ * @param action The [NotificationAction] to check.
+ * @return `true` if this creator can handle the [action], `false` otherwise.
+ */
+ fun accept(action: NotificationAction): Boolean
+
+ /**
+ * Creates a [PendingIntent] for the given notification action.
+ *
+ * @param action The notification action to create an intent for.
+ * @return The created [PendingIntent], or `null` if the action is not supported or an error occurs.
+ */
+ fun create(action: TNotificationAction): PendingIntent?
+
+ object TypeQualifier
+}
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt
new file mode 100644
index 00000000000..95d9a2176ac
--- /dev/null
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt
@@ -0,0 +1,18 @@
+package net.thunderbird.feature.notification.impl.ui.action
+
+import android.app.PendingIntent
+import androidx.annotation.DrawableRes
+
+/**
+ * Represents an action that can be performed on an Android notification.
+ *
+ * @property icon The drawable resource ID for the action's icon.
+ * @property title The title of the action.
+ * @property pendingIntent The [PendingIntent] to be executed when the action is triggered.
+ */
+data class AndroidNotificationAction(
+ @DrawableRes
+ val icon: Int?,
+ val title: String?,
+ val pendingIntent: PendingIntent?,
+)
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/DefaultSystemNotificationActionCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/DefaultSystemNotificationActionCreator.kt
new file mode 100644
index 00000000000..dc7395e13ae
--- /dev/null
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/DefaultSystemNotificationActionCreator.kt
@@ -0,0 +1,29 @@
+package net.thunderbird.feature.notification.impl.ui.action
+
+import net.thunderbird.core.logging.Logger
+import net.thunderbird.feature.notification.api.content.SystemNotification
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator
+
+private const val TAG = "DefaultSystemNotificationActionCreator"
+
+internal class DefaultSystemNotificationActionCreator(
+ private val logger: Logger,
+ private val actionIntentCreators: List>,
+) : NotificationActionCreator {
+ override suspend fun create(
+ notification: SystemNotification,
+ action: NotificationAction,
+ ): AndroidNotificationAction {
+ logger.debug(TAG) { "create() called with: notification = $notification, action = $action" }
+ val intent = actionIntentCreators
+ .first { it.accept(action) }
+ .create(action)
+
+ return AndroidNotificationAction(
+ icon = action.icon?.systemNotificationIcon,
+ title = action.resolveTitle(),
+ pendingIntent = intent,
+ )
+ }
+}
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt
new file mode 100644
index 00000000000..db6cc1f7b20
--- /dev/null
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt
@@ -0,0 +1,25 @@
+package net.thunderbird.feature.notification.impl.ui.action
+
+import net.thunderbird.feature.notification.api.content.Notification
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+
+/**
+ * Interface responsible for creating Android-specific notification actions ([AndroidNotificationAction])
+ * from generic notification actions ([NotificationAction]).
+ *
+ * This allows decoupling the core notification logic from the Android platform specifics.
+ *
+ * @param TNotification The type of [Notification] this creator can handle.
+ */
+interface NotificationActionCreator {
+ /**
+ * Creates an [AndroidNotificationAction] for the given [notification] and [action].
+ *
+ * @param notification The notification to create the action for.
+ * @param action The action to create.
+ * @return The created [AndroidNotificationAction].
+ */
+ suspend fun create(notification: TNotification, action: NotificationAction): AndroidNotificationAction
+
+ enum class TypeQualifier { System, InApp }
+}
diff --git a/feature/notification/impl/src/androidUnitTest/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreatorTest.kt b/feature/notification/impl/src/androidUnitTest/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreatorTest.kt
new file mode 100644
index 00000000000..e45bb2da940
--- /dev/null
+++ b/feature/notification/impl/src/androidUnitTest/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreatorTest.kt
@@ -0,0 +1,132 @@
+package net.thunderbird.feature.notification.impl.intent.action
+
+import android.app.Application
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.core.app.PendingIntentCompat
+import androidx.test.core.app.ApplicationProvider
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import net.thunderbird.core.logging.testing.TestLogger
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mockStatic
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DefaultNotificationActionIntentCreatorTest {
+ private val application: Application = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun `accept should return true for any type of notification action`() {
+ // Arrange
+ val multipleActions = listOf(
+ NotificationAction.Tap,
+ NotificationAction.Reply,
+ NotificationAction.MarkAsRead,
+ NotificationAction.Delete,
+ NotificationAction.MarkAsSpam,
+ NotificationAction.Archive,
+ NotificationAction.UpdateServerSettings,
+ NotificationAction.Retry,
+ NotificationAction.CustomAction(title = "Custom Action 1"),
+ NotificationAction.CustomAction(title = "Custom Action 2"),
+ NotificationAction.CustomAction(title = "Custom Action 3"),
+ )
+ val testSubject = createTestSubject()
+
+ // Act
+ val accepted = multipleActions.fold(initial = true) { accepted, action ->
+ accepted and testSubject.accept(action)
+ }
+
+ // Assert
+ assertThat(accepted).isTrue()
+ }
+
+ @Test
+ fun `create should return PendingIntent for any type of notification action`() {
+ // Arrange
+ mockStatic(PendingIntentCompat::class.java).use { pendingIntentCompat ->
+ // Arrange (cont.)
+ val expectedIntent = Intent(Intent.ACTION_MAIN).apply {
+ addCategory(Intent.CATEGORY_LAUNCHER)
+ setPackage(application.packageName)
+ }
+ val mockedPackageManager = mock {
+ on {
+ getLaunchIntentForPackage(eq(application.packageName))
+ } doReturn expectedIntent
+ }
+ val context = spy(application) {
+ on { packageManager } doReturn mockedPackageManager
+ }
+
+ pendingIntentCompat
+ .`when` {
+ PendingIntentCompat.getActivity(
+ /* context = */
+ any(),
+ /* requestCode = */
+ any(),
+ /* intent = */
+ any(),
+ /* flags = */
+ any(),
+ /* isMutable = */
+ eq(false),
+ )
+ }
+ .thenReturn(mock())
+
+ val intentCaptor = argumentCaptor()
+ val testSubject = createTestSubject(context)
+
+ // Act
+ testSubject.create(NotificationAction.Tap)
+
+ // Assert
+ pendingIntentCompat.verify {
+ PendingIntentCompat.getActivity(
+ /* context = */
+ eq(context),
+ /* requestCode = */
+ eq(1),
+ /* intent = */
+ intentCaptor.capture(),
+ /* flags = */
+ eq(0),
+ /* isMutable = */
+ eq(false),
+ )
+ }
+ assertThat(intentCaptor.firstValue).all {
+ prop(Intent::getAction).isEqualTo(Intent.ACTION_MAIN)
+ prop(Intent::getPackage).isEqualTo(application.packageName)
+ transform { intent -> intent.hasCategory(Intent.CATEGORY_LAUNCHER) }
+ .isTrue()
+ }
+ }
+ }
+
+ private fun createTestSubject(
+ context: Context = application,
+ ): DefaultNotificationActionIntentCreator {
+ return DefaultNotificationActionIntentCreator(
+ logger = TestLogger(),
+ applicationContext = context,
+ )
+ }
+}
diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt
index 2e0c65053ba..a98af699488 100644
--- a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt
+++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt
@@ -5,9 +5,14 @@ import net.thunderbird.feature.notification.impl.command.NotificationCommandFact
import net.thunderbird.feature.notification.impl.receiver.InAppNotificationNotifier
import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier
import net.thunderbird.feature.notification.impl.sender.DefaultNotificationSender
+import org.koin.core.module.Module
import org.koin.dsl.module
+internal expect val platformFeatureNotificationModule: Module
+
val featureNotificationModule = module {
+ includes(platformFeatureNotificationModule)
+
factory { SystemNotificationNotifier() }
factory { InAppNotificationNotifier() }
diff --git a/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt
new file mode 100644
index 00000000000..045bda3545f
--- /dev/null
+++ b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt
@@ -0,0 +1,5 @@
+package net.thunderbird.feature.notification.impl.inject
+
+import org.koin.dsl.module
+
+internal actual val platformFeatureNotificationModule = module { }