diff --git a/app-common/build.gradle.kts b/app-common/build.gradle.kts index e004188de87..56ef6856522 100644 --- a/app-common/build.gradle.kts +++ b/app-common/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.feature.mail.account.api) implementation(projects.feature.migration.provider) implementation(projects.feature.notification.api) + implementation(projects.feature.notification.impl) implementation(projects.feature.widget.messageList) implementation(projects.mail.protocols.imap) diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/AppCommonModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/AppCommonModule.kt index dcec479b5dc..ee713270d7b 100644 --- a/app-common/src/main/kotlin/net/thunderbird/app/common/AppCommonModule.kt +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/AppCommonModule.kt @@ -6,6 +6,7 @@ import com.fsck.k9.legacyUiModules import net.thunderbird.app.common.account.appCommonAccountModule import net.thunderbird.app.common.core.appCommonCoreModule import net.thunderbird.app.common.feature.appCommonFeatureModule +import net.thunderbird.app.common.notification.appCommonNotificationModule import org.koin.core.module.Module import org.koin.dsl.module @@ -18,5 +19,6 @@ val appCommonModule: Module = module { appCommonAccountModule, appCommonCoreModule, appCommonFeatureModule, + appCommonNotificationModule, ) } diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/feature/AppCommonFeatureModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/feature/AppCommonFeatureModule.kt index d68b7d286ee..ddc8e2ae0b5 100644 --- a/app-common/src/main/kotlin/net/thunderbird/app/common/feature/AppCommonFeatureModule.kt +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/feature/AppCommonFeatureModule.kt @@ -3,11 +3,13 @@ package net.thunderbird.app.common.feature import app.k9mail.feature.launcher.FeatureLauncherExternalContract import app.k9mail.feature.launcher.di.featureLauncherModule import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract +import net.thunderbird.feature.notification.impl.inject.featureNotificationModule import org.koin.android.ext.koin.androidContext import org.koin.dsl.module internal val appCommonFeatureModule = module { includes(featureLauncherModule) + includes(featureNotificationModule) factory { AccountSetupFinishedLauncher( diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/notification/AppCommonNotificationModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/notification/AppCommonNotificationModule.kt new file mode 100644 index 00000000000..848dd792360 --- /dev/null +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/notification/AppCommonNotificationModule.kt @@ -0,0 +1,8 @@ +package net.thunderbird.app.common.notification + +import net.thunderbird.feature.notification.api.NotificationIdFactory +import org.koin.dsl.module + +internal val appCommonNotificationModule = module { + single { LegacyNotificationIdFactory() } +} diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/notification/LegacyNotificationIdFactory.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/notification/LegacyNotificationIdFactory.kt new file mode 100644 index 00000000000..3912c429b7f --- /dev/null +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/notification/LegacyNotificationIdFactory.kt @@ -0,0 +1,17 @@ +package net.thunderbird.app.common.notification + +import com.fsck.k9.notification.NotificationIds +import net.thunderbird.feature.notification.api.NotificationId +import net.thunderbird.feature.notification.api.NotificationIdFactory + +// TODO(#9416): Migrate logic from NotificationIds to NotificationIdFactory +class LegacyNotificationIdFactory : NotificationIdFactory { + override fun next( + accountNumber: Int, + offset: Int, + ): NotificationId { + return NotificationId( + NotificationIds.getBaseNotificationId(accountNumber) + offset, + ) + } +} diff --git a/app-k9mail/badging/fossRelease-badging.txt b/app-k9mail/badging/fossRelease-badging.txt index 425de4d22fd..5c4fe46ebf0 100644 --- a/app-k9mail/badging/fossRelease-badging.txt +++ b/app-k9mail/badging/fossRelease-badging.txt @@ -97,3 +97,4 @@ uses-permission: name='android.permission.USE_FINGERPRINT' uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.fsck.k9.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-k9mail/badging/fullRelease-badging.txt b/app-k9mail/badging/fullRelease-badging.txt index 26340d87344..63604be1fe4 100644 --- a/app-k9mail/badging/fullRelease-badging.txt +++ b/app-k9mail/badging/fullRelease-badging.txt @@ -98,3 +98,4 @@ uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.android.vending.BILLING' uses-permission: name='com.fsck.k9.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-thunderbird/badging/fossBeta-badging.txt b/app-thunderbird/badging/fossBeta-badging.txt index 2b40b099f52..263c4a9f8a2 100644 --- a/app-thunderbird/badging/fossBeta-badging.txt +++ b/app-thunderbird/badging/fossBeta-badging.txt @@ -97,3 +97,4 @@ uses-permission: name='android.permission.USE_FINGERPRINT' uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='net.thunderbird.android.beta.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-thunderbird/badging/fossDaily-badging.txt b/app-thunderbird/badging/fossDaily-badging.txt index 6ce07802e89..5259c8492f9 100644 --- a/app-thunderbird/badging/fossDaily-badging.txt +++ b/app-thunderbird/badging/fossDaily-badging.txt @@ -97,3 +97,4 @@ uses-permission: name='android.permission.USE_FINGERPRINT' uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='net.thunderbird.android.daily.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-thunderbird/badging/fossRelease-badging.txt b/app-thunderbird/badging/fossRelease-badging.txt index a25cf76ae88..ec7b69f04d2 100644 --- a/app-thunderbird/badging/fossRelease-badging.txt +++ b/app-thunderbird/badging/fossRelease-badging.txt @@ -97,3 +97,4 @@ uses-permission: name='android.permission.USE_FINGERPRINT' uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='net.thunderbird.android.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-thunderbird/badging/fullBeta-badging.txt b/app-thunderbird/badging/fullBeta-badging.txt index e915c9ced5d..4110c54d045 100644 --- a/app-thunderbird/badging/fullBeta-badging.txt +++ b/app-thunderbird/badging/fullBeta-badging.txt @@ -98,3 +98,4 @@ uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.android.vending.BILLING' uses-permission: name='net.thunderbird.android.beta.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-thunderbird/badging/fullDaily-badging.txt b/app-thunderbird/badging/fullDaily-badging.txt index a42f760dbea..31ba4f9925f 100644 --- a/app-thunderbird/badging/fullDaily-badging.txt +++ b/app-thunderbird/badging/fullDaily-badging.txt @@ -98,3 +98,4 @@ uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.android.vending.BILLING' uses-permission: name='net.thunderbird.android.daily.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-thunderbird/badging/fullRelease-badging.txt b/app-thunderbird/badging/fullRelease-badging.txt index caf47049a5d..474e9d1f638 100644 --- a/app-thunderbird/badging/fullRelease-badging.txt +++ b/app-thunderbird/badging/fullRelease-badging.txt @@ -98,3 +98,4 @@ uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.android.vending.BILLING' uses-permission: name='net.thunderbird.android.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION' diff --git a/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomContent.kt b/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomContent.kt index e7bb95f1c82..c188bb53787 100644 --- a/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomContent.kt +++ b/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomContent.kt @@ -8,6 +8,7 @@ import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.BUTTON import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.COLOR import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.ICON import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.IMAGE +import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.OTHER import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.SELECTION_CONTROL import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.TEXT_FIELD import net.thunderbird.ui.catalog.ui.page.atom.CatalogAtomPage.TYPOGRAPHY @@ -15,6 +16,7 @@ import net.thunderbird.ui.catalog.ui.page.atom.items.buttonItems import net.thunderbird.ui.catalog.ui.page.atom.items.colorItems import net.thunderbird.ui.catalog.ui.page.atom.items.iconItems import net.thunderbird.ui.catalog.ui.page.atom.items.imageItems +import net.thunderbird.ui.catalog.ui.page.atom.items.otherItems import net.thunderbird.ui.catalog.ui.page.atom.items.selectionControlItems import net.thunderbird.ui.catalog.ui.page.atom.items.textFieldItems import net.thunderbird.ui.catalog.ui.page.atom.items.typographyItems @@ -40,6 +42,7 @@ fun CatalogAtomContent( TEXT_FIELD -> textFieldItems() ICON -> iconItems() IMAGE -> imageItems() + OTHER -> otherItems() } }, onRenderFullScreenPage = {}, diff --git a/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomPage.kt b/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomPage.kt index 8b498359e01..25482ad14bf 100644 --- a/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomPage.kt +++ b/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/CatalogAtomPage.kt @@ -14,6 +14,7 @@ enum class CatalogAtomPage( TEXT_FIELD("TextFields"), ICON("Icons"), IMAGE("Images"), + OTHER("Other"), ; override fun toString(): String { diff --git a/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/items/OtherItems.kt b/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/items/OtherItems.kt new file mode 100644 index 00000000000..d6fdfa4455f --- /dev/null +++ b/app-ui-catalog/src/main/kotlin/net/thunderbird/ui/catalog/ui/page/atom/items/OtherItems.kt @@ -0,0 +1,115 @@ +package net.thunderbird.ui.catalog.ui.page.atom.items + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.material3.Slider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import app.k9mail.core.ui.compose.designsystem.atom.divider.HorizontalDivider +import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelSmall +import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedSelect +import app.k9mail.core.ui.compose.theme2.MainTheme +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import net.thunderbird.ui.catalog.ui.page.common.list.defaultItemPadding +import net.thunderbird.ui.catalog.ui.page.common.list.fullSpanItem +import net.thunderbird.ui.catalog.ui.page.common.list.sectionHeaderItem +import net.thunderbird.ui.catalog.ui.page.common.list.sectionSubtitleItem + +private const val MAX_HORIZONTAL_DIVIDER_THICKNESS = 25 +fun LazyGridScope.otherItems() { + sectionHeaderItem(text = "Others") + + horizontalDivider() +} + +private fun LazyGridScope.horizontalDivider() { + sectionSubtitleItem(text = "Horizontal Divider") + fullSpanItem { + Column( + modifier = Modifier + .fillMaxSize() + .padding(defaultItemPadding()), + verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default), + ) { + var thickness by remember { mutableIntStateOf(1) } + val outlineColor = MainTheme.colors.outline + var color by remember { mutableStateOf(outlineColor) } + val options = rememberColorOptions() + + TextLabelSmall(text = "Change thickness ($thickness.dp):") + Slider( + value = thickness.toFloat(), + onValueChange = { thickness = it.toInt() }, + valueRange = 1f..MAX_HORIZONTAL_DIVIDER_THICKNESS.toFloat(), + steps = MAX_HORIZONTAL_DIVIDER_THICKNESS - 1, + modifier = Modifier + .padding(horizontal = MainTheme.spacings.double) + .align(Alignment.CenterHorizontally), + ) + + TextLabelSmall(text = "Change divider color:") + TextFieldOutlinedSelect( + options = remember(options) { options.keys.toImmutableList() }, + selectedOption = color, + onValueChange = { selected -> color = selected }, + optionToStringTransformation = { color -> options.getValue(color) }, + ) + + TextLabelSmall(text = "Result:") + HorizontalDivider( + thickness = thickness.dp, + color = color, + modifier = Modifier + .padding(vertical = MainTheme.spacings.double), + ) + } + } +} + +@Composable +private fun rememberColorOptions(): ImmutableMap { + val colorScheme = MainTheme.colors + return remember { + persistentMapOf( + colorScheme.primary to "Primary", + colorScheme.primaryContainer to "Primary Container", + colorScheme.secondary to "Secondary", + colorScheme.secondaryContainer to "Secondary Container", + colorScheme.tertiary to "Tertiary", + colorScheme.tertiaryContainer to "Tertiary Container", + colorScheme.error to "Error", + colorScheme.errorContainer to "Error Container", + colorScheme.surface to "Surface", + colorScheme.surfaceContainerLowest to "Surface Container Lowest", + colorScheme.surfaceContainerLow to "Surface Container Low", + colorScheme.surfaceContainer to "Surface Container", + colorScheme.surfaceContainerHigh to "Surface Container High", + colorScheme.surfaceContainerHighest to "Surface Container Highest", + colorScheme.inverseSurface to "Inverse Surface", + colorScheme.inversePrimary to "Inverse Primary", + colorScheme.outline to "Outline", + colorScheme.outlineVariant to "Outline Variant", + colorScheme.surfaceBright to "Surface Bright", + colorScheme.surfaceDim to "Surface Dim", + colorScheme.info to "Info", + colorScheme.infoContainer to "Info Container", + colorScheme.success to "Success", + colorScheme.successContainer to "Success Container", + colorScheme.warning to "Warning", + colorScheme.warningContainer to "Warning Container", + ) + } +} diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index e12b37fefb9..bd5b99852c3 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id(ThunderbirdPlugins.Library.kmp) + id("kotlin-parcelize") } android { @@ -28,4 +29,12 @@ kotlin { ), ) } + + compilerOptions { + freeCompilerArgs.addAll( + "-P", + "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=net.thunderbird.core.common.io.KmpParcelize", + "-Xexpect-actual-classes", + ) + } } diff --git a/core/common/src/androidMain/kotlin/net/thunderbird/core/common/CoreCommonModule.android.kt b/core/common/src/androidMain/kotlin/net/thunderbird/core/common/CoreCommonModule.android.kt new file mode 100644 index 00000000000..43125952280 --- /dev/null +++ b/core/common/src/androidMain/kotlin/net/thunderbird/core/common/CoreCommonModule.android.kt @@ -0,0 +1,14 @@ +package net.thunderbird.core.common + +import android.content.Context +import net.thunderbird.core.common.provider.AndroidContextProvider +import net.thunderbird.core.common.provider.ContextProvider +import org.koin.android.ext.koin.androidApplication +import org.koin.core.module.Module +import org.koin.dsl.module + +internal actual val platformCoreCommonModule: Module = module { + single> { + AndroidContextProvider(context = androidApplication()) + } +} diff --git a/core/common/src/androidMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.android.kt b/core/common/src/androidMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.android.kt new file mode 100644 index 00000000000..ce1b853b4a8 --- /dev/null +++ b/core/common/src/androidMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.android.kt @@ -0,0 +1,11 @@ +package net.thunderbird.core.common.io + +import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.RawValue + +actual typealias KmpParcelable = Parcelable + +actual typealias KmpIgnoredOnParcel = IgnoredOnParcel + +actual typealias KmpRawValue = RawValue diff --git a/core/common/src/androidMain/kotlin/net/thunderbird/core/common/provider/AndroidContextProvider.kt b/core/common/src/androidMain/kotlin/net/thunderbird/core/common/provider/AndroidContextProvider.kt new file mode 100644 index 00000000000..5fc63baf93d --- /dev/null +++ b/core/common/src/androidMain/kotlin/net/thunderbird/core/common/provider/AndroidContextProvider.kt @@ -0,0 +1,5 @@ +package net.thunderbird.core.common.provider + +import android.content.Context + +class AndroidContextProvider(override val context: Context) : ContextProvider diff --git a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/CoreCommonModule.kt b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/CoreCommonModule.kt index e2717ddc1e9..9352b92e99c 100644 --- a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/CoreCommonModule.kt +++ b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/CoreCommonModule.kt @@ -6,7 +6,10 @@ import net.thunderbird.core.common.oauth.OAuthConfigurationProvider import org.koin.core.module.Module import org.koin.dsl.module +internal expect val platformCoreCommonModule: Module + val coreCommonModule: Module = module { + includes(platformCoreCommonModule) single { Clock.System } single { diff --git a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.kt b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.kt new file mode 100644 index 00000000000..8a98ac96e1b --- /dev/null +++ b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.kt @@ -0,0 +1,15 @@ +package net.thunderbird.core.common.io + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class KmpParcelize + +expect interface KmpParcelable + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +expect annotation class KmpIgnoredOnParcel() + +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.BINARY) +expect annotation class KmpRawValue() diff --git a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/provider/ContextProvider.kt b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/provider/ContextProvider.kt new file mode 100644 index 00000000000..a8eb3f8917f --- /dev/null +++ b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/provider/ContextProvider.kt @@ -0,0 +1,10 @@ +package net.thunderbird.core.common.provider + +/** + * Represents a provider of context. + * + * @param TContext The type of context provided. + */ +interface ContextProvider { + val context: TContext +} diff --git a/core/common/src/jvmMain/kotlin/net/thunderbird/core/common/CoreCommonModule.jvm.kt b/core/common/src/jvmMain/kotlin/net/thunderbird/core/common/CoreCommonModule.jvm.kt new file mode 100644 index 00000000000..f6859fb3367 --- /dev/null +++ b/core/common/src/jvmMain/kotlin/net/thunderbird/core/common/CoreCommonModule.jvm.kt @@ -0,0 +1,6 @@ +package net.thunderbird.core.common + +import org.koin.core.module.Module +import org.koin.dsl.module + +internal actual val platformCoreCommonModule: Module = module { } diff --git a/core/common/src/jvmMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.jvm.kt b/core/common/src/jvmMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.jvm.kt new file mode 100644 index 00000000000..f184f533a12 --- /dev/null +++ b/core/common/src/jvmMain/kotlin/net/thunderbird/core/common/io/KmpParcelize.jvm.kt @@ -0,0 +1,11 @@ +package net.thunderbird.core.common.io + +actual interface KmpParcelable + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +actual annotation class KmpIgnoredOnParcel() + +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.BINARY) +actual annotation class KmpRawValue diff --git a/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/PreviewWithThemeLightDark.kt b/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/PreviewWithThemeLightDark.kt index f6cf6d21e72..64dd02fdb8d 100644 --- a/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/PreviewWithThemeLightDark.kt +++ b/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/PreviewWithThemeLightDark.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import app.k9mail.core.ui.compose.theme2.MainTheme /** @@ -30,25 +31,25 @@ import app.k9mail.core.ui.compose.theme2.MainTheme * @see app.k9mail.core.ui.compose.theme2.default.defaultThemeSpacings for MainTheme.spacings */ @Composable -fun PreviewWithThemeLightDark( +fun PreviewWithThemesLightDark( modifier: Modifier = Modifier, useRow: Boolean = false, useScrim: Boolean = false, scrimAlpha: Float = 0.8f, - scrimPadding: PaddingValues = PaddingValues(MainTheme.spacings.triple), - arrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(MainTheme.spacings.triple), + scrimPadding: PaddingValues = PaddingValues(24.dp), + arrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(24.dp), content: @Composable () -> Unit, ) { val movableContent = remember { movableContentOf { - ThemePreview( + PreviewWithThemeLightDark( themeType = PreviewThemeType.THUNDERBIRD, useScrim = useScrim, scrimAlpha = scrimAlpha, scrimPadding = scrimPadding, content = content, ) - ThemePreview( + PreviewWithThemeLightDark( themeType = PreviewThemeType.K9MAIL, useScrim = useScrim, scrimAlpha = scrimAlpha, @@ -75,12 +76,13 @@ fun PreviewWithThemeLightDark( } } +@Suppress("ModifierMissing") @Composable -private fun ThemePreview( - themeType: PreviewThemeType, - useScrim: Boolean, - scrimAlpha: Float, - scrimPadding: PaddingValues, +fun PreviewWithThemeLightDark( + themeType: PreviewThemeType = PreviewThemeType.THUNDERBIRD, + useScrim: Boolean = false, + scrimAlpha: Float = 0f, + scrimPadding: PaddingValues = PaddingValues(0.dp), content: @Composable (() -> Unit), ) { val movableContent = remember { movableContentOf { content() } } diff --git a/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/BasicDialogPreview.kt b/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/BasicDialogPreview.kt index 9a1a7a46204..7dcbabc6212 100644 --- a/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/BasicDialogPreview.kt +++ b/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/BasicDialogPreview.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.k9mail.core.ui.compose.designsystem.PreviewLightDarkLandscape -import app.k9mail.core.ui.compose.designsystem.PreviewWithThemeLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadlineSmall import app.k9mail.core.ui.compose.theme2.MainTheme @@ -29,11 +29,11 @@ import app.k9mail.core.ui.compose.theme2.MainTheme @PreviewLightDarkLandscape @Composable private fun BasicDialogPreview() { - PreviewWithThemeLightDark( + PreviewWithThemesLightDark( useRow = true, useScrim = true, - scrimPadding = PaddingValues(MainTheme.spacings.quadruple), - arrangement = Arrangement.spacedBy(MainTheme.spacings.triple), + scrimPadding = PaddingValues(32.dp), + arrangement = Arrangement.spacedBy(24.dp), ) { BasicDialogContent( headline = { @@ -115,11 +115,11 @@ private fun BasicDialogPreview() { @PreviewLightDarkLandscape @Composable private fun PreviewOnlySupportingText() { - PreviewWithThemeLightDark( + PreviewWithThemesLightDark( useRow = true, useScrim = true, - scrimPadding = PaddingValues(MainTheme.spacings.quadruple), - arrangement = Arrangement.spacedBy(MainTheme.spacings.triple), + scrimPadding = PaddingValues(32.dp), + arrangement = Arrangement.spacedBy(24.dp), ) { BasicDialogContent( headline = { diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/divider/HorizontalDivider.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/divider/HorizontalDivider.kt new file mode 100644 index 00000000000..78f848664ef --- /dev/null +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/divider/HorizontalDivider.kt @@ -0,0 +1,21 @@ +package app.k9mail.core.ui.compose.designsystem.atom.divider + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import app.k9mail.core.ui.compose.theme2.MainTheme +import androidx.compose.material3.HorizontalDivider as MaterialHorizontalDivider + +@Composable +fun HorizontalDivider( + modifier: Modifier = Modifier, + thickness: Dp = MainTheme.sizes.tiny, + color: Color = MainTheme.colors.outline, +) { + MaterialHorizontalDivider( + modifier = modifier, + thickness = thickness, + color = color, + ) +} diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/filled/Notification.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/filled/Notification.kt new file mode 100644 index 00000000000..a09e31433b0 --- /dev/null +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/filled/Notification.kt @@ -0,0 +1,341 @@ +package app.k9mail.core.ui.compose.designsystem.atom.icon.filled + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons + +@Suppress("MagicNumber", "MaxLineLength", "UnusedReceiverParameter") +val Icons.Filled.Notification: ImageVector + get() { + val current = _notification + if (current != null) return current + + return ImageVector.Builder( + name = "app.k9mail.core.ui.compose.theme2.MainTheme.Notification", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + // M12 3.5 C8.953 3.5 6.5 5.953 6.5 9 V12 V13.5 C5.392 13.5 4.5 14.392 4.5 15.5 V17 C4.5 17.277 4.723 17.5 5 17.5 H6.5 H7 H17 H17.5 H19 C19.277 17.5 19.5 17.277 19.5 17 V15.5 C19.5 14.392 18.608 13.5 17.5 13.5 V10.5 V9 C17.5 5.953 15.047 3.5 12 3.5Z + path( + fill = SolidColor(Color(0xFF1A202C)), + fillAlpha = 0.2f, + strokeAlpha = 0.2f, + ) { + // M 12 3.5 + moveTo(x = 12.0f, y = 3.5f) + // C 8.953 3.5 6.5 5.953 6.5 9 + curveTo( + x1 = 8.953f, + y1 = 3.5f, + x2 = 6.5f, + y2 = 5.953f, + x3 = 6.5f, + y3 = 9.0f, + ) + // V 12 + verticalLineTo(y = 12.0f) + // V 13.5 + verticalLineTo(y = 13.5f) + // C 5.392 13.5 4.5 14.392 4.5 15.5 + curveTo( + x1 = 5.392f, + y1 = 13.5f, + x2 = 4.5f, + y2 = 14.392f, + x3 = 4.5f, + y3 = 15.5f, + ) + // V 17 + verticalLineTo(y = 17.0f) + // C 4.5 17.277 4.723 17.5 5 17.5 + curveTo( + x1 = 4.5f, + y1 = 17.277f, + x2 = 4.723f, + y2 = 17.5f, + x3 = 5.0f, + y3 = 17.5f, + ) + // H 6.5 + horizontalLineTo(x = 6.5f) + // H 7 + horizontalLineTo(x = 7.0f) + // H 17 + horizontalLineTo(x = 17.0f) + // H 17.5 + horizontalLineTo(x = 17.5f) + // H 19 + horizontalLineTo(x = 19.0f) + // C 19.277 17.5 19.5 17.277 19.5 17 + curveTo( + x1 = 19.277f, + y1 = 17.5f, + x2 = 19.5f, + y2 = 17.277f, + x3 = 19.5f, + y3 = 17.0f, + ) + // V 15.5 + verticalLineTo(y = 15.5f) + // C 19.5 14.392 18.608 13.5 17.5 13.5 + curveTo( + x1 = 19.5f, + y1 = 14.392f, + x2 = 18.608f, + y2 = 13.5f, + x3 = 17.5f, + y3 = 13.5f, + ) + // V 10.5 + verticalLineTo(y = 10.5f) + // V 9 + verticalLineTo(y = 9.0f) + // C 17.5 5.953 15.047 3.5 12 3.5z + curveTo( + x1 = 17.5f, + y1 = 5.953f, + x2 = 15.047f, + y2 = 3.5f, + x3 = 12.0f, + y3 = 3.5f, + ) + close() + } + // M12 3 C8.68466 3 6 5.68465 6 9 V12 V13.207 C4.89477 13.4651 4 14.3184 4 15.5 V17 C4 17.5454 4.45465 18 5 18 H6.5 H7 H9 C9 19.6534 10.3467 21 12 21 C13.6533 21 15 19.6534 15 18 H17 H17.5 H19 C19.5454 18 20 17.5454 20 17 V15.5 C20 14.3184 19.1052 13.4651 18 13.207 V10.5 V9 C18 5.68465 15.3153 3 12 3Z M12 4 C14.7786 4 17 6.22136 17 9 V10.5 V13.5 C17 13.6326 17.0527 13.7598 17.1465 13.8535 C17.2402 13.9473 17.3674 14 17.5 14 C18.3396 14 19 14.6603 19 15.5 V17 H17.5 H17 H14.5 H9.5 H7 H6.5 H5 V15.5 C5 14.6603 5.66036 14 6.5 14 C6.6326 14 6.75977 13.9473 6.85354 13.8535 C6.9473 13.7598 6.99999 13.6326 7 13.5 V12 V9 C7 6.22136 9.22136 4 12 4Z M10 18 H14 C14 19.1166 13.1166 20 12 20 C10.8834 20 10 19.1166 10 18Z + path( + fill = SolidColor(Color(0xFF1A202C)), + ) { + // M 12 3 + moveTo(x = 12.0f, y = 3.0f) + // C 8.68466 3 6 5.68465 6 9 + curveTo( + x1 = 8.68466f, + y1 = 3.0f, + x2 = 6.0f, + y2 = 5.68465f, + x3 = 6.0f, + y3 = 9.0f, + ) + // V 12 + verticalLineTo(y = 12.0f) + // V 13.207 + verticalLineTo(y = 13.207f) + // C 4.89477 13.4651 4 14.3184 4 15.5 + curveTo( + x1 = 4.89477f, + y1 = 13.4651f, + x2 = 4.0f, + y2 = 14.3184f, + x3 = 4.0f, + y3 = 15.5f, + ) + // V 17 + verticalLineTo(y = 17.0f) + // C 4 17.5454 4.45465 18 5 18 + curveTo( + x1 = 4.0f, + y1 = 17.5454f, + x2 = 4.45465f, + y2 = 18.0f, + x3 = 5.0f, + y3 = 18.0f, + ) + // H 6.5 + horizontalLineTo(x = 6.5f) + // H 7 + horizontalLineTo(x = 7.0f) + // H 9 + horizontalLineTo(x = 9.0f) + // C 9 19.6534 10.3467 21 12 21 + curveTo( + x1 = 9.0f, + y1 = 19.6534f, + x2 = 10.3467f, + y2 = 21.0f, + x3 = 12.0f, + y3 = 21.0f, + ) + // C 13.6533 21 15 19.6534 15 18 + curveTo( + x1 = 13.6533f, + y1 = 21.0f, + x2 = 15.0f, + y2 = 19.6534f, + x3 = 15.0f, + y3 = 18.0f, + ) + // H 17 + horizontalLineTo(x = 17.0f) + // H 17.5 + horizontalLineTo(x = 17.5f) + // H 19 + horizontalLineTo(x = 19.0f) + // C 19.5454 18 20 17.5454 20 17 + curveTo( + x1 = 19.5454f, + y1 = 18.0f, + x2 = 20.0f, + y2 = 17.5454f, + x3 = 20.0f, + y3 = 17.0f, + ) + // V 15.5 + verticalLineTo(y = 15.5f) + // C 20 14.3184 19.1052 13.4651 18 13.207 + curveTo( + x1 = 20.0f, + y1 = 14.3184f, + x2 = 19.1052f, + y2 = 13.4651f, + x3 = 18.0f, + y3 = 13.207f, + ) + // V 10.5 + verticalLineTo(y = 10.5f) + // V 9 + verticalLineTo(y = 9.0f) + // C 18 5.68465 15.3153 3 12 3z + curveTo( + x1 = 18.0f, + y1 = 5.68465f, + x2 = 15.3153f, + y2 = 3.0f, + x3 = 12.0f, + y3 = 3.0f, + ) + close() + // M 12 4 + moveTo(x = 12.0f, y = 4.0f) + // C 14.7786 4 17 6.22136 17 9 + curveTo( + x1 = 14.7786f, + y1 = 4.0f, + x2 = 17.0f, + y2 = 6.22136f, + x3 = 17.0f, + y3 = 9.0f, + ) + // V 10.5 + verticalLineTo(y = 10.5f) + // V 13.5 + verticalLineTo(y = 13.5f) + // C 17 13.6326 17.0527 13.7598 17.1465 13.8535 + curveTo( + x1 = 17.0f, + y1 = 13.6326f, + x2 = 17.0527f, + y2 = 13.7598f, + x3 = 17.1465f, + y3 = 13.8535f, + ) + // C 17.2402 13.9473 17.3674 14 17.5 14 + curveTo( + x1 = 17.2402f, + y1 = 13.9473f, + x2 = 17.3674f, + y2 = 14.0f, + x3 = 17.5f, + y3 = 14.0f, + ) + // C 18.3396 14 19 14.6603 19 15.5 + curveTo( + x1 = 18.3396f, + y1 = 14.0f, + x2 = 19.0f, + y2 = 14.6603f, + x3 = 19.0f, + y3 = 15.5f, + ) + // V 17 + verticalLineTo(y = 17.0f) + // H 17.5 + horizontalLineTo(x = 17.5f) + // H 17 + horizontalLineTo(x = 17.0f) + // H 14.5 + horizontalLineTo(x = 14.5f) + // H 9.5 + horizontalLineTo(x = 9.5f) + // H 7 + horizontalLineTo(x = 7.0f) + // H 6.5 + horizontalLineTo(x = 6.5f) + // H 5 + horizontalLineTo(x = 5.0f) + // V 15.5 + verticalLineTo(y = 15.5f) + // C 5 14.6603 5.66036 14 6.5 14 + curveTo( + x1 = 5.0f, + y1 = 14.6603f, + x2 = 5.66036f, + y2 = 14.0f, + x3 = 6.5f, + y3 = 14.0f, + ) + // C 6.6326 14 6.75977 13.9473 6.85354 13.8535 + curveTo( + x1 = 6.6326f, + y1 = 14.0f, + x2 = 6.75977f, + y2 = 13.9473f, + x3 = 6.85354f, + y3 = 13.8535f, + ) + // C 6.9473 13.7598 6.99999 13.6326 7 13.5 + curveTo( + x1 = 6.9473f, + y1 = 13.7598f, + x2 = 6.99999f, + y2 = 13.6326f, + x3 = 7.0f, + y3 = 13.5f, + ) + // V 12 + verticalLineTo(y = 12.0f) + // V 9 + verticalLineTo(y = 9.0f) + // C 7 6.22136 9.22136 4 12 4z + curveTo( + x1 = 7.0f, + y1 = 6.22136f, + x2 = 9.22136f, + y2 = 4.0f, + x3 = 12.0f, + y3 = 4.0f, + ) + close() + // M 10 18 + moveTo(x = 10.0f, y = 18.0f) + // H 14 + horizontalLineTo(x = 14.0f) + // C 14 19.1166 13.1166 20 12 20 + curveTo( + x1 = 14.0f, + y1 = 19.1166f, + x2 = 13.1166f, + y2 = 20.0f, + x3 = 12.0f, + y3 = 20.0f, + ) + // C 10.8834 20 10 19.1166 10 18z + curveTo( + x1 = 10.8834f, + y1 = 20.0f, + x2 = 10.0f, + y2 = 19.1166f, + x3 = 10.0f, + y3 = 18.0f, + ) + close() + } + }.build().also { _notification = it } + } + +@Suppress("ObjectPropertyName") +private var _notification: ImageVector? = null diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/outlined/NewEmail.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/outlined/NewEmail.kt new file mode 100644 index 00000000000..a2f3081c65b --- /dev/null +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/outlined/NewEmail.kt @@ -0,0 +1,299 @@ +package app.k9mail.core.ui.compose.designsystem.atom.icon.outlined + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons + +@Suppress("MagicNumber", "MaxLineLength", "UnusedReceiverParameter") +val Icons.Outlined.NewEmail: ImageVector + get() { + val current = _newEmail + if (current != null) return current + + return ImageVector.Builder( + name = "app.k9mail.core.ui.compose.theme2.MainTheme.NewEmail", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 960.0f, + viewportHeight = 960.0f, + ).apply { + // M160 800 Q127 800 103.5 776.5 Q80 753 80 720 L80 240 Q80 207 103.5 183.5 Q127 160 160 160 L564 160 Q560 180 560 200 Q560 220 564 240 L160 240 L480 440 L626 349 Q640 362 656.5 371.5 Q673 381 691 388 L480 520 L160 320 L160 720 Q160 720 160 720 Q160 720 160 720 L800 720 Q800 720 800 720 Q800 720 800 720 L800 396 Q823 391 843 382 Q863 373 880 360 L880 720 Q880 753 856.5 776.5 Q833 800 800 800 L160 800Z M160 240 L160 240 L160 240 L160 720 Q160 720 160 720 Q160 720 160 720 L160 720 Q160 720 160 720 Q160 720 160 720 L160 240 Q160 240 160 240 Q160 240 160 240 Q160 240 160 240 Q160 240 160 240Z M760 320 Q710 320 675 285 Q640 250 640 200 Q640 150 675 115 Q710 80 760 80 Q810 80 845 115 Q880 150 880 200 Q880 250 845 285 Q810 320 760 320Z + path( + fill = SolidColor(Color(0xFFFFFFFF)), + ) { + // M 160 800 + moveTo(x = 160.0f, y = 800.0f) + // Q 127 800 103.5 776.5 + quadTo( + x1 = 127.0f, + y1 = 800.0f, + x2 = 103.5f, + y2 = 776.5f, + ) + // Q 80 753 80 720 + quadTo( + x1 = 80.0f, + y1 = 753.0f, + x2 = 80.0f, + y2 = 720.0f, + ) + // L 80 240 + lineTo(x = 80.0f, y = 240.0f) + // Q 80 207 103.5 183.5 + quadTo( + x1 = 80.0f, + y1 = 207.0f, + x2 = 103.5f, + y2 = 183.5f, + ) + // Q 127 160 160 160 + quadTo( + x1 = 127.0f, + y1 = 160.0f, + x2 = 160.0f, + y2 = 160.0f, + ) + // L 564 160 + lineTo(x = 564.0f, y = 160.0f) + // Q 560 180 560 200 + quadTo( + x1 = 560.0f, + y1 = 180.0f, + x2 = 560.0f, + y2 = 200.0f, + ) + // Q 560 220 564 240 + quadTo( + x1 = 560.0f, + y1 = 220.0f, + x2 = 564.0f, + y2 = 240.0f, + ) + // L 160 240 + lineTo(x = 160.0f, y = 240.0f) + // L 480 440 + lineTo(x = 480.0f, y = 440.0f) + // L 626 349 + lineTo(x = 626.0f, y = 349.0f) + // Q 640 362 656.5 371.5 + quadTo( + x1 = 640.0f, + y1 = 362.0f, + x2 = 656.5f, + y2 = 371.5f, + ) + // Q 673 381 691 388 + quadTo( + x1 = 673.0f, + y1 = 381.0f, + x2 = 691.0f, + y2 = 388.0f, + ) + // L 480 520 + lineTo(x = 480.0f, y = 520.0f) + // L 160 320 + lineTo(x = 160.0f, y = 320.0f) + // L 160 720 + lineTo(x = 160.0f, y = 720.0f) + // Q 160 720 160 720 + quadTo( + x1 = 160.0f, + y1 = 720.0f, + x2 = 160.0f, + y2 = 720.0f, + ) + // Q 160 720 160 720 + quadTo( + x1 = 160.0f, + y1 = 720.0f, + x2 = 160.0f, + y2 = 720.0f, + ) + // L 800 720 + lineTo(x = 800.0f, y = 720.0f) + // Q 800 720 800 720 + quadTo( + x1 = 800.0f, + y1 = 720.0f, + x2 = 800.0f, + y2 = 720.0f, + ) + // Q 800 720 800 720 + quadTo( + x1 = 800.0f, + y1 = 720.0f, + x2 = 800.0f, + y2 = 720.0f, + ) + // L 800 396 + lineTo(x = 800.0f, y = 396.0f) + // Q 823 391 843 382 + quadTo( + x1 = 823.0f, + y1 = 391.0f, + x2 = 843.0f, + y2 = 382.0f, + ) + // Q 863 373 880 360 + quadTo( + x1 = 863.0f, + y1 = 373.0f, + x2 = 880.0f, + y2 = 360.0f, + ) + // L 880 720 + lineTo(x = 880.0f, y = 720.0f) + // Q 880 753 856.5 776.5 + quadTo( + x1 = 880.0f, + y1 = 753.0f, + x2 = 856.5f, + y2 = 776.5f, + ) + // Q 833 800 800 800 + quadTo( + x1 = 833.0f, + y1 = 800.0f, + x2 = 800.0f, + y2 = 800.0f, + ) + // L 160 800z + lineTo(x = 160.0f, y = 800.0f) + close() + // M 160 240 + moveTo(x = 160.0f, y = 240.0f) + // L 160 240 + lineTo(x = 160.0f, y = 240.0f) + // L 160 240 + lineTo(x = 160.0f, y = 240.0f) + // L 160 720 + lineTo(x = 160.0f, y = 720.0f) + // Q 160 720 160 720 + quadTo( + x1 = 160.0f, + y1 = 720.0f, + x2 = 160.0f, + y2 = 720.0f, + ) + // Q 160 720 160 720 + quadTo( + x1 = 160.0f, + y1 = 720.0f, + x2 = 160.0f, + y2 = 720.0f, + ) + // L 160 720 + lineTo(x = 160.0f, y = 720.0f) + // Q 160 720 160 720 + quadTo( + x1 = 160.0f, + y1 = 720.0f, + x2 = 160.0f, + y2 = 720.0f, + ) + // Q 160 720 160 720 + quadTo( + x1 = 160.0f, + y1 = 720.0f, + x2 = 160.0f, + y2 = 720.0f, + ) + // L 160 240 + lineTo(x = 160.0f, y = 240.0f) + // Q 160 240 160 240 + quadTo( + x1 = 160.0f, + y1 = 240.0f, + x2 = 160.0f, + y2 = 240.0f, + ) + // Q 160 240 160 240 + quadTo( + x1 = 160.0f, + y1 = 240.0f, + x2 = 160.0f, + y2 = 240.0f, + ) + // Q 160 240 160 240 + quadTo( + x1 = 160.0f, + y1 = 240.0f, + x2 = 160.0f, + y2 = 240.0f, + ) + // Q 160 240 160 240z + quadTo( + x1 = 160.0f, + y1 = 240.0f, + x2 = 160.0f, + y2 = 240.0f, + ) + close() + // M 760 320 + moveTo(x = 760.0f, y = 320.0f) + // Q 710 320 675 285 + quadTo( + x1 = 710.0f, + y1 = 320.0f, + x2 = 675.0f, + y2 = 285.0f, + ) + // Q 640 250 640 200 + quadTo( + x1 = 640.0f, + y1 = 250.0f, + x2 = 640.0f, + y2 = 200.0f, + ) + // Q 640 150 675 115 + quadTo( + x1 = 640.0f, + y1 = 150.0f, + x2 = 675.0f, + y2 = 115.0f, + ) + // Q 710 80 760 80 + quadTo( + x1 = 710.0f, + y1 = 80.0f, + x2 = 760.0f, + y2 = 80.0f, + ) + // Q 810 80 845 115 + quadTo( + x1 = 810.0f, + y1 = 80.0f, + x2 = 845.0f, + y2 = 115.0f, + ) + // Q 880 150 880 200 + quadTo( + x1 = 880.0f, + y1 = 150.0f, + x2 = 880.0f, + y2 = 200.0f, + ) + // Q 880 250 845 285 + quadTo( + x1 = 880.0f, + y1 = 250.0f, + x2 = 845.0f, + y2 = 285.0f, + ) + // Q 810 320 760 320z + quadTo( + x1 = 810.0f, + y1 = 320.0f, + x2 = 760.0f, + y2 = 320.0f, + ) + close() + } + }.build().also { _newEmail = it } + } + +@Suppress("ObjectPropertyName") +private var _newEmail: ImageVector? = null diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/outlined/Warning.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/outlined/Warning.kt new file mode 100644 index 00000000000..ef0d309938a --- /dev/null +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/outlined/Warning.kt @@ -0,0 +1,129 @@ +package app.k9mail.core.ui.compose.designsystem.atom.icon.outlined + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons + +@Suppress("MagicNumber", "MaxLineLength", "UnusedReceiverParameter") +val Icons.Outlined.Warning: ImageVector + get() { + val current = _warning + if (current != null) return current + + return ImageVector.Builder( + name = "app.k9mail.core.ui.compose.theme2.MainTheme.Warning", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 960.0f, + viewportHeight = 960.0f, + ).apply { + // M40 840 L480 80 L920 840 L40 840Z M178 760 L782 760 L480 240 L178 760Z M480 720 Q497 720 508.5 708.5 Q520 697 520 680 Q520 663 508.5 651.5 Q497 640 480 640 Q463 640 451.5 651.5 Q440 663 440 680 Q440 697 451.5 708.5 Q463 720 480 720Z M440 600 L520 600 L520 400 L440 400 L440 600Z M480 500 L480 500 L480 500 L480 500Z + path( + fill = SolidColor(Color(0xFFFFFFFF)), + ) { + // M 40 840 + moveTo(x = 40.0f, y = 840.0f) + // L 480 80 + lineTo(x = 480.0f, y = 80.0f) + // L 920 840 + lineTo(x = 920.0f, y = 840.0f) + // L 40 840z + lineTo(x = 40.0f, y = 840.0f) + close() + // M 178 760 + moveTo(x = 178.0f, y = 760.0f) + // L 782 760 + lineTo(x = 782.0f, y = 760.0f) + // L 480 240 + lineTo(x = 480.0f, y = 240.0f) + // L 178 760z + lineTo(x = 178.0f, y = 760.0f) + close() + // M 480 720 + moveTo(x = 480.0f, y = 720.0f) + // Q 497 720 508.5 708.5 + quadTo( + x1 = 497.0f, + y1 = 720.0f, + x2 = 508.5f, + y2 = 708.5f, + ) + // Q 520 697 520 680 + quadTo( + x1 = 520.0f, + y1 = 697.0f, + x2 = 520.0f, + y2 = 680.0f, + ) + // Q 520 663 508.5 651.5 + quadTo( + x1 = 520.0f, + y1 = 663.0f, + x2 = 508.5f, + y2 = 651.5f, + ) + // Q 497 640 480 640 + quadTo( + x1 = 497.0f, + y1 = 640.0f, + x2 = 480.0f, + y2 = 640.0f, + ) + // Q 463 640 451.5 651.5 + quadTo( + x1 = 463.0f, + y1 = 640.0f, + x2 = 451.5f, + y2 = 651.5f, + ) + // Q 440 663 440 680 + quadTo( + x1 = 440.0f, + y1 = 663.0f, + x2 = 440.0f, + y2 = 680.0f, + ) + // Q 440 697 451.5 708.5 + quadTo( + x1 = 440.0f, + y1 = 697.0f, + x2 = 451.5f, + y2 = 708.5f, + ) + // Q 463 720 480 720z + quadTo( + x1 = 463.0f, + y1 = 720.0f, + x2 = 480.0f, + y2 = 720.0f, + ) + close() + // M 440 600 + moveTo(x = 440.0f, y = 600.0f) + // L 520 600 + lineTo(x = 520.0f, y = 600.0f) + // L 520 400 + lineTo(x = 520.0f, y = 400.0f) + // L 440 400 + lineTo(x = 440.0f, y = 400.0f) + // L 440 600z + lineTo(x = 440.0f, y = 600.0f) + close() + // M 480 500 + moveTo(x = 480.0f, y = 500.0f) + // L 480 500 + lineTo(x = 480.0f, y = 500.0f) + // L 480 500 + lineTo(x = 480.0f, y = 500.0f) + // L 480 500z + lineTo(x = 480.0f, y = 500.0f) + close() + } + }.build().also { _warning = it } + } + +@Suppress("ObjectPropertyName") +private var _warning: ImageVector? = null diff --git a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt index f0031f46d60..260ba496402 100644 --- a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt +++ b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.unit.Dp @Immutable data class ThemeSizes( + val tiny: Dp, val smaller: Dp, val small: Dp, val medium: Dp, diff --git a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt index 3e403806b7f..84887f725b3 100644 --- a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt +++ b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.unit.dp import app.k9mail.core.ui.compose.theme2.ThemeSizes val defaultThemeSizes = ThemeSizes( + tiny = 1.dp, smaller = 8.dp, small = 16.dp, medium = 32.dp, diff --git a/feature/debug-settings/build.gradle.kts b/feature/debug-settings/build.gradle.kts new file mode 100644 index 00000000000..9127c475713 --- /dev/null +++ b/feature/debug-settings/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id(ThunderbirdPlugins.Library.androidCompose) +} + +android { + namespace = "net.thunderbird.feature.debugSettings" + buildFeatures { + buildConfig = true + } +} + +dependencies { + implementation(projects.core.ui.compose.designsystem) + implementation(projects.core.ui.compose.navigation) + implementation(projects.core.common) + implementation(projects.core.outcome) + implementation(projects.feature.mail.account.api) + implementation(projects.feature.notification.api) +} diff --git a/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/DebugSectionPreview.kt b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/DebugSectionPreview.kt new file mode 100644 index 00000000000..0f99f7742bc --- /dev/null +++ b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/DebugSectionPreview.kt @@ -0,0 +1,24 @@ +package net.thunderbird.feature.debugSettings + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark +import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge +import app.k9mail.core.ui.compose.theme2.MainTheme + +@PreviewLightDark +@Composable +private fun DebugSectionPreview() { + PreviewWithThemesLightDark { + Box(modifier = Modifier.padding(MainTheme.spacings.triple)) { + DebugSection( + title = "Debug section", + ) { + TextBodyLarge("Content") + } + } + } +} diff --git a/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/SecretDebugSettingsScreenPreview.kt b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/SecretDebugSettingsScreenPreview.kt new file mode 100644 index 00000000000..c7bff4b8492 --- /dev/null +++ b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/SecretDebugSettingsScreenPreview.kt @@ -0,0 +1,54 @@ +package net.thunderbird.feature.debugSettings + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import app.k9mail.core.ui.compose.common.koin.koinPreview +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import net.thunderbird.core.outcome.Outcome +import net.thunderbird.feature.debugSettings.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.CommandOutcome.Failure +import net.thunderbird.feature.notification.api.command.NotificationCommand.CommandOutcome.Success +import net.thunderbird.feature.notification.api.content.Notification +import net.thunderbird.feature.notification.api.sender.NotificationSender + +@PreviewLightDark +@Composable +private fun SecretDebugSettingsScreenPreview() { + koinPreview { + single { + DebugNotificationSectionViewModel( + accountManager = object : AccountManager { + override fun getAccounts(): List = listOf() + override fun getAccountsFlow(): Flow> = flowOf(listOf()) + override fun getAccount(accountUuid: String): BaseAccount? = null + override fun getAccountFlow(accountUuid: String): Flow = flowOf(null) + override fun moveAccount( + account: BaseAccount, + newPosition: Int, + ) = Unit + + override fun saveAccount(account: BaseAccount) = Unit + }, + notificationSender = object : NotificationSender { + override fun send( + notification: Notification, + ): Flow, Failure>> = + error("not implementd") + }, + ) + } + } WithContent { + PreviewWithThemesLightDark { + SecretDebugSettingsScreen( + onNavigateBack = { }, + modifier = Modifier.fillMaxSize(), + ) + } + } +} diff --git a/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/inject/FeatureDebugSettingsModule.kt b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/inject/FeatureDebugSettingsModule.kt new file mode 100644 index 00000000000..a4fc652f7ee --- /dev/null +++ b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/inject/FeatureDebugSettingsModule.kt @@ -0,0 +1,17 @@ +package net.thunderbird.feature.debugSettings.inject + +import net.thunderbird.feature.debugSettings.navigation.DefaultSecretDebugSettingsNavigation +import net.thunderbird.feature.debugSettings.navigation.SecretDebugSettingsNavigation +import net.thunderbird.feature.debugSettings.notification.DebugNotificationSectionViewModel +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val featureDebugSettingsModule = module { + single { DefaultSecretDebugSettingsNavigation() } + viewModel { + DebugNotificationSectionViewModel( + accountManager = get(), + notificationSender = get(), + ) + } +} diff --git a/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionPreview.kt b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionPreview.kt new file mode 100644 index 00000000000..ee2f9fb5a73 --- /dev/null +++ b/feature/debug-settings/src/debug/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionPreview.kt @@ -0,0 +1,78 @@ +package net.thunderbird.feature.debugSettings.notification + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemeLightDark +import app.k9mail.core.ui.compose.theme2.MainTheme +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +import kotlinx.collections.immutable.toPersistentList +import net.thunderbird.feature.mail.account.api.BaseAccount +import net.thunderbird.feature.notification.api.content.MailNotification + +@OptIn(ExperimentalUuidApi::class) +@PreviewLightDark +@Composable +private fun DebugNotificationSectionPreview() { + PreviewWithThemeLightDark { + val accounts = remember { + List(size = 10) { + object : BaseAccount { + override val uuid: String = Uuid.random().toString() + override val name: String? = "Account $it" + override val email: String = "account-$it@mail.com" + } + }.toPersistentList() + } + var state by remember { + mutableStateOf( + DebugNotificationSectionContract.State( + accounts = accounts, + selectedAccount = accounts.first(), + ), + ) + } + DebugNotificationSection( + state = state, + modifier = Modifier.padding(MainTheme.spacings.triple), + onAccountSelect = { state = state.copy(selectedAccount = it) }, + ) + } +} + +@OptIn(ExperimentalUuidApi::class) +@PreviewLightDark +@Composable +private fun PreviewSingleMailNotification() { + PreviewWithThemeLightDark { + val accounts = remember { + List(size = 10) { + object : BaseAccount { + override val uuid: String = Uuid.random().toString() + override val name: String? = "Account $it" + override val email: String = "account-$it@mail.com" + } + }.toPersistentList() + } + var state by remember { + mutableStateOf( + DebugNotificationSectionContract.State( + accounts = accounts, + selectedAccount = accounts.first(), + selectedSystemNotificationType = MailNotification.NewMail.SingleMail::class, + ), + ) + } + DebugNotificationSection( + state = state, + modifier = Modifier.padding(MainTheme.spacings.triple), + onAccountSelect = { state = state.copy(selectedAccount = it) }, + ) + } +} diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/DebugSection.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/DebugSection.kt new file mode 100644 index 00000000000..219cc78abe2 --- /dev/null +++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/DebugSection.kt @@ -0,0 +1,49 @@ +package net.thunderbird.feature.debugSettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import app.k9mail.core.ui.compose.designsystem.atom.divider.HorizontalDivider +import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleLarge +import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium +import app.k9mail.core.ui.compose.theme2.MainTheme + +@Composable +internal fun DebugSection( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + DebugSection( + title = { TextTitleLarge(title) }, + modifier = modifier, + content = content, + ) +} + +@Composable +internal fun DebugSubSection( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + DebugSection( + title = { TextTitleMedium(title) }, + modifier = modifier, + content = content, + ) +} + +@Composable +internal fun DebugSection( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column(modifier = modifier) { + title() + HorizontalDivider(modifier = Modifier.padding(vertical = MainTheme.spacings.double)) + content() + } +} diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/SecretDebugSettingsScreen.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/SecretDebugSettingsScreen.kt new file mode 100644 index 00000000000..d5d0061b2d5 --- /dev/null +++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/SecretDebugSettingsScreen.kt @@ -0,0 +1,44 @@ +package net.thunderbird.feature.debugSettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonIcon +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons +import app.k9mail.core.ui.compose.designsystem.organism.TopAppBar +import app.k9mail.core.ui.compose.designsystem.template.Scaffold +import app.k9mail.core.ui.compose.theme2.MainTheme +import net.thunderbird.feature.debugSettings.notification.DebugNotificationSection + +@Composable +fun SecretDebugSettingsScreen( + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + topBar = { + TopAppBar( + title = "Secret Debug Settings Screen", + navigationIcon = { + ButtonIcon( + onClick = onNavigateBack, + imageVector = Icons.Outlined.ArrowBack, + ) + }, + ) + }, + modifier = modifier, + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(MainTheme.spacings.double), + ) { + DebugNotificationSection() + } + } +} diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/navigation/SecretDebugSettingsNavigation.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/navigation/SecretDebugSettingsNavigation.kt new file mode 100644 index 00000000000..d6b4daf0487 --- /dev/null +++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/navigation/SecretDebugSettingsNavigation.kt @@ -0,0 +1,31 @@ +package net.thunderbird.feature.debugSettings.navigation + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import app.k9mail.core.ui.compose.navigation.Navigation +import app.k9mail.core.ui.compose.navigation.deepLinkComposable +import net.thunderbird.feature.debugSettings.BuildConfig +import net.thunderbird.feature.debugSettings.SecretDebugSettingsScreen +import net.thunderbird.feature.debugSettings.navigation.SecretDebugSettingsRoute.Notification + +interface SecretDebugSettingsNavigation : Navigation + +internal class DefaultSecretDebugSettingsNavigation : SecretDebugSettingsNavigation { + override fun registerRoutes( + navGraphBuilder: NavGraphBuilder, + onBack: () -> Unit, + onFinish: (SecretDebugSettingsRoute) -> Unit, + ) { + if (BuildConfig.DEBUG) { + with(navGraphBuilder) { + deepLinkComposable(Notification.basePath) { + SecretDebugSettingsScreen( + onNavigateBack = onBack, + modifier = Modifier.fillMaxSize(), + ) + } + } + } + } +} diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/navigation/SecretDebugSettingsRoute.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/navigation/SecretDebugSettingsRoute.kt new file mode 100644 index 00000000000..9aeb27b1766 --- /dev/null +++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/navigation/SecretDebugSettingsRoute.kt @@ -0,0 +1,17 @@ +package net.thunderbird.feature.debugSettings.navigation + +import app.k9mail.core.ui.compose.navigation.Route +import kotlinx.serialization.Serializable + +sealed interface SecretDebugSettingsRoute : Route { + @Serializable + data object Notification : SecretDebugSettingsRoute { + override val basePath: String = "$SECRET_DEBUG_SETTINGS/notification" + + override fun route(): String = basePath + } + + companion object { + const val SECRET_DEBUG_SETTINGS = "app://secret_debug_settings" + } +} diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSection.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSection.kt new file mode 100644 index 00000000000..c8ca5780bfc --- /dev/null +++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSection.kt @@ -0,0 +1,225 @@ +package net.thunderbird.feature.debugSettings.notification + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.k9mail.core.ui.compose.common.mvi.observeWithoutEffect +import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonFilled +import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput +import app.k9mail.core.ui.compose.designsystem.molecule.input.TextInput +import app.k9mail.core.ui.compose.theme2.MainTheme +import kotlin.reflect.KClass +import kotlinx.collections.immutable.ImmutableList +import net.thunderbird.feature.debugSettings.DebugSection +import net.thunderbird.feature.debugSettings.DebugSubSection +import net.thunderbird.feature.debugSettings.R +import net.thunderbird.feature.debugSettings.notification.DebugNotificationSectionContract.Event +import net.thunderbird.feature.debugSettings.notification.DebugNotificationSectionContract.ViewModel +import net.thunderbird.feature.mail.account.api.BaseAccount +import net.thunderbird.feature.notification.api.content.MailNotification +import net.thunderbird.feature.notification.api.content.Notification +import org.koin.androidx.compose.koinViewModel + +private const val UUID_MAX_CHAR_DISPLAY = 4 + +@Composable +internal fun DebugNotificationSection( + modifier: Modifier = Modifier, + viewModel: ViewModel = koinViewModel(), +) { + val (state, dispatchEvent) = viewModel.observeWithoutEffect() + + DebugNotificationSection( + state = state.value, + modifier = modifier, + onAccountSelect = { account -> + dispatchEvent(Event.SelectAccount(account)) + }, + onOptionChange = { notificationType -> + dispatchEvent(Event.SelectNotificationType(notificationType)) + }, + onTriggerSystemNotificationClick = { dispatchEvent(Event.TriggerSystemNotification) }, + onTriggerInAppNotificationClick = { dispatchEvent(Event.TriggerInAppNotification) }, + onSenderChange = { dispatchEvent(Event.OnSenderChange(it)) }, + onSubjectChange = { dispatchEvent(Event.OnSubjectChange(it)) }, + onSummaryChange = { dispatchEvent(Event.OnSummaryChange(it)) }, + onPreviewChange = { dispatchEvent(Event.OnPreviewChange(it)) }, + ) +} + +@Composable +internal fun DebugNotificationSection( + state: DebugNotificationSectionContract.State, + modifier: Modifier = Modifier, + onAccountSelect: (BaseAccount) -> Unit = {}, + onOptionChange: (KClass) -> Unit = {}, + onTriggerSystemNotificationClick: () -> Unit = {}, + onTriggerInAppNotificationClick: () -> Unit = {}, + onSenderChange: (String) -> Unit = {}, + onSubjectChange: (String) -> Unit = {}, + onSummaryChange: (String) -> Unit = {}, + onPreviewChange: (String) -> Unit = {}, +) { + DebugSection( + title = stringResource(R.string.debug_settings_notifications_title), + modifier = modifier, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.quadruple), + ) { + CommonNotificationInformation(state, onAccountSelect) + SystemNotificationSection( + state = state, + onOptionChange = onOptionChange, + onClick = onTriggerSystemNotificationClick, + onSenderChange = onSenderChange, + onSubjectChange = onSubjectChange, + onSummaryChange = onSummaryChange, + onPreviewChange = onPreviewChange, + ) + InAppNotificationSection( + selectedNotificationType = state.selectedInAppNotificationType, + options = state.inAppNotificationTypes, + onOptionChange = onOptionChange, + onClick = onTriggerInAppNotificationClick, + ) + } + } +} + +@Composable +private fun CommonNotificationInformation( + state: DebugNotificationSectionContract.State, + onAccountSelect: (BaseAccount) -> Unit, + modifier: Modifier = Modifier, +) { + DebugSubSection( + title = stringResource(R.string.debug_settings_notifications_common_notification_information), + modifier = modifier.padding(start = MainTheme.spacings.double), + ) { + val loadingText = stringResource(R.string.debug_settings_notifications_loading) + SelectInput( + options = state.accounts, + selectedOption = state.selectedAccount, + onOptionChange = { account -> + account?.let(onAccountSelect) + }, + optionToStringTransformation = { account -> + account?.let { account -> + val uuidStart = account.uuid.take(UUID_MAX_CHAR_DISPLAY) + val uuidEnd = account.uuid.take(UUID_MAX_CHAR_DISPLAY) + val accountDisplay = account.name ?: account.email + "$uuidStart..$uuidEnd - $accountDisplay" + } ?: loadingText + }, + ) + } +} + +@Composable +private fun SystemNotificationSection( + state: DebugNotificationSectionContract.State, + onOptionChange: (KClass) -> Unit, + onClick: () -> Unit, + onSenderChange: (String) -> Unit, + onSubjectChange: (String) -> Unit, + onSummaryChange: (String) -> Unit, + onPreviewChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + DebugSubSection( + title = stringResource(R.string.debug_settings_notifications_system_notification), + modifier = modifier.padding(start = MainTheme.spacings.double), + ) { + Column { + TriggerNotificationSection( + selectedNotificationType = state.selectedSystemNotificationType, + options = state.systemNotificationTypes, + onOptionChange = onOptionChange, + onClick = onClick, + ) + AnimatedVisibility(state.selectedSystemNotificationType == MailNotification.NewMail.SingleMail::class) { + Column { + TextInput( + onTextChange = onSenderChange, + text = state.singleNotificationData.sender, + label = stringResource(R.string.debug_settings_notifications_single_mail_sender), + ) + TextInput( + onTextChange = onSubjectChange, + text = state.singleNotificationData.subject, + label = stringResource(R.string.debug_settings_notifications_single_mail_subject), + ) + TextInput( + onTextChange = onSummaryChange, + text = state.singleNotificationData.summary, + label = stringResource(R.string.debug_settings_notifications_single_mail_summary), + ) + TextInput( + onTextChange = onPreviewChange, + text = state.singleNotificationData.preview, + label = stringResource(R.string.debug_settings_notifications_single_mail_preview), + ) + } + } + } + } +} + +@Composable +private fun InAppNotificationSection( + selectedNotificationType: KClass?, + options: ImmutableList>, + onOptionChange: (KClass) -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + DebugSubSection( + title = stringResource(R.string.debug_settings_notifications_in_app_notification), + modifier = modifier.padding(start = MainTheme.spacings.double), + ) { + TriggerNotificationSection( + selectedNotificationType = selectedNotificationType, + options = options, + onOptionChange = onOptionChange, + onClick = onClick, + ) + } +} + +@Composable +private fun TriggerNotificationSection( + selectedNotificationType: KClass?, + options: ImmutableList>, + onOptionChange: (KClass) -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.oneHalf), + modifier = modifier, + ) { + val selectedOption = remember(selectedNotificationType, options) { + selectedNotificationType ?: options.firstOrNull() + } + val loadingText = stringResource(R.string.debug_settings_notifications_loading) + SelectInput( + options = options, + selectedOption = selectedOption, + onOptionChange = { it?.let(onOptionChange) }, + optionToStringTransformation = { kClass -> kClass?.realName ?: loadingText }, + ) + + ButtonFilled( + text = stringResource(R.string.debug_settings_notifications_trigger_notification), + onClick = onClick, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } +} diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionContract.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionContract.kt new file mode 100644 index 00000000000..305da612bd5 --- /dev/null +++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionContract.kt @@ -0,0 +1,49 @@ +package net.thunderbird.feature.debugSettings.notification + +import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel +import kotlin.reflect.KClass +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import net.thunderbird.feature.mail.account.api.BaseAccount +import net.thunderbird.feature.notification.api.content.Notification + +internal interface DebugNotificationSectionContract { + + interface ViewModel : UnidirectionalViewModel + + data class State( + val accounts: ImmutableList = persistentListOf(), + val selectedAccount: BaseAccount? = null, + val notificationStatusLog: ImmutableList = persistentListOf("Ready to send notification"), + val selectedSystemNotificationType: KClass? = null, + val selectedInAppNotificationType: KClass? = null, + val folderName: String? = null, + val singleNotificationData: MailSingleNotificationData = MailSingleNotificationData.Undefined, + val systemNotificationTypes: ImmutableList> = persistentListOf(), + val inAppNotificationTypes: ImmutableList> = persistentListOf(), + ) { + data class MailSingleNotificationData( + val sender: String = "", + val subject: String = "", + val summary: String = "", + val preview: String = "", + ) { + companion object { + val Undefined = MailSingleNotificationData() + } + } + } + + sealed interface Event { + data class SelectAccount(val account: BaseAccount) : Event + data class SelectNotificationType(val notificationType: KClass) : Event + data object TriggerSystemNotification : Event + data object TriggerInAppNotification : Event + data class OnSenderChange(val sender: String) : Event + data class OnSubjectChange(val subject: String) : Event + data class OnSummaryChange(val summary: String) : Event + data class OnPreviewChange(val preview: String) : Event + } + + sealed interface Effect +} diff --git a/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionViewModel.kt b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionViewModel.kt new file mode 100644 index 00000000000..c5ca0808361 --- /dev/null +++ b/feature/debug-settings/src/main/kotlin/net/thunderbird/feature/debugSettings/notification/DebugNotificationSectionViewModel.kt @@ -0,0 +1,286 @@ +package net.thunderbird.feature.debugSettings.notification + +import androidx.lifecycle.viewModelScope +import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import kotlin.reflect.KClass +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.thunderbird.feature.debugSettings.notification.DebugNotificationSectionContract.Effect +import net.thunderbird.feature.debugSettings.notification.DebugNotificationSectionContract.Event +import net.thunderbird.feature.debugSettings.notification.DebugNotificationSectionContract.State +import net.thunderbird.feature.mail.account.api.AccountManager +import net.thunderbird.feature.mail.account.api.BaseAccount +import net.thunderbird.feature.notification.api.NotificationGroup +import net.thunderbird.feature.notification.api.NotificationGroupKey +import net.thunderbird.feature.notification.api.content.AuthenticationErrorNotification +import net.thunderbird.feature.notification.api.content.CertificateErrorNotification +import net.thunderbird.feature.notification.api.content.FailedToCreateNotification +import net.thunderbird.feature.notification.api.content.InAppNotification +import net.thunderbird.feature.notification.api.content.MailNotification +import net.thunderbird.feature.notification.api.content.Notification +import net.thunderbird.feature.notification.api.content.PushServiceNotification +import net.thunderbird.feature.notification.api.content.SystemNotification +import net.thunderbird.feature.notification.api.sender.NotificationSender + +internal class DebugNotificationSectionViewModel( + private val accountManager: AccountManager, + private val notificationSender: NotificationSender, + private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main, + ioDispatcher: CoroutineDispatcher = Dispatchers.IO, +) : BaseViewModel(initialState = State()), DebugNotificationSectionContract.ViewModel { + + init { + viewModelScope.launch(ioDispatcher) { + val accounts = accountManager.getAccounts() + withContext(mainDispatcher) { + updateState { + val systemNotificationTypes = buildList { + add(AuthenticationErrorNotification::class) + add(CertificateErrorNotification::class) + add(FailedToCreateNotification::class) + add(MailNotification.Fetching::class) + add(MailNotification.NewMail.SingleMail::class) + add(MailNotification.NewMail.SummaryMail::class) + add(MailNotification.SendFailed::class) + add(MailNotification.Sending::class) + add(PushServiceNotification.AlarmPermissionMissing::class) + add(PushServiceNotification.Initializing::class) + add(PushServiceNotification.Listening::class) + add(PushServiceNotification.WaitBackgroundSync::class) + add(PushServiceNotification.WaitNetwork::class) + }.toPersistentList() + + val inAppNotificationTypes = buildList { + add(AuthenticationErrorNotification::class) + add(CertificateErrorNotification::class) + add(FailedToCreateNotification::class) + add(MailNotification.SendFailed::class) + add(PushServiceNotification.AlarmPermissionMissing::class) + }.toPersistentList() + State( + accounts = accounts.toPersistentList(), + selectedAccount = accounts.first(), + systemNotificationTypes = systemNotificationTypes, + inAppNotificationTypes = inAppNotificationTypes, + selectedSystemNotificationType = systemNotificationTypes.first(), + selectedInAppNotificationType = inAppNotificationTypes.first(), + ) + } + } + } + } + + override fun event(event: DebugNotificationSectionContract.Event) { + when (event) { + is DebugNotificationSectionContract.Event.TriggerSystemNotification -> viewModelScope.launch { + if (state.value.selectedSystemNotificationType == null) { + updateState { + it.copy(selectedSystemNotificationType = state.value.systemNotificationTypes.first()) + } + } + triggerNotification( + notification = requireNotNull(buildNotification(state.value.selectedSystemNotificationType)), + ) + } + + is DebugNotificationSectionContract.Event.TriggerInAppNotification -> viewModelScope.launch { + if (state.value.selectedInAppNotificationType == null) { + updateState { + it.copy(selectedInAppNotificationType = state.value.inAppNotificationTypes.first()) + } + } + triggerNotification( + notification = requireNotNull(buildNotification(state.value.selectedInAppNotificationType)), + ) + } + + is DebugNotificationSectionContract.Event.SelectAccount -> updateState { state -> + state.copy(selectedAccount = event.account) + } + + is DebugNotificationSectionContract.Event.SelectNotificationType -> viewModelScope.launch { + buildNotification(event.notificationType) + } + + is DebugNotificationSectionContract.Event.OnSenderChange -> updateState { + it.copy(singleNotificationData = it.singleNotificationData.copy(sender = event.sender)) + } + + is DebugNotificationSectionContract.Event.OnSubjectChange -> updateState { + it.copy(singleNotificationData = it.singleNotificationData.copy(subject = event.subject)) + } + + is DebugNotificationSectionContract.Event.OnSummaryChange -> updateState { + it.copy(singleNotificationData = it.singleNotificationData.copy(summary = event.summary)) + } + + is DebugNotificationSectionContract.Event.OnPreviewChange -> updateState { + it.copy(singleNotificationData = it.singleNotificationData.copy(preview = event.preview)) + } + } + } + + private suspend fun triggerNotification( + notification: Notification, + ) { + notification.let { notification -> + notificationSender + .send(notification) + .collect { result -> + updateState { + it.copy(notificationStatusLog = it.notificationStatusLog + "Result: $result") + } + } + } + } + + private suspend fun buildNotification(notificationType: KClass?): Notification? { + updateState { + it.copy( + notificationStatusLog = it.notificationStatusLog + + "Preparing notification ${notificationType?.realName}", + ) + } + + val state = state.value + val selectedAccount = state.selectedAccount ?: return null + val accountDisplay = selectedAccount.name ?: selectedAccount.email + val accountNumber = 1 // TODO: retrieve accountNumber from? + + val notification = buildNotification(notificationType, accountNumber, selectedAccount, accountDisplay, state) + + updateState { state -> + state.copy( + selectedSystemNotificationType = (notification as? SystemNotification)?.let { it::class } + ?: state.selectedSystemNotificationType, + selectedInAppNotificationType = (notification as? InAppNotification)?.let { it::class } + ?: state.selectedInAppNotificationType, + ) + } + + return notification + } + + @Suppress("CyclomaticComplexMethod", "LongMethod") + private suspend fun buildNotification( + notificationType: KClass?, + accountNumber: Int, + selectedAccount: BaseAccount, + accountDisplay: String, + state: State, + ): Notification? = when (notificationType) { + AuthenticationErrorNotification::class -> AuthenticationErrorNotification( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + accountDisplayName = accountDisplay, + ) + + CertificateErrorNotification::class -> CertificateErrorNotification( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + accountDisplayName = accountDisplay, + ) + + FailedToCreateNotification::class -> FailedToCreateNotification( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + failedNotification = AuthenticationErrorNotification( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + accountDisplayName = accountDisplay, + ), + ) + + MailNotification.Fetching::class -> MailNotification.Fetching( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + accountDisplayName = accountDisplay, + folderName = state.folderName, + ) + + MailNotification.NewMail.SingleMail::class -> state.buildSingleMailNotification( + accountNumber = accountNumber, + selectedAccount = selectedAccount, + accountDisplay = accountDisplay, + ) + + MailNotification.NewMail.SummaryMail::class -> MailNotification.NewMail.SummaryMail( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + accountDisplayName = accountDisplay, + messagesNotificationChannelSuffix = "", + newMessageCount = 10, + additionalMessagesCount = 10, + group = NotificationGroup( + key = NotificationGroupKey("key"), + summary = "", + ), + ) + + MailNotification.SendFailed::class -> MailNotification.SendFailed( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + exception = Exception("What a failure"), + ) + + MailNotification.Sending::class -> MailNotification.Sending( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + accountDisplayName = accountDisplay, + ) + + PushServiceNotification.AlarmPermissionMissing::class -> PushServiceNotification.AlarmPermissionMissing( + accountNumber = accountNumber, + ) + + PushServiceNotification.Initializing::class -> PushServiceNotification.Initializing( + accountNumber = accountNumber, + ) + + PushServiceNotification.Listening::class -> PushServiceNotification.Listening( + accountNumber = accountNumber, + ) + + PushServiceNotification.WaitBackgroundSync::class -> PushServiceNotification.WaitBackgroundSync( + accountNumber = accountNumber, + ) + + PushServiceNotification.WaitNetwork::class -> PushServiceNotification.WaitNetwork( + accountNumber = accountNumber, + ) + + else -> null + } + + private fun State.buildSingleMailNotification( + accountNumber: Int, + selectedAccount: BaseAccount, + accountDisplay: String, + ): MailNotification.NewMail.SingleMail? = MailNotification.NewMail.SingleMail( + accountNumber = accountNumber, + accountUuid = selectedAccount.uuid, + accountName = accountDisplay, + messagesNotificationChannelSuffix = "", + summary = singleNotificationData.summary, + sender = singleNotificationData.sender, + subject = singleNotificationData.subject, + preview = singleNotificationData.preview, + ) + + private operator fun ImmutableList.plus(other: String): ImmutableList = + (this.toMutableList() + other).toPersistentList() +} + +internal val KClass.realName: String + get() { + val clazz = java + + return clazz.name + .replace(clazz.`package`?.name.orEmpty(), "") + .removePrefix(".") + .replace("$", ".") + } diff --git a/feature/debug-settings/src/main/res/values/strings.xml b/feature/debug-settings/src/main/res/values/strings.xml new file mode 100644 index 00000000000..b2994350cdf --- /dev/null +++ b/feature/debug-settings/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + Loading… + Trigger notification + Sender + Subject + Summary + Preview + Common notification information + In-App notification + Notifications + System notification + diff --git a/feature/debug-settings/src/release/kotlin/net/thunderbird/feature/debugSettings/inject/FeatureDebugSettingsModule.kt b/feature/debug-settings/src/release/kotlin/net/thunderbird/feature/debugSettings/inject/FeatureDebugSettingsModule.kt new file mode 100644 index 00000000000..3d3222f7334 --- /dev/null +++ b/feature/debug-settings/src/release/kotlin/net/thunderbird/feature/debugSettings/inject/FeatureDebugSettingsModule.kt @@ -0,0 +1,6 @@ +package net.thunderbird.feature.debugSettings.inject + +import org.koin.dsl.module + +val featureDebugSettingsModule = module { +} diff --git a/feature/launcher/build.gradle.kts b/feature/launcher/build.gradle.kts index 29327a8ae3a..c66a683c93e 100644 --- a/feature/launcher/build.gradle.kts +++ b/feature/launcher/build.gradle.kts @@ -22,4 +22,6 @@ dependencies { implementation(libs.androidx.activity.compose) testImplementation(projects.core.ui.compose.testing) + + implementation(projects.feature.debugSettings) } diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherTarget.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherTarget.kt index f8b3908ca89..a358eb7f957 100644 --- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherTarget.kt +++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/FeatureLauncherTarget.kt @@ -8,6 +8,7 @@ import app.k9mail.feature.account.setup.navigation.AccountSetupRoute import app.k9mail.feature.funding.api.FundingRoute import app.k9mail.feature.onboarding.main.navigation.OnboardingRoute import net.thunderbird.feature.account.settings.api.AccountSettingsRoute +import net.thunderbird.feature.debugSettings.navigation.SecretDebugSettingsRoute sealed class FeatureLauncherTarget( val deepLinkUri: Uri, @@ -37,4 +38,8 @@ sealed class FeatureLauncherTarget( deepLinkUri = OnboardingRoute.Onboarding().route().toUri(), flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, ) + + data object SecretDebugSettings : FeatureLauncherTarget( + deepLinkUri = SecretDebugSettingsRoute.Notification.route().toUri(), + ) } diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt index 3cd557bf5e7..13074f3154b 100644 --- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt +++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/di/FeatureLauncherModule.kt @@ -4,6 +4,7 @@ import app.k9mail.feature.account.edit.featureAccountEditModule import app.k9mail.feature.account.setup.featureAccountSetupModule import app.k9mail.feature.onboarding.main.featureOnboardingModule import app.k9mail.feature.settings.import.featureSettingsImportModule +import net.thunderbird.feature.debugSettings.inject.featureDebugSettingsModule import org.koin.dsl.module val featureLauncherModule = module { @@ -12,5 +13,6 @@ val featureLauncherModule = module { featureSettingsImportModule, featureAccountSetupModule, featureAccountEditModule, + featureDebugSettingsModule, ) } diff --git a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt index 8a5602f18ef..6730661c8f3 100644 --- a/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt +++ b/feature/launcher/src/main/kotlin/app/k9mail/feature/launcher/navigation/FeatureLauncherNavHost.kt @@ -14,6 +14,7 @@ import app.k9mail.feature.launcher.FeatureLauncherExternalContract.AccountSetupF import app.k9mail.feature.onboarding.main.navigation.OnboardingNavigation import app.k9mail.feature.onboarding.main.navigation.OnboardingRoute import net.thunderbird.feature.account.settings.api.AccountSettingsNavigation +import net.thunderbird.feature.debugSettings.navigation.SecretDebugSettingsNavigation import org.koin.compose.koinInject @Composable @@ -27,6 +28,7 @@ fun FeatureLauncherNavHost( accountSetupNavigation: AccountSetupNavigation = koinInject(), onboardingNavigation: OnboardingNavigation = koinInject(), fundingNavigation: FundingNavigation = koinInject(), + secretDebugSettingsNavigation: SecretDebugSettingsNavigation = koinInject(), ) { val activity = LocalActivity.current as ComponentActivity @@ -77,5 +79,11 @@ fun FeatureLauncherNavHost( onBack = onBack, onFinish = { onBack() }, ) + + secretDebugSettingsNavigation.registerRoutes( + navGraphBuilder = this, + onBack = onBack, + onFinish = { onBack() }, + ) } } diff --git a/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/ChooseArchiveFolderDialogContentPreview.kt b/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/ChooseArchiveFolderDialogContentPreview.kt index 56aec741f25..130a38b5b68 100644 --- a/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/ChooseArchiveFolderDialogContentPreview.kt +++ b/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/ChooseArchiveFolderDialogContentPreview.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import app.k9mail.core.ui.compose.designsystem.PreviewLightDarkLandscape -import app.k9mail.core.ui.compose.designsystem.PreviewWithThemeLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark import app.k9mail.core.ui.compose.designsystem.atom.Surface import app.k9mail.core.ui.compose.theme2.MainTheme import net.thunderbird.feature.mail.folder.api.FolderType @@ -63,7 +63,7 @@ private class ChooseArchiveFolderDialogContentParamsCol : private fun ChooseArchiveFolderDialogContentPreview( @PreviewParameter(ChooseArchiveFolderDialogContentParamsCol::class) state: State.ChooseArchiveFolder, ) { - PreviewWithThemeLightDark( + PreviewWithThemesLightDark( useRow = true, useScrim = true, scrimPadding = PaddingValues(32.dp), diff --git a/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/CreateNewArchiveFolderDialogContentPreview.kt b/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/CreateNewArchiveFolderDialogContentPreview.kt index 6c512b26d3c..d6cbc62777b 100644 --- a/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/CreateNewArchiveFolderDialogContentPreview.kt +++ b/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/CreateNewArchiveFolderDialogContentPreview.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import app.k9mail.core.ui.compose.designsystem.PreviewLightDarkLandscape -import app.k9mail.core.ui.compose.designsystem.PreviewWithThemeLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark import app.k9mail.core.ui.compose.designsystem.atom.Surface import app.k9mail.core.ui.compose.theme2.MainTheme @@ -67,7 +67,7 @@ private class CreateArchiveFolderPreviewParamsCollection : private fun CreateNewArchiveFolderDialogContentPreview( @PreviewParameter(CreateArchiveFolderPreviewParamsCollection::class) params: CreateArchiveFolderPreviewParams, ) { - PreviewWithThemeLightDark( + PreviewWithThemesLightDark( useRow = true, useScrim = true, scrimPadding = PaddingValues(32.dp), diff --git a/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/EmailCantBeArchivedDialogButtonsPreview.kt b/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/EmailCantBeArchivedDialogButtonsPreview.kt index 7875f38b066..4622bf88866 100644 --- a/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/EmailCantBeArchivedDialogButtonsPreview.kt +++ b/feature/mail/message/list/src/debug/kotlin/net/thunderbird/feature/mail/message/list/ui/dialog/EmailCantBeArchivedDialogButtonsPreview.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.k9mail.core.ui.compose.designsystem.PreviewLightDarkLandscape -import app.k9mail.core.ui.compose.designsystem.PreviewWithThemeLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark import app.k9mail.core.ui.compose.designsystem.atom.Surface import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium import app.k9mail.core.ui.compose.theme2.MainTheme @@ -24,7 +24,7 @@ import net.thunderbird.feature.mail.message.list.R @PreviewLightDarkLandscape @Composable private fun EmailCantBeArchivedDialogButtonsPreview() { - PreviewWithThemeLightDark( + PreviewWithThemesLightDark( useRow = true, useScrim = true, scrimPadding = PaddingValues(32.dp), diff --git a/feature/notification/api/build.gradle.kts b/feature/notification/api/build.gradle.kts index 81460519e1c..58419a57df5 100644 --- a/feature/notification/api/build.gradle.kts +++ b/feature/notification/api/build.gradle.kts @@ -1,5 +1,8 @@ +import org.jetbrains.kotlin.gradle.internal.config.LanguageFeature + plugins { id(ThunderbirdPlugins.Library.kmpCompose) + id("kotlin-parcelize") } kotlin { @@ -8,6 +11,20 @@ kotlin { implementation(projects.core.common) implementation(projects.core.outcome) } + androidMain.dependencies { + implementation(projects.core.ui.compose.designsystem) + } + } + + compilerOptions { + freeCompilerArgs.addAll( + "-P", + "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=net.thunderbird.core.common.io.KmpParcelize", + ) + } + + sourceSets.all { + languageSettings.enableLanguageFeature(LanguageFeature.ExpectActualClasses.name) } } diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt new file mode 100644 index 00000000000..ea2044a3184 --- /dev/null +++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt @@ -0,0 +1,46 @@ +package net.thunderbird.feature.notification.api.ui.action.icon + +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons +import net.thunderbird.feature.notification.api.R +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon + +internal actual val NotificationActionIcons.Reply: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_reply, + ) + +internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_mark_email_read, + ) + +internal actual val NotificationActionIcons.Delete: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_delete, + ) + +internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_report, + ) + +internal actual val NotificationActionIcons.Archive: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_archive, + ) + +internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_settings, + ) + +internal actual val NotificationActionIcons.Retry: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_refresh, + ) + +internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_settings, + inAppNotificationIcon = Icons.Outlined.Settings, + ) diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.android.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.android.kt new file mode 100644 index 00000000000..096c63b7a2d --- /dev/null +++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.android.kt @@ -0,0 +1,84 @@ +package net.thunderbird.feature.notification.api.ui.icon + +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons +import app.k9mail.core.ui.compose.designsystem.atom.icon.filled.Notification +import app.k9mail.core.ui.compose.designsystem.atom.icon.outlined.Warning +import net.thunderbird.feature.notification.api.R + +internal actual val NotificationIcons.AuthenticationError: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_warning, + inAppNotificationIcon = Icons.Outlined.Warning, + ) + +internal actual val NotificationIcons.CertificateError: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_warning, + inAppNotificationIcon = Icons.Outlined.Warning, + ) + +internal actual val NotificationIcons.FailedToCreate: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_warning, + inAppNotificationIcon = Icons.Outlined.Warning, + ) + +internal actual val NotificationIcons.MailFetching: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_sync_animated, + inAppNotificationIcon = Icons.Outlined.Sync, + ) + +internal actual val NotificationIcons.MailSending: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_sync_animated, + inAppNotificationIcon = Icons.Outlined.Sync, + ) + +internal actual val NotificationIcons.MailSendFailed: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_warning, + inAppNotificationIcon = Icons.Outlined.Warning, + ) + +internal actual val NotificationIcons.NewMailSingleMail: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_new_email, + inAppNotificationIcon = Icons.Outlined.Sync, + ) + +internal actual val NotificationIcons.NewMailSummaryMail: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_new_email, + inAppNotificationIcon = Icons.Outlined.Sync, + ) + +internal actual val NotificationIcons.PushServiceInitializing: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_notification, + inAppNotificationIcon = Icons.Filled.Notification, + ) + +internal actual val NotificationIcons.PushServiceListening: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_notification, + inAppNotificationIcon = Icons.Filled.Notification, + ) + +internal actual val NotificationIcons.PushServiceWaitBackgroundSync: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_notification, + inAppNotificationIcon = Icons.Filled.Notification, + ) + +internal actual val NotificationIcons.PushServiceWaitNetwork: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_notification, + inAppNotificationIcon = Icons.Filled.Notification, + ) + +internal actual val NotificationIcons.AlarmPermissionMissing: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_notification, + inAppNotificationIcon = Icons.Filled.Notification, + ) diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.android.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.android.kt new file mode 100644 index 00000000000..9b05b039eff --- /dev/null +++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.android.kt @@ -0,0 +1,3 @@ +package net.thunderbird.feature.notification.api.ui.icon + +actual typealias SystemNotificationIcon = Int diff --git a/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_0.png b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_0.png new file mode 100644 index 00000000000..837495c235e Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_0.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_1.png b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_1.png new file mode 100644 index 00000000000..6db06a75c76 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_1.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_2.png b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_2.png new file mode 100644 index 00000000000..d27b881a7b9 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_2.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_3.png b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_3.png new file mode 100644 index 00000000000..2d5837622f6 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_3.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_4.png b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_4.png new file mode 100644 index 00000000000..cc8891de58c Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_4.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_5.png b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_5.png new file mode 100644 index 00000000000..396582de5c6 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-hdpi/notification_icon_check_mail_anim_5.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_0.png b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_0.png new file mode 100644 index 00000000000..a8ea61f4eec Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_0.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_1.png b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_1.png new file mode 100644 index 00000000000..7dc44ba1934 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_1.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_2.png b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_2.png new file mode 100644 index 00000000000..c05e17a4ea8 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_2.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_3.png b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_3.png new file mode 100644 index 00000000000..93f0ea1c034 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_3.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_4.png b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_4.png new file mode 100644 index 00000000000..2ab90799e5a Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_4.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_5.png b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_5.png new file mode 100644 index 00000000000..f2b77f4eed8 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-mdpi/notification_icon_check_mail_anim_5.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_0.png b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_0.png new file mode 100644 index 00000000000..aa48274d0d0 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_0.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_1.png b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_1.png new file mode 100644 index 00000000000..de8b57e1823 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_1.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_2.png b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_2.png new file mode 100644 index 00000000000..a9d28b9ff6b Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_2.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_3.png b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_3.png new file mode 100644 index 00000000000..5212e3bfdf5 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_3.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_4.png b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_4.png new file mode 100644 index 00000000000..9c3dbdd5b84 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_4.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_5.png b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_5.png new file mode 100644 index 00000000000..3a4557820c1 Binary files /dev/null and b/feature/notification/api/src/androidMain/res/drawable-xhdpi/notification_icon_check_mail_anim_5.png differ diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_archive.xml b/feature/notification/api/src/androidMain/res/drawable/ic_archive.xml new file mode 100644 index 00000000000..cff1ed18e49 --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_archive.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_delete.xml b/feature/notification/api/src/androidMain/res/drawable/ic_delete.xml new file mode 100644 index 00000000000..9102cf11eb9 --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_delete.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_mark_email_read.xml b/feature/notification/api/src/androidMain/res/drawable/ic_mark_email_read.xml new file mode 100644 index 00000000000..96fb7e36af8 --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_mark_email_read.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_new_email.xml b/feature/notification/api/src/androidMain/res/drawable/ic_new_email.xml new file mode 100644 index 00000000000..39bced6ef3d --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_new_email.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_notification.xml b/feature/notification/api/src/androidMain/res/drawable/ic_notification.xml new file mode 100644 index 00000000000..eb14b87e968 --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_notification.xml @@ -0,0 +1,14 @@ + + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_refresh.xml b/feature/notification/api/src/androidMain/res/drawable/ic_refresh.xml new file mode 100644 index 00000000000..5a0f23ae303 --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_refresh.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_reply.xml b/feature/notification/api/src/androidMain/res/drawable/ic_reply.xml new file mode 100644 index 00000000000..ace979916ce --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_reply.xml @@ -0,0 +1,14 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_report.xml b/feature/notification/api/src/androidMain/res/drawable/ic_report.xml new file mode 100644 index 00000000000..4bb9c95a395 --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_report.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_settings.xml b/feature/notification/api/src/androidMain/res/drawable/ic_settings.xml new file mode 100644 index 00000000000..66954d3f97f --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_settings.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_sync_animated.xml b/feature/notification/api/src/androidMain/res/drawable/ic_sync_animated.xml new file mode 100644 index 00000000000..51302173b4f --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_sync_animated.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/feature/notification/api/src/androidMain/res/drawable/ic_warning.xml b/feature/notification/api/src/androidMain/res/drawable/ic_warning.xml new file mode 100644 index 00000000000..faf88abe7cf --- /dev/null +++ b/feature/notification/api/src/androidMain/res/drawable/ic_warning.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/notification/api/src/androidMain/res/values/strings.xml b/feature/notification/api/src/androidMain/res/values/strings.xml new file mode 100644 index 00000000000..6cfc43e2d6b --- /dev/null +++ b/feature/notification/api/src/androidMain/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Post In-App notifications + Allows the app to post In-App notifications + diff --git a/feature/notification/api/src/commonMain/composeResources/values/strings.xml b/feature/notification/api/src/commonMain/composeResources/values/strings.xml index 2320de026e7..19ec870598c 100644 --- a/feature/notification/api/src/commonMain/composeResources/values/strings.xml +++ b/feature/notification/api/src/commonMain/composeResources/values/strings.xml @@ -1,6 +1,6 @@ - + Synchronize (Push) Displayed while waiting for new messages @@ -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 @@ -45,4 +45,14 @@ Disable Push + Reply + Mark Read + Mark All Read + Delete + Delete All + Archive + Archive All + Spam + Retry + Update Server Settings diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/LockscreenNotificationAppearance.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/LockscreenNotificationAppearance.kt index 90962fcd478..326f951df37 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/LockscreenNotificationAppearance.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/LockscreenNotificationAppearance.kt @@ -1,9 +1,13 @@ package net.thunderbird.feature.notification.api +import net.thunderbird.core.common.io.KmpParcelable +import net.thunderbird.core.common.io.KmpParcelize + /** * Defines the appearance of notifications on the lock screen. */ -sealed interface LockscreenNotificationAppearance { +@KmpParcelize +sealed interface LockscreenNotificationAppearance : KmpParcelable { /** * No notifications are shown on the lock screen. */ diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationChannel.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationChannel.kt index f1b0ed12618..ff34e2381d5 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationChannel.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationChannel.kt @@ -1,5 +1,8 @@ package net.thunderbird.feature.notification.api +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelable +import net.thunderbird.core.common.io.KmpParcelize import net.thunderbird.feature.notification.resources.Res import net.thunderbird.feature.notification.resources.notification_channel_messages_description import net.thunderbird.feature.notification.resources.notification_channel_messages_title @@ -21,25 +24,31 @@ import org.jetbrains.compose.resources.StringResource */ sealed class NotificationChannel( val id: String, - val name: StringResource, - val description: StringResource, val importance: NotificationChannelImportance, -) { +) : KmpParcelable { + abstract val name: StringResource + abstract val description: StringResource + /** * Represents a notification channel for new messages. * * @property accountUuid The unique identifier of the account associated with these messages. * @property suffix An optional suffix to further differentiate the channel, e.g., for different folder types. */ + @KmpParcelize data class Messages( val accountUuid: String, val suffix: String, ) : NotificationChannel( id = "messages_channel_$accountUuid$suffix", - name = Res.string.notification_channel_messages_title, - description = Res.string.notification_channel_messages_description, importance = NotificationChannelImportance.Default, - ) + ) { + @KmpIgnoredOnParcel + override val name = Res.string.notification_channel_messages_title + + @KmpIgnoredOnParcel + override val description = Res.string.notification_channel_messages_description + } /** * Represents a notification channel for miscellaneous notifications. @@ -50,6 +59,7 @@ sealed class NotificationChannel( * * @property accountUuid The unique identifier of the account associated with these notifications, if applicable. */ + @KmpParcelize data class Miscellaneous( val accountUuid: String? = null, ) : NotificationChannel( @@ -58,10 +68,14 @@ sealed class NotificationChannel( } else { "miscellaneous_channel_$accountUuid" }, - name = Res.string.notification_channel_miscellaneous_title, - description = Res.string.notification_channel_miscellaneous_description, importance = NotificationChannelImportance.Low, - ) + ) { + @KmpIgnoredOnParcel + override val name = Res.string.notification_channel_miscellaneous_title + + @KmpIgnoredOnParcel + override val description = Res.string.notification_channel_miscellaneous_description + } /** * Represents a notification channel for push service messages. @@ -69,12 +83,17 @@ sealed class NotificationChannel( * This channel is used for notifications related to the background push service, * such as connection status or errors. */ + @KmpParcelize data object PushService : NotificationChannel( id = "push", - name = Res.string.notification_channel_push_title, - description = Res.string.notification_channel_push_description, importance = NotificationChannelImportance.Low, - ) + ) { + @KmpIgnoredOnParcel + override val name = Res.string.notification_channel_push_title + + @KmpIgnoredOnParcel + override val description = Res.string.notification_channel_push_description + } } /** diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroup.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroup.kt index a66fa48183a..8a79e208092 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroup.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroup.kt @@ -1,9 +1,19 @@ package net.thunderbird.feature.notification.api -import net.thunderbird.feature.notification.api.NotificationGroupKey +import net.thunderbird.core.common.io.KmpParcelable +import net.thunderbird.core.common.io.KmpParcelize -// TODO: Properly handle notification groups, adding summary, etc. +// TODO(9419): Properly handle notification groups, adding summary, etc. +@KmpParcelize data class NotificationGroup( val key: NotificationGroupKey, val summary: String, -) + val alertBehaviour: NotificationGroupAlertBehaviour = NotificationGroupAlertBehaviour.AlertSummary, +) : KmpParcelable + +enum class NotificationGroupAlertBehaviour { + AlertAll, + AlertSummary, + AlertChildren, + Silent, +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroupKey.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroupKey.kt index c88b437f72c..164b076cc33 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroupKey.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationGroupKey.kt @@ -1,5 +1,8 @@ package net.thunderbird.feature.notification.api +import net.thunderbird.core.common.io.KmpParcelable +import net.thunderbird.core.common.io.KmpParcelize + /** * Represents a key for a notification group. * @@ -10,4 +13,5 @@ package net.thunderbird.feature.notification.api * @param value The string value of the notification group key. */ @JvmInline -value class NotificationGroupKey(val value: String) +@KmpParcelize +value class NotificationGroupKey(val value: String) : KmpParcelable diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationId.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationId.kt index a205c776371..4023e935d7d 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationId.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationId.kt @@ -1,5 +1,8 @@ package net.thunderbird.feature.notification.api +import net.thunderbird.core.common.io.KmpParcelable +import net.thunderbird.core.common.io.KmpParcelize + /** * Represents a unique identifier for a notification. * @@ -10,4 +13,9 @@ package net.thunderbird.feature.notification.api * @property value The integer value of the notification ID. */ @JvmInline -value class NotificationId(val value: Int) : Comparable by value +@KmpParcelize +value class NotificationId(val value: Int) : KmpParcelable, Comparable by value { + companion object { + val Undefined = NotificationId(value = -1) + } +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationIdFactory.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationIdFactory.kt new file mode 100644 index 00000000000..fd2bd256aa0 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationIdFactory.kt @@ -0,0 +1,6 @@ +package net.thunderbird.feature.notification.api + +// TODO(#9416): Migrate logic from NotificationIds to NotificationIdFactory +interface NotificationIdFactory { + fun next(accountNumber: Int, offset: Int = 0): NotificationId +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationSeverity.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationSeverity.kt index b943b3f3d73..961ca6a6bab 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationSeverity.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/NotificationSeverity.kt @@ -12,7 +12,7 @@ package net.thunderbird.feature.notification.api * For [Temporary] and [Warning], user action might be recommended or optional. * For [Information], no user action is usually needed. */ -enum class NotificationSeverity { +enum class NotificationSeverity(val dismissable: Boolean) { /** * Completely blocks the user from performing essential tasks or accessing core functionality. * @@ -24,7 +24,7 @@ enum class NotificationSeverity { * - Retry * - Provide other credentials */ - Fatal, + Fatal(dismissable = false), /** * Prevents the user from completing specific core actions or causes significant disruption to functionality. @@ -36,7 +36,7 @@ enum class NotificationSeverity { * - **Notification Actions:** * - Retry */ - Critical, + Critical(dismissable = false), /** * Causes a temporary disruption or delay to functionality, which may resolve on its own. @@ -48,7 +48,7 @@ enum class NotificationSeverity { * - **Notification Message:** You are offline, the message will be sent later. * - **Notification Actions:** N/A */ - Temporary, + Temporary(dismissable = true), /** * Alerts the user to a potential issue or limitation that may affect functionality if not addressed. @@ -61,7 +61,7 @@ enum class NotificationSeverity { * - **Notification Actions:** * - Manage Storage */ - Warning, + Warning(dismissable = true), /** * Provides status or context without impacting functionality or requiring action. @@ -72,5 +72,5 @@ enum class NotificationSeverity { * - **Notification Message:** Last time email synchronization succeeded * - **Notification Actions:** N/A */ - Information, + Information(dismissable = true), } diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/command/NotificationCommand.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/command/NotificationCommand.kt index 8ef3f82526d..08d41294514 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/command/NotificationCommand.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/command/NotificationCommand.kt @@ -24,7 +24,7 @@ abstract class NotificationCommand( * Executes the command. * @return The result of the execution. */ - abstract fun execute(): Outcome, Failure> + abstract suspend fun execute(): Outcome, Failure> /** * Represents the outcome of a command's execution. 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 887d0bf5bca..46a36416d98 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 @@ -4,12 +4,14 @@ import kotlinx.datetime.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import net.thunderbird.core.common.io.KmpParcelable import net.thunderbird.feature.notification.api.LockscreenNotificationAppearance import net.thunderbird.feature.notification.api.NotificationChannel import net.thunderbird.feature.notification.api.NotificationGroup -import net.thunderbird.feature.notification.api.NotificationId import net.thunderbird.feature.notification.api.NotificationSeverity +import net.thunderbird.feature.notification.api.ui.NotificationStyle import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon /** * Represents a notification that can be displayed to the user. @@ -17,29 +19,31 @@ import net.thunderbird.feature.notification.api.ui.action.NotificationAction * This interface defines the common properties that all notifications must have. * Must not be directly implemented. You must extend [AppNotification] instead. * - * @property id The unique identifier of the notification. * @property title The title of the notification. * @property accessibilityText The text to be used for accessibility purposes. * @property contentText The main content text of the notification, can be null. + * @property subText Additional text displayed below the content text, can be null. * @property severity The severity level of the notification. * @property createdAt The date and time when the notification was created. * @property actions A set of actions that can be performed on the notification. * @property authenticationRequired Indicates whether authentication is required to view the notification. * @property channel The notification channel to which this notification belongs. * @property group The notification group to which this notification belongs, can be null. + * @property icon The notification icon. * @see AppNotification */ -sealed interface Notification { - val id: NotificationId +sealed interface Notification : KmpParcelable { val title: String val accessibilityText: String val contentText: String? + val subText: String? get() = null val severity: NotificationSeverity val createdAt: LocalDateTime val actions: Set val authenticationRequired: Boolean val channel: NotificationChannel val group: NotificationGroup? + val icon: NotificationIcon } /** @@ -67,24 +71,34 @@ sealed class AppNotification : Notification { * Represents a notification displayed by the system, **requiring user permission**. * This type of notification can appear on the lock screen. * + * @property accountNumber The account number associated with this notification. * @property lockscreenNotification The notification to display on the lock screen. * Override if you need to hide any content when showing this notification in the lockscreen. * By default, this is the same as the notification itself. + * @property systemNotificationStyle The style of the system notification. + * Defaults to [NotificationStyle.System.Undefined]. * @property lockscreenNotificationAppearance The appearance of the notification on the lockscreen. * By default, the notification is [LockscreenNotificationAppearance.Public]. * @see LockscreenNotificationAppearance + * @see NotificationStyle.System + * @see net.thunderbird.feature.notification.api.ui.builder.notificationStyle */ sealed interface SystemNotification : Notification { + val accountNumber: Int val lockscreenNotification: SystemNotification get() = this + + val systemNotificationStyle: NotificationStyle.System get() = NotificationStyle.System.Undefined + val lockscreenNotificationAppearance: LockscreenNotificationAppearance get() = LockscreenNotificationAppearance.Public } /** - * * Represents a notification displayed within the application. * * In-app notifications are typically less intrusive than system notifications and **do not require** * system notification permissions to be displayed. */ -sealed interface InAppNotification : Notification +sealed interface InAppNotification : Notification { + val inAppNotificationStyle: NotificationStyle.InApp get() = NotificationStyle.InApp.Undefined +} 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 e71371f7ae3..6edd38a27a6 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 @@ -1,9 +1,13 @@ package net.thunderbird.feature.notification.api.content +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelize import net.thunderbird.feature.notification.api.NotificationChannel -import net.thunderbird.feature.notification.api.NotificationId import net.thunderbird.feature.notification.api.NotificationSeverity 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.resources.Res import net.thunderbird.feature.notification.resources.notification_authentication_error_text import net.thunderbird.feature.notification.resources.notification_authentication_error_title @@ -15,40 +19,45 @@ import org.jetbrains.compose.resources.getString * This notification is both a [SystemNotification] and an [InAppNotification]. */ @ConsistentCopyVisibility +@KmpParcelize data class AuthenticationErrorNotification private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String?, override val channel: NotificationChannel, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.AuthenticationError, ) : AppNotification(), SystemNotification, InAppNotification { + @KmpIgnoredOnParcel override val severity: NotificationSeverity = NotificationSeverity.Fatal + + @KmpIgnoredOnParcel override val actions: Set = setOf( NotificationAction.Retry, NotificationAction.UpdateServerSettings, ) - override val lockscreenNotification: SystemNotification = copy(contentText = null) + override val lockscreenNotification: SystemNotification get() = copy(contentText = null) companion object { /** * Creates an [AuthenticationErrorNotification]. * - * @param id The unique identifier for this notification. * @param accountUuid The UUID of the account associated with the authentication error. * @param accountDisplayName The display name of the account associated with the authentication error. * @return An [AuthenticationErrorNotification] instance. */ suspend operator fun invoke( - id: NotificationId, + accountNumber: Int, accountUuid: String, accountDisplayName: String, ): AuthenticationErrorNotification = AuthenticationErrorNotification( - id = id, - title = getString( - resource = Res.string.notification_authentication_error_title, + 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 4ce605f888e..71b04bd464b 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 @@ -1,9 +1,13 @@ package net.thunderbird.feature.notification.api.content +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelize import net.thunderbird.feature.notification.api.NotificationChannel -import net.thunderbird.feature.notification.api.NotificationId import net.thunderbird.feature.notification.api.NotificationSeverity import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.api.ui.icon.CertificateError +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons import net.thunderbird.feature.notification.resources.Res import net.thunderbird.feature.notification.resources.notification_certificate_error_public import net.thunderbird.feature.notification.resources.notification_certificate_error_text @@ -16,17 +20,23 @@ import org.jetbrains.compose.resources.getString * preventing secure communication. It prompts the user to update their server settings. */ @ConsistentCopyVisibility +@KmpParcelize data class CertificateErrorNotification private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String, val lockScreenTitle: String, override val channel: NotificationChannel, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.CertificateError, ) : AppNotification(), SystemNotification, InAppNotification { + @KmpIgnoredOnParcel override val severity: NotificationSeverity = NotificationSeverity.Fatal + + @KmpIgnoredOnParcel override val actions: Set = setOf(NotificationAction.UpdateServerSettings) - override val lockscreenNotification: SystemNotification = copy( + override val lockscreenNotification: SystemNotification get() = copy( contentText = lockScreenTitle, ) @@ -34,17 +44,16 @@ data class CertificateErrorNotification private constructor( /** * Creates a [CertificateErrorNotification]. * - * @param id The unique identifier for this notification. * @param accountUuid The UUID of the account associated with the notification. * @param accountDisplayName The display name of the account associated with the notification. * @return A [CertificateErrorNotification] instance. */ suspend operator fun invoke( - id: NotificationId, + accountNumber: Int, accountUuid: String, accountDisplayName: String, ): CertificateErrorNotification = CertificateErrorNotification( - id = id, + accountNumber = accountNumber, title = getString(resource = Res.string.notification_certificate_error_public, 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 9783c079fa3..fee81e23e63 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 @@ -1,8 +1,12 @@ package net.thunderbird.feature.notification.api.content +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelize import net.thunderbird.feature.notification.api.NotificationChannel -import net.thunderbird.feature.notification.api.NotificationId import net.thunderbird.feature.notification.api.NotificationSeverity +import net.thunderbird.feature.notification.api.ui.icon.FailedToCreate +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons import net.thunderbird.feature.notification.resources.Res import net.thunderbird.feature.notification.resources.notification_notify_error_text import net.thunderbird.feature.notification.resources.notification_notify_error_title @@ -15,30 +19,33 @@ import org.jetbrains.compose.resources.getString * It has a critical severity level. */ @ConsistentCopyVisibility +@KmpParcelize data class FailedToCreateNotification private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String?, override val channel: NotificationChannel, val failedNotification: AppNotification, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.FailedToCreate, ) : AppNotification(), SystemNotification, InAppNotification { + @KmpIgnoredOnParcel override val severity: NotificationSeverity = NotificationSeverity.Critical companion object { /** * Creates a [FailedToCreateNotification] instance. * - * @param id The unique identifier for this notification. * @param accountUuid The UUID of the account associated with the failed notification. * @param failedNotification The original [AppNotification] that failed to be created. * @return A [FailedToCreateNotification] instance. */ suspend operator fun invoke( - id: NotificationId, + accountNumber: Int, accountUuid: String, failedNotification: AppNotification, ): FailedToCreateNotification = FailedToCreateNotification( - id = id, + accountNumber = accountNumber, 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 deac302f7e6..45b23ab38ec 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 @@ -1,12 +1,21 @@ package net.thunderbird.feature.notification.api.content import net.thunderbird.core.common.exception.rootCauseMassage -import net.thunderbird.feature.notification.api.LockscreenNotificationAppearance +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelize import net.thunderbird.feature.notification.api.NotificationChannel import net.thunderbird.feature.notification.api.NotificationGroup -import net.thunderbird.feature.notification.api.NotificationId import net.thunderbird.feature.notification.api.NotificationSeverity +import net.thunderbird.feature.notification.api.ui.NotificationStyle import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.api.ui.builder.notificationStyle +import net.thunderbird.feature.notification.api.ui.icon.MailFetching +import net.thunderbird.feature.notification.api.ui.icon.MailSendFailed +import net.thunderbird.feature.notification.api.ui.icon.MailSending +import net.thunderbird.feature.notification.api.ui.icon.NewMailSingleMail +import net.thunderbird.feature.notification.api.ui.icon.NewMailSummaryMail +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons import net.thunderbird.feature.notification.resources.Res import net.thunderbird.feature.notification.resources.notification_additional_messages import net.thunderbird.feature.notification.resources.notification_bg_send_ticker @@ -23,38 +32,46 @@ import org.jetbrains.compose.resources.getString * Represents mail-related notifications. By default, all mail-related subclasses are [SystemNotification], * however they may also implement [InAppNotification] for more severe notifications. */ +@KmpParcelize sealed class MailNotification : AppNotification(), SystemNotification { + @KmpIgnoredOnParcel override val severity: NotificationSeverity = NotificationSeverity.Information + + @KmpIgnoredOnParcel override val authenticationRequired: Boolean = true - data class Fetching( - override val id: NotificationId, + @KmpParcelize + @ConsistentCopyVisibility + data class Fetching private constructor( + override val accountNumber: Int, override val title: String, override val accessibilityText: String, override val contentText: String?, override val channel: NotificationChannel, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.MailFetching, ) : MailNotification() { - override val lockscreenNotification: SystemNotification = copy(contentText = null) + @KmpIgnoredOnParcel + override val lockscreenNotification: SystemNotification get() = copy(contentText = null) companion object { /** * Creates a [Fetching] notification. * - * @param id The unique identifier for this notification. * @param accountUuid The UUID of the account being fetched. * @param accountDisplayName The display name of the account being fetched. * @param folderName The name of the folder being fetched, or null if fetching all folders. * @return A [Fetching] notification. */ suspend operator fun invoke( - id: NotificationId, + accountNumber: Int, accountUuid: String, accountDisplayName: String, folderName: String?, ): Fetching { val title = getString(resource = Res.string.notification_bg_sync_title) return Fetching( - id = id, + accountNumber = accountNumber, title = title, accessibilityText = folderName?.let { folderName -> getString( @@ -76,30 +93,34 @@ sealed class MailNotification : AppNotification(), SystemNotification { } } - data class Sending( - override val id: NotificationId, + @KmpParcelize + @ConsistentCopyVisibility + data class Sending private constructor( + override val accountNumber: Int, override val title: String, override val accessibilityText: String, override val contentText: String?, override val channel: NotificationChannel, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.MailSending, ) : MailNotification() { - override val lockscreenNotification: SystemNotification = copy(contentText = null) + @KmpIgnoredOnParcel + override val lockscreenNotification: SystemNotification get() = copy(contentText = null) companion object { /** * Creates a [Sending] notification. * - * @param id The unique identifier for this notification. * @param accountUuid The UUID of the account sending the message. * @param accountDisplayName The display name of the account sending the message. * @return A [Sending] notification. */ suspend operator fun invoke( - id: NotificationId, + accountNumber: Int, accountUuid: String, accountDisplayName: String, ): Sending = Sending( - id = id, + accountNumber = accountNumber, title = getString(resource = Res.string.notification_bg_send_title), accessibilityText = getString( resource = Res.string.notification_bg_send_ticker, @@ -111,14 +132,23 @@ sealed class MailNotification : AppNotification(), SystemNotification { } } - data class SendFailed( - override val id: NotificationId, + @KmpParcelize + @ConsistentCopyVisibility + data class SendFailed private constructor( + override val accountNumber: Int, override val title: String, override val contentText: String?, override val channel: NotificationChannel, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.MailSendFailed, ) : MailNotification(), InAppNotification { + @KmpIgnoredOnParcel override val severity: NotificationSeverity = NotificationSeverity.Critical - override val lockscreenNotification: SystemNotification = copy(contentText = null) + + @KmpIgnoredOnParcel + override val lockscreenNotification: SystemNotification get() = copy(contentText = null) + + @KmpIgnoredOnParcel override val actions: Set = setOf( NotificationAction.Retry, ) @@ -127,17 +157,16 @@ sealed class MailNotification : AppNotification(), SystemNotification { /** * Creates a [SendFailed] notification. * - * @param id The unique identifier for this notification. * @param accountUuid The UUID of the account sending the message. * @param exception The exception that occurred during sending. * @return A [SendFailed] notification. */ suspend operator fun invoke( - id: NotificationId, + accountNumber: Int, accountUuid: String, exception: Exception, ): SendFailed = SendFailed( - id = id, + accountNumber = accountNumber, title = getString(resource = Res.string.send_failure_subject), contentText = exception.rootCauseMassage, channel = NotificationChannel.Miscellaneous(accountUuid = accountUuid), @@ -153,15 +182,18 @@ sealed class MailNotification : AppNotification(), SystemNotification { * @property channel The notification channel for this notification. * @property actions The set of actions available for this notification. */ + @KmpParcelize sealed class NewMail : MailNotification() { abstract val accountUuid: String abstract val messagesNotificationChannelSuffix: String - override val channel: NotificationChannel = NotificationChannel.Messages( + @KmpIgnoredOnParcel + override val channel: NotificationChannel get() = NotificationChannel.Messages( accountUuid = accountUuid, suffix = messagesNotificationChannelSuffix, ) + @KmpIgnoredOnParcel override val actions: Set = setOf( NotificationAction.Reply, NotificationAction.MarkAsRead, @@ -173,19 +205,16 @@ sealed class MailNotification : AppNotification(), SystemNotification { /** * Represents a notification for a single new email. * - * @property id The unique identifier for this notification. * @property accountUuid The UUID of the account that received the email. * @property accountName The display name of the account that received the email. - * @property messagesNotificationChannelSuffix The suffix for the messages notification channel. * @property summary A short summary of the email content. * @property sender The sender of the email. * @property subject The subject of the email. * @property preview A preview of the email content. - * @property group The notification group this notification belongs to, if any. - * @property lockscreenNotificationAppearance Specifies how this notification should appear on the lockscreen. */ + @KmpParcelize data class SingleMail( - override val id: NotificationId, + override val accountNumber: Int, override val accountUuid: String, val accountName: String, override val messagesNotificationChannelSuffix: String, @@ -193,17 +222,26 @@ sealed class MailNotification : AppNotification(), SystemNotification { val sender: String, val subject: String, val preview: String, - override val group: NotificationGroup?, - override val lockscreenNotificationAppearance: LockscreenNotificationAppearance, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.NewMailSingleMail, ) : NewMail() { + @KmpIgnoredOnParcel override val title: String = sender + + @KmpIgnoredOnParcel override val contentText: String = subject + + @KmpIgnoredOnParcel + override val systemNotificationStyle: NotificationStyle.System = notificationStyle { + systemStyle { + bigText(preview) + } + }.systemStyle } /** * Represents a summary notification for new mail. * - * @property id The unique identifier for this notification. * @property accountUuid The UUID of the account. * @property accountName The display name of the account. * @property messagesNotificationChannelSuffix The suffix for the messages notification channel. @@ -211,21 +249,23 @@ sealed class MailNotification : AppNotification(), SystemNotification { * @property contentText The content text of the notification, or null if there is no content text. * @property group The notification group this summary belongs to. */ + @KmpParcelize @ConsistentCopyVisibility data class SummaryMail private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val accountUuid: String, val accountName: String, override val messagesNotificationChannelSuffix: String, override val title: String, override val contentText: String?, override val group: NotificationGroup, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.NewMailSummaryMail, ) : NewMail() { companion object { /** * Creates a [SummaryMail] notification. * - * @param id The unique identifier for this notification. * @param accountUuid The UUID of the account. * @param accountDisplayName The display name of the account. * @param messagesNotificationChannelSuffix The suffix for the messages notification channel. @@ -236,7 +276,7 @@ sealed class MailNotification : AppNotification(), SystemNotification { * @return A [SummaryMail] notification. */ suspend operator fun invoke( - id: NotificationId, + accountNumber: Int, accountUuid: String, accountDisplayName: String, messagesNotificationChannelSuffix: String, @@ -244,7 +284,7 @@ sealed class MailNotification : AppNotification(), SystemNotification { additionalMessagesCount: Int, group: NotificationGroup, ): SummaryMail = SummaryMail( - id = id, + accountNumber = accountNumber, accountUuid = accountUuid, accountName = accountDisplayName, messagesNotificationChannelSuffix = messagesNotificationChannelSuffix, 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 541c155f45e..1159e06c9bc 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 @@ -1,9 +1,19 @@ package net.thunderbird.feature.notification.api.content +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelize import net.thunderbird.feature.notification.api.NotificationChannel -import net.thunderbird.feature.notification.api.NotificationId import net.thunderbird.feature.notification.api.NotificationSeverity import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.api.ui.action.icon.DisablePushAction +import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons +import net.thunderbird.feature.notification.api.ui.icon.AlarmPermissionMissing +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons +import net.thunderbird.feature.notification.api.ui.icon.PushServiceInitializing +import net.thunderbird.feature.notification.api.ui.icon.PushServiceListening +import net.thunderbird.feature.notification.api.ui.icon.PushServiceWaitBackgroundSync +import net.thunderbird.feature.notification.api.ui.icon.PushServiceWaitNetwork import net.thunderbird.feature.notification.resources.Res import net.thunderbird.feature.notification.resources.push_info_disable_push_action import net.thunderbird.feature.notification.resources.push_notification_grant_alarm_permission @@ -28,21 +38,23 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification { * @property severity The severity level is set to [NotificationSeverity.Information]. */ @ConsistentCopyVisibility + @KmpParcelize data class Initializing private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String?, override val actions: Set, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.PushServiceInitializing, ) : PushServiceNotification() { companion object { /** * Creates an [Initializing] notification. * - * @param id The ID of the notification. * @return An [Initializing] notification. */ - suspend operator fun invoke(id: NotificationId): Initializing = Initializing( - id = id, + suspend operator fun invoke(accountNumber: Int): Initializing = Initializing( + accountNumber = accountNumber, title = getString(resource = Res.string.push_notification_state_initializing), contentText = getString(resource = Res.string.push_notification_info), actions = buildNotificationActions(), @@ -55,21 +67,23 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification { * @property severity The severity level is set to [NotificationSeverity.Information]. */ @ConsistentCopyVisibility + @KmpParcelize data class Listening private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String?, override val actions: Set, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.PushServiceListening, ) : PushServiceNotification() { companion object { /** * Creates a new [Listening] push service notification. * - * @param id The ID of the notification. * @return A new [Listening] notification. */ - suspend operator fun invoke(id: NotificationId): Listening = Listening( - id = id, + suspend operator fun invoke(accountNumber: Int): Listening = Listening( + accountNumber = accountNumber, title = getString(resource = Res.string.push_notification_state_listening), contentText = getString(resource = Res.string.push_notification_info), actions = buildNotificationActions(), @@ -82,21 +96,23 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification { * @property severity The severity level is set to [NotificationSeverity.Information]. */ @ConsistentCopyVisibility + @KmpParcelize data class WaitBackgroundSync private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String?, override val actions: Set, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.PushServiceWaitBackgroundSync, ) : PushServiceNotification() { companion object { /** * Creates a [WaitBackgroundSync] notification. * - * @param id The ID of the notification. * @return A [WaitBackgroundSync] notification. */ - suspend operator fun invoke(id: NotificationId): WaitBackgroundSync = WaitBackgroundSync( - id = id, + suspend operator fun invoke(accountNumber: Int): WaitBackgroundSync = WaitBackgroundSync( + accountNumber = accountNumber, title = getString(resource = Res.string.push_notification_state_wait_background_sync), contentText = getString(resource = Res.string.push_notification_info), actions = buildNotificationActions(), @@ -109,21 +125,23 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification { * @property severity The severity level is set to [NotificationSeverity.Information]. */ @ConsistentCopyVisibility + @KmpParcelize data class WaitNetwork private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String?, override val actions: Set, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.PushServiceWaitNetwork, ) : PushServiceNotification() { companion object { /** * Creates a [WaitNetwork] notification. * - * @param id The ID of the notification. * @return A [WaitNetwork] notification. */ - suspend operator fun invoke(id: NotificationId): WaitNetwork = WaitNetwork( - id = id, + suspend operator fun invoke(accountNumber: Int): WaitNetwork = WaitNetwork( + accountNumber = accountNumber, title = getString(resource = Res.string.push_notification_state_wait_network), contentText = getString(resource = Res.string.push_notification_info), actions = buildNotificationActions(), @@ -140,22 +158,25 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification { * @property severity The severity level is set to [NotificationSeverity.Critical]. */ @ConsistentCopyVisibility + @KmpParcelize data class AlarmPermissionMissing private constructor( - override val id: NotificationId, + override val accountNumber: Int, override val title: String, override val contentText: String?, + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationIcons.AlarmPermissionMissing, ) : PushServiceNotification(), InAppNotification { + @KmpIgnoredOnParcel override val severity: NotificationSeverity = NotificationSeverity.Critical companion object { /** * Creates an [AlarmPermissionMissing] notification. * - * @param id The ID of the notification. * @return An [AlarmPermissionMissing] instance. */ - suspend operator fun invoke(id: NotificationId): AlarmPermissionMissing = AlarmPermissionMissing( - id = id, + suspend operator fun invoke(accountNumber: Int): AlarmPermissionMissing = AlarmPermissionMissing( + accountNumber = accountNumber, title = getString(resource = Res.string.push_notification_state_alarm_permission_missing), contentText = getString(resource = Res.string.push_notification_grant_alarm_permission), ) @@ -173,6 +194,7 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification { */ private suspend fun buildNotificationActions(): Set = setOf( NotificationAction.CustomAction( - message = getString(resource = Res.string.push_info_disable_push_action), + title = getString(resource = Res.string.push_info_disable_push_action), + icon = NotificationActionIcons.DisablePushAction, ), ) diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/NotificationNotifier.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/NotificationNotifier.kt index f052e981620..d71c7c5a6f9 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/NotificationNotifier.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/receiver/NotificationNotifier.kt @@ -1,5 +1,6 @@ package net.thunderbird.feature.notification.api.receiver +import net.thunderbird.feature.notification.api.NotificationId import net.thunderbird.feature.notification.api.content.Notification /** @@ -10,5 +11,23 @@ import net.thunderbird.feature.notification.api.content.Notification * @param TNotification The type of notification to display. */ interface NotificationNotifier { - fun show(notification: TNotification) + /** + * Shows a notification to the user. + * + * @param id The notification id. Mostly used by System Notifications. + * @param notification The notification to show. + */ + suspend fun show(id: NotificationId, notification: TNotification) + + /** + * Disposes of any resources used by the notifier. + * + * This should be called when the notifier is no longer needed to prevent memory leaks. + */ + fun dispose() + + object TypeQualifier { + object InAppNotification + object SystemNotification + } } diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/NotificationStyle.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/NotificationStyle.kt new file mode 100644 index 00000000000..6b811a1a0d4 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/NotificationStyle.kt @@ -0,0 +1,110 @@ +package net.thunderbird.feature.notification.api.ui + +import org.jetbrains.annotations.VisibleForTesting + +/** + * Represents the different styles a notification can have, catering to both system-level + * and in-app display formats. + */ +sealed interface NotificationStyle { + /** + * Represents the style of a system notification. + */ + sealed interface System : NotificationStyle { + /** + * Represents an undefined notification style. + * This can be used as a default or placeholder when no specific style is applicable. + */ + data object Undefined : System + + /** + * Style for large-format notifications that include a lot of text. + * + * @property text The main text content of the notification. + */ + data class BigTextStyle @VisibleForTesting constructor( + val text: String, + ) : System + + /** + * Style for large-format notifications that include a list of (up to 5) strings. + * + * @property bigContentTitle Overrides the title of the notification. + * @property summary Overrides the summary of the notification. + * @property lines List of strings to display in the notification. + */ + data class InboxStyle @VisibleForTesting constructor( + val bigContentTitle: String, + val summary: String, + val lines: List, + ) : System + } + + /** + * Represents the style of an in-app notification. + * + * In-app notifications are displayed within the application itself to provide immediate + * feedback or information. + * + * TODO: The subtypes of [InApp] Style might change after designer's feedback. + */ + enum class InApp : NotificationStyle { + /** + * Represents an undefined in-app notification style. + * This can be used as a default or placeholder when no specific style is applicable. + */ + Undefined, + + /** + * Represents a fatal error notification that cannot be dismissed by the user. + * + * This type of notification typically indicates a fatal issue that requires user attention + * and prevents normal operation of the application. + */ + Fatal, + + /** + * Represents a critical in-app notification style. + * + * This style is used for important messages that require user attention but do not + * necessarily halt the application's functionality like a [Fatal] error. + */ + Critical, + + /** + * Represents a temporary in-app notification style. + * + * This style is typically used for notifications that are displayed briefly and then dismissed + * automatically or by user interaction. + */ + Temporary, + + /** + * Represents a general warning notification. + */ + Warning, + + /** + * Represents an in-app notification that displays general information. + * + * This style is typically used for notifications that convey important updates or messages + * that don't fit into more specific categories like errors or successes. + */ + Information, + } +} + +/** + * Holds the specific styles for both system-level and in-app notifications. + * + * This allows for defining distinct visual and behavioral characteristics for notifications + * depending on whether they are displayed by the operating system or within the application. + * + * @property systemStyle The style to be applied for system notifications. + * @property inAppStyle The style to be applied for in-app notifications. + */ +@ConsistentCopyVisibility +data class NotificationStyles internal constructor( + val systemStyle: NotificationStyle.System, + val inAppStyle: NotificationStyle.InApp, +) 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 d1183716826..5597fb76083 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 @@ -1,50 +1,129 @@ package net.thunderbird.feature.notification.api.ui.action +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelable +import net.thunderbird.core.common.io.KmpParcelize +import net.thunderbird.feature.notification.api.ui.action.icon.Archive +import net.thunderbird.feature.notification.api.ui.action.icon.Delete +import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsRead +import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsSpam +import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons +import net.thunderbird.feature.notification.api.ui.action.icon.Reply +import net.thunderbird.feature.notification.api.ui.action.icon.Retry +import net.thunderbird.feature.notification.api.ui.action.icon.UpdateServerSettings +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon +import net.thunderbird.feature.notification.resources.Res +import net.thunderbird.feature.notification.resources.notification_action_archive +import net.thunderbird.feature.notification.resources.notification_action_delete +import net.thunderbird.feature.notification.resources.notification_action_mark_as_read +import net.thunderbird.feature.notification.resources.notification_action_reply +import net.thunderbird.feature.notification.resources.notification_action_retry +import net.thunderbird.feature.notification.resources.notification_action_spam +import net.thunderbird.feature.notification.resources.notification_action_update_server_settings +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.getString + /** * Represents the various actions that can be performed on a notification. */ -sealed interface NotificationAction { +@KmpParcelize +sealed interface NotificationAction : KmpParcelable { + val icon: NotificationIcon + val titleResource: StringResource + + suspend fun resolveTitle(): String = getString(titleResource) + /** * Action to reply to the email message associated with the notification. */ - data object Reply : NotificationAction + data object Reply : NotificationAction { + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationActionIcons.Reply + + @KmpIgnoredOnParcel + override val titleResource: StringResource = Res.string.notification_action_reply + } /** * Action to mark the email message associated with the notification as read. */ - data object MarkAsRead : NotificationAction + data object MarkAsRead : NotificationAction { + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationActionIcons.MarkAsRead + + @KmpIgnoredOnParcel + override val titleResource: StringResource = Res.string.notification_action_mark_as_read + } /** * Action to delete the email message associated with the notification. */ - data object Delete : NotificationAction + data object Delete : NotificationAction { + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationActionIcons.Delete + + @KmpIgnoredOnParcel + override val titleResource: StringResource = Res.string.notification_action_delete + } /** * Action to mark the email message associated with the notification as spam. */ - data object MarkAsSpam : NotificationAction + data object MarkAsSpam : NotificationAction { + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationActionIcons.MarkAsSpam + + @KmpIgnoredOnParcel + override val titleResource: StringResource = Res.string.notification_action_spam + } /** * Action to archive the email message associated with the notification. */ - data object Archive : NotificationAction + data object Archive : NotificationAction { + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationActionIcons.Archive + + @KmpIgnoredOnParcel + override val titleResource: StringResource = Res.string.notification_action_archive + } /** * Action to prompt the user to update server settings, typically when authentication fails. */ - data object UpdateServerSettings : NotificationAction + data object UpdateServerSettings : NotificationAction { + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationActionIcons.UpdateServerSettings + + @KmpIgnoredOnParcel + override val titleResource: StringResource = Res.string.notification_action_update_server_settings + } /** * Action to retry a failed operation, such as sending a message or fetching new messages. */ - data object Retry : NotificationAction + data object Retry : NotificationAction { + @KmpIgnoredOnParcel + override val icon: NotificationIcon = NotificationActionIcons.Retry + + @KmpIgnoredOnParcel + override val titleResource: StringResource = Res.string.notification_action_retry + } /** * Represents a custom notification action. * * This can be used for actions that are not predefined and require a specific message. * - * @property message The text to be displayed for this custom action. + * @property title The text to be displayed for this custom action. */ - data class CustomAction(val message: String) : NotificationAction + data class CustomAction( + val title: String, + override val icon: NotificationIcon, + ) : NotificationAction { + @KmpIgnoredOnParcel + override val titleResource: StringResource get() = error("Custom Action must not supply a title resource") + + override suspend fun resolveTitle(): String = title + } } diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt new file mode 100644 index 00000000000..259d37d6bc8 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt @@ -0,0 +1,14 @@ +package net.thunderbird.feature.notification.api.ui.action.icon + +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon + +internal object NotificationActionIcons + +internal expect val NotificationActionIcons.Reply: NotificationIcon +internal expect val NotificationActionIcons.MarkAsRead: NotificationIcon +internal expect val NotificationActionIcons.Delete: NotificationIcon +internal expect val NotificationActionIcons.MarkAsSpam: NotificationIcon +internal expect val NotificationActionIcons.Archive: NotificationIcon +internal expect val NotificationActionIcons.UpdateServerSettings: NotificationIcon +internal expect val NotificationActionIcons.Retry: NotificationIcon +internal expect val NotificationActionIcons.DisablePushAction: NotificationIcon diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/InAppNotificationStyleBuilder.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/InAppNotificationStyleBuilder.kt new file mode 100644 index 00000000000..cfbde4ee121 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/InAppNotificationStyleBuilder.kt @@ -0,0 +1,42 @@ +package net.thunderbird.feature.notification.api.ui.builder + +import net.thunderbird.feature.notification.api.NotificationSeverity +import net.thunderbird.feature.notification.api.ui.NotificationStyle + +/** + * Builder for creating [NotificationStyle.InApp] instances. + * This interface defines the methods available for configuring the style of an in-app notification. + */ +class InAppNotificationStyleBuilder internal constructor() { + private var style = NotificationStyle.InApp.Undefined + + /** + * Sets the severity of the in-app notification. + * + * @param severity The severity level for the notification. + */ + fun severity(severity: NotificationSeverity) { + require(style == NotificationStyle.InApp.Undefined) { + "In-App Notifications must have only one severity." + } + style = when (severity) { + NotificationSeverity.Fatal -> NotificationStyle.InApp.Fatal + NotificationSeverity.Critical -> NotificationStyle.InApp.Critical + NotificationSeverity.Temporary -> NotificationStyle.InApp.Temporary + NotificationSeverity.Warning -> NotificationStyle.InApp.Warning + NotificationSeverity.Information -> NotificationStyle.InApp.Information + } + } + + /** + * Builds the [NotificationStyle.InApp] based on the provided parameters. + * + * @return The constructed [NotificationStyle.InApp]. + */ + fun build(): NotificationStyle.InApp { + check(style != NotificationStyle.InApp.Undefined) { + "You must add severity of the in-app notification." + } + return style + } +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/InboxSystemNotificationStyleBuilder.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/InboxSystemNotificationStyleBuilder.kt new file mode 100644 index 00000000000..993d8f6e4af --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/InboxSystemNotificationStyleBuilder.kt @@ -0,0 +1,82 @@ +package net.thunderbird.feature.notification.api.ui.builder + +import net.thunderbird.feature.notification.api.ui.NotificationStyle +import org.jetbrains.annotations.VisibleForTesting + +@VisibleForTesting +internal const val MAX_LINES = 5 +private const val MAX_LINES_ERROR_MESSAGE = "The maximum number of lines for a inbox notification is $MAX_LINES" + +/** + * Builder for [NotificationStyle.System.InboxStyle]. + * + * This style is used to display a list of items in the notification's content. + * It is commonly used for email or messaging apps. + */ +class InboxSystemNotificationStyleBuilder internal constructor( + private var bigContentTitle: String? = null, + private var summary: String? = null, + private val lines: MutableList = mutableListOf(), +) { + /** + * Sets the title for the notification's big content view. + * + * This method is used to specify the main title text that will be displayed + * when the notification is expanded to show its detailed content. + * + * @param title The string to be used as the big content title. + */ + fun title(title: String) { + bigContentTitle = title + } + + /** + * Sets the summary of the item. + * + * @param summary The summary of the item. + */ + fun summary(summary: String) { + this.summary = summary + } + + /** + * Append a line to the digest section of the Inbox notification. + * + * @param line The line to add. + */ + fun line(line: CharSequence) { + require(lines.size < MAX_LINES) { MAX_LINES_ERROR_MESSAGE } + lines += line + } + + /** + * Adds one or more lines to the digest section of the Inbox notification. + * + * @param lines A variable number of CharSequence objects representing the lines to be added. + */ + fun lines(vararg lines: CharSequence) { + require(lines.size < MAX_LINES) { MAX_LINES_ERROR_MESSAGE } + this.lines += lines + } + + /** + * Builds and returns a [NotificationStyle.System.InboxStyle] object. + * + * This method performs checks to ensure that mandatory fields like the big content title + * and summary are provided before creating the notification style object. + * + * @return A [NotificationStyle.System.InboxStyle] object configured with the specified + * title, summary, and lines. + * @throws IllegalStateException if the big content title or summary is not set. + */ + @Suppress("VisibleForTests") + fun build(): NotificationStyle.System.InboxStyle = NotificationStyle.System.InboxStyle( + bigContentTitle = checkNotNull(bigContentTitle) { + "The inbox notification's title is required" + }, + summary = checkNotNull(summary) { + "The inbox notification's summary is required" + }, + lines = lines.toList(), + ) +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleBuilder.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleBuilder.kt new file mode 100644 index 00000000000..3d73d203d0a --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleBuilder.kt @@ -0,0 +1,102 @@ +package net.thunderbird.feature.notification.api.ui.builder + +import net.thunderbird.feature.notification.api.ui.NotificationStyle +import net.thunderbird.feature.notification.api.ui.NotificationStyles + +/** + * Builder for creating notification styles for both system and in-app notifications. + * + * This builder allows for a declarative way to define the appearance and behavior of notifications. + * It uses a Kotlin DSL approach for configuring the styles. + * + * Example usage: + * ``` + * val (systemStyle, inAppStyle) = notificationStyle { + * systemStyle { + * bigText("This is a big text notification.") + * } + * inAppStyle { + * // Configure in-app notification style + * } + * } + * ``` + */ +class NotificationStyleBuilder { + private var systemNotificationStyle: NotificationStyle.System = NotificationStyle.System.Undefined + private var inAppNotificationStyle: NotificationStyle.InApp = NotificationStyle.InApp.Undefined + + /** + * Configures the system notification style. + * + * @param builder A lambda function with [SystemNotificationStyleBuilder] as its receiver, + * used to configure the system notification style. + * + * Example: + * ``` + * systemStyle { + * bigText("This is a big text notification.") + * // or + * inbox { + * // Add more inbox style configurations here + * } + * } + * ``` + */ + @NotificationStyleMarker + fun systemStyle(builder: @NotificationStyleMarker SystemNotificationStyleBuilder.() -> Unit) { + systemNotificationStyle = SystemNotificationStyleBuilder().apply(builder).build() + } + + @NotificationStyleMarker + fun inAppStyle(builder: @NotificationStyleMarker InAppNotificationStyleBuilder.() -> Unit) { + inAppNotificationStyle = InAppNotificationStyleBuilder().apply(builder).build() + } + + /** + * Builds and returns the configured system and in-app notification styles. + * + * This function should be called after all desired configurations have been applied + * using the `systemStyle` and `inAppStyle` DSL blocks. + * + * @return A [Pair] containing the [NotificationStyle.System] and [NotificationStyle.InApp]. + * If a style was not explicitly configured, it will default to its `Undefined` state. + * + * @see notificationStyle + * @see NotificationStyle.System.Undefined + * @see NotificationStyle.InApp.Undefined + */ + fun build(): NotificationStyles = NotificationStyles( + systemStyle = systemNotificationStyle, + inAppStyle = inAppNotificationStyle, + ) +} + +/** + * DSL entry point for creating notification styles. + * + * This function provides a convenient way to build both system and in-app notification + * styles using a Kotlin DSL. + * + * Example usage: + * ``` + * val (systemStyle, inAppStyle) = notificationStyle { + * systemStyle { + * bigText("This is a big text notification.") + * } + * inAppStyle { + * // Configure in-app notification style + * } + * } + * ``` + * + * @param builder A lambda expression that configures the notification styles using the + * [NotificationStyleBuilder] DSL. + * @return A [Pair] containing the configured [NotificationStyle.System] and + * [NotificationStyle.InApp] styles. + */ +@NotificationStyleMarker +fun notificationStyle( + builder: @NotificationStyleMarker NotificationStyleBuilder.() -> Unit, +): NotificationStyles { + return NotificationStyleBuilder().apply(builder).build() +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleMarker.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleMarker.kt new file mode 100644 index 00000000000..5c9ade9cf1e --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleMarker.kt @@ -0,0 +1,50 @@ +package net.thunderbird.feature.notification.api.ui.builder + +/** + * A DSL marker for building notification styles. + * + * This annotation is used to restrict the scope of lambda receivers, ensuring that + * methods belonging to an outer scope cannot be called from an inner scope. + * This helps in creating a more structured and type-safe DSL for constructing + * different notification styles. + * + * For example, when defining a `systemStyle` configuration within a `NotificationStyleBuilder` scope, + * methods specific to the `NotificationStyleBuilder` should not be directly callable + * from the `systemStyle`'s configuration block. + * + * Example: + * ``` + * // OK: + * val (systemStyle, inAppStyle) = notificationStyle { + * systemStyle { + * bigText("This is a big text notification.") + * } + * inAppStyle { + * // Configure in-app notification style + * } + * } + * + * // OK, but discouraged: + * val (systemStyle, inAppStyle) = notificationStyle { + * systemStyle { + * bigText("This is a big text notification.") + * this@notificationStyle.inAppStyle { + * // inAppStyle must be called within notificationStyle and not within systemStyle. + * } + * } + * } + * + * // Compile error: + * val (systemStyle, inAppStyle) = notificationStyle { + * systemStyle { + * bigText("This is a big text notification.") + * inAppStyle { + * // inAppStyle must be called within notificationStyle and not within systemStyle. + * } + * } + * } + * ``` + */ +@DslMarker +@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) +annotation class NotificationStyleMarker diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/SystemNotificationStyleBuilder.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/SystemNotificationStyleBuilder.kt new file mode 100644 index 00000000000..893ea182a72 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/builder/SystemNotificationStyleBuilder.kt @@ -0,0 +1,93 @@ +package net.thunderbird.feature.notification.api.ui.builder + +import net.thunderbird.feature.notification.api.ui.NotificationStyle +import net.thunderbird.feature.notification.api.ui.NotificationStyle.System.BigTextStyle +import net.thunderbird.feature.notification.api.ui.NotificationStyle.System.InboxStyle + +/** + * A builder for creating system notification styles. + * + * This builder allows for the creation of either a [BigTextStyle] or an [InboxStyle] for a system notification. + * It ensures that only one style type is set at a time, throwing an error if both are attempted. + * + * Example usage for [BigTextStyle]: + * ``` + * val style = notificationStyle { + * bigText("This is a long piece of text that will be displayed in the expanded notification.") + * } + * ``` + * + * Example usage for [InboxStyle]: + * ``` + * val style = notificationStyle { + * inbox { + * title("5 New Messages") + * summary("You have new messages") + * addLine("Alice: Hey, are you free later?") + * addLine("Bob: Meeting reminder for 3 PM") + * } + * } + * ``` + * @see notificationStyle + */ +class SystemNotificationStyleBuilder internal constructor() { + private var bigText: BigTextStyle? = null + private var inboxStyle: InboxStyle? = null + + /** + * Sets the style of the notification to [NotificationStyle.System.BigTextStyle]. + * + * This style displays a large block of text. + * + * **Note:** A system notification can either have a BigText or InboxStyle, not both. + * + * @param text The text to be displayed in the notification. + */ + fun bigText(text: String) { + @Suppress("VisibleForTests") + bigText = BigTextStyle(text = text) + } + + /** + * Sets the style of the notification to [NotificationStyle.System.InboxStyle]. + * + * This style is designed for aggregated notifications. + * + * **Note:** A system notification can either have a BigText or InboxStyle, not both. + * + * @param builder A lambda with [InboxSystemNotificationStyleBuilder] as its receiver, + * used to configure the Inbox style. + * @see InboxSystemNotificationStyleBuilder + */ + @NotificationStyleMarker + fun inbox(builder: @NotificationStyleMarker InboxSystemNotificationStyleBuilder.() -> Unit) { + inboxStyle = InboxSystemNotificationStyleBuilder().apply(builder).build() + } + + /** + * Builds and returns the configured [NotificationStyle.System]. + * + * This method validates that either a [BigTextStyle] or an [InboxStyle] has been set, but not both. + * If both styles are set, or if neither style is set (which should be an unexpected state), + * it will throw an [IllegalStateException]. + * + * @return The configured [NotificationStyle.System] which will be either a [BigTextStyle] or an [InboxStyle]. + * @throws IllegalStateException if both `bigText` and `inboxStyle` are set, or if neither are set. + */ + fun build(): NotificationStyle.System { + // shadowing properties to safely capture its value at the call time. + val bigText = bigText + val inboxStyle = inboxStyle + return when { + bigText != null && inboxStyle != null -> error( + "A system notification can either have a BigText or InboxStyle, not both.", + ) + + bigText != null -> bigText + + inboxStyle != null -> inboxStyle + + else -> error("You must configure at least one of the following styles: bigText or inbox.") + } + } +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt new file mode 100644 index 00000000000..661b84dd892 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt @@ -0,0 +1,66 @@ +package net.thunderbird.feature.notification.api.ui.icon + +import androidx.compose.ui.graphics.vector.ImageVector +import net.thunderbird.core.common.io.KmpIgnoredOnParcel +import net.thunderbird.core.common.io.KmpParcelable +import net.thunderbird.core.common.io.KmpParcelize +import net.thunderbird.core.common.io.KmpRawValue +import net.thunderbird.feature.notification.api.content.InAppNotification +import net.thunderbird.feature.notification.api.content.SystemNotification + +/** + * Represents the icon to be displayed for a notification. + * + * This class allows specifying different icons for system notifications and in-app notifications. + * At least one type of icon must be provided. + * + * @property systemNotificationIcon The icon to be used for system notifications. + * @property inAppNotificationIcon The icon to be used for in-app notifications. + */ +@KmpParcelize +data class NotificationIcon( + private val systemNotificationIcon: @KmpRawValue SystemNotificationIcon? = null, + @KmpIgnoredOnParcel private val inAppNotificationIcon: ImageVector? = null, +) : KmpParcelable { + + init { + check(systemNotificationIcon != null || inAppNotificationIcon != null) { + "Both systemNotificationIcon and inAppNotificationIcon are null. " + + "You must specify at least one type of icon." + } + } + + /** + * Resolves the [SystemNotificationIcon] for a given [SystemNotification]. + * + * This function is used to retrieve the appropriate system notification icon + * associated with this [NotificationIcon] instance. + * + * @param notification The [SystemNotification] for which to resolve the icon. + * @return The [SystemNotificationIcon] associated with this notification. + * @throws IllegalArgumentException if [systemNotificationIcon] is `null`, indicating + * that this [NotificationIcon] instance was not configured for a system notification icon. + */ + fun resolve(notification: SystemNotification): SystemNotificationIcon { + return requireNotNull(systemNotificationIcon) { + "$notification requires a not null SystemNotificationIcon." + } + } + + /** + * Resolves the [ImageVector] for a given [InAppNotification]. + * + * This function is used to retrieve the appropriate system notification icon + * associated with this [NotificationIcon] instance. + * + * @param notification The [InAppNotification] for which to resolve the icon. + * @return The [InAppNotification] associated with this notification. + * @throws IllegalArgumentException if [inAppNotificationIcon] is `null`, indicating + * that this [InAppNotification] instance was not configured for a in-app notification icon. + */ + fun resolve(notification: InAppNotification): ImageVector { + return requireNotNull(inAppNotificationIcon) { + "$notification requires a not null InAppNotification." + } + } +} diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.kt new file mode 100644 index 00000000000..2d7cac73b20 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.kt @@ -0,0 +1,101 @@ +package net.thunderbird.feature.notification.api.ui.icon + +/** + * Represents a set of icons specifically designed for notifications within the application. + * + * This object serves as a namespace for various notification icons, allowing for easy access + * and organization of these visual assets. Each property within this object is expected to + * represent a specific notification icon. + */ +internal object NotificationIcons + +/** + * Represents the icon for authentication error notifications. + * + * @see net.thunderbird.feature.notification.api.content.AuthenticationErrorNotification + */ +internal expect val NotificationIcons.AuthenticationError: NotificationIcon + +/** + * Represents the icon for the "Certificate Error" notification. + * + * @see net.thunderbird.feature.notification.api.content.CertificateErrorNotification + */ +internal expect val NotificationIcons.CertificateError: NotificationIcon + +/** + * Represents the icon for the "Failed To Create notification" notification. + * + * @see net.thunderbird.feature.notification.api.content.FailedToCreateNotification + */ +internal expect val NotificationIcons.FailedToCreate: NotificationIcon + +/** + * Represents the icon for the "Mail Fetching" notification. + * + * @see net.thunderbird.feature.notification.api.content.MailNotification.Fetching + */ +internal expect val NotificationIcons.MailFetching: NotificationIcon + +/** + * Represents the icon for the "Mail Sending" notification. + * + * @see net.thunderbird.feature.notification.api.content.MailNotification.Sending + */ +internal expect val NotificationIcons.MailSending: NotificationIcon + +/** + * Represents the icon for the "Mail Send Failed" notification. + * + * @see net.thunderbird.feature.notification.api.content.MailNotification.SendFailed + */ +internal expect val NotificationIcons.MailSendFailed: NotificationIcon + +/** + * Represents the icon for the "New Mail (Single)" notification. + * + * @see net.thunderbird.feature.notification.api.content.MailNotification.NewMail.SingleMail + */ +internal expect val NotificationIcons.NewMailSingleMail: NotificationIcon + +/** + * Represents the icon for the "New Mail Summary" notification. + * + * @see net.thunderbird.feature.notification.api.content.MailNotification.NewMail.SummaryMail + */ +internal expect val NotificationIcons.NewMailSummaryMail: NotificationIcon + +/** + * Represents the icon for the "Push Service Initializing" notification. + * + * @see net.thunderbird.feature.notification.api.content.PushServiceNotification.Initializing + */ +internal expect val NotificationIcons.PushServiceInitializing: NotificationIcon + +/** + * Represents the icon for the "Push Service Listening" notification. + * + * @see net.thunderbird.feature.notification.api.content.PushServiceNotification.Listening + */ +internal expect val NotificationIcons.PushServiceListening: NotificationIcon + +/** + * Represents the icon for the "Push Service Wait Background Sync" notification. + * + * @see net.thunderbird.feature.notification.api.content.PushServiceNotification.WaitBackgroundSync + */ +internal expect val NotificationIcons.PushServiceWaitBackgroundSync: NotificationIcon + +/** + * Represents the icon for the "Push Service Wait Network" notification. + * + * @see net.thunderbird.feature.notification.api.content.PushServiceNotification.WaitNetwork + */ +internal expect val NotificationIcons.PushServiceWaitNetwork: NotificationIcon + +/** + * Represents the icon for the "Alarm Permission Missing" notification. + * + * @see net.thunderbird.feature.notification.api.content.PushServiceNotification.AlarmPermissionMissing + */ +internal expect val NotificationIcons.AlarmPermissionMissing: NotificationIcon diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.kt new file mode 100644 index 00000000000..dca7f88cabf --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.kt @@ -0,0 +1,10 @@ +package net.thunderbird.feature.notification.api.ui.icon + +/** + * Represents an icon for a system notification. + * + * This is an expect class, meaning its actual implementation is provided by platform-specific modules. + * On Android, this would typically wrap a drawable resource ID. + * On other platforms, it might represent a file path or another platform-specific icon identifier. + */ +expect class SystemNotificationIcon diff --git a/feature/notification/api/src/commonTest/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleBuilderTest.kt b/feature/notification/api/src/commonTest/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleBuilderTest.kt new file mode 100644 index 00000000000..2795926d54f --- /dev/null +++ b/feature/notification/api/src/commonTest/kotlin/net/thunderbird/feature/notification/api/ui/builder/NotificationStyleBuilderTest.kt @@ -0,0 +1,348 @@ +package net.thunderbird.feature.notification.api.ui.builder + +import assertk.assertThat +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop +import kotlin.test.Test +import kotlin.test.assertFails +import net.thunderbird.feature.notification.api.NotificationSeverity +import net.thunderbird.feature.notification.api.ui.NotificationStyle + +@Suppress("MaxLineLength") +class NotificationStyleBuilderTest { + @Test + fun `notificationStyle dsl should create inbox system notification style`() { + // Arrange + val title = "The title" + val summary = "The summary" + val expected = NotificationStyle.System.InboxStyle( + bigContentTitle = title, + summary = summary, + lines = listOf(), + ) + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + systemStyle { + inbox { + title(title) + summary(summary) + } + } + } + + // Assert + assertThat(systemStyle) + .isInstanceOf() + .isEqualTo(expected) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(NotificationStyle.InApp.Undefined) + } + + @Test + fun `notificationStyle dsl should create inbox system notification style with multiple lines`() { + // Arrange + val title = "The title" + val summary = "The summary" + val contentLines = List(size = 5) { + "line $it" + } + val expected = NotificationStyle.System.InboxStyle( + bigContentTitle = title, + summary = summary, + lines = contentLines, + ) + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + systemStyle { + inbox { + title(title) + summary(summary) + for (line in contentLines) { + line(line) + } + } + } + } + + // Assert + assertThat(systemStyle) + .isInstanceOf() + .isEqualTo(expected) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(NotificationStyle.InApp.Undefined) + } + + @Test + fun `notificationStyle dsl should create big text system notification style`() { + // Arrange + val bigText = "The ${"big ".repeat(n = 1000)}text" + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + systemStyle { + bigText(bigText) + } + } + + // Assert + assertThat(systemStyle) + .isInstanceOf() + .prop("text") { it.text } + .isEqualTo(bigText) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(NotificationStyle.InApp.Undefined) + } + + @Test + fun `notificationStyle dsl should throw IllegalStateException when inbox system notification is missing title`() { + // Arrange & Act + val exception = assertFails { + notificationStyle { + systemStyle { + inbox { + summary("summary") + } + } + } + } + + // Assert + assertThat(exception) + .isInstanceOf() + .hasMessage("The inbox notification's title is required") + } + + @Test + fun `notificationStyle dsl should throw IllegalStateException when inbox system notification is missing summary`() { + // Arrange & Act + val exception = assertFails { + notificationStyle { + systemStyle { + inbox { + title("title") + } + } + } + } + + // Assert + assertThat(exception) + .isInstanceOf() + .hasMessage("The inbox notification's summary is required") + } + + @Test + fun `notificationStyle dsl should throw IllegalArgumentException when inbox system notification adds more then 5 lines`() { + // Arrange + val lines = List(size = MAX_LINES + 1) { "line $it" } + + // Act + val exception = assertFails { + notificationStyle { + systemStyle { + inbox { + title("title") + summary("summary") + lines(lines = lines.toTypedArray()) + } + } + } + } + + // Assert + assertThat(exception) + .isInstanceOf() + .hasMessage("The maximum number of lines for a inbox notification is $MAX_LINES") + } + + @Test + fun `notificationStyle dsl should throw IllegalStateException when system notification style set both big text and inbox styles`() { + // Arrange + val bigText = "The ${"big ".repeat(n = 1000)}text" + + // Act + val exception = assertFails { + notificationStyle { + systemStyle { + bigText(bigText) + inbox { + title("title") + summary("summary") + } + } + } + } + + // Assert + assertThat(exception) + .isInstanceOf() + .hasMessage("A system notification can either have a BigText or InboxStyle, not both.") + } + + @Test + fun `notificationStyle dsl should throw IllegalStateException when system notification style is called without any style configuration`() { + // Arrange & Act + val exception = assertFails { + notificationStyle { + systemStyle { + // intentionally empty. + } + } + } + + // Assert + assertThat(exception) + .isInstanceOf() + .hasMessage("You must configure at least one of the following styles: bigText or inbox.") + } + + @Test + fun `notificationStyle dsl should create a fatal in-app notification style when NotificationSeverity Fatal is provided`() { + // Arrange + val expected = NotificationStyle.InApp.Fatal + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + inAppStyle { + severity(NotificationSeverity.Fatal) + } + } + + // Assert + assertThat(systemStyle) + .isEqualTo(NotificationStyle.System.Undefined) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(expected) + } + + @Test + fun `notificationStyle dsl should create a critical in-app notification style when NotificationSeverity Critical is provided`() { + // Arrange + val expected = NotificationStyle.InApp.Critical + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + inAppStyle { + severity(NotificationSeverity.Critical) + } + } + + // Assert + assertThat(systemStyle) + .isEqualTo(NotificationStyle.System.Undefined) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(expected) + } + + @Test + fun `notificationStyle dsl should create a temporary in-app notification style when NotificationSeverity Temporary is provided`() { + // Arrange + val expected = NotificationStyle.InApp.Temporary + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + inAppStyle { + severity(NotificationSeverity.Temporary) + } + } + + // Assert + assertThat(systemStyle) + .isEqualTo(NotificationStyle.System.Undefined) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(expected) + } + + @Test + fun `notificationStyle dsl should create a warning in-app notification style when NotificationSeverity Warning is provided`() { + // Arrange + val expected = NotificationStyle.InApp.Warning + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + inAppStyle { + severity(NotificationSeverity.Warning) + } + } + + // Assert + assertThat(systemStyle) + .isEqualTo(NotificationStyle.System.Undefined) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(expected) + } + + @Test + fun `notificationStyle dsl should create a information in-app notification style when NotificationSeverity Information is provided`() { + // Arrange + val expected = NotificationStyle.InApp.Information + + // Act + val (systemStyle, inAppStyle) = notificationStyle { + inAppStyle { + severity(NotificationSeverity.Information) + } + } + + // Assert + assertThat(systemStyle) + .isEqualTo(NotificationStyle.System.Undefined) + + assertThat(inAppStyle) + .isInstanceOf() + .isEqualTo(expected) + } + + @Test + fun `notificationStyle dsl should throw IllegalArgumentException when severity method is called multiple times within inAppNotification dsl`() { + // Arrange & Act + val exception = assertFails { + notificationStyle { + inAppStyle { + severity(severity = NotificationSeverity.Fatal) + severity(severity = NotificationSeverity.Critical) + } + } + } + + // Assert + assertThat(exception) + .isInstanceOf() + .hasMessage("In-App Notifications must have only one severity.") + } + + @Test + fun `notificationStyle dsl should throw IllegalStateException when in-app notification style is called without any style configuration`() { + // Arrange & Act + val exception = assertFails { + notificationStyle { + inAppStyle { + // intentionally empty. + } + } + } + + // Assert + assertThat(exception) + .isInstanceOf() + .hasMessage("You must add severity of the in-app notification.") + } +} diff --git a/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt new file mode 100644 index 00000000000..0688e848301 --- /dev/null +++ b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt @@ -0,0 +1,20 @@ +package net.thunderbird.feature.notification.api.ui.action.icon + +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon + +private const val ERROR_MESSAGE = "Can't send notifications from a jvm library. Use android library or app instead." + +internal actual val NotificationActionIcons.Reply: NotificationIcon get() = error(ERROR_MESSAGE) + +internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon get() = error(ERROR_MESSAGE) + +internal actual val NotificationActionIcons.Delete: NotificationIcon get() = error(ERROR_MESSAGE) + +internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon get() = error(ERROR_MESSAGE) + +internal actual val NotificationActionIcons.Archive: NotificationIcon get() = error(ERROR_MESSAGE) + +internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon get() = error(ERROR_MESSAGE) + +internal actual val NotificationActionIcons.Retry: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon get() = error(ERROR_MESSAGE) diff --git a/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.jvm.kt b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.jvm.kt new file mode 100644 index 00000000000..c51755d2485 --- /dev/null +++ b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcons.jvm.kt @@ -0,0 +1,17 @@ +package net.thunderbird.feature.notification.api.ui.icon + +private const val ERROR_MESSAGE = "Can't send notifications from a jvm library. Use android library or app instead." + +internal actual val NotificationIcons.AlarmPermissionMissing: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.AuthenticationError: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.CertificateError: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.FailedToCreate: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.MailFetching: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.MailSending: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.MailSendFailed: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.NewMailSingleMail: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.NewMailSummaryMail: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.PushServiceInitializing: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.PushServiceListening: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.PushServiceWaitBackgroundSync: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationIcons.PushServiceWaitNetwork: NotificationIcon get() = error(ERROR_MESSAGE) diff --git a/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.jvm.kt b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.jvm.kt new file mode 100644 index 00000000000..9b05b039eff --- /dev/null +++ b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/SystemNotificationIcon.jvm.kt @@ -0,0 +1,3 @@ +package net.thunderbird.feature.notification.api.ui.icon + +actual typealias SystemNotificationIcon = Int diff --git a/feature/notification/impl/build.gradle.kts b/feature/notification/impl/build.gradle.kts index 2ff08aea370..db4fbf57091 100644 --- a/feature/notification/impl/build.gradle.kts +++ b/feature/notification/impl/build.gradle.kts @@ -7,16 +7,16 @@ kotlin { commonMain.dependencies { implementation(projects.core.common) implementation(projects.core.outcome) + implementation(projects.core.logging.api) + implementation(projects.feature.account.api) implementation(projects.feature.notification.api) } + androidMain.dependencies { + implementation(projects.feature.launcher) + } } } android { - namespace = "net.thunderbird.feature.notification" -} - -compose.resources { - publicResClass = false - packageOfResClass = "net.thunderbird.feature.notification.resources" + namespace = "net.thunderbird.feature.notification.impl" } diff --git a/feature/notification/impl/src/androidMain/AndroidManifest.xml b/feature/notification/impl/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000000..f44236fe897 --- /dev/null +++ b/feature/notification/impl/src/androidMain/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + 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 new file mode 100644 index 00000000000..67c65462707 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.android.kt @@ -0,0 +1,67 @@ +package net.thunderbird.feature.notification.impl.inject + +import android.content.Context +import net.thunderbird.feature.notification.api.content.InAppNotification +import net.thunderbird.feature.notification.api.content.SystemNotification +import net.thunderbird.feature.notification.api.receiver.NotificationNotifier +import net.thunderbird.feature.notification.impl.intent.AlarmPermissionMissingNotificationIntentCreator +import net.thunderbird.feature.notification.impl.intent.SystemNotificationIntentCreator +import net.thunderbird.feature.notification.impl.intent.action.DefaultNotificationActionIntentCreator +import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator +import net.thunderbird.feature.notification.impl.receiver.InAppNotificationNotifier +import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier +import net.thunderbird.feature.notification.impl.ui.action.DefaultSystemNotificationActionCreator +import net.thunderbird.feature.notification.impl.ui.action.NotificationActionCreator +import org.koin.android.ext.koin.androidApplication +import org.koin.core.module.Module +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.koin.dsl.onClose + +internal actual val platformFeatureNotificationModule: Module = module { + single>( + qualifier = named(), + ) { + InAppNotificationNotifier( + logger = get(), + contextProvider = get(), + ) + }.onClose { notifier -> + notifier?.dispose() + } + + factory>( + qualifier = named(), + ) { + SystemNotificationNotifier( + logger = get(), + contextProvider = get(), + ) + }.onClose { notifier -> + notifier?.dispose() + } + + single>>(named()) { + listOf( + AlarmPermissionMissingNotificationIntentCreator( + context = androidApplication(), + logger = get(), + ), + ) + } + + single>>(named()) { + listOf( + DefaultNotificationActionIntentCreator( + logger = get(), + ), + ) + } + + single>(named(NotificationActionCreator.TypeQualifier.System)) { + DefaultSystemNotificationActionCreator( + logger = get(), + actionIntentCreators = get(named()), + ) + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/AlarmPermissionMissingNotificationIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/AlarmPermissionMissingNotificationIntentCreator.kt new file mode 100644 index 00000000000..728ac9eaafe --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/AlarmPermissionMissingNotificationIntentCreator.kt @@ -0,0 +1,49 @@ +package net.thunderbird.feature.notification.impl.intent + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.Settings +import androidx.annotation.RequiresApi +import androidx.core.app.PendingIntentCompat +import androidx.core.net.toUri +import net.thunderbird.core.logging.Logger +import net.thunderbird.feature.notification.api.content.PushServiceNotification +import net.thunderbird.feature.notification.api.content.SystemNotification + +private const val TAG = "AlarmPermissionMissingNotificationIntentCreator" +class AlarmPermissionMissingNotificationIntentCreator( + private val context: Context, + private val logger: Logger, +) : + SystemNotificationIntentCreator { + override fun accept(notification: SystemNotification): Boolean = + Build.VERSION.SDK_INT > Build.VERSION_CODES.S && + notification is PushServiceNotification.AlarmPermissionMissing + + @RequiresApi(Build.VERSION_CODES.S) + override fun create(notification: PushServiceNotification.AlarmPermissionMissing): PendingIntent { + logger.debug(TAG) { "create() called with: notification = $notification" } + val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { + data = "package:${context.packageName}".toUri() + } + + return requireNotNull( + PendingIntentCompat.getActivity( + /* context = */ + context, + /* requestCode = */ + 1, + /* intent = */ + intent, + /* flags = */ + 0, + /* isMutable = */ + false, + ), + ) { + "Could not create PendingIntent for AlarmPermissionMissing Notification." + } + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/SystemNotificationIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/SystemNotificationIntentCreator.kt new file mode 100644 index 00000000000..e08f0358020 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/SystemNotificationIntentCreator.kt @@ -0,0 +1,11 @@ +package net.thunderbird.feature.notification.impl.intent + +import android.app.PendingIntent +import net.thunderbird.feature.notification.api.content.SystemNotification + +internal interface SystemNotificationIntentCreator { + fun accept(notification: SystemNotification): Boolean + fun create(notification: TNotification): PendingIntent + + object TypeQualifier +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt new file mode 100644 index 00000000000..775fbb32a26 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt @@ -0,0 +1,24 @@ +package net.thunderbird.feature.notification.impl.intent.action + +import android.app.PendingIntent +import net.thunderbird.core.logging.Logger +import net.thunderbird.feature.notification.api.ui.action.NotificationAction + +internal interface NotificationActionIntentCreator { + fun accept(action: NotificationAction): Boolean + fun create(action: TNotificationAction): PendingIntent? + + object TypeQualifier +} + +private const val TAG = "DefaultNotificationActionIntentCreator" +internal class DefaultNotificationActionIntentCreator( + private val logger: Logger, +) : NotificationActionIntentCreator { + override fun accept(action: NotificationAction): Boolean = true + + override fun create(action: NotificationAction): PendingIntent? { + logger.debug(TAG) { "create() called with: action = $action" } + return null + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationBroadcastReceiver.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationBroadcastReceiver.kt new file mode 100644 index 00000000000..7a19ec210ba --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationBroadcastReceiver.kt @@ -0,0 +1,44 @@ +package net.thunderbird.feature.notification.impl.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Parcelable +import androidx.core.content.ContextCompat +import net.thunderbird.core.logging.Logger +import net.thunderbird.feature.notification.api.content.InAppNotification +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +internal const val POST_IN_APP_NOTIFICATION_PERMISSION = + "net.thunderbird.permission.POST_IN_APP_NOTIFICATION_PERMISSION" +internal const val POST_IN_APP_NOTIFICATION_ACTION = + "net.thunderbird.feature.notification.receiver.POST_IN_APP_NOTIFICATION_ACTION" +internal const val IN_APP_NOTIFICATION_EXTRA = + "net.thunderbird.feature.notification.receiver.POST_IN_APP_NOTIFICATION_EXTRA" +private const val TAG = "InAppNotificationBroadcastReceiver" + +class InAppNotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent { + private val logger: Logger by inject() + + override fun onReceive(context: Context, intent: Intent) { + logger.debug(TAG) { "onReceive() called with: context = $context, intent = $intent" } + val intent = intent.takeIf { it.action == POST_IN_APP_NOTIFICATION_ACTION } ?: return + + val notification = intent.getParcelable(IN_APP_NOTIFICATION_EXTRA) + + logger.debug(TAG) { "Received In-app notification: $notification" } + } + + companion object { + const val FLAGS = ContextCompat.RECEIVER_NOT_EXPORTED + } + + private inline fun Intent.getParcelable(name: String): TParcelable? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableExtra(name, TParcelable::class.java) + } else { + getParcelableExtra(name) + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.android.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.android.kt new file mode 100644 index 00000000000..f30e5f41f39 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.android.kt @@ -0,0 +1,80 @@ +package net.thunderbird.feature.notification.impl.receiver + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.content.ContextCompat +import net.thunderbird.core.common.provider.ContextProvider +import net.thunderbird.core.logging.Logger +import net.thunderbird.feature.notification.api.NotificationId +import net.thunderbird.feature.notification.api.content.InAppNotification +import org.jetbrains.annotations.VisibleForTesting + +private const val TAG = "InAppNotificationNotifier" +internal actual inline fun InAppNotificationNotifier( + logger: Logger, + contextProvider: ContextProvider, +): InAppNotificationNotifier { + require(TContext::class == Context::class) { + "InAppNotificationNotifier expects an Android Context." + } + val context = contextProvider.context as Context + return AndroidInAppNotificationNotifier(logger = logger, context = context) +} + +@VisibleForTesting +internal class AndroidInAppNotificationNotifier( + private val logger: Logger, + context: Context, +) : InAppNotificationNotifier { + private val context = context.applicationContext + private var broadcastReceiver: InAppNotificationBroadcastReceiver? = null + + init { + startBroadcast() + } + + override suspend fun show(id: NotificationId, notification: InAppNotification) { + logger.debug(TAG) { "show() called with id = $id, notification = $notification" } + val intent = Intent(POST_IN_APP_NOTIFICATION_ACTION).apply { + setPackage(context.packageName) + putExtra(IN_APP_NOTIFICATION_EXTRA, notification) + } + context.sendOrderedBroadcast( + /* intent = */ + intent, + /* receiverPermission = */ + POST_IN_APP_NOTIFICATION_PERMISSION, + ) + } + + override fun dispose() { + logger.debug(TAG) { "dispose() called" } + context.unregisterReceiver(broadcastReceiver) + broadcastReceiver = null + } + + private fun startBroadcast() { + logger.debug(TAG) { "startBroadcast() called" } + val filter = IntentFilter(POST_IN_APP_NOTIFICATION_ACTION).apply { + priority = IntentFilter.SYSTEM_HIGH_PRIORITY + } + broadcastReceiver = InAppNotificationBroadcastReceiver() + + ContextCompat.registerReceiver( + /* context = */ + context, + /* receiver = */ + broadcastReceiver, + /* filter = */ + filter, + /* broadcastPermission = */ + POST_IN_APP_NOTIFICATION_PERMISSION, + /* scheduler = */ + null, + /* flags = */ + InAppNotificationBroadcastReceiver.FLAGS, + + ) + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.android.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.android.kt new file mode 100644 index 00000000000..71655eabc72 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.android.kt @@ -0,0 +1,122 @@ +package net.thunderbird.feature.notification.impl.receiver + +import android.app.Notification +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import net.thunderbird.core.common.provider.ContextProvider +import net.thunderbird.core.logging.Logger +import net.thunderbird.feature.notification.api.NotificationId +import net.thunderbird.feature.notification.api.content.SystemNotification +import net.thunderbird.feature.notification.api.ui.NotificationStyle +import net.thunderbird.feature.notification.impl.intent.SystemNotificationIntentCreator +import net.thunderbird.feature.notification.impl.ui.action.NotificationActionCreator +import org.koin.core.component.KoinComponent +import org.koin.core.qualifier.named +import org.koin.java.KoinJavaComponent.inject + +private const val TAG = "SystemNotificationNotifier" +internal actual inline fun SystemNotificationNotifier( + logger: Logger, + contextProvider: ContextProvider, +): SystemNotificationNotifier { + require(TContext::class == Context::class) { + "SystemNotificationNotifier expects an Android Context." + } + return AndroidSystemNotificationNotifier(logger, contextProvider.context as Context) +} + +internal class AndroidSystemNotificationNotifier( + private val logger: Logger, + private val context: Context, + notificationIntentCreators: Lazy>> = inject( + clazz = List::class.java, + qualifier = named(), + ), + notificationActionCreator: Lazy> = inject( + clazz = NotificationActionCreator::class.java, + qualifier = named(NotificationActionCreator.TypeQualifier.System), + ), +) : SystemNotificationNotifier, KoinComponent { + private val notificationIntentCreators by notificationIntentCreators + private val notificationActionCreator by notificationActionCreator + private val notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(context) + + override suspend fun show( + id: NotificationId, + notification: SystemNotification, + ) { + logger.debug(TAG) { "show() called with: id = $id, notification = $notification" } + val androidNotification = notification.toAndroidNotification() + notificationManager.notify(id.value, androidNotification) + } + + override fun dispose() { + logger.debug(TAG) { "dispose() called" } + } + + private suspend fun SystemNotification.toAndroidNotification(): Notification { + logger.debug(TAG) { "toAndroidNotification() called with systemNotification = $this" } + return NotificationCompat + .Builder(context, channel.id) + .apply { + setSmallIcon(icon.resolve(notification = this@toAndroidNotification)) + setContentTitle(title) + setTicker(accessibilityText) + contentText?.let(::setContentText) + subText?.let(::setSubText) + setOngoing(severity.dismissable.not()) + setWhen(createdAt.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()) + if (this@toAndroidNotification != lockscreenNotification) { + setPublicVersion(lockscreenNotification.toAndroidNotification()) + } + + notificationIntentCreators + .firstOrNull { it.accept(this@toAndroidNotification) } + ?.let { creator -> setContentIntent(creator.create(notification = this@toAndroidNotification)) } + + setNotificationStyle(notification = this@toAndroidNotification) + + if (actions.isNotEmpty()) { + for (action in actions) { + val notificationAction = notificationActionCreator + .create(notification = this@toAndroidNotification, action) + + addAction( + /* icon = */ + notificationAction.icon, + /* title = */ + notificationAction.title, + /* intent = */ + notificationAction.pendingIntent, + ) + } + } + } + .build() + } + + private fun NotificationCompat.Builder.setNotificationStyle( + notification: SystemNotification, + ) { + when (val style = notification.systemNotificationStyle) { + is NotificationStyle.System.BigTextStyle -> setStyle( + NotificationCompat.BigTextStyle().bigText(style.text), + ) + + is NotificationStyle.System.InboxStyle -> { + val inboxStyle = NotificationCompat.InboxStyle() + .setBigContentTitle(style.bigContentTitle) + .setSummaryText(style.summary) + + style.lines.forEach(inboxStyle::addLine) + + setStyle(inboxStyle) + } + + NotificationStyle.System.Undefined -> Unit + } + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt new file mode 100644 index 00000000000..fb12297d608 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt @@ -0,0 +1,18 @@ +package net.thunderbird.feature.notification.impl.ui.action + +import android.app.PendingIntent +import androidx.annotation.DrawableRes + +/** + * Represents an action that can be performed on an Android notification. + * + * @property icon The drawable resource ID for the action's icon. + * @property title The title of the action. + * @property pendingIntent The [PendingIntent] to be executed when the action is triggered. + */ +data class AndroidNotificationAction( + @DrawableRes + val icon: Int, + val title: String, + val pendingIntent: PendingIntent?, +) diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt new file mode 100644 index 00000000000..3b9edf0c358 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt @@ -0,0 +1,36 @@ +package net.thunderbird.feature.notification.impl.ui.action + +import net.thunderbird.core.logging.Logger +import net.thunderbird.feature.notification.api.content.Notification +import net.thunderbird.feature.notification.api.content.SystemNotification +import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator + +interface NotificationActionCreator { + suspend fun create(notification: TNotification, action: NotificationAction): AndroidNotificationAction + + enum class TypeQualifier { System, InApp } +} + +private const val TAG = "DefaultSystemNotificationActionCreator" + +internal class DefaultSystemNotificationActionCreator( + private val logger: Logger, + private val actionIntentCreators: List>, +) : NotificationActionCreator { + override suspend fun create( + notification: SystemNotification, + action: NotificationAction, + ): AndroidNotificationAction { + logger.debug(TAG) { "create() called with: notification = $notification, action = $action" } + val intent = actionIntentCreators + .first { it.accept(action) } + .create(action) + + return AndroidNotificationAction( + icon = action.icon.resolve(notification), + title = action.resolveTitle(), + pendingIntent = intent, + ) + } +} diff --git a/feature/notification/impl/src/androidMain/res/values/strings.xml b/feature/notification/impl/src/androidMain/res/values/strings.xml new file mode 100644 index 00000000000..6cfc43e2d6b --- /dev/null +++ b/feature/notification/impl/src/androidMain/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Post In-App notifications + Allows the app to post In-App notifications + diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/InAppNotificationCommand.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/InAppNotificationCommand.kt index faf69cf9fa4..d8b6bc6453c 100644 --- a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/InAppNotificationCommand.kt +++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/InAppNotificationCommand.kt @@ -1,6 +1,7 @@ package net.thunderbird.feature.notification.impl.command 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.command.NotificationCommand.CommandOutcome.Failure import net.thunderbird.feature.notification.api.command.NotificationCommand.CommandOutcome.Success @@ -19,7 +20,18 @@ internal class InAppNotificationCommand( notification: InAppNotification, notifier: NotificationNotifier, ) : NotificationCommand(notification, notifier) { - override fun execute(): Outcome, Failure> { - TODO("Implementation on GitHub Issue #9245") + override suspend fun execute(): Outcome, Failure> { + return if (canExecuteCommand()) { + notifier.show(id = NotificationId.Undefined, notification = notification) + Outcome.success(Success(command = this)) + } else { + Outcome.failure(Failure(command = this, throwable = Exception("Can't execute command."))) + } } + + // TODO(#9392): Verify if the app is on foreground. IF it isn't, then should fail + // executing the command + // TODO(#9420): If the app is on background and the severity is Fatal or Critical, we should + // let the command execute, but store it in a database instead of triggering the show notification logic. + private fun canExecuteCommand(): Boolean = true } diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/NotificationCommandFactory.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/NotificationCommandFactory.kt index 22b7591f3c6..74bfa8869e5 100644 --- a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/NotificationCommandFactory.kt +++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/NotificationCommandFactory.kt @@ -1,18 +1,19 @@ package net.thunderbird.feature.notification.impl.command +import net.thunderbird.feature.notification.api.NotificationIdFactory import net.thunderbird.feature.notification.api.command.NotificationCommand import net.thunderbird.feature.notification.api.content.InAppNotification import net.thunderbird.feature.notification.api.content.Notification import net.thunderbird.feature.notification.api.content.SystemNotification -import net.thunderbird.feature.notification.impl.receiver.InAppNotificationNotifier -import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier +import net.thunderbird.feature.notification.api.receiver.NotificationNotifier /** * A factory for creating a set of notification commands based on a given notification. */ internal class NotificationCommandFactory( - private val systemNotificationNotifier: SystemNotificationNotifier, - private val inAppNotificationNotifier: InAppNotificationNotifier, + private val notificationIdFactory: NotificationIdFactory, + private val systemNotificationNotifier: NotificationNotifier, + private val inAppNotificationNotifier: NotificationNotifier, ) { /** * Creates a set of [NotificationCommand]s for the given [notification]. @@ -28,6 +29,7 @@ internal class NotificationCommandFactory( if (notification is SystemNotification) { commands.add( SystemNotificationCommand( + notificationIdFactory = notificationIdFactory, notification = notification, notifier = systemNotificationNotifier, ), diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/SystemNotificationCommand.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/SystemNotificationCommand.kt index d4f87a36908..62449512117 100644 --- a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/SystemNotificationCommand.kt +++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/command/SystemNotificationCommand.kt @@ -1,9 +1,12 @@ package net.thunderbird.feature.notification.impl.command import net.thunderbird.core.outcome.Outcome +import net.thunderbird.feature.notification.api.NotificationIdFactory +import net.thunderbird.feature.notification.api.NotificationSeverity import net.thunderbird.feature.notification.api.command.NotificationCommand import net.thunderbird.feature.notification.api.command.NotificationCommand.CommandOutcome.Failure import net.thunderbird.feature.notification.api.command.NotificationCommand.CommandOutcome.Success +import net.thunderbird.feature.notification.api.content.InAppNotification import net.thunderbird.feature.notification.api.content.SystemNotification import net.thunderbird.feature.notification.api.receiver.NotificationNotifier @@ -14,10 +17,34 @@ import net.thunderbird.feature.notification.api.receiver.NotificationNotifier * @param notifier The notifier responsible for displaying the notification. */ internal class SystemNotificationCommand( + private val notificationIdFactory: NotificationIdFactory, notification: SystemNotification, notifier: NotificationNotifier, ) : NotificationCommand(notification, notifier) { - override fun execute(): Outcome, Failure> { - TODO("Implementation on GitHub Issue #9245") + override suspend fun execute(): Outcome, Failure> { + return if (canExecuteCommand()) { + notifier.show( + id = notificationIdFactory.next(notification.accountNumber), + notification = notification, + ) + Outcome.success(Success(command = this)) + } else { + Outcome.failure(Failure(command = this, throwable = Exception("Can't execute command."))) + } + } + + private fun canExecuteCommand(): Boolean { + val isBackgrounded = false // TODO(#9391): Verify if the app is backgrounded. + val shouldAlwaysShow = when (notification.severity) { + NotificationSeverity.Fatal, NotificationSeverity.Critical -> true + else -> false + } + + return when { + shouldAlwaysShow -> true + isBackgrounded -> true + notification !is InAppNotification -> true + else -> false + } } } diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt new file mode 100644 index 00000000000..00c0e424666 --- /dev/null +++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt @@ -0,0 +1,29 @@ +package net.thunderbird.feature.notification.impl.inject + +import net.thunderbird.feature.notification.api.receiver.NotificationNotifier +import net.thunderbird.feature.notification.api.sender.NotificationSender +import net.thunderbird.feature.notification.impl.command.NotificationCommandFactory +import net.thunderbird.feature.notification.impl.sender.DefaultNotificationSender +import org.koin.core.module.Module +import org.koin.core.qualifier.named +import org.koin.dsl.module + +internal expect val platformFeatureNotificationModule: Module + +val featureNotificationModule = module { + includes(platformFeatureNotificationModule) + + factory { + NotificationCommandFactory( + notificationIdFactory = get(), + systemNotificationNotifier = get(named()), + inAppNotificationNotifier = get(named()), + ) + } + + single { + DefaultNotificationSender( + commandFactory = get(), + ) + } +} diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.kt index 47eec3fa778..1e9b81187e8 100644 --- a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.kt +++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.kt @@ -1,17 +1,17 @@ package net.thunderbird.feature.notification.impl.receiver +import net.thunderbird.core.common.provider.ContextProvider +import net.thunderbird.core.logging.Logger import net.thunderbird.feature.notification.api.content.InAppNotification import net.thunderbird.feature.notification.api.receiver.NotificationNotifier /** * This notifier is responsible for taking a [InAppNotification] data object and * presenting it to the user in a suitable way. - * - * **Note:** The current implementation is a placeholder and needs to be completed - * as part of GitHub Issue #9245. */ -internal class InAppNotificationNotifier : NotificationNotifier { - override fun show(notification: InAppNotification) { - TODO("Implementation on GitHub Issue #9245") - } -} +internal interface InAppNotificationNotifier : NotificationNotifier + +internal expect inline fun InAppNotificationNotifier( + logger: Logger, + contextProvider: ContextProvider, +): InAppNotificationNotifier diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.kt index 2ff265f7600..9e1ba8c6a51 100644 --- a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.kt +++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.kt @@ -1,17 +1,17 @@ package net.thunderbird.feature.notification.impl.receiver +import net.thunderbird.core.common.provider.ContextProvider +import net.thunderbird.core.logging.Logger import net.thunderbird.feature.notification.api.content.SystemNotification import net.thunderbird.feature.notification.api.receiver.NotificationNotifier /** * This notifier is responsible for taking a [SystemNotification] data object and * presenting it to the user in a suitable way. - * - * **Note:** The current implementation is a placeholder and needs to be completed - * as part of GitHub Issue #9245. */ -internal class SystemNotificationNotifier : NotificationNotifier { - override fun show(notification: SystemNotification) { - TODO("Implementation on GitHub Issue #9245") - } -} +internal interface SystemNotificationNotifier : NotificationNotifier + +internal expect inline fun SystemNotificationNotifier( + logger: Logger, + contextProvider: ContextProvider, +): SystemNotificationNotifier diff --git a/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt new file mode 100644 index 00000000000..6d400b4cb13 --- /dev/null +++ b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt @@ -0,0 +1,7 @@ +package net.thunderbird.feature.notification.impl.inject + +import org.koin.core.module.Module +import org.koin.dsl.module + +internal actual val platformFeatureNotificationModule: Module = module { +} diff --git a/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.jvm.kt b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.jvm.kt new file mode 100644 index 00000000000..6d3973ab924 --- /dev/null +++ b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/receiver/InAppNotificationNotifier.jvm.kt @@ -0,0 +1,11 @@ +package net.thunderbird.feature.notification.impl.receiver + +import net.thunderbird.core.common.provider.ContextProvider +import net.thunderbird.core.logging.Logger + +internal actual inline fun InAppNotificationNotifier( + logger: Logger, + contextProvider: ContextProvider, +): InAppNotificationNotifier { + error("Can't send in-app notification from a jvm library. Use android library or app instead.") +} diff --git a/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.jvm.kt b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.jvm.kt new file mode 100644 index 00000000000..cdd5d0df229 --- /dev/null +++ b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/receiver/SystemNotificationNotifier.jvm.kt @@ -0,0 +1,11 @@ +package net.thunderbird.feature.notification.impl.receiver + +import net.thunderbird.core.common.provider.ContextProvider +import net.thunderbird.core.logging.Logger + +internal actual inline fun SystemNotificationNotifier( + logger: Logger, + contextProvider: ContextProvider, +): SystemNotificationNotifier { + error("Can't send system notification from a jvm library. Use android library or app instead.") +} diff --git a/legacy/core/src/main/java/com/fsck/k9/notification/NotificationIds.kt b/legacy/core/src/main/java/com/fsck/k9/notification/NotificationIds.kt index 660f3a05977..5fb3753c9b2 100644 --- a/legacy/core/src/main/java/com/fsck/k9/notification/NotificationIds.kt +++ b/legacy/core/src/main/java/com/fsck/k9/notification/NotificationIds.kt @@ -2,7 +2,8 @@ package com.fsck.k9.notification import net.thunderbird.core.android.account.LegacyAccount -internal object NotificationIds { +// TODO(#9416): Migrate logic from NotificationIds to NotificationIdFactory +object NotificationIds { const val PUSH_NOTIFICATION_ID = 1 const val BACKGROUND_WORK_NOTIFICATION_ID = 2 @@ -59,8 +60,12 @@ internal object NotificationIds { } private fun getBaseNotificationId(account: LegacyAccount): Int { + return getBaseNotificationId(accountNumber = account.accountNumber) + } + + fun getBaseNotificationId(accountNumber: Int): Int { /* skip notification ID 0 */ return 1 + NUMBER_OF_GENERAL_NOTIFICATIONS + - account.accountNumber * NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT + accountNumber * NUMBER_OF_NOTIFICATIONS_PER_ACCOUNT } } diff --git a/legacy/ui/legacy/build.gradle.kts b/legacy/ui/legacy/build.gradle.kts index 428d401e2d3..54e6a110ecb 100644 --- a/legacy/ui/legacy/build.gradle.kts +++ b/legacy/ui/legacy/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.ui.theme.api) implementation(projects.feature.launcher) implementation(projects.core.common) + implementation(projects.core.outcome) implementation(projects.feature.navigation.drawer.api) implementation(projects.feature.navigation.drawer.dropdown) implementation(projects.feature.navigation.drawer.siderail) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/KoinModule.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/KoinModule.kt index 7fb499bc206..9762b95d021 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/KoinModule.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/KoinModule.kt @@ -17,7 +17,10 @@ val settingsUiModule = module { viewModel { SettingsViewModel(accountManager = get()) } viewModel { - GeneralSettingsViewModel(logFileWriter = get(), syncDebugFileLogSink = get(named("syncDebug"))) + GeneralSettingsViewModel( + logFileWriter = get(), + syncDebugFileLogSink = get(named("syncDebug")), + ) } factory { GeneralSettingsDataStore( diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsFragment.kt index 04076c26843..76adfeda506 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsFragment.kt @@ -10,6 +10,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceScreen import app.k9mail.feature.telemetry.api.TelemetryManager +import com.fsck.k9.ui.BuildConfig import com.fsck.k9.ui.R import com.fsck.k9.ui.base.extensions.withArguments import com.fsck.k9.ui.observe @@ -70,6 +71,19 @@ class GeneralSettingsFragment : PreferenceFragmentCompat() { } } + findPreference("debug_secret_debug_screen")?.apply { + if (!BuildConfig.DEBUG) { + remove() + onPreferenceClickListener = null + } else { + onPreferenceClickListener = Preference.OnPreferenceClickListener { preference -> + viewModel.onOpenSecretDebugScreen(requireContext()) + + true + } + } + } + initializeDataCollection() viewModel.uiState.observe(this) { uiState -> @@ -147,6 +161,7 @@ class GeneralSettingsFragment : PreferenceFragmentCompat() { .also { snackbar = it } .show() } + private fun formatFileExportUriString(): String { val now = Calendar.getInstance() return String.format( diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsViewModel.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsViewModel.kt index dc4003f5f94..1d3fd45988c 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsViewModel.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsViewModel.kt @@ -1,8 +1,12 @@ package com.fsck.k9.ui.settings.general +import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.k9mail.feature.launcher.FeatureLauncherActivity +import app.k9mail.feature.launcher.FeatureLauncherTarget +import com.fsck.k9.ui.BuildConfig import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -15,8 +19,7 @@ import net.thunderbird.core.logging.legacy.Log class GeneralSettingsViewModel( private val logFileWriter: LogFileWriter, private val syncDebugFileLogSink: FileLogSink, -) : - ViewModel() { +) : ViewModel() { private var snackbarJob: Job? = null private val uiStateFlow = MutableStateFlow(GeneralSettingsUiState.Idle) val uiState: Flow = uiStateFlow @@ -70,6 +73,12 @@ class GeneralSettingsViewModel( uiStateFlow.value = uiState } + fun onOpenSecretDebugScreen(context: Context) { + if (BuildConfig.DEBUG) { + FeatureLauncherActivity.launch(context = context, target = FeatureLauncherTarget.SecretDebugSettings) + } + } + companion object { const val DEFAULT_FILENAME = "k9mail-logs.txt" const val SNACKBAR_DURATION = 3000L diff --git a/legacy/ui/legacy/src/main/res/xml/general_settings.xml b/legacy/ui/legacy/src/main/res/xml/general_settings.xml index 77990475d0d..55a1a021d3d 100644 --- a/legacy/ui/legacy/src/main/res/xml/general_settings.xml +++ b/legacy/ui/legacy/src/main/res/xml/general_settings.xml @@ -554,6 +554,10 @@ android:title="@string/debug_enable_sensitive_logging_title" /> + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 370de5b6cdf..a75a6f28661 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -248,6 +248,10 @@ include( ":quality:konsist", ) +include( + ":feature:debug-settings", +) + check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { """ Java 17+ is required to build Thunderbird for Android.