Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,15 @@

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

<string name="notification_action_reply">Reply</string>
<string name="notification_action_mark_as_read">Mark Read</string>
<string name="notification_action_mark_all_as_read">Mark All Read</string>
<string name="notification_action_delete">Delete</string>
<string name="notification_action_delete_all">Delete All</string>
<string name="notification_action_archive">Archive</string>
<string name="notification_action_archive_all">Archive All</string>
<string name="notification_action_spam">Spam</string>
<string name="notification_action_retry">Retry</string>
<string name="notification_action_update_server_settings">Update Server Settings</string>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,7 +170,9 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
* @return A set of [NotificationAction] instances.
*/
private suspend fun buildNotificationActions(): Set<NotificationAction> = 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,
),
)
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions feature/notification/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<NotificationActionIntentCreator<*>>>(named<NotificationActionIntentCreator.TypeQualifier>()) {
listOf(
DefaultNotificationActionIntentCreator(
logger = get(),
applicationContext = androidApplication(),
),
)
}

single<NotificationActionCreator<SystemNotification>>(named(NotificationActionCreator.TypeQualifier.System)) {
DefaultSystemNotificationActionCreator(
logger = get(),
actionIntentCreators = get(named<NotificationActionIntentCreator.TypeQualifier>()),
)
}
}
Original file line number Diff line number Diff line change
@@ -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<NotificationAction> {
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,
)
}
}
Original file line number Diff line number Diff line change
@@ -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<in TNotificationAction : NotificationAction> {
/**
* 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
}
Loading