diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debug/settings/notification/DebugNotificationSectionViewModel.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debug/settings/notification/DebugNotificationSectionViewModel.kt
index b3554185cab..aba33d8c01a 100644
--- a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debug/settings/notification/DebugNotificationSectionViewModel.kt
+++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debug/settings/notification/DebugNotificationSectionViewModel.kt
@@ -201,20 +201,26 @@ internal class DebugNotificationSectionViewModel(
state: State,
): Notification? = when (notificationType) {
AuthenticationErrorNotification::class -> AuthenticationErrorNotification(
+ isIncomingServerError = true,
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
+ accountNumber = 0,
)
CertificateErrorNotification::class -> CertificateErrorNotification(
+ isIncomingServerError = true,
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
+ accountNumber = 0,
)
FailedToCreateNotification::class -> FailedToCreateNotification(
accountUuid = selectedAccount.uuid,
failedNotification = AuthenticationErrorNotification(
+ isIncomingServerError = true,
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
+ accountNumber = 0,
),
)
diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/InAppNotificationHost.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/InAppNotificationHost.kt
index 6863d66e7a5..e55d54ac12b 100644
--- a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/InAppNotificationHost.kt
+++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/InAppNotificationHost.kt
@@ -100,7 +100,7 @@ fun InAppNotificationHost(
val state by hostStateHolder.currentInAppNotificationHostState.collectAsState()
- LaunchedEffect(inAppNotificationEvents) {
+ LaunchedEffect(inAppNotificationEvents, eventFilter) {
val event = inAppNotificationEvents
if (event != null && eventFilter(event)) {
when (event) {
diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/layout/InAppNotificationHostLayout.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/layout/InAppNotificationHostLayout.kt
index 39d78fba6da..908654c259c 100644
--- a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/layout/InAppNotificationHostLayout.kt
+++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/layout/InAppNotificationHostLayout.kt
@@ -15,6 +15,7 @@ import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
@@ -73,10 +74,17 @@ internal fun InAppNotificationHostLayout(
// In case the maxHeight is not defined (for example when no content is passed to the content lambda),
// we manually calculate the layout height to avoid a crash caused by the pre-condition check
// of the layout function.
- val layoutHeight = if (constraints.maxHeight == Constraints.Infinity) {
- bannerGlobalHeight + bannerInlineListHeight.roundToInt() + mainContentHeight
- } else {
- constraints.maxHeight
+ val layoutHeight = when {
+ constraints.minHeight == 0 ||
+ constraints.maxHeight == Constraints.Infinity -> {
+ constraints.constrainHeight(
+ bannerGlobalHeight + bannerInlineListHeight.roundToInt() + mainContentHeight,
+ )
+ }
+
+ else -> {
+ constraints.maxHeight
+ }
}
layout(layoutWidth, layoutHeight) {
diff --git a/feature/notification/api/src/commonMain/composeResources/values/strings.xml b/feature/notification/api/src/commonMain/composeResources/values/strings.xml
index 62b41da29e6..14fea1b75db 100644
--- a/feature/notification/api/src/commonMain/composeResources/values/strings.xml
+++ b/feature/notification/api/src/commonMain/composeResources/values/strings.xml
@@ -15,23 +15,23 @@
An error has occurred while trying to create a system notification for a new message. The reason is most likely a missing notification sound.\n\nTap to open notification settings.
Authentication failed
- Authentication failed for %s. Update your server settings.
+ Authentication failed for %1$s. Update your server settings.
Certificate error
- Certificate error for %s
+ Certificate error for %1$s
Check your server settings
Checking mail: %1$s: %2$s
Checking mail
%1$s: %2$s
- Sending mail: %s
+ Sending mail: %1$s
Sending mail
Failed to send some messages
- - %d new message
- - %d new messages
+ - %1$d new message
+ - %1$d new messages
+ %1$d more on %2$s
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AppNotification.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AppNotification.kt
index ee791336678..b4df1ef0c25 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AppNotification.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AppNotification.kt
@@ -30,6 +30,7 @@ import net.thunderbird.feature.notification.api.ui.style.SystemNotificationStyle
* @see AppNotification
*/
sealed interface Notification {
+ val accountUuid: String?
val title: String
val accessibilityText: String
val contentText: String?
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AuthenticationErrorNotification.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AuthenticationErrorNotification.kt
index 0ce79c5d8a5..606c161c88b 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AuthenticationErrorNotification.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/AuthenticationErrorNotification.kt
@@ -6,6 +6,7 @@ import net.thunderbird.feature.notification.api.ui.action.NotificationAction
import net.thunderbird.feature.notification.api.ui.icon.AuthenticationError
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons
+import net.thunderbird.feature.notification.api.ui.style.inAppNotificationStyle
import net.thunderbird.feature.notification.resources.api.Res
import net.thunderbird.feature.notification.resources.api.notification_authentication_error_text
import net.thunderbird.feature.notification.resources.api.notification_authentication_error_title
@@ -18,16 +19,25 @@ import org.jetbrains.compose.resources.getString
*/
@ConsistentCopyVisibility
data class AuthenticationErrorNotification private constructor(
+ val isIncomingServerError: Boolean,
+ override val accountUuid: String,
+ val accountNumber: Int,
override val title: String,
override val contentText: String?,
override val channel: NotificationChannel,
override val icon: NotificationIcon = NotificationIcons.AuthenticationError,
) : AppNotification(), SystemNotification, InAppNotification {
override val severity: NotificationSeverity = NotificationSeverity.Fatal
- override val actions: Set = setOf(
- NotificationAction.Retry,
- NotificationAction.UpdateServerSettings,
- )
+ override val actions: Set = buildSet {
+ val action = if (isIncomingServerError) {
+ NotificationAction.UpdateIncomingServerSettings(accountUuid, accountNumber)
+ } else {
+ NotificationAction.UpdateOutgoingServerSettings(accountUuid, accountNumber)
+ }
+ add(action)
+ add(NotificationAction.Tap(override = action))
+ }
+ override val inAppNotificationStyle = inAppNotificationStyle { bannerInline() }
override fun asLockscreenNotification(): SystemNotification.LockscreenNotification =
SystemNotification.LockscreenNotification(
@@ -45,12 +55,17 @@ data class AuthenticationErrorNotification private constructor(
suspend operator fun invoke(
accountUuid: String,
accountDisplayName: String,
+ accountNumber: Int,
+ isIncomingServerError: Boolean,
): AuthenticationErrorNotification = AuthenticationErrorNotification(
- title = getString(
- resource = Res.string.notification_authentication_error_title,
+ isIncomingServerError = isIncomingServerError,
+ accountUuid = accountUuid,
+ accountNumber = accountNumber,
+ title = getString(resource = Res.string.notification_authentication_error_title),
+ contentText = getString(
+ resource = Res.string.notification_authentication_error_text,
accountDisplayName,
),
- contentText = getString(resource = Res.string.notification_authentication_error_text),
channel = NotificationChannel.Miscellaneous(accountUuid = accountUuid),
)
}
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/CertificateErrorNotification.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/CertificateErrorNotification.kt
index 61fc5458308..980b05c6d6a 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/CertificateErrorNotification.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/CertificateErrorNotification.kt
@@ -20,6 +20,9 @@ import org.jetbrains.compose.resources.getString
*/
@ConsistentCopyVisibility
data class CertificateErrorNotification private constructor(
+ val isIncomingServerError: Boolean,
+ override val accountUuid: String,
+ val accountNumber: Int,
override val title: String,
override val contentText: String,
val lockScreenTitle: String,
@@ -27,7 +30,13 @@ data class CertificateErrorNotification private constructor(
override val icon: NotificationIcon = NotificationIcons.CertificateError,
) : AppNotification(), SystemNotification, InAppNotification {
override val severity: NotificationSeverity = NotificationSeverity.Fatal
- override val actions: Set = setOf(NotificationAction.UpdateServerSettings)
+ override val actions: Set = setOf(
+ if (isIncomingServerError) {
+ NotificationAction.UpdateIncomingServerSettings(accountUuid, accountNumber)
+ } else {
+ NotificationAction.UpdateOutgoingServerSettings(accountUuid, accountNumber)
+ },
+ )
override fun asLockscreenNotification(): SystemNotification.LockscreenNotification =
SystemNotification.LockscreenNotification(
@@ -45,7 +54,12 @@ data class CertificateErrorNotification private constructor(
suspend operator fun invoke(
accountUuid: String,
accountDisplayName: String,
+ accountNumber: Int,
+ isIncomingServerError: Boolean,
): CertificateErrorNotification = CertificateErrorNotification(
+ isIncomingServerError = isIncomingServerError,
+ accountUuid = accountUuid,
+ accountNumber = accountNumber,
title = getString(resource = Res.string.notification_certificate_error_title, accountDisplayName),
lockScreenTitle = getString(resource = Res.string.notification_certificate_error_public),
contentText = getString(resource = Res.string.notification_certificate_error_text),
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/FailedToCreateNotification.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/FailedToCreateNotification.kt
index 2df7d6e2dbd..404112edb09 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/FailedToCreateNotification.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/FailedToCreateNotification.kt
@@ -18,6 +18,7 @@ import org.jetbrains.compose.resources.getString
*/
@ConsistentCopyVisibility
data class FailedToCreateNotification private constructor(
+ override val accountUuid: String,
override val title: String,
override val contentText: String?,
override val channel: NotificationChannel,
@@ -38,6 +39,7 @@ data class FailedToCreateNotification private constructor(
accountUuid: String,
failedNotification: AppNotification,
): FailedToCreateNotification = FailedToCreateNotification(
+ accountUuid = accountUuid,
title = getString(resource = Res.string.notification_notify_error_title),
contentText = getString(resource = Res.string.notification_notify_error_text),
channel = NotificationChannel.Miscellaneous(accountUuid = accountUuid),
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/MailNotification.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/MailNotification.kt
index 0f2220b2ba9..349c21cb5c8 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/MailNotification.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/MailNotification.kt
@@ -34,6 +34,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
override val severity: NotificationSeverity = NotificationSeverity.Information
data class Fetching(
+ override val accountUuid: String,
override val title: String,
override val accessibilityText: String,
override val contentText: String?,
@@ -61,6 +62,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
): Fetching {
val title = getString(resource = Res.string.notification_bg_sync_title)
return Fetching(
+ accountUuid = accountUuid,
title = title,
accessibilityText = folderName?.let { folderName ->
getString(
@@ -83,6 +85,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
}
data class Sending(
+ override val accountUuid: String,
override val title: String,
override val accessibilityText: String,
override val contentText: String?,
@@ -106,6 +109,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
accountUuid: String,
accountDisplayName: String,
): Sending = Sending(
+ accountUuid = accountUuid,
title = getString(resource = Res.string.notification_bg_send_title),
accessibilityText = getString(
resource = Res.string.notification_bg_send_ticker,
@@ -118,6 +122,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
}
data class SendFailed(
+ override val accountUuid: String,
override val title: String,
override val contentText: String?,
override val channel: NotificationChannel,
@@ -143,6 +148,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
accountUuid: String,
exception: Exception,
): SendFailed = SendFailed(
+ accountUuid = accountUuid,
title = getString(resource = Res.string.send_failure_subject),
contentText = exception.rootCauseMassage,
channel = NotificationChannel.Miscellaneous(accountUuid = accountUuid),
@@ -163,7 +169,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
* @property group The notification group this notification belongs to, if any.
*/
data class NewMailSingleMail(
- val accountUuid: String,
+ override val accountUuid: String,
val accountName: String,
val messagesNotificationChannelSuffix: String,
val summary: String,
@@ -208,7 +214,7 @@ sealed class MailNotification : AppNotification(), SystemNotification {
*/
@ConsistentCopyVisibility
data class NewMailSummaryMail private constructor(
- val accountUuid: String,
+ override val accountUuid: String,
val accountName: String,
val messagesNotificationChannelSuffix: String,
override val title: String,
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/NotificationFactoryCoroutineCompat.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/NotificationFactoryCoroutineCompat.kt
new file mode 100644
index 00000000000..6390aa06b75
--- /dev/null
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/NotificationFactoryCoroutineCompat.kt
@@ -0,0 +1,10 @@
+package net.thunderbird.feature.notification.api.content
+
+import androidx.annotation.Discouraged
+import kotlinx.coroutines.runBlocking
+
+object NotificationFactoryCoroutineCompat {
+ @JvmStatic
+ @Discouraged("Should not be used outside a Java class.")
+ fun create(builder: suspend () -> T): T = runBlocking { builder() }
+}
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 60196f54890..170c01b0e1c 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
@@ -42,6 +42,8 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
override val actions: Set,
override val icon: NotificationIcon = NotificationIcons.PushServiceInitializing,
) : PushServiceNotification() {
+ override val accountUuid: String? = null
+
companion object {
/**
* Creates an [Initializing] notification.
@@ -67,6 +69,8 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
override val actions: Set,
override val icon: NotificationIcon = NotificationIcons.PushServiceListening,
) : PushServiceNotification() {
+ override val accountUuid: String? = null
+
companion object {
/**
* Creates a new [Listening] push service notification.
@@ -92,6 +96,8 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
override val actions: Set,
override val icon: NotificationIcon = NotificationIcons.PushServiceWaitBackgroundSync,
) : PushServiceNotification() {
+ override val accountUuid: String? = null
+
companion object {
/**
* Creates a [WaitBackgroundSync] notification.
@@ -117,6 +123,8 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
override val actions: Set,
override val icon: NotificationIcon = NotificationIcons.PushServiceWaitNetwork,
) : PushServiceNotification() {
+ override val accountUuid: String? = null
+
companion object {
/**
* Creates a [WaitNetwork] notification.
@@ -145,6 +153,7 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
override val contentText: String?,
override val icon: NotificationIcon = NotificationIcons.AlarmPermissionMissing,
) : PushServiceNotification(), InAppNotification {
+ override val accountUuid: String? = null
override val severity: NotificationSeverity = NotificationSeverity.Critical
companion object {
@@ -170,7 +179,7 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification {
* @return A set of [NotificationAction] instances.
*/
private suspend fun buildNotificationActions(): Set = setOf(
- NotificationAction.Tap,
+ NotificationAction.Tap(),
NotificationAction.CustomAction(
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/receiver/InAppNotificationReceiver.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/InAppNotificationReceiver.kt
index 0f021255f7c..8a5499c59ce 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/InAppNotificationReceiver.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/InAppNotificationReceiver.kt
@@ -14,6 +14,8 @@ interface InAppNotificationReceiver {
}
sealed interface InAppNotificationEvent {
- data class Show(val notification: InAppNotification) : InAppNotificationEvent
- data class Dismiss(val notification: InAppNotification) : InAppNotificationEvent
+ val notification: InAppNotification
+
+ data class Show(override val notification: InAppNotification) : InAppNotificationEvent
+ data class Dismiss(override val notification: InAppNotification) : InAppNotificationEvent
}
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/sender/NotificationSender.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/sender/NotificationSender.kt
index e09dd5e89ca..4cc4051c8fe 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/sender/NotificationSender.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/sender/NotificationSender.kt
@@ -10,7 +10,7 @@ import net.thunderbird.feature.notification.api.content.Notification
/**
* Responsible for sending notifications by creating and executing the appropriate commands.
*/
-interface NotificationSender {
+fun interface NotificationSender {
/**
* Sends a notification by creating and executing the appropriate commands.
*
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 35111a9921d..cc6cde1f070 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
@@ -39,10 +39,12 @@ sealed class NotificationAction {
*
* All [SystemNotification] will have this action implicitly, even if not specified in the
* [SystemNotification.actions] set.
+ *
+ * @property override The action that will override the tap action for this notification.
*/
- data object Tap : NotificationAction() {
- override val icon: NotificationIcon? = null
- override val titleResource: StringResource? = null
+ data class Tap(val override: NotificationAction? = null) : NotificationAction() {
+ override val icon: NotificationIcon? = override?.icon
+ override val titleResource: StringResource? = override?.titleResource
}
/**
@@ -93,9 +95,16 @@ sealed class NotificationAction {
/**
* Action to prompt the user to update server settings, typically when authentication fails.
*/
- data object UpdateServerSettings : NotificationAction() {
+ data class UpdateIncomingServerSettings(val accountUuid: String, val accountNumber: Int) : NotificationAction() {
override val icon: NotificationIcon = NotificationActionIcons.UpdateServerSettings
+ override val titleResource: StringResource = Res.string.notification_action_update_server_settings
+ }
+ /**
+ * Action to prompt the user to update server settings, typically when authentication fails.
+ */
+ data class UpdateOutgoingServerSettings(val accountUuid: String, val accountNumber: Int) : NotificationAction() {
+ override val icon: NotificationIcon = NotificationActionIcons.UpdateServerSettings
override val titleResource: StringResource = Res.string.notification_action_update_server_settings
}
diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/host/visual/InAppNotificationVisual.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/host/visual/InAppNotificationVisual.kt
index 835486c0591..5ad9a41d1ef 100644
--- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/host/visual/InAppNotificationVisual.kt
+++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/host/visual/InAppNotificationVisual.kt
@@ -62,7 +62,7 @@ data class BannerGlobalVisual(
},
severity = notification.severity,
action = notification
- .actions
+ .actionsWithoutTap
.let { actions ->
check(actions.size in 0..1) {
"A notification with a BannerGlobalNotification style must have at zero or one action"
@@ -125,7 +125,7 @@ data class BannerInlineVisual(
supportingText = checkContentText(notification.contentText),
severity = notification.severity,
actions = notification
- .actions
+ .actionsWithoutTap
.let { actions ->
check(actions.size in 1..2) {
"A notification with a BannerInlineNotification style must have at one or two actions"
@@ -197,7 +197,7 @@ data class SnackbarVisual(
message = checkNotNull(notification.contentText) {
"A notification with a SnackbarNotification style must have a contentText not null"
},
- action = checkNotNull(notification.actions.singleOrNull()) {
+ action = checkNotNull(notification.actionsWithoutTap.singleOrNull()) {
"A notification with a SnackbarNotification style must have exactly one action"
},
duration = style.duration,
@@ -219,3 +219,8 @@ private inline fun <
transform(style)
}
}
+
+private val InAppNotification.actionsWithoutTap: Set
+ get() = actions
+ .filterNot { it is NotificationAction.Tap }
+ .toSet()
diff --git a/feature/notification/api/src/commonTest/kotlin/net/thunderbird/feature/notification/api/receiver/compat/InAppNotificationReceiverCompatTest.kt b/feature/notification/api/src/commonTest/kotlin/net/thunderbird/feature/notification/api/receiver/compat/InAppNotificationReceiverCompatTest.kt
index d281cfc78d1..8d8b2cfe5e8 100644
--- a/feature/notification/api/src/commonTest/kotlin/net/thunderbird/feature/notification/api/receiver/compat/InAppNotificationReceiverCompatTest.kt
+++ b/feature/notification/api/src/commonTest/kotlin/net/thunderbird/feature/notification/api/receiver/compat/InAppNotificationReceiverCompatTest.kt
@@ -1,7 +1,5 @@
package net.thunderbird.feature.notification.api.receiver.compat
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.dp
import assertk.assertThat
import assertk.assertions.containsExactlyInAnyOrder
import dev.mokkery.matcher.any
@@ -15,13 +13,10 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import net.thunderbird.feature.notification.api.NotificationSeverity
-import net.thunderbird.feature.notification.api.content.AppNotification
-import net.thunderbird.feature.notification.api.content.InAppNotification
import net.thunderbird.feature.notification.api.receiver.InAppNotificationEvent
import net.thunderbird.feature.notification.api.receiver.InAppNotificationReceiver
import net.thunderbird.feature.notification.api.receiver.compat.InAppNotificationReceiverCompat.OnReceiveEventListener
-import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
+import net.thunderbird.feature.notification.testing.fake.FakeInAppOnlyNotification
class InAppNotificationReceiverCompatTest {
@OptIn(ExperimentalCoroutinesApi::class)
@@ -34,10 +29,10 @@ class InAppNotificationReceiverCompatTest {
val title = "notification $index"
when {
index % 2 == 0 -> {
- InAppNotificationEvent.Show(FakeNotification(title = title))
+ InAppNotificationEvent.Show(FakeInAppOnlyNotification(title = title))
}
- else -> InAppNotificationEvent.Dismiss(FakeNotification(title = title))
+ else -> InAppNotificationEvent.Dismiss(FakeInAppOnlyNotification(title = title))
}
}.toTypedArray()
val onReceiveEventListener = spy(
@@ -69,18 +64,4 @@ class InAppNotificationReceiverCompatTest {
_events.emit(event)
}
}
-
- data class FakeNotification(
- override val title: String = "fake title",
- override val contentText: String? = "fake content",
- override val severity: NotificationSeverity = NotificationSeverity.Information,
- override val icon: NotificationIcon = NotificationIcon(
- inAppNotificationIcon = ImageVector.Builder(
- defaultWidth = 0.dp,
- defaultHeight = 0.dp,
- viewportWidth = 0f,
- viewportHeight = 0f,
- ).build(),
- ),
- ) : AppNotification(), InAppNotification
}
diff --git a/feature/notification/impl/build.gradle.kts b/feature/notification/impl/build.gradle.kts
index f5e5006b877..b28b48d4dc5 100644
--- a/feature/notification/impl/build.gradle.kts
+++ b/feature/notification/impl/build.gradle.kts
@@ -16,6 +16,10 @@ kotlin {
implementation(projects.core.logging.testing)
implementation(projects.feature.notification.testing)
}
+ androidMain.dependencies {
+ // should split feature.launcher into api/impl?
+ implementation(projects.feature.launcher)
+ }
androidUnitTest.dependencies {
implementation(libs.androidx.test.core)
implementation(libs.mockito.core)
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
index 2fe43819ef5..25fac51a3c9 100644
--- 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
@@ -5,6 +5,7 @@ import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
import net.thunderbird.feature.notification.impl.intent.action.AlarmPermissionMissingNotificationTapActionIntentCreator
import net.thunderbird.feature.notification.impl.intent.action.DefaultNotificationActionIntentCreator
import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator
+import net.thunderbird.feature.notification.impl.intent.action.UpdateServerSettingsNotificationActionIntentCreator
import net.thunderbird.feature.notification.impl.receiver.AndroidSystemNotificationNotifier
import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier
import net.thunderbird.feature.notification.impl.ui.action.DefaultSystemNotificationActionCreator
@@ -22,6 +23,10 @@ internal actual val platformFeatureNotificationModule: Module = module {
context = androidApplication(),
logger = get(),
),
+ UpdateServerSettingsNotificationActionIntentCreator(
+ context = androidApplication(),
+ logger = get(),
+ ),
// The Default implementation must always be the last.
DefaultNotificationActionIntentCreator(
logger = get(),
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/UpdateServerSettingsNotificationActionIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/UpdateServerSettingsNotificationActionIntentCreator.kt
new file mode 100644
index 00000000000..beb0eec4d25
--- /dev/null
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/UpdateServerSettingsNotificationActionIntentCreator.kt
@@ -0,0 +1,58 @@
+package net.thunderbird.feature.notification.impl.intent.action
+
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.content.Context
+import androidx.core.app.PendingIntentCompat
+import app.k9mail.feature.launcher.FeatureLauncherActivity
+import app.k9mail.feature.launcher.FeatureLauncherTarget
+import net.thunderbird.core.logging.Logger
+import net.thunderbird.feature.notification.api.content.AuthenticationErrorNotification
+import net.thunderbird.feature.notification.api.content.Notification
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+
+private const val TAG = "UpdateServerSettingsNotificationActionIntentCreator"
+
+class UpdateServerSettingsNotificationActionIntentCreator(
+ private val context: Context,
+ private val logger: Logger,
+) : NotificationActionIntentCreator {
+ override fun accept(notification: Notification, action: NotificationAction): Boolean =
+ notification is AuthenticationErrorNotification &&
+ (
+ action is NotificationAction.UpdateIncomingServerSettings ||
+ action is NotificationAction.UpdateOutgoingServerSettings
+ )
+
+ override fun create(
+ notification: AuthenticationErrorNotification,
+ action: NotificationAction,
+ ): PendingIntent? {
+ logger.debug(TAG) { "create() called with: notification = $notification, action = $action" }
+ val (accountNumber, intent) = when (action) {
+ is NotificationAction.UpdateIncomingServerSettings -> {
+ action.accountNumber to FeatureLauncherActivity.getIntent(
+ context = context,
+ target = FeatureLauncherTarget.AccountEditIncomingSettings(action.accountUuid),
+ )
+ }
+
+ is NotificationAction.UpdateOutgoingServerSettings -> {
+ action.accountNumber to FeatureLauncherActivity.getIntent(
+ context = context,
+ target = FeatureLauncherTarget.AccountEditOutgoingSettings(action.accountUuid),
+ )
+ }
+
+ else -> error("Unsupported action: $action")
+ }
+
+ return PendingIntentCompat.getActivity(
+ context,
+ accountNumber,
+ intent,
+ FLAG_UPDATE_CURRENT,
+ false,
+ )
+ }
+}
diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/AndroidSystemNotificationNotifier.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/AndroidSystemNotificationNotifier.kt
index 7a1ac13f9e5..a36f33bc240 100644
--- a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/AndroidSystemNotificationNotifier.kt
+++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/AndroidSystemNotificationNotifier.kt
@@ -60,16 +60,23 @@ internal class AndroidSystemNotificationNotifier(
}
}
+ val overrideTap = systemNotification
+ .actions
+ .firstOrNull {
+ it is NotificationAction.Tap && it.override != null
+ } as? NotificationAction.Tap
+
val tapAction = notificationActionCreator.create(
notification = systemNotification,
- action = NotificationAction.Tap,
+ action = overrideTap?.override ?: NotificationAction.Tap(),
)
setContentIntent(tapAction.pendingIntent)
setNotificationStyle(notification = systemNotification)
- if (actions.isNotEmpty()) {
- for (action in actions) {
+ val actionsWithoutTap = actions.filterNot { it is NotificationAction.Tap }
+ if (actionsWithoutTap.isNotEmpty()) {
+ for (action in actionsWithoutTap) {
val notificationAction = notificationActionCreator
.create(notification = systemNotification, action)
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
index 36f8f600496..71dca5a0c33 100644
--- 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
@@ -34,13 +34,14 @@ class DefaultNotificationActionIntentCreatorTest {
fun `accept should return true for any type of notification action`() {
// Arrange
val multipleActions = listOf(
- NotificationAction.Tap,
+ NotificationAction.Tap(),
NotificationAction.Reply,
NotificationAction.MarkAsRead,
NotificationAction.Delete,
NotificationAction.MarkAsSpam,
NotificationAction.Archive,
- NotificationAction.UpdateServerSettings,
+ NotificationAction.UpdateIncomingServerSettings("uuid", 1),
+ NotificationAction.UpdateOutgoingServerSettings("uuid", 1),
NotificationAction.Retry,
NotificationAction.CustomAction(title = "Custom Action 1"),
NotificationAction.CustomAction(title = "Custom Action 2"),
@@ -96,7 +97,7 @@ class DefaultNotificationActionIntentCreatorTest {
val testSubject = createTestSubject(context)
// Act
- testSubject.create(notification = FakeNotification(), action = NotificationAction.Tap)
+ testSubject.create(notification = FakeNotification(), action = NotificationAction.Tap())
// Assert
pendingIntentCompat.verify {
diff --git a/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeInAppOnlyNotification.kt b/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeInAppOnlyNotification.kt
index 6259db449fb..c7acce19f3e 100644
--- a/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeInAppOnlyNotification.kt
+++ b/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeInAppOnlyNotification.kt
@@ -10,6 +10,7 @@ import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.api.ui.style.InAppNotificationStyle
data class FakeInAppOnlyNotification(
+ override val accountUuid: String? = null,
override val title: String = "fake title",
override val contentText: String? = "fake content",
override val severity: NotificationSeverity = NotificationSeverity.Information,
diff --git a/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeNotification.kt b/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeNotification.kt
index e2c4ce02e9a..0bddb633c18 100644
--- a/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeNotification.kt
+++ b/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeNotification.kt
@@ -11,6 +11,7 @@ import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.testing.fake.icon.EMPTY_SYSTEM_NOTIFICATION_ICON
data class FakeNotification(
+ override val accountUuid: String? = null,
override val title: String = "fake title",
override val contentText: String? = "fake content",
override val severity: NotificationSeverity = NotificationSeverity.Information,
diff --git a/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeSystemOnlyNotification.kt b/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeSystemOnlyNotification.kt
index 8136797291c..0d27b8e4f65 100644
--- a/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeSystemOnlyNotification.kt
+++ b/feature/notification/testing/src/commonMain/kotlin/net/thunderbird/feature/notification/testing/fake/FakeSystemOnlyNotification.kt
@@ -8,6 +8,7 @@ import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon
import net.thunderbird.feature.notification.testing.fake.icon.EMPTY_SYSTEM_NOTIFICATION_ICON
data class FakeSystemOnlyNotification(
+ override val accountUuid: String? = null,
override val title: String = "fake title",
override val contentText: String? = "fake content",
override val severity: NotificationSeverity = NotificationSeverity.Information,
diff --git a/legacy/core/build.gradle.kts b/legacy/core/build.gradle.kts
index 2417ba6edbc..a9d4f9f1cd8 100644
--- a/legacy/core/build.gradle.kts
+++ b/legacy/core/build.gradle.kts
@@ -15,6 +15,7 @@ dependencies {
api(projects.core.logging.implFile)
api(projects.core.logging.implComposite)
api(projects.core.android.network)
+ api(projects.core.outcome)
api(projects.feature.mail.folder.api)
api(projects.feature.account.storage.legacy)
@@ -62,6 +63,7 @@ dependencies {
// test fakes
testImplementation(projects.feature.account.fake)
+ testImplementation(projects.feature.notification.testing)
}
android {
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt b/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
index 8c73f80f390..05048efa803 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
@@ -13,6 +13,7 @@ import com.fsck.k9.notification.NotificationController
import com.fsck.k9.notification.NotificationStrategy
import net.thunderbird.core.featureflag.FeatureFlagProvider
import net.thunderbird.core.logging.Logger
+import net.thunderbird.feature.notification.api.sender.NotificationSender
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -32,6 +33,7 @@ val controllerModule = module {
get(named("controllerExtensions")),
get(),
get(named("syncDebug")),
+ get(),
)
}
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
index 0e4554c4be9..14974b24c61 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
@@ -65,7 +65,6 @@
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessageDownloadState;
-import net.thunderbird.core.common.exception.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.power.PowerManager;
@@ -84,9 +83,16 @@
import com.fsck.k9.notification.NotificationStrategy;
import net.thunderbird.core.android.account.DeletePolicy;
import net.thunderbird.core.android.account.LegacyAccountDto;
+import net.thunderbird.core.common.exception.MessagingException;
import net.thunderbird.core.featureflag.FeatureFlagProvider;
+import net.thunderbird.core.featureflag.FeatureFlagResult.Enabled;
+import net.thunderbird.core.featureflag.compat.FeatureFlagProviderCompat;
import net.thunderbird.core.logging.Logger;
import net.thunderbird.core.logging.legacy.Log;
+import net.thunderbird.feature.notification.api.content.AuthenticationErrorNotification;
+import net.thunderbird.feature.notification.api.content.NotificationFactoryCoroutineCompat;
+import net.thunderbird.feature.notification.api.sender.NotificationSender;
+import net.thunderbird.feature.notification.api.sender.compat.NotificationSenderCompat;
import net.thunderbird.feature.search.legacy.LocalMessageSearch;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -135,7 +141,9 @@ public class MessagingController implements MessagingControllerRegistry, Messagi
private final DraftOperations draftOperations;
private final NotificationOperations notificationOperations;
private final ArchiveOperations archiveOperations;
+ private final FeatureFlagProvider featureFlagProvider;
private final Logger syncDebugLogger;
+ private final NotificationSenderCompat notificationSender;
private volatile boolean stopped = false;
@@ -159,7 +167,8 @@ public static MessagingController getInstance(Context context) {
LocalDeleteOperationDecider localDeleteOperationDecider,
List controllerExtensions,
FeatureFlagProvider featureFlagProvider,
- Logger syncDebugLogger
+ Logger syncDebugLogger,
+ NotificationSender notificationSender
) {
this.context = context;
this.notificationController = notificationController;
@@ -171,7 +180,9 @@ public static MessagingController getInstance(Context context) {
this.saveMessageDataCreator = saveMessageDataCreator;
this.specialLocalFoldersCreator = specialLocalFoldersCreator;
this.localDeleteOperationDecider = localDeleteOperationDecider;
+ this.featureFlagProvider = featureFlagProvider;
this.syncDebugLogger = syncDebugLogger;
+ this.notificationSender = new NotificationSenderCompat(notificationSender);
controllerThread = new Thread(new Runnable() {
@Override
@@ -691,7 +702,30 @@ public void handleAuthenticationFailure(LegacyAccountDto account, boolean incomi
migrateAccountToOAuth(account);
}
- notificationController.showAuthenticationErrorNotification(account, incoming);
+ if (FeatureFlagProviderCompat.provide(featureFlagProvider, "display_in_app_notifications") ==
+ Enabled.INSTANCE) {
+ Log.d("handleAuthenticationFailure: sending in-app notification");
+ final AuthenticationErrorNotification notification = NotificationFactoryCoroutineCompat.create(
+ continuation ->
+ AuthenticationErrorNotification.Companion.invoke(
+ account.getUuid(),
+ account.getDisplayName(),
+ account.getAccountNumber(),
+ incoming,
+ continuation
+ )
+ );
+
+ notificationSender.send(notification, outcome -> {
+ Log.v("notificationSender outcome = " + outcome);
+ });
+ }
+
+ if (FeatureFlagProviderCompat.provide(featureFlagProvider,
+ "use_notification_sender_for_system_notifications") != Enabled.INSTANCE) {
+ Log.d("handleAuthenticationFailure: sending system notification via old notification controller");
+ notificationController.showAuthenticationErrorNotification(account, incoming);
+ }
}
private void migrateAccountToOAuth(LegacyAccountDto account) {
@@ -2543,6 +2577,18 @@ public void notifyUserIfCertificateProblem(LegacyAccountDto account, Exception e
}
}
+ public void checkAuthenticationProblem(LegacyAccountDto account) {
+ // checking incoming server configuration
+ if (isAuthenticationProblem(account, true)) {
+ handleAuthenticationFailure(account, true);
+ return;
+ }
+ // checking outgoing server configuration
+ if (isAuthenticationProblem(account, false)) {
+ handleAuthenticationFailure(account, false);
+ }
+ }
+
private boolean isAuthenticationProblem(LegacyAccountDto account, boolean incoming) {
ServerSettings serverSettings = incoming ?
account.getIncomingServerSettings() : account.getOutgoingServerSettings();
diff --git a/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java b/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java
index c33c2a33b28..fd561d4069c 100644
--- a/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java
+++ b/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java
@@ -38,6 +38,9 @@
import com.fsck.k9.notification.NotificationStrategy;
import net.thunderbird.core.common.mail.Protocols;
import net.thunderbird.core.logging.Logger;
+import net.thunderbird.core.outcome.Outcome;
+import net.thunderbird.feature.notification.api.sender.NotificationSender;
+import net.thunderbird.feature.notification.testing.fake.FakeInAppOnlyNotification;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -127,6 +130,10 @@ public void setUp() throws MessagingException {
preferences = Preferences.getPreferences();
featureFlagProvider = key -> Disabled.INSTANCE;
+ final NotificationSender notificationSender = notification ->
+ (flowCollector, continuation) ->
+ Outcome.Companion.success(new FakeInAppOnlyNotification());
+
controller = new MessagingController(
appContext,
notificationController,
@@ -140,7 +147,8 @@ public void setUp() throws MessagingException {
new LocalDeleteOperationDecider(),
Collections.emptyList(),
featureFlagProvider,
- syncLogger
+ syncLogger,
+ notificationSender
);
configureAccount();
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt
index 0a41373b0de..4bc9b866037 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt
@@ -8,22 +8,32 @@ import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
+import app.k9mail.feature.launcher.FeatureLauncherActivity
+import app.k9mail.feature.launcher.FeatureLauncherTarget
import app.k9mail.legacy.message.controller.MessageReference
import com.fsck.k9.contacts.ContactPictureLoader
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
+import com.fsck.k9.ui.messagelist.item.BannerInlineListInAppNotificationViewHolder
import com.fsck.k9.ui.messagelist.item.FooterViewHolder
import com.fsck.k9.ui.messagelist.item.MessageListViewHolder
import com.fsck.k9.ui.messagelist.item.MessageViewHolder
import com.fsck.k9.ui.messagelist.item.MessageViewHolderColors
+import net.thunderbird.core.featureflag.FeatureFlagKey
+import net.thunderbird.core.featureflag.FeatureFlagProvider
+import net.thunderbird.core.featureflag.FeatureFlagResult
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
private const val FOOTER_ID = 1L
private const val TYPE_MESSAGE = 0
private const val TYPE_FOOTER = 1
+private const val TYPE_IN_APP_NOTIFICATION_BANNER_INLINE_LIST = 2
+@Suppress("LongParameterList")
class MessageListAdapter internal constructor(
private val theme: Theme,
private val res: Resources,
@@ -32,6 +42,7 @@ class MessageListAdapter internal constructor(
private val listItemListener: MessageListItemActionListener,
private val appearance: MessageListAppearance,
private val relativeDateTimeFormatter: RelativeDateTimeFormatter,
+ private val featureFlagProvider: FeatureFlagProvider,
) : RecyclerView.Adapter() {
val colors: MessageViewHolderColors = MessageViewHolderColors.resolveColors(theme)
@@ -42,6 +53,7 @@ class MessageListAdapter internal constructor(
val oldMessageList = field
field = value
+ accountUuids = value.map { it.account.uuid }.toSet()
messagesMap = value.associateBy { it.uniqueId }
if (selected.isNotEmpty()) {
@@ -56,6 +68,7 @@ class MessageListAdapter internal constructor(
}
private var messagesMap = emptyMap()
+ private var accountUuids = emptySet()
var activeMessage: MessageReference? = null
set(value) {
@@ -161,6 +174,9 @@ class MessageListAdapter internal constructor(
listItemListener.onToggleMessageSelection(messageListItem)
}
+ private val isInAppNotificationEnabled: Boolean
+ get() = featureFlagProvider.provide(FeatureFlagKey.DisplayInAppNotifications) == FeatureFlagResult.Enabled
+
init {
setHasStableIds(true)
}
@@ -176,7 +192,11 @@ class MessageListAdapter internal constructor(
}
override fun getItemViewType(position: Int): Int {
- return if (position <= lastMessagePosition) TYPE_MESSAGE else TYPE_FOOTER
+ return when {
+ position == 0 && isInAppNotificationEnabled -> TYPE_IN_APP_NOTIFICATION_BANNER_INLINE_LIST
+ position <= lastMessagePosition -> TYPE_MESSAGE
+ else -> TYPE_FOOTER
+ }
}
private fun getItem(position: Int): MessageListItem = messages[position]
@@ -209,6 +229,32 @@ class MessageListAdapter internal constructor(
return when (viewType) {
TYPE_MESSAGE -> createMessageViewHolder(parent)
TYPE_FOOTER -> FooterViewHolder.create(layoutInflater, parent, footerClickListener)
+ TYPE_IN_APP_NOTIFICATION_BANNER_INLINE_LIST if isInAppNotificationEnabled ->
+ BannerInlineListInAppNotificationViewHolder(
+ view = ComposeView(context = parent.context),
+ eventFilter = { event ->
+ val accountUuid = event.notification.accountUuid
+ accountUuid != null && accountUuid in accountUuids
+ },
+ onNotificationActionClick = { action ->
+ when (action) {
+ is NotificationAction.UpdateIncomingServerSettings ->
+ FeatureLauncherActivity.launch(
+ context = parent.context,
+ target = FeatureLauncherTarget.AccountEditIncomingSettings(action.accountUuid),
+ )
+
+ is NotificationAction.UpdateOutgoingServerSettings ->
+ FeatureLauncherActivity.launch(
+ context = parent.context,
+ target = FeatureLauncherTarget.AccountEditOutgoingSettings(action.accountUuid),
+ )
+
+ else -> Unit
+ }
+ },
+ )
+
else -> error("Unsupported type: $viewType")
}
}
@@ -231,6 +277,9 @@ class MessageListAdapter internal constructor(
override fun onBindViewHolder(holder: MessageListViewHolder, position: Int) {
when (val viewType = getItemViewType(position)) {
+ TYPE_IN_APP_NOTIFICATION_BANNER_INLINE_LIST if isInAppNotificationEnabled ->
+ (holder as BannerInlineListInAppNotificationViewHolder).bind()
+
TYPE_MESSAGE -> {
val messageListItem = getItem(position)
val messageViewHolder = holder as MessageViewHolder
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
index 9c49fc1f2be..03d91e85c74 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
@@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.SystemClock
-import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
@@ -15,6 +14,11 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode
+import androidx.compose.animation.animateContentSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.ComposeView
+import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat.Type.navigationBars
@@ -23,6 +27,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.Observer
@@ -61,6 +66,7 @@ import com.google.android.material.textview.MaterialTextView
import java.util.concurrent.Future
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
+import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
@@ -76,11 +82,19 @@ import net.thunderbird.core.android.network.ConnectivityManager
import net.thunderbird.core.architecture.data.DataMapper
import net.thunderbird.core.common.action.SwipeAction
import net.thunderbird.core.common.exception.MessagingException
+import net.thunderbird.core.featureflag.FeatureFlagKey
+import net.thunderbird.core.featureflag.FeatureFlagProvider
+import net.thunderbird.core.featureflag.FeatureFlagResult
import net.thunderbird.core.logging.legacy.Log
import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultLegacyAccountWrapperDataMapper
import net.thunderbird.feature.mail.message.list.domain.DomainContract
import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory
+import net.thunderbird.feature.notification.api.ui.InAppNotificationHost
+import net.thunderbird.feature.notification.api.ui.host.DisplayInAppNotificationFlag
+import net.thunderbird.feature.notification.api.ui.host.visual.SnackbarVisual
+import net.thunderbird.feature.notification.api.ui.style.SnackbarDuration
import net.thunderbird.feature.search.legacy.LocalMessageSearch
import net.thunderbird.feature.search.legacy.SearchAccount
import net.thunderbird.feature.search.legacy.serialization.LocalMessageSearchSerializer
@@ -118,6 +132,8 @@ class MessageListFragment :
private val buildSwipeActions: DomainContract.UseCase.BuildSwipeActions by inject {
parametersOf(preferences.storage)
}
+ private val featureFlagProvider: FeatureFlagProvider by inject()
+ private val featureThemeProvider: FeatureThemeProvider by inject()
private val handler = MessageListHandler(this)
private val activityListener = MessageListActivityListener()
@@ -139,6 +155,7 @@ class MessageListFragment :
private lateinit var fragmentListener: MessageListFragmentListener
private lateinit var recentChangesSnackbar: Snackbar
+ private var coordinatorLayout: CoordinatorLayout? = null
private var recyclerView: RecyclerView? = null
private var itemTouchHelper: ItemTouchHelper? = null
private var swipeRefreshLayout: SwipeRefreshLayout? = null
@@ -317,6 +334,7 @@ class MessageListFragment :
listItemListener = this,
appearance = messageListAppearance,
relativeDateTimeFormatter = RelativeDateTimeFormatter(requireContext(), clock),
+ featureFlagProvider = featureFlagProvider,
).apply {
activeMessage = this@MessageListFragment.activeMessage
}
@@ -325,13 +343,6 @@ class MessageListFragment :
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return if (error == null) {
inflater.inflate(R.layout.message_list_fragment, container, false).also { view ->
- val typedValued = TypedValue()
- requireContext().theme.resolveAttribute(
- com.google.android.material.R.attr.colorSurface,
- typedValued,
- true,
- )
-
setFragmentResultListener(
SetupArchiveFolderDialogFragmentFactory.RESULT_CODE_DISMISS_REQUEST_KEY,
) { key, bundle ->
@@ -474,10 +485,65 @@ class MessageListFragment :
recyclerView.adapter = adapter
+ if (featureFlagProvider.provide(FeatureFlagKey.DisplayInAppNotifications) == FeatureFlagResult.Enabled) {
+ view.findViewById(R.id.banner_global_compose_view).apply {
+ setContent {
+ featureThemeProvider.WithTheme {
+ InAppNotificationHost(
+ onActionClick = { },
+ enabled = persistentSetOf(
+ DisplayInAppNotificationFlag.BannerGlobalNotifications,
+ DisplayInAppNotificationFlag.SnackbarNotifications,
+ ),
+ onSnackbarNotificationEvent = ::onSnackbarInAppNotificationEvent,
+ eventFilter = { event ->
+ val accountUuid = event.notification.accountUuid
+ accountUuid != null && accounts.any { it.uuid == accountUuid }
+ },
+ modifier = Modifier
+ .animateContentSize()
+ .onSizeChanged { size ->
+ recyclerView.updatePadding(top = size.height)
+ },
+ )
+ }
+ }
+ }
+ }
+
this.recyclerView = recyclerView
this.itemTouchHelper = itemTouchHelper
}
+ private fun requireCoordinatorLayout(): CoordinatorLayout {
+ val coordinatorLayout = coordinatorLayout
+ ?: requireView().findViewById(R.id.message_list_coordinator)
+ .also { coordinatorLayout = it }
+
+ return coordinatorLayout ?: error("Coordinator layout not initialized")
+ }
+
+ private suspend fun onSnackbarInAppNotificationEvent(visual: SnackbarVisual) {
+ val (message, action, duration) = visual
+ Snackbar.make(
+ requireCoordinatorLayout(),
+ message,
+ when (duration) {
+ SnackbarDuration.Short -> Snackbar.LENGTH_SHORT
+ SnackbarDuration.Long -> Snackbar.LENGTH_LONG
+ SnackbarDuration.Indefinite -> Snackbar.LENGTH_INDEFINITE
+ },
+ ).apply {
+ if (action != null) {
+ setAction(
+ action.resolveTitle(),
+ ) {
+ // TODO.
+ }
+ }
+ }.show()
+ }
+
private val shouldShowRecentChangesHintObserver = Observer { showRecentChangesHint ->
val recentChangesSnackbarVisible = recentChangesSnackbar.isShown
if (showRecentChangesHint && !recentChangesSnackbarVisible) {
@@ -488,7 +554,7 @@ class MessageListFragment :
}
private fun initializeRecentChangesSnackbar() {
- val coordinatorLayout = requireView().findViewById(R.id.message_list_coordinator)
+ val coordinatorLayout = requireCoordinatorLayout()
recentChangesSnackbar = Snackbar
.make(coordinatorLayout, R.string.changelog_snackbar_text, RECENT_CHANGES_SNACKBAR_DURATION)
@@ -668,6 +734,7 @@ class MessageListFragment :
}
override fun onDestroyView() {
+ coordinatorLayout = null
recyclerView = null
messageListSwipeCallback = null
itemTouchHelper = null
@@ -1618,6 +1685,11 @@ class MessageListFragment :
adapter.restoreSelected(it)
}
+ messageListItems
+ .map { it.account }
+ .toSet()
+ .forEach(messagingController::checkAuthenticationProblem)
+
resetActionMode()
computeBatchDirection()
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageListInAppNotificationViewHolder.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageListInAppNotificationViewHolder.kt
new file mode 100644
index 00000000000..c9b33db9092
--- /dev/null
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageListInAppNotificationViewHolder.kt
@@ -0,0 +1,43 @@
+package com.fsck.k9.ui.messagelist.item
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import kotlinx.collections.immutable.persistentSetOf
+import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
+import net.thunderbird.feature.notification.api.receiver.InAppNotificationEvent
+import net.thunderbird.feature.notification.api.ui.InAppNotificationHost
+import net.thunderbird.feature.notification.api.ui.action.NotificationAction
+import net.thunderbird.feature.notification.api.ui.host.DisplayInAppNotificationFlag
+import org.koin.compose.koinInject
+
+abstract class MessageListInAppNotificationViewHolder(protected val view: ComposeView) : MessageListViewHolder(view) {
+ fun bind() {
+ view.setContent {
+ val themeProvider = koinInject()
+ themeProvider.WithTheme {
+ Content()
+ }
+ }
+ }
+
+ @Composable
+ abstract fun Content()
+}
+
+class BannerInlineListInAppNotificationViewHolder(
+ private val onNotificationActionClick: (NotificationAction) -> Unit,
+ private val eventFilter: (InAppNotificationEvent) -> Boolean,
+ view: ComposeView,
+) : MessageListInAppNotificationViewHolder(view) {
+ @Composable
+ override fun Content() {
+ InAppNotificationHost(
+ onActionClick = onNotificationActionClick,
+ enabled = persistentSetOf(DisplayInAppNotificationFlag.BannerInlineNotifications),
+ eventFilter = eventFilter,
+ modifier = Modifier.animateContentSize(),
+ )
+ }
+}
diff --git a/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml b/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml
index 8b8dd3980c0..66ac02e7df6 100644
--- a/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml
+++ b/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml
@@ -43,6 +43,15 @@
android:scrollbars="vertical"
tools:listitem="@layout/message_list_item"
/>
+
+
+
diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt
index 9882e03ebf3..db22c48bd96 100644
--- a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt
+++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt
@@ -27,6 +27,7 @@ import com.google.android.material.textview.MaterialTextView
import kotlin.time.ExperimentalTime
import net.thunderbird.core.android.account.LegacyAccountDto
import net.thunderbird.core.android.testing.RobolectricTest
+import net.thunderbird.core.featureflag.FeatureFlagResult
import net.thunderbird.core.testing.TestClock
import org.junit.Test
import org.mockito.kotlin.mock
@@ -423,6 +424,7 @@ class MessageListAdapterTest : RobolectricTest() {
listItemListener = listItemListener,
appearance = appearance,
relativeDateTimeFormatter = RelativeDateTimeFormatter(context, TestClock()),
+ featureFlagProvider = { FeatureFlagResult.Disabled },
)
}