Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,19 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "net.thunderbird.feature.debug.settings"
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)
}

View file

@ -0,0 +1,24 @@
package net.thunderbird.feature.debug.settings
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")
}
}
}
}

View file

@ -0,0 +1,67 @@
package net.thunderbird.feature.debug.settings
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.SharedFlow
import kotlinx.coroutines.flow.flowOf
import net.thunderbird.core.common.resources.StringsResourceManager
import net.thunderbird.core.outcome.Outcome
import net.thunderbird.feature.debug.settings.notification.DebugNotificationSectionViewModel
import net.thunderbird.feature.mail.account.api.AccountManager
import net.thunderbird.feature.mail.account.api.BaseAccount
import net.thunderbird.feature.notification.api.command.NotificationCommand.Failure
import net.thunderbird.feature.notification.api.command.NotificationCommand.Success
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.receiver.InAppNotificationEvent
import net.thunderbird.feature.notification.api.receiver.InAppNotificationReceiver
import net.thunderbird.feature.notification.api.sender.NotificationSender
@PreviewLightDark
@Composable
private fun SecretDebugSettingsScreenPreview() {
koinPreview {
single<DebugNotificationSectionViewModel> {
DebugNotificationSectionViewModel(
stringsResourceManager = object : StringsResourceManager {
override fun stringResource(resourceId: Int): String = "fake"
override fun stringResource(resourceId: Int, vararg formatArgs: Any?): String = "fake"
},
accountManager = object : AccountManager<BaseAccount> {
override fun getAccounts(): List<BaseAccount> = listOf()
override fun getAccountsFlow(): Flow<List<BaseAccount>> = flowOf(listOf())
override fun getAccount(accountUuid: String): BaseAccount? = null
override fun getAccountFlow(accountUuid: String): Flow<BaseAccount?> = 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<Outcome<Success<Notification>, Failure<Notification>>> =
error("not implemented")
},
notificationReceiver = object : InAppNotificationReceiver {
override val events: SharedFlow<InAppNotificationEvent>
get() = error("not implemented")
},
)
}
} WithContent {
PreviewWithThemesLightDark {
SecretDebugSettingsScreen(
onNavigateBack = { },
modifier = Modifier.fillMaxSize(),
)
}
}
}

View file

@ -0,0 +1,19 @@
package net.thunderbird.feature.debug.settings.inject
import net.thunderbird.feature.debug.settings.navigation.DefaultSecretDebugSettingsNavigation
import net.thunderbird.feature.debug.settings.navigation.SecretDebugSettingsNavigation
import net.thunderbird.feature.debug.settings.notification.DebugNotificationSectionViewModel
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val featureDebugSettingsModule = module {
single<SecretDebugSettingsNavigation> { DefaultSecretDebugSettingsNavigation() }
viewModel {
DebugNotificationSectionViewModel(
stringsResourceManager = get(),
accountManager = get(),
notificationSender = get(),
notificationReceiver = get(),
)
}
}

View file

@ -0,0 +1,25 @@
package net.thunderbird.feature.debug.settings.navigation
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.navigation.NavGraphBuilder
import app.k9mail.core.ui.compose.navigation.deepLinkComposable
import net.thunderbird.feature.debug.settings.SecretDebugSettingsScreen
import net.thunderbird.feature.debug.settings.navigation.SecretDebugSettingsRoute.Notification
internal class DefaultSecretDebugSettingsNavigation : SecretDebugSettingsNavigation {
override fun registerRoutes(
navGraphBuilder: NavGraphBuilder,
onBack: () -> Unit,
onFinish: (SecretDebugSettingsRoute) -> Unit,
) {
with(navGraphBuilder) {
deepLinkComposable<Notification>(Notification.basePath) {
SecretDebugSettingsScreen(
onNavigateBack = onBack,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}

View file

@ -0,0 +1,78 @@
package net.thunderbird.feature.debug.settings.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.NewMailSingleMail::class,
),
)
}
DebugNotificationSection(
state = state,
modifier = Modifier.padding(MainTheme.spacings.triple),
onAccountSelect = { state = state.copy(selectedAccount = it) },
)
}
}

View file

@ -0,0 +1,49 @@
package net.thunderbird.feature.debug.settings
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.DividerHorizontal
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()
DividerHorizontal(modifier = Modifier.padding(vertical = MainTheme.spacings.double))
content()
}
}

View file

@ -0,0 +1,45 @@
package net.thunderbird.feature.debug.settings
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 androidx.compose.ui.res.stringResource
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.debug.settings.notification.DebugNotificationSection
@Composable
fun SecretDebugSettingsScreen(
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
topBar = {
TopAppBar(
title = stringResource(R.string.debug_settings_screen_title),
navigationIcon = {
ButtonIcon(
onClick = onNavigateBack,
imageVector = Icons.Outlined.ArrowBack,
)
},
)
},
modifier = modifier,
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(MainTheme.spacings.double),
) {
DebugNotificationSection()
}
}
}

View file

@ -0,0 +1,5 @@
package net.thunderbird.feature.debug.settings.navigation
import app.k9mail.core.ui.compose.navigation.Navigation
interface SecretDebugSettingsNavigation : Navigation<SecretDebugSettingsRoute>

View file

@ -0,0 +1,17 @@
package net.thunderbird.feature.debug.settings.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"
}
}

View file

@ -0,0 +1,270 @@
package net.thunderbird.feature.debug.settings.notification
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
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 androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
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.atom.button.ButtonText
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
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.debug.settings.DebugSection
import net.thunderbird.feature.debug.settings.DebugSubSection
import net.thunderbird.feature.debug.settings.R
import net.thunderbird.feature.debug.settings.notification.DebugNotificationSectionContract.Event
import net.thunderbird.feature.debug.settings.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<DebugNotificationSectionViewModel>(),
) {
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)) },
onClearStatusLog = { dispatchEvent(Event.ClearStatusLog) },
)
}
@Composable
internal fun DebugNotificationSection(
state: DebugNotificationSectionContract.State,
modifier: Modifier = Modifier,
onAccountSelect: (BaseAccount) -> Unit = {},
onOptionChange: (KClass<out Notification>) -> Unit = {},
onTriggerSystemNotificationClick: () -> Unit = {},
onTriggerInAppNotificationClick: () -> Unit = {},
onSenderChange: (String) -> Unit = {},
onSubjectChange: (String) -> Unit = {},
onSummaryChange: (String) -> Unit = {},
onPreviewChange: (String) -> Unit = {},
onClearStatusLog: () -> 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,
)
NotificationStatusLog(state.notificationStatusLog, onClearStatusLog)
}
}
}
@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<out Notification>) -> 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.NewMailSingleMail::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<out Notification>?,
options: ImmutableList<KClass<out Notification>>,
onOptionChange: (KClass<out Notification>) -> 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<out Notification>?,
options: ImmutableList<KClass<out Notification>>,
onOptionChange: (KClass<out Notification>) -> 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),
)
}
}
@Composable
private fun ColumnScope.NotificationStatusLog(
notificationStatusLog: ImmutableList<String>,
onClearStatusLog: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = notificationStatusLog.isNotEmpty(),
modifier = modifier.padding(start = MainTheme.spacings.double),
) {
DebugSubSection(
title = stringResource(R.string.debug_settings_notification_status_log),
) {
Column(modifier = Modifier.fillMaxWidth()) {
TextBodyMedium(
text = buildAnnotatedString {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(R.string.debug_settings_notifications_status))
}
notificationStatusLog.forEach { status ->
appendLine(status)
}
},
)
ButtonText(
text = stringResource(R.string.debug_settings_notifications_clear_status_log),
onClick = onClearStatusLog,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}
}

View file

@ -0,0 +1,50 @@
package net.thunderbird.feature.debug.settings.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<State, Event, Effect>
data class State(
val accounts: ImmutableList<BaseAccount> = persistentListOf(),
val selectedAccount: BaseAccount? = null,
val notificationStatusLog: ImmutableList<String> = persistentListOf("Ready to send notification"),
val selectedSystemNotificationType: KClass<out Notification>? = null,
val selectedInAppNotificationType: KClass<out Notification>? = null,
val folderName: String? = null,
val singleNotificationData: MailSingleNotificationData = MailSingleNotificationData.Undefined,
val systemNotificationTypes: ImmutableList<KClass<out Notification>> = persistentListOf(),
val inAppNotificationTypes: ImmutableList<KClass<out Notification>> = 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<out Notification>) : 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
data object ClearStatusLog : Event
}
sealed interface Effect
}

View file

@ -0,0 +1,293 @@
package net.thunderbird.feature.debug.settings.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.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.thunderbird.core.common.resources.StringsResourceManager
import net.thunderbird.feature.debug.settings.R
import net.thunderbird.feature.debug.settings.notification.DebugNotificationSectionContract.Effect
import net.thunderbird.feature.debug.settings.notification.DebugNotificationSectionContract.Event
import net.thunderbird.feature.debug.settings.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.receiver.InAppNotificationReceiver
import net.thunderbird.feature.notification.api.sender.NotificationSender
internal class DebugNotificationSectionViewModel(
private val stringsResourceManager: StringsResourceManager,
private val accountManager: AccountManager<BaseAccount>,
private val notificationSender: NotificationSender,
private val notificationReceiver: InAppNotificationReceiver,
private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : BaseViewModel<State, Event, Effect>(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.NewMailSingleMail::class)
add(MailNotification.NewMailSummaryMail::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(),
)
}
}
}
viewModelScope.launch {
notificationReceiver
.events
.collectLatest { event ->
updateState { state ->
state.copy(
notificationStatusLog = state.notificationStatusLog + " In-app notification event: $event",
)
}
}
}
}
override fun event(event: Event) {
when (event) {
is 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 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 Event.SelectAccount -> updateState { state ->
state.copy(selectedAccount = event.account)
}
is Event.SelectNotificationType -> viewModelScope.launch {
buildNotification(event.notificationType)
}
is Event.OnSenderChange -> updateState {
it.copy(singleNotificationData = it.singleNotificationData.copy(sender = event.sender))
}
is Event.OnSubjectChange -> updateState {
it.copy(singleNotificationData = it.singleNotificationData.copy(subject = event.subject))
}
is Event.OnSummaryChange -> updateState {
it.copy(singleNotificationData = it.singleNotificationData.copy(summary = event.summary))
}
is Event.OnPreviewChange -> updateState {
it.copy(singleNotificationData = it.singleNotificationData.copy(preview = event.preview))
}
Event.ClearStatusLog -> updateState { it.copy(notificationStatusLog = persistentListOf()) }
}
}
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<out Notification>?): Notification? {
updateState {
it.copy(
notificationStatusLog = it.notificationStatusLog +
stringsResourceManager.stringResource(
R.string.debug_settings_notifications_preparing_notification,
notificationType?.realName,
),
)
}
val state = state.value
val selectedAccount = state.selectedAccount ?: return null
val accountDisplay = selectedAccount.name ?: selectedAccount.email
val notification = buildNotification(
notificationType = notificationType,
selectedAccount = selectedAccount,
accountDisplay = accountDisplay,
state = 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<out Notification>?,
selectedAccount: BaseAccount,
accountDisplay: String,
state: State,
): Notification? = when (notificationType) {
AuthenticationErrorNotification::class -> AuthenticationErrorNotification(
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
)
CertificateErrorNotification::class -> CertificateErrorNotification(
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
)
FailedToCreateNotification::class -> FailedToCreateNotification(
accountUuid = selectedAccount.uuid,
failedNotification = AuthenticationErrorNotification(
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
),
)
MailNotification.Fetching::class -> MailNotification.Fetching(
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
folderName = state.folderName,
)
MailNotification.NewMailSingleMail::class -> state.buildSingleMailNotification(
selectedAccount = selectedAccount,
accountDisplay = accountDisplay,
)
MailNotification.NewMailSummaryMail::class -> MailNotification.NewMailSummaryMail(
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
messagesNotificationChannelSuffix = "",
newMessageCount = 10,
additionalMessagesCount = 10,
group = NotificationGroup(
key = NotificationGroupKey("key"),
summary = "",
),
)
MailNotification.SendFailed::class -> MailNotification.SendFailed(
accountUuid = selectedAccount.uuid,
exception = Exception("What a failure"),
)
MailNotification.Sending::class -> MailNotification.Sending(
accountUuid = selectedAccount.uuid,
accountDisplayName = accountDisplay,
)
PushServiceNotification.AlarmPermissionMissing::class -> PushServiceNotification.AlarmPermissionMissing()
PushServiceNotification.Initializing::class -> PushServiceNotification.Initializing()
PushServiceNotification.Listening::class -> PushServiceNotification.Listening()
PushServiceNotification.WaitBackgroundSync::class -> PushServiceNotification.WaitBackgroundSync()
PushServiceNotification.WaitNetwork::class -> PushServiceNotification.WaitNetwork()
else -> null
}
private fun State.buildSingleMailNotification(
selectedAccount: BaseAccount,
accountDisplay: String,
): MailNotification.NewMailSingleMail? = MailNotification.NewMailSingleMail(
accountUuid = selectedAccount.uuid,
accountName = accountDisplay,
messagesNotificationChannelSuffix = "",
summary = singleNotificationData.summary,
sender = singleNotificationData.sender,
subject = singleNotificationData.subject,
preview = singleNotificationData.preview,
group = null,
)
private operator fun ImmutableList<String>.plus(other: String): ImmutableList<String> =
(this.toMutableList() + other).toPersistentList()
}
internal val KClass<out Notification>.realName: String
get() {
val clazz = java
return clazz.name
.replace(clazz.`package`?.name.orEmpty(), "")
.removePrefix(".")
.replace("$", ".")
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="debug_settings_screen_title">Secret Debug Settings Screen</string>
<string name="debug_settings_notifications_loading">Loading…</string>
<string name="debug_settings_notifications_trigger_notification">Trigger notification</string>
<string name="debug_settings_notifications_single_mail_sender">Sender</string>
<string name="debug_settings_notifications_single_mail_subject">Subject</string>
<string name="debug_settings_notifications_single_mail_summary">Summary</string>
<string name="debug_settings_notifications_single_mail_preview">Preview</string>
<string name="debug_settings_notifications_common_notification_information">Common notification information</string>
<string name="debug_settings_notifications_in_app_notification">In-App notification</string>
<string name="debug_settings_notifications_title">Notifications</string>
<string name="debug_settings_notifications_system_notification">System notification</string>
<string name="debug_settings_notifications_preparing_notification">Preparing notification %1$s</string>
<string name="debug_settings_notification_status_log">Notification status log</string>
<string name="debug_settings_notifications_status">"Status: "</string>
<string name="debug_settings_notifications_clear_status_log">Clear status log</string>
</resources>

View file

@ -0,0 +1,9 @@
package net.thunderbird.feature.debug.settings.inject
import net.thunderbird.feature.debug.settings.navigation.NoOpSecretDebugSettingsNavigation
import net.thunderbird.feature.debug.settings.navigation.SecretDebugSettingsNavigation
import org.koin.dsl.module
val featureDebugSettingsModule = module {
single<SecretDebugSettingsNavigation> { NoOpSecretDebugSettingsNavigation }
}

View file

@ -0,0 +1,11 @@
package net.thunderbird.feature.debug.settings.navigation
import androidx.navigation.NavGraphBuilder
object NoOpSecretDebugSettingsNavigation : SecretDebugSettingsNavigation {
override fun registerRoutes(
navGraphBuilder: NavGraphBuilder,
onBack: () -> Unit,
onFinish: (SecretDebugSettingsRoute) -> Unit,
) = Unit
}