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
Expand Up @@ -10,12 +10,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.flowOf
import net.thunderbird.core.common.resources.StringsResourceManager
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.debug.settings.notification.DebugNotificationSectionViewModel
import net.thunderbird.feature.mail.account.api.AccountManager
import net.thunderbird.feature.mail.account.api.BaseAccount
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.receiver.InAppNotificationEvent
import net.thunderbird.feature.notification.api.receiver.InAppNotificationReceiver
Expand All @@ -25,6 +23,12 @@ import net.thunderbird.feature.notification.api.sender.NotificationSender
@Composable
private fun SecretDebugSettingsScreenPreview() {
koinPreview {
single<InAppNotificationReceiver> {
object : InAppNotificationReceiver {
override val events: SharedFlow<InAppNotificationEvent>
get() = error("not implemented")
}
}
single<DebugNotificationSectionViewModel> {
DebugNotificationSectionViewModel(
stringsResourceManager = object : StringsResourceManager {
Expand All @@ -47,13 +51,10 @@ private fun SecretDebugSettingsScreenPreview() {
notificationSender = object : NotificationSender {
override fun send(
notification: Notification,
): Flow<Outcome<Success<Notification>, Failure<Notification>>> =
): Flow<NotificationCommandOutcome<Notification>> =
error("not implemented")
},
notificationReceiver = object : InAppNotificationReceiver {
override val events: SharedFlow<InAppNotificationEvent>
get() = error("not implemented")
},
notificationReceiver = get(),
)
}
} WithContent {
Expand Down
1 change: 1 addition & 0 deletions feature/notification/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
implementation(projects.core.featureflag)
implementation(projects.core.outcome)
}
commonTest.dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ package net.thunderbird.feature.notification.api
* @property value The integer value of the notification ID.
*/
@JvmInline
value class NotificationId(val value: Int) : Comparable<Int> by value
value class NotificationId(val value: Int) : Comparable<Int> by value {
companion object {
val Undefined = NotificationId(Int.MIN_VALUE)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package net.thunderbird.feature.notification.api.command

import androidx.annotation.Discouraged
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.NotificationId
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier

Expand All @@ -24,45 +22,5 @@ abstract class NotificationCommand<TNotification : Notification>(
* Executes the command.
* @return The result of the execution.
*/
abstract suspend fun execute(): Outcome<Success<TNotification>, Failure<TNotification>>

/**
* Represents the outcome of a command's execution.
*/
sealed interface CommandOutcome

/**
* Represents a successful command execution.
*
* @param TNotification The type of notification associated with the command.
* @property notificationId The ID of the notification that was successfully acted upon.
* @property command The command that was executed successfully.
*/
data class Success<out TNotification : Notification>(
val notificationId: NotificationId,
val command: NotificationCommand<out TNotification>,
) : CommandOutcome {
companion object {
@Discouraged(
message = "This is a utility function to enable usage in Java code. " +
"Use Success(NotificationId, NotificationCommand) instead.",
)
operator fun invoke(
notificationId: Int,
command: NotificationCommand<*>,
): Success<Notification> = Success(NotificationId(notificationId), command)
}
}

/**
* Represents a failed command execution.
*
* @param TNotification The type of notification associated with the command.
* @property command The command that failed.
* @property throwable The exception that caused the failure.
*/
data class Failure<out TNotification : Notification>(
val command: NotificationCommand<out TNotification>?,
val throwable: Throwable,
) : CommandOutcome
abstract suspend fun execute(): NotificationCommandOutcome<TNotification>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.thunderbird.feature.notification.api.command.outcome

import net.thunderbird.feature.notification.api.command.NotificationCommand
import net.thunderbird.feature.notification.api.content.Notification

/**
* Represents a failure that occurred while attempting to execute a command.
*
* Use this when a command was recognized and supported, but its execution failed due to an error
* (e.g., I/O failure, unexpected state, or an exception thrown by the underlying system).
*
* @param TNotification The type of notification associated with the command.
* @property command The command whose execution failed. May be null if the command couldn't be
* fully constructed.
* @property message A human-readable description of the failure. Defaults to a generic message.
* @property throwable An optional exception that provides additional context about the failure.
*/
data class CommandExecutionFailed<out TNotification : Notification>(
override val command: NotificationCommand<out TNotification>,
val message: String = "Command failed to execute.",
val throwable: Throwable? = null,
) : Failure<TNotification>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.thunderbird.feature.notification.api.command.outcome

import net.thunderbird.feature.notification.api.command.NotificationCommand
import net.thunderbird.feature.notification.api.content.Notification

data class CommandNotCreated<out TNotification : Notification>(
val message: String,
) : Failure<TNotification> {
override val command: NotificationCommand<out TNotification>? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
@file:JvmName("NotificationCommandOutcome")

package net.thunderbird.feature.notification.api.command.outcome

import androidx.annotation.Discouraged
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.NotificationId
import net.thunderbird.feature.notification.api.command.NotificationCommand
import net.thunderbird.feature.notification.api.content.Notification

/**
* Outcome type for executing a NotificationCommand.
*
* When executing a command, the result will be either a [Success] or a [Failure].
*/
typealias NotificationCommandOutcome<TNotification> = Outcome<Success<TNotification>, Failure<TNotification>>

/**
* Represents a successful command execution.
*
* @param TNotification The type of notification associated with the command.
* @property notificationId The ID of the notification that was successfully acted upon.
* @property command The command that was executed successfully.
*/
sealed interface Success<out TNotification : Notification> {
val notificationId: NotificationId
val command: NotificationCommand<out TNotification>?

/**
* Indicates that the command was executed as requested.
*
* This is the canonical success case that carries the command instance that was executed.
*
* @param TNotification The type of notification associated with the command.
* @property command The concrete command that was executed.
*/
@ConsistentCopyVisibility
data class Executed<out TNotification : Notification> internal constructor(
override val notificationId: NotificationId,
override val command: NotificationCommand<out TNotification>,
) : Success<TNotification>

/**
* Indicates that executing the command resulted in no operation.
*
* This can be used when the system is already in the desired state or when there is
* intentionally nothing to do. The [command] may be null if there isn't a specific
* command instance associated with the no-op result.
*
* @param TNotification The type of notification associated with the command.
* @property command The command related to this no-op outcome, if any.
*/
data class NoOperation<out TNotification : Notification>(
override val command: NotificationCommand<out TNotification>? = null,
) : Success<TNotification> {
override val notificationId: NotificationId = NotificationId.Undefined
}
}

/**
* Convenience factory that wraps the given command in a [Success.Executed] instance.
*/
fun <TNotification : Notification> Success(
notificationId: NotificationId,
command: NotificationCommand<out TNotification>,
): Success<TNotification> = Success.Executed(notificationId, command)

/**
* Convenience factory that wraps the given command in a [Success.Executed] instance.
*/
@Discouraged(
message = "This is a utility function to enable usage in Java code. " +
"Use Success(NotificationId, NotificationCommand) instead.",
)
fun <TNotification : Notification> Success(
notificationId: Int,
command: NotificationCommand<out TNotification>,
): Success<TNotification> = Success.Executed(NotificationId(notificationId), command)

/**
* Represents a failed command execution.
*
* @param TNotification The type of notification associated with the command.
* @property command The command that failed.
*/
sealed interface Failure<out TNotification : Notification> {
val command: NotificationCommand<out TNotification>?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package net.thunderbird.feature.notification.api.command.outcome

import net.thunderbird.core.featureflag.FeatureFlagKey
import net.thunderbird.feature.notification.api.command.NotificationCommand
import net.thunderbird.feature.notification.api.content.Notification

/**
* Represents a command that cannot be executed because it is not supported in the current context.
*
* Typical reasons include disabled feature flags or an unknown/unsupported command on the
* current platform or build variant.
*
* @param TNotification The type of notification associated with the command.
* @property command The command that was deemed unsupported. May be null when the unsupported
* state is determined before a concrete command instance is created.
* @property reason The specific reason why the command is not supported.
*/
data class UnsupportedCommand<out TNotification : Notification>(
override val command: NotificationCommand<out TNotification>?,
val reason: Reason,
) : Failure<TNotification> {

/**
* Describes why a command is unsupported.
*/
sealed interface Reason {
/**
* The command is behind a feature flag that is currently disabled.
*
* @property key The feature flag key that disabled this command.
*/
data class FeatureFlagDisabled(val key: FeatureFlagKey) : Reason

/**
* A generic, unknown reason for an unsupported command.
*
* @property message A human-readable description of the issue.
* @property throwable An optional underlying exception that provides more context.
*/
data class Unknown(val message: String, val throwable: Throwable? = null) : Reason
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package net.thunderbird.feature.notification.api.dismisser
import kotlinx.coroutines.flow.Flow
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.NotificationId
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.command.outcome.Failure
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
import net.thunderbird.feature.notification.api.command.outcome.Success
import net.thunderbird.feature.notification.api.content.Notification

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

/**
* Dismisses a notification.
Expand All @@ -28,5 +29,5 @@ interface NotificationDismisser {
* The [Outcome] will be a [Success] containing the dismissed [Notification] if the operation was successful,
* or a [Failure] containing the [Notification] if the operation failed.
*/
fun dismiss(notification: Notification): Flow<Outcome<Success<Notification>, Failure<Notification>>>
fun dismiss(notification: Notification): Flow<NotificationCommandOutcome<Notification>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.dismisser.NotificationDismisser

Expand Down Expand Up @@ -45,6 +43,6 @@ class NotificationDismisserCompat @JvmOverloads constructor(
}

fun interface OnResultListener {
fun onResult(outcome: Outcome<Success<Notification>, Failure<Notification>>)
fun onResult(outcome: NotificationCommandOutcome<Notification>)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package net.thunderbird.feature.notification.api.sender

import kotlinx.coroutines.flow.Flow
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
import net.thunderbird.feature.notification.api.content.Notification

/**
Expand All @@ -15,7 +13,7 @@ fun interface NotificationSender {
* Sends a notification by creating and executing the appropriate commands.
*
* @param notification The [Notification] to be sent.
* @return A [Flow] that emits the [NotificationCommand.CommandOutcome] for each executed command.
* @return A [Flow] that emits the [NotificationCommand] for each executed command.
*/
fun send(notification: Notification): Flow<Outcome<Success<Notification>, Failure<Notification>>>
fun send(notification: Notification): Flow<NotificationCommandOutcome<Notification>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.command.outcome.NotificationCommandOutcome
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.sender.NotificationSender

Expand Down Expand Up @@ -45,6 +43,6 @@ class NotificationSenderCompat @JvmOverloads constructor(
}

fun interface OnResultListener {
fun onResult(outcome: Outcome<Success<Notification>, Failure<Notification>>)
fun onResult(outcome: NotificationCommandOutcome<Notification>)
}
}
Loading
Loading