Skip to content

Commit 7f2c229

Browse files
committed
feat(notifications): add capability to dismiss a notification using NotificationDismisser or NotificationManager
1 parent 5ab8497 commit 7f2c229

File tree

27 files changed

+1188
-109
lines changed

27 files changed

+1188
-109
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package net.thunderbird.feature.notification.api
2+
3+
import net.thunderbird.feature.notification.api.dismisser.NotificationDismisser
4+
import net.thunderbird.feature.notification.api.sender.NotificationSender
5+
6+
/**
7+
* Manages sending and dismissing notifications.
8+
*
9+
* This interface combines the functionalities of [NotificationSender] and [NotificationDismisser]
10+
* to provide a unified API for notification management.
11+
*/
12+
interface NotificationManager : NotificationSender, NotificationDismisser

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationRegistry.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,20 @@ interface NotificationRegistry {
5858
* @param notification The [Notification] object to unregister.
5959
*/
6060
fun unregister(notification: Notification)
61+
62+
/**
63+
* Checks if a specific notification is currently registered.
64+
*
65+
* @param notification The [Notification] object to check.
66+
* @return `true` if the notification is registered, `false` otherwise.
67+
*/
68+
operator fun contains(notification: Notification): Boolean
69+
70+
/**
71+
* Checks if a notification with the given [notificationId] is currently registered.
72+
*
73+
* @param notificationId The ID of the notification to check.
74+
* @return `true` if the notification is registered, `false` otherwise.
75+
*/
76+
operator fun contains(notificationId: NotificationId): Boolean
6177
}

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/command/NotificationCommand.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ abstract class NotificationCommand<TNotification : Notification>(
4747
* @property throwable The exception that caused the failure.
4848
*/
4949
data class Failure<out TNotification : Notification>(
50-
val command: NotificationCommand<out TNotification>,
50+
val command: NotificationCommand<out TNotification>?,
5151
val throwable: Throwable,
5252
) : CommandOutcome
5353
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package net.thunderbird.feature.notification.api.dismisser
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import net.thunderbird.core.outcome.Outcome
5+
import net.thunderbird.feature.notification.api.NotificationId
6+
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
7+
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
8+
import net.thunderbird.feature.notification.api.content.Notification
9+
10+
/**
11+
* Responsible for dismissing notifications by creating and executing the appropriate commands.
12+
*/
13+
interface NotificationDismisser {
14+
/**
15+
* Dismisses a notification with the given ID.
16+
*
17+
* @param id The ID of the notification to dismiss.
18+
* @return A [Flow] of [Outcome] that emits either a [Success] with the dismissed [Notification]
19+
* or a [Failure] with the [Notification] that failed to be dismissed.
20+
*/
21+
fun dismiss(id: NotificationId): Flow<Outcome<Success<Notification>, Failure<Notification>>>
22+
23+
/**
24+
* Dismisses a notification.
25+
*
26+
* @param notification The notification to dismiss.
27+
* @return A [Flow] of [Outcome] that emits the result of the dismiss operation.
28+
* The [Outcome] will be a [Success] containing the dismissed [Notification] if the operation was successful,
29+
* or a [Failure] containing the [Notification] if the operation failed.
30+
*/
31+
fun dismiss(notification: Notification): Flow<Outcome<Success<Notification>, Failure<Notification>>>
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package net.thunderbird.feature.notification.api.dismisser.compat
2+
3+
import androidx.annotation.Discouraged
4+
import kotlinx.coroutines.CoroutineDispatcher
5+
import kotlinx.coroutines.CoroutineScope
6+
import kotlinx.coroutines.Dispatchers
7+
import kotlinx.coroutines.DisposableHandle
8+
import kotlinx.coroutines.SupervisorJob
9+
import kotlinx.coroutines.cancel
10+
import kotlinx.coroutines.flow.launchIn
11+
import kotlinx.coroutines.flow.onEach
12+
import net.thunderbird.core.outcome.Outcome
13+
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
14+
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
15+
import net.thunderbird.feature.notification.api.content.Notification
16+
import net.thunderbird.feature.notification.api.dismisser.NotificationDismisser
17+
18+
/**
19+
* A compatibility layer for dismissing notifications from Java code.
20+
*
21+
* This class wraps [NotificationDismisser] and provides a Java-friendly API for sending notifications
22+
* and receiving results via a callback interface.
23+
*
24+
* It is marked as [Discouraged] because it is intended only for use within Java classes.
25+
* Kotlin code should use [NotificationDismisser] directly.
26+
*
27+
* @property notificationDismisser The underlying [NotificationDismisser] instance.
28+
* @property mainImmediateDispatcher The [CoroutineDispatcher] used for launching coroutines.
29+
*/
30+
@Discouraged("Only for usage within a Java class. Use NotificationDismisser instead.")
31+
class NotificationDismisserCompat @JvmOverloads constructor(
32+
private val notificationDismisser: NotificationDismisser,
33+
mainImmediateDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
34+
) : DisposableHandle {
35+
private val scope = CoroutineScope(SupervisorJob() + mainImmediateDispatcher)
36+
37+
fun dismiss(notification: Notification, onResultListener: OnResultListener) {
38+
notificationDismisser.dismiss(notification)
39+
.onEach { outcome -> onResultListener.onResult(outcome) }
40+
.launchIn(scope)
41+
}
42+
43+
override fun dispose() {
44+
scope.cancel()
45+
}
46+
47+
fun interface OnResultListener {
48+
fun onResult(outcome: Outcome<Success<Notification>, Failure<Notification>>)
49+
}
50+
}

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/NotificationNotifier.kt

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,40 @@ import net.thunderbird.feature.notification.api.NotificationId
44
import net.thunderbird.feature.notification.api.content.Notification
55

66
/**
7-
* Interface for displaying notifications.
7+
* Abstraction for components that present and manage a specific kind of notification.
88
*
9-
* This is a sealed interface, meaning that all implementations must be declared in this file.
9+
* Implementations are responsible for rendering notifications (e.g., system tray notifications,
10+
* in-app notifications) and for dismissing them when requested. The generic type parameter
11+
* allows an implementation to declare which [Notification] sub-type it can handle.
1012
*
11-
* @param TNotification The type of notification to display.
13+
* Type parameters:
14+
* @param TNotification The specific subtype of [Notification] this notifier can display. The
15+
* contravariant `in` variance allows a notifier for a base type to also accept its subtypes.
1216
*/
1317
interface NotificationNotifier<in TNotification : Notification> {
1418
/**
15-
* Shows a notification to the user.
19+
* Displays or updates a notification associated with the given [id].
1620
*
17-
* @param id The notification id. Mostly used by System Notifications.
18-
* @param notification The notification to show.
21+
* Implementations should render [notification] according to their medium. If a notification
22+
* with the same [id] is already visible, this call should update/replace it when supported
23+
* by the underlying mechanism.
24+
*
25+
* @param id A stable identifier that correlates to this notification instance across updates
26+
* and dismissal. Often maps to a system notification ID when using platform notifications.
27+
* @param notification The domain model describing what to present to the user.
1928
*/
2029
suspend fun show(id: NotificationId, notification: TNotification)
2130

31+
/**
32+
* Dismisses the notification previously shown with [id].
33+
*
34+
* If no notification is currently displayed for [id], implementations should treat this as a
35+
* no-op.
36+
*
37+
* @param id The identifier of the notification to dismiss.
38+
*/
39+
suspend fun dismiss(id: NotificationId)
40+
2241
/**
2342
* Disposes of any resources used by the notifier.
2443
*

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/host/InAppNotificationHostStateHolder.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ class InAppNotificationHostStateHolder(private val enabled: ImmutableSet<Display
8282
}
8383
}
8484

85+
/**
86+
* Dismisses all visual representations of the given in-app notification.
87+
*
88+
* This function will attempt to dismiss the global banner, inline banners,
89+
* and snackbar associated with the provided notification.
90+
*
91+
* @param notification The [InAppNotification] to dismiss.
92+
*/
93+
fun dismiss(notification: InAppNotification) {
94+
val data = notification.toInAppNotificationData()
95+
data.bannerInlineVisuals.singleOrNull()?.let(::dismiss)
96+
data.bannerGlobalVisual?.let(::dismiss)
97+
data.snackbarVisual?.let(::dismiss)
98+
}
99+
85100
/**
86101
* Dismisses the given in-app notification visual.
87102
*

feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/AndroidSystemNotificationNotifier.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ internal class AndroidSystemNotificationNotifier(
3333
notificationManager.notify(id.value, androidNotification)
3434
}
3535

36+
override suspend fun dismiss(id: NotificationId) {
37+
logger.debug(TAG) { "dismiss() called with: id = $id" }
38+
notificationManager.cancel(id.value)
39+
}
40+
3641
override fun dispose() {
3742
logger.debug(TAG) { "dispose() called" }
3843
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package net.thunderbird.feature.notification.impl
2+
3+
import net.thunderbird.feature.notification.api.NotificationManager
4+
import net.thunderbird.feature.notification.api.dismisser.NotificationDismisser
5+
import net.thunderbird.feature.notification.api.sender.NotificationSender
6+
7+
/**
8+
* Default implementation of [NotificationManager].
9+
*
10+
* This class acts as a central point for managing notifications, delegating sending and dismissing
11+
* operations to the provided [NotificationSender] and [NotificationDismisser] respectively.
12+
*
13+
* @param notificationSender The [NotificationSender] responsible for displaying notifications.
14+
* @param notificationDismisser The [NotificationDismisser] responsible for removing notifications.
15+
*/
16+
class DefaultNotificationManager(
17+
private val notificationSender: NotificationSender,
18+
private val notificationDismisser: NotificationDismisser,
19+
) : NotificationManager, NotificationSender by notificationSender, NotificationDismisser by notificationDismisser

feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/DefaultNotificationRegistry.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,12 @@ class DefaultNotificationRegistry : NotificationRegistry {
5757
override fun unregister(notification: Notification) {
5858
_registrar.remove(notification)
5959
}
60+
61+
override fun contains(notification: Notification): Boolean {
62+
return _registrar.containsKey(notification)
63+
}
64+
65+
override fun contains(notificationId: NotificationId): Boolean {
66+
return _registrar.containsValue(notificationId)
67+
}
6068
}

0 commit comments

Comments
 (0)