Skip to content

Commit d001f47

Browse files
committed
refactor(notifications): simplify notification command outcomes
1 parent 7ab4a4a commit d001f47

File tree

30 files changed

+440
-405
lines changed

30 files changed

+440
-405
lines changed

feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debug/settings/SecretDebugSettingsScreenPreview.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ import kotlinx.coroutines.flow.Flow
1010
import kotlinx.coroutines.flow.SharedFlow
1111
import kotlinx.coroutines.flow.flowOf
1212
import net.thunderbird.core.common.resources.StringsResourceManager
13-
import net.thunderbird.core.outcome.Outcome
1413
import net.thunderbird.feature.debug.settings.notification.DebugNotificationSectionViewModel
1514
import net.thunderbird.feature.mail.account.api.AccountManager
1615
import net.thunderbird.feature.mail.account.api.BaseAccount
17-
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
18-
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
16+
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
1917
import net.thunderbird.feature.notification.api.content.Notification
2018
import net.thunderbird.feature.notification.api.receiver.InAppNotificationEvent
2119
import net.thunderbird.feature.notification.api.receiver.InAppNotificationReceiver
@@ -25,6 +23,12 @@ import net.thunderbird.feature.notification.api.sender.NotificationSender
2523
@Composable
2624
private fun SecretDebugSettingsScreenPreview() {
2725
koinPreview {
26+
single<InAppNotificationReceiver> {
27+
object : InAppNotificationReceiver {
28+
override val events: SharedFlow<InAppNotificationEvent>
29+
get() = error("not implemented")
30+
}
31+
}
2832
single<DebugNotificationSectionViewModel> {
2933
DebugNotificationSectionViewModel(
3034
stringsResourceManager = object : StringsResourceManager {
@@ -47,13 +51,10 @@ private fun SecretDebugSettingsScreenPreview() {
4751
notificationSender = object : NotificationSender {
4852
override fun send(
4953
notification: Notification,
50-
): Flow<Outcome<Success<Notification>, Failure<Notification>>> =
54+
): Flow<NotificationCommandOutcome<Notification>> =
5155
error("not implemented")
5256
},
53-
notificationReceiver = object : InAppNotificationReceiver {
54-
override val events: SharedFlow<InAppNotificationEvent>
55-
get() = error("not implemented")
56-
},
57+
notificationReceiver = get(),
5758
)
5859
}
5960
} WithContent {

feature/notification/api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ kotlin {
77
sourceSets {
88
commonMain.dependencies {
99
implementation(projects.core.common)
10+
implementation(projects.core.featureflag)
1011
implementation(projects.core.outcome)
1112
}
1213
commonTest.dependencies {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ package net.thunderbird.feature.notification.api
1010
* @property value The integer value of the notification ID.
1111
*/
1212
@JvmInline
13-
value class NotificationId(val value: Int) : Comparable<Int> by value
13+
value class NotificationId(val value: Int) : Comparable<Int> by value {
14+
companion object {
15+
val Undefined = NotificationId(Int.MIN_VALUE)
16+
}
17+
}
Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package net.thunderbird.feature.notification.api.command
22

3-
import androidx.annotation.Discouraged
4-
import net.thunderbird.core.outcome.Outcome
5-
import net.thunderbird.feature.notification.api.NotificationId
3+
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
64
import net.thunderbird.feature.notification.api.content.Notification
75
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
86

@@ -24,45 +22,5 @@ abstract class NotificationCommand<TNotification : Notification>(
2422
* Executes the command.
2523
* @return The result of the execution.
2624
*/
27-
abstract suspend fun execute(): Outcome<Success<TNotification>, Failure<TNotification>>
28-
29-
/**
30-
* Represents the outcome of a command's execution.
31-
*/
32-
sealed interface CommandOutcome
33-
34-
/**
35-
* Represents a successful command execution.
36-
*
37-
* @param TNotification The type of notification associated with the command.
38-
* @property notificationId The ID of the notification that was successfully acted upon.
39-
* @property command The command that was executed successfully.
40-
*/
41-
data class Success<out TNotification : Notification>(
42-
val notificationId: NotificationId,
43-
val command: NotificationCommand<out TNotification>,
44-
) : CommandOutcome {
45-
companion object {
46-
@Discouraged(
47-
message = "This is a utility function to enable usage in Java code. " +
48-
"Use Success(NotificationId, NotificationCommand) instead.",
49-
)
50-
operator fun invoke(
51-
notificationId: Int,
52-
command: NotificationCommand<*>,
53-
): Success<Notification> = Success(NotificationId(notificationId), command)
54-
}
55-
}
56-
57-
/**
58-
* Represents a failed command execution.
59-
*
60-
* @param TNotification The type of notification associated with the command.
61-
* @property command The command that failed.
62-
* @property throwable The exception that caused the failure.
63-
*/
64-
data class Failure<out TNotification : Notification>(
65-
val command: NotificationCommand<out TNotification>?,
66-
val throwable: Throwable,
67-
) : CommandOutcome
25+
abstract suspend fun execute(): NotificationCommandOutcome<TNotification>
6826
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package net.thunderbird.feature.notification.api.command.outcome
2+
3+
import net.thunderbird.feature.notification.api.command.NotificationCommand
4+
import net.thunderbird.feature.notification.api.content.Notification
5+
6+
/**
7+
* Represents a failure that occurred while attempting to execute a command.
8+
*
9+
* Use this when a command was recognized and supported, but its execution failed due to an error
10+
* (e.g., I/O failure, unexpected state, or an exception thrown by the underlying system).
11+
*
12+
* @param TNotification The type of notification associated with the command.
13+
* @property command The command whose execution failed. May be null if the command couldn't be
14+
* fully constructed.
15+
* @property message A human-readable description of the failure. Defaults to a generic message.
16+
* @property throwable An optional exception that provides additional context about the failure.
17+
*/
18+
data class CommandExecutionFailed<out TNotification : Notification>(
19+
override val command: NotificationCommand<out TNotification>,
20+
val message: String = "Command failed to execute.",
21+
val throwable: Throwable? = null,
22+
) : Failure<TNotification>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package net.thunderbird.feature.notification.api.command.outcome
2+
3+
import net.thunderbird.feature.notification.api.command.NotificationCommand
4+
import net.thunderbird.feature.notification.api.content.Notification
5+
6+
data class CommandNotCreated<out TNotification : Notification>(
7+
val message: String,
8+
) : Failure<TNotification> {
9+
override val command: NotificationCommand<out TNotification>? = null
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
@file:JvmName("NotificationCommandOutcome")
2+
3+
package net.thunderbird.feature.notification.api.command.outcome
4+
5+
import androidx.annotation.Discouraged
6+
import net.thunderbird.core.outcome.Outcome
7+
import net.thunderbird.feature.notification.api.NotificationId
8+
import net.thunderbird.feature.notification.api.command.NotificationCommand
9+
import net.thunderbird.feature.notification.api.content.Notification
10+
11+
/**
12+
* Outcome type for executing a NotificationCommand.
13+
*
14+
* When executing a command, the result will be either a [Success] or a [Failure].
15+
*/
16+
typealias NotificationCommandOutcome<TNotification> = Outcome<Success<TNotification>, Failure<TNotification>>
17+
18+
/**
19+
* Represents a successful command execution.
20+
*
21+
* @param TNotification The type of notification associated with the command.
22+
* @property notificationId The ID of the notification that was successfully acted upon.
23+
* @property command The command that was executed successfully.
24+
*/
25+
sealed interface Success<out TNotification : Notification> {
26+
val notificationId: NotificationId
27+
val command: NotificationCommand<out TNotification>?
28+
29+
/**
30+
* Indicates that the command was executed as requested.
31+
*
32+
* This is the canonical success case that carries the command instance that was executed.
33+
*
34+
* @param TNotification The type of notification associated with the command.
35+
* @property command The concrete command that was executed.
36+
*/
37+
@ConsistentCopyVisibility
38+
data class Executed<out TNotification : Notification> internal constructor(
39+
override val notificationId: NotificationId,
40+
override val command: NotificationCommand<out TNotification>,
41+
) : Success<TNotification>
42+
43+
/**
44+
* Indicates that executing the command resulted in no operation.
45+
*
46+
* This can be used when the system is already in the desired state or when there is
47+
* intentionally nothing to do. The [command] may be null if there isn't a specific
48+
* command instance associated with the no-op result.
49+
*
50+
* @param TNotification The type of notification associated with the command.
51+
* @property command The command related to this no-op outcome, if any.
52+
*/
53+
data class NoOperation<out TNotification : Notification>(
54+
override val command: NotificationCommand<out TNotification>? = null,
55+
) : Success<TNotification> {
56+
override val notificationId: NotificationId = NotificationId.Undefined
57+
}
58+
}
59+
60+
/**
61+
* Convenience factory that wraps the given command in a [Success.Executed] instance.
62+
*/
63+
fun <TNotification : Notification> Success(
64+
notificationId: NotificationId,
65+
command: NotificationCommand<out TNotification>,
66+
): Success<TNotification> = Success.Executed(notificationId, command)
67+
68+
/**
69+
* Convenience factory that wraps the given command in a [Success.Executed] instance.
70+
*/
71+
@Discouraged(
72+
message = "This is a utility function to enable usage in Java code. " +
73+
"Use Success(NotificationId, NotificationCommand) instead.",
74+
)
75+
fun <TNotification : Notification> Success(
76+
notificationId: Int,
77+
command: NotificationCommand<out TNotification>,
78+
): Success<TNotification> = Success.Executed(NotificationId(notificationId), command)
79+
80+
/**
81+
* Represents a failed command execution.
82+
*
83+
* @param TNotification The type of notification associated with the command.
84+
* @property command The command that failed.
85+
*/
86+
sealed interface Failure<out TNotification : Notification> {
87+
val command: NotificationCommand<out TNotification>?
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.thunderbird.feature.notification.api.command.outcome
2+
3+
import net.thunderbird.core.featureflag.FeatureFlagKey
4+
import net.thunderbird.feature.notification.api.command.NotificationCommand
5+
import net.thunderbird.feature.notification.api.content.Notification
6+
7+
/**
8+
* Represents a command that cannot be executed because it is not supported in the current context.
9+
*
10+
* Typical reasons include disabled feature flags or an unknown/unsupported command on the
11+
* current platform or build variant.
12+
*
13+
* @param TNotification The type of notification associated with the command.
14+
* @property command The command that was deemed unsupported. May be null when the unsupported
15+
* state is determined before a concrete command instance is created.
16+
* @property reason The specific reason why the command is not supported.
17+
*/
18+
data class UnsupportedCommand<out TNotification : Notification>(
19+
override val command: NotificationCommand<out TNotification>?,
20+
val reason: Reason,
21+
) : Failure<TNotification> {
22+
23+
/**
24+
* Describes why a command is unsupported.
25+
*/
26+
sealed interface Reason {
27+
/**
28+
* The command is behind a feature flag that is currently disabled.
29+
*
30+
* @property key The feature flag key that disabled this command.
31+
*/
32+
data class FeatureFlagDisabled(val key: FeatureFlagKey) : Reason
33+
34+
/**
35+
* A generic, unknown reason for an unsupported command.
36+
*
37+
* @property message A human-readable description of the issue.
38+
* @property throwable An optional underlying exception that provides more context.
39+
*/
40+
data class Unknown(val message: String, val throwable: Throwable? = null) : Reason
41+
}
42+
}

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/dismisser/NotificationDismisser.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package net.thunderbird.feature.notification.api.dismisser
33
import kotlinx.coroutines.flow.Flow
44
import net.thunderbird.core.outcome.Outcome
55
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
6+
import net.thunderbird.feature.notification.api.command.outcome.Failure
7+
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
8+
import net.thunderbird.feature.notification.api.command.outcome.Success
89
import net.thunderbird.feature.notification.api.content.Notification
910

1011
/**
@@ -18,7 +19,7 @@ interface NotificationDismisser {
1819
* @return A [Flow] of [Outcome] that emits either a [Success] with the dismissed [Notification]
1920
* or a [Failure] with the [Notification] that failed to be dismissed.
2021
*/
21-
fun dismiss(id: NotificationId): Flow<Outcome<Success<Notification>, Failure<Notification>>>
22+
fun dismiss(id: NotificationId): Flow<NotificationCommandOutcome<Notification>>
2223

2324
/**
2425
* Dismisses a notification.
@@ -28,5 +29,5 @@ interface NotificationDismisser {
2829
* The [Outcome] will be a [Success] containing the dismissed [Notification] if the operation was successful,
2930
* or a [Failure] containing the [Notification] if the operation failed.
3031
*/
31-
fun dismiss(notification: Notification): Flow<Outcome<Success<Notification>, Failure<Notification>>>
32+
fun dismiss(notification: Notification): Flow<NotificationCommandOutcome<Notification>>
3233
}

feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/dismisser/compat/NotificationDismisserCompat.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import kotlinx.coroutines.SupervisorJob
99
import kotlinx.coroutines.cancel
1010
import kotlinx.coroutines.flow.launchIn
1111
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
12+
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
1513
import net.thunderbird.feature.notification.api.content.Notification
1614
import net.thunderbird.feature.notification.api.dismisser.NotificationDismisser
1715

@@ -45,6 +43,6 @@ class NotificationDismisserCompat @JvmOverloads constructor(
4543
}
4644

4745
fun interface OnResultListener {
48-
fun onResult(outcome: Outcome<Success<Notification>, Failure<Notification>>)
46+
fun onResult(outcome: NotificationCommandOutcome<Notification>)
4947
}
5048
}

0 commit comments

Comments
 (0)