From 300ca40af4ce13b1a84f4f04689e8dc7d870fe76 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 May 2024 11:06:34 +0200 Subject: [PATCH] Move setting to change push provider to the `Notifications` setting screen. #2912 Also improve previews of NotificationSettingsView. --- .../impl/advanced/AdvancedSettingsEvents.kt | 3 - .../advanced/AdvancedSettingsPresenter.kt | 72 --------------- .../impl/advanced/AdvancedSettingsState.kt | 5 -- .../advanced/AdvancedSettingsStateProvider.kt | 11 --- .../impl/advanced/AdvancedSettingsView.kt | 49 ---------- .../NotificationSettingsEvents.kt | 3 + .../NotificationSettingsPresenter.kt | 71 ++++++++++++++- .../NotificationSettingsState.kt | 4 + .../NotificationSettingsStateProvider.kt | 18 +++- .../notifications/NotificationSettingsView.kt | 61 ++++++++++++- .../advanced/AdvancedSettingsPresenterTest.kt | 90 ------------------- .../impl/advanced/AdvancedSettingsViewTest.kt | 30 ------- .../NotificationSettingsPresenterTests.kt | 90 ++++++++++++++++++- .../NotificationSettingsViewTest.kt | 29 ++++++ 14 files changed, 271 insertions(+), 265 deletions(-) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt index 4ed4277599..ab4987f9d9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt @@ -24,7 +24,4 @@ sealed interface AdvancedSettingsEvents { data object ChangeTheme : AdvancedSettingsEvents data object CancelChangeTheme : AdvancedSettingsEvents data class SetTheme(val theme: Theme) : AdvancedSettingsEvents - data object ChangePushProvider : AdvancedSettingsEvents - data object CancelChangePushProvider : AdvancedSettingsEvents - data class SetPushProvider(val index: Int) : AdvancedSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index 21d84567e1..d6fbfb4c24 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -17,10 +17,8 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -29,22 +27,13 @@ import io.element.android.compound.theme.Theme import io.element.android.compound.theme.mapToTheme import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.features.preferences.api.store.SessionPreferencesStore -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.push.api.PushService -import io.element.android.libraries.pushproviders.api.Distributor -import io.element.android.libraries.pushproviders.api.PushProvider -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class AdvancedSettingsPresenter @Inject constructor( private val appPreferencesStore: AppPreferencesStore, private val sessionPreferencesStore: SessionPreferencesStore, - private val matrixClient: MatrixClient, - private val pushService: PushService, ) : Presenter { @Composable override fun present(): AdvancedSettingsState { @@ -61,61 +50,6 @@ class AdvancedSettingsPresenter @Inject constructor( .collectAsState(initial = Theme.System) var showChangeThemeDialog by remember { mutableStateOf(false) } - // List of PushProvider -> Distributor - val distributors = remember { - pushService.getAvailablePushProviders() - .flatMap { pushProvider -> - pushProvider.getDistributors().map { distributor -> - pushProvider to distributor - } - } - } - // List of Distributor names - val distributorNames = remember { - distributors.map { it.second.name } - } - - var currentDistributorName by remember { mutableStateOf>(AsyncAction.Uninitialized) } - var refreshPushProvider by remember { mutableIntStateOf(0) } - - LaunchedEffect(refreshPushProvider) { - val p = pushService.getCurrentPushProvider() - val name = p?.getCurrentDistributor(matrixClient)?.name - currentDistributorName = if (name != null) { - AsyncAction.Success(name) - } else { - AsyncAction.Failure(Exception("Failed to get current push provider")) - } - } - - var showChangePushProviderDialog by remember { mutableStateOf(false) } - - fun CoroutineScope.changePushProvider( - data: Pair? - ) = launch { - showChangePushProviderDialog = false - data ?: return@launch - // No op if the value is the same. - if (data.second.name == currentDistributorName.dataOrNull()) return@launch - currentDistributorName = AsyncAction.Loading - data.let { (pushProvider, distributor) -> - pushService.registerWith( - matrixClient = matrixClient, - pushProvider = pushProvider, - distributor = distributor - ) - .fold( - { - currentDistributorName = AsyncAction.Success(distributor.name) - refreshPushProvider++ - }, - { - currentDistributorName = AsyncAction.Failure(it) - } - ) - } - } - fun handleEvents(event: AdvancedSettingsEvents) { when (event) { is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { @@ -130,9 +64,6 @@ class AdvancedSettingsPresenter @Inject constructor( appPreferencesStore.setTheme(event.theme.name) showChangeThemeDialog = false } - AdvancedSettingsEvents.ChangePushProvider -> showChangePushProviderDialog = true - AdvancedSettingsEvents.CancelChangePushProvider -> showChangePushProviderDialog = false - is AdvancedSettingsEvents.SetPushProvider -> localCoroutineScope.changePushProvider(distributors.getOrNull(event.index)) } } @@ -141,9 +72,6 @@ class AdvancedSettingsPresenter @Inject constructor( isSharePresenceEnabled = isSharePresenceEnabled, theme = theme, showChangeThemeDialog = showChangeThemeDialog, - currentPushDistributor = currentDistributorName, - availablePushDistributors = distributorNames.toImmutableList(), - showChangePushProviderDialog = showChangePushProviderDialog, eventSink = { handleEvents(it) } ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 23de2fda13..527515d867 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -17,16 +17,11 @@ package io.element.android.features.preferences.impl.advanced import io.element.android.compound.theme.Theme -import io.element.android.libraries.architecture.AsyncAction -import kotlinx.collections.immutable.ImmutableList data class AdvancedSettingsState( val isDeveloperModeEnabled: Boolean, val isSharePresenceEnabled: Boolean, val theme: Theme, val showChangeThemeDialog: Boolean, - val currentPushDistributor: AsyncAction, - val availablePushDistributors: ImmutableList, - val showChangePushProviderDialog: Boolean, val eventSink: (AdvancedSettingsEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index 5e6af364f2..21ccec52e4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -18,8 +18,6 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.compound.theme.Theme -import io.element.android.libraries.architecture.AsyncAction -import kotlinx.collections.immutable.toImmutableList open class AdvancedSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,9 +26,6 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider = AsyncAction.Success("Firebase"), - availablePushDistributors: List = listOf("Firebase", "ntfy"), - showChangePushProviderDialog: Boolean = false, eventSink: (AdvancedSettingsEvents) -> Unit = {}, ) = AdvancedSettingsState( isDeveloperModeEnabled = isDeveloperModeEnabled, isSharePresenceEnabled = isSendPublicReadReceiptsEnabled, theme = Theme.System, showChangeThemeDialog = showChangeThemeDialog, - currentPushDistributor = currentPushDistributor, - availablePushDistributors = availablePushDistributors.toImmutableList(), - showChangePushProviderDialog = showChangePushProviderDialog, eventSink = eventSink ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt index 9b82a87bbc..68e1c7ea22 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -16,24 +16,19 @@ package io.element.android.features.preferences.impl.advanced -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.progressSemantics import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import io.element.android.compound.theme.Theme import io.element.android.compound.theme.themes import io.element.android.features.preferences.impl.R -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.dialogs.ListOption import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings @@ -86,34 +81,6 @@ fun AdvancedSettingsView( ), onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) } ) - ListItem( - headlineContent = { - Text(text = stringResource(id = R.string.screen_advanced_settings_push_provider_android)) - }, - trailingContent = when (state.currentPushDistributor) { - AsyncAction.Uninitialized, - AsyncAction.Confirming, - AsyncAction.Loading -> ListItemContent.Custom { - CircularProgressIndicator( - modifier = Modifier - .progressSemantics() - .size(20.dp), - strokeWidth = 2.dp - ) - } - is AsyncAction.Failure -> ListItemContent.Text( - stringResource(id = CommonStrings.common_error) - ) - is AsyncAction.Success -> ListItemContent.Text( - state.currentPushDistributor.dataOrNull() ?: "" - ) - }, - onClick = { - if (state.currentPushDistributor.isReady()) { - state.eventSink(AdvancedSettingsEvents.ChangePushProvider) - } - } - ) } if (state.showChangeThemeDialog) { @@ -130,22 +97,6 @@ fun AdvancedSettingsView( onDismissRequest = { state.eventSink(AdvancedSettingsEvents.CancelChangeTheme) }, ) } - - if (state.showChangePushProviderDialog) { - SingleSelectionDialog( - title = stringResource(id = R.string.screen_advanced_settings_choose_distributor_dialog_title_android), - options = state.availablePushDistributors.map { - ListOption(title = it) - }.toImmutableList(), - initialSelection = state.availablePushDistributors.indexOf(state.currentPushDistributor.dataOrNull()), - onOptionSelected = { index -> - state.eventSink( - AdvancedSettingsEvents.SetPushProvider(index) - ) - }, - onDismissRequest = { state.eventSink(AdvancedSettingsEvents.CancelChangePushProvider) }, - ) - } } @Composable diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt index f38c211b67..72896fd613 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt @@ -25,4 +25,7 @@ sealed interface NotificationSettingsEvents { data object FixConfigurationMismatch : NotificationSettingsEvents data object ClearConfigurationMismatchError : NotificationSettingsEvents data object ClearNotificationChangeError : NotificationSettingsEvents + data object ChangePushProvider : NotificationSettingsEvents + data object CancelChangePushProvider : NotificationSettingsEvents + data class SetPushProvider(val index: Int) : NotificationSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index 9aa9cafb81..83253e17d2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -20,17 +20,24 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce @@ -44,7 +51,8 @@ class NotificationSettingsPresenter @Inject constructor( private val notificationSettingsService: NotificationSettingsService, private val userPushStoreFactory: UserPushStoreFactory, private val matrixClient: MatrixClient, - private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider + private val pushService: PushService, + private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider, ) : Presenter { @Composable override fun present(): NotificationSettingsState { @@ -68,6 +76,61 @@ class NotificationSettingsPresenter @Inject constructor( observeNotificationSettings(matrixSettings) } + // List of PushProvider -> Distributor + val distributors = remember { + pushService.getAvailablePushProviders() + .flatMap { pushProvider -> + pushProvider.getDistributors().map { distributor -> + pushProvider to distributor + } + } + } + // List of Distributor names + val distributorNames = remember { + distributors.map { it.second.name } + } + + var currentDistributorName by remember { mutableStateOf>(AsyncAction.Uninitialized) } + var refreshPushProvider by remember { mutableIntStateOf(0) } + + LaunchedEffect(refreshPushProvider) { + val p = pushService.getCurrentPushProvider() + val name = p?.getCurrentDistributor(matrixClient)?.name + currentDistributorName = if (name != null) { + AsyncAction.Success(name) + } else { + AsyncAction.Failure(Exception("Failed to get current push provider")) + } + } + + var showChangePushProviderDialog by remember { mutableStateOf(false) } + + fun CoroutineScope.changePushProvider( + data: Pair? + ) = launch { + showChangePushProviderDialog = false + data ?: return@launch + // No op if the value is the same. + if (data.second.name == currentDistributorName.dataOrNull()) return@launch + currentDistributorName = AsyncAction.Loading + data.let { (pushProvider, distributor) -> + pushService.registerWith( + matrixClient = matrixClient, + pushProvider = pushProvider, + distributor = distributor + ) + .fold( + { + currentDistributorName = AsyncAction.Success(distributor.name) + refreshPushProvider++ + }, + { + currentDistributorName = AsyncAction.Failure(it) + } + ) + } + } + fun handleEvents(event: NotificationSettingsEvents) { when (event) { is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> { @@ -88,6 +151,9 @@ class NotificationSettingsPresenter @Inject constructor( systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled() } NotificationSettingsEvents.ClearNotificationChangeError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized + NotificationSettingsEvents.ChangePushProvider -> showChangePushProviderDialog = true + NotificationSettingsEvents.CancelChangePushProvider -> showChangePushProviderDialog = false + is NotificationSettingsEvents.SetPushProvider -> localCoroutineScope.changePushProvider(distributors.getOrNull(event.index)) } } @@ -98,6 +164,9 @@ class NotificationSettingsPresenter @Inject constructor( appNotificationsEnabled = appNotificationsEnabled.value ), changeNotificationSettingAction = changeNotificationSettingAction.value, + currentPushDistributor = currentDistributorName, + availablePushDistributors = distributorNames.toImmutableList(), + showChangePushProviderDialog = showChangePushProviderDialog, eventSink = ::handleEvents ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt index 5c23e74cd5..0a1c1d76a7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -19,12 +19,16 @@ package io.element.android.features.preferences.impl.notifications import androidx.compose.runtime.Immutable import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import kotlinx.collections.immutable.ImmutableList @Immutable data class NotificationSettingsState( val matrixSettings: MatrixSettings, val appSettings: AppSettings, val changeNotificationSettingAction: AsyncAction, + val currentPushDistributor: AsyncAction, + val availablePushDistributors: ImmutableList, + val showChangePushProviderDialog: Boolean, val eventSink: (NotificationSettingsEvents) -> Unit, ) { sealed interface MatrixSettings { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt index dc1e972aa6..6b363fc422 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt @@ -19,13 +19,19 @@ package io.element.android.features.preferences.impl.notifications import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList open class NotificationSettingsStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aValidNotificationSettingsState(), + aValidNotificationSettingsState(systemNotificationsEnabled = false), aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading), aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))), + aValidNotificationSettingsState(showChangePushProviderDialog = true), + aValidNotificationSettingsState(currentPushDistributor = AsyncAction.Loading), + aValidNotificationSettingsState(currentPushDistributor = AsyncAction.Failure(Exception("Failed to change distributor"))), aInvalidNotificationSettingsState(), aInvalidNotificationSettingsState(fixFailed = true), ) @@ -36,7 +42,11 @@ fun aValidNotificationSettingsState( atRoomNotificationsEnabled: Boolean = true, callNotificationsEnabled: Boolean = true, inviteForMeNotificationsEnabled: Boolean = true, + systemNotificationsEnabled: Boolean = true, appNotificationEnabled: Boolean = true, + currentPushDistributor: AsyncAction = AsyncAction.Success("Firebase"), + availablePushDistributors: List = listOf("Firebase", "ntfy"), + showChangePushProviderDialog: Boolean = false, eventSink: (NotificationSettingsEvents) -> Unit = {}, ) = NotificationSettingsState( matrixSettings = NotificationSettingsState.MatrixSettings.Valid( @@ -47,10 +57,13 @@ fun aValidNotificationSettingsState( defaultOneToOneNotificationMode = RoomNotificationMode.ALL_MESSAGES, ), appSettings = NotificationSettingsState.AppSettings( - systemNotificationsEnabled = false, + systemNotificationsEnabled = systemNotificationsEnabled, appNotificationsEnabled = appNotificationEnabled, ), changeNotificationSettingAction = changeNotificationSettingAction, + currentPushDistributor = currentPushDistributor, + availablePushDistributors = availablePushDistributors.toImmutableList(), + showChangePushProviderDialog = showChangePushProviderDialog, eventSink = eventSink, ) @@ -66,5 +79,8 @@ fun aInvalidNotificationSettingsState( appNotificationsEnabled = true, ), changeNotificationSettingAction = AsyncAction.Uninitialized, + currentPushDistributor = AsyncAction.Uninitialized, + availablePushDistributors = persistentListOf(), + showChangePushProviderDialog = false, eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index 7dda9b684b..439b98953f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -16,27 +16,38 @@ package io.element.android.features.preferences.impl.notifications +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.preferences.impl.R import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.components.dialogs.ListOption +import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableList /** * A view that allows a user edit their global notification settings. @@ -69,7 +80,7 @@ fun NotificationSettingsView( NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferencePage is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView( matrixSettings = state.matrixSettings, - systemSettings = state.appSettings, + state = state, onNotificationsEnabledChanged = { state.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(it)) }, onGroupChatsClicked = { onOpenEditDefault(false) }, onDirectChatsClicked = { onOpenEditDefault(true) }, @@ -92,7 +103,7 @@ fun NotificationSettingsView( @Composable private fun NotificationSettingsContentView( matrixSettings: NotificationSettingsState.MatrixSettings.Valid, - systemSettings: NotificationSettingsState.AppSettings, + state: NotificationSettingsState, onNotificationsEnabledChanged: (Boolean) -> Unit, onGroupChatsClicked: () -> Unit, onDirectChatsClicked: () -> Unit, @@ -103,6 +114,7 @@ private fun NotificationSettingsContentView( onTroubleshootNotificationsClicked: () -> Unit, ) { val context = LocalContext.current + val systemSettings: NotificationSettingsState.AppSettings = state.appSettings if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) { PreferenceText( icon = CompoundIcons.NotificationsOffSolid(), @@ -169,6 +181,51 @@ private fun NotificationSettingsContentView( onClick = onTroubleshootNotificationsClicked ) } + PreferenceCategory(title = stringResource(id = CommonStrings.common_advanced_settings)) { + ListItem( + headlineContent = { + Text(text = stringResource(id = R.string.screen_advanced_settings_push_provider_android)) + }, + trailingContent = when (state.currentPushDistributor) { + AsyncAction.Uninitialized, + AsyncAction.Confirming, + AsyncAction.Loading -> ListItemContent.Custom { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(20.dp), + strokeWidth = 2.dp + ) + } + is AsyncAction.Failure -> ListItemContent.Text( + stringResource(id = CommonStrings.common_error) + ) + is AsyncAction.Success -> ListItemContent.Text( + state.currentPushDistributor.dataOrNull() ?: "" + ) + }, + onClick = { + if (state.currentPushDistributor.isReady()) { + state.eventSink(NotificationSettingsEvents.ChangePushProvider) + } + } + ) + } + if (state.showChangePushProviderDialog) { + SingleSelectionDialog( + title = stringResource(id = R.string.screen_advanced_settings_choose_distributor_dialog_title_android), + options = state.availablePushDistributors.map { + ListOption(title = it) + }.toImmutableList(), + initialSelection = state.availablePushDistributors.indexOf(state.currentPushDistributor.dataOrNull()), + onOptionSelected = { index -> + state.eventSink( + NotificationSettingsEvents.SetPushProvider(index) + ) + }, + onDismissRequest = { state.eventSink(NotificationSettingsEvents.CancelChangePushProvider) }, + ) + } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 7e4175d3bd..e3ca6bf8b4 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -21,16 +21,8 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.compound.theme.Theme -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore -import io.element.android.libraries.push.api.PushService -import io.element.android.libraries.push.test.FakePushService -import io.element.android.libraries.pushproviders.api.Distributor -import io.element.android.libraries.pushproviders.api.PushProvider -import io.element.android.libraries.pushproviders.test.FakePushProvider import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest @@ -108,93 +100,11 @@ class AdvancedSettingsPresenterTest { } } - @Test - fun `present - change push provider`() = runTest { - val presenter = createAdvancedSettingsPresenter( - pushService = createFakePushService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitLastSequentialItem() - assertThat(initialState.currentPushDistributor).isEqualTo(AsyncAction.Success("aDistributorName0")) - assertThat(initialState.availablePushDistributors).containsExactly("aDistributorName0", "aDistributorName1") - initialState.eventSink.invoke(AdvancedSettingsEvents.ChangePushProvider) - val withDialog = awaitItem() - assertThat(withDialog.showChangePushProviderDialog).isTrue() - // Cancel - withDialog.eventSink(AdvancedSettingsEvents.CancelChangePushProvider) - val withoutDialog = awaitItem() - assertThat(withoutDialog.showChangePushProviderDialog).isFalse() - withDialog.eventSink.invoke(AdvancedSettingsEvents.ChangePushProvider) - assertThat(awaitItem().showChangePushProviderDialog).isTrue() - withDialog.eventSink(AdvancedSettingsEvents.SetPushProvider(1)) - val withNewProvider = awaitItem() - assertThat(withNewProvider.showChangePushProviderDialog).isFalse() - assertThat(withNewProvider.currentPushDistributor).isEqualTo(AsyncAction.Loading) - val lastItem = awaitItem() - assertThat(lastItem.currentPushDistributor).isEqualTo(AsyncAction.Success("aDistributorName1")) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - change push provider error`() = runTest { - val presenter = createAdvancedSettingsPresenter( - pushService = createFakePushService( - registerWithLambda = { _, _, _ -> - Result.failure(Exception("An error")) - }, - ), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitLastSequentialItem() - initialState.eventSink.invoke(AdvancedSettingsEvents.ChangePushProvider) - val withDialog = awaitItem() - assertThat(withDialog.showChangePushProviderDialog).isTrue() - withDialog.eventSink(AdvancedSettingsEvents.SetPushProvider(1)) - val withNewProvider = awaitItem() - assertThat(withNewProvider.showChangePushProviderDialog).isFalse() - assertThat(withNewProvider.currentPushDistributor).isEqualTo(AsyncAction.Loading) - val lastItem = awaitItem() - assertThat(lastItem.currentPushDistributor).isInstanceOf(AsyncAction.Failure::class.java) - } - } - - private fun createFakePushService( - registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result = { _, _, _ -> - Result.success(Unit) - } - ): PushService { - val pushProvider1 = FakePushProvider( - index = 0, - name = "aFakePushProvider0", - isAvailable = true, - distributors = listOf(Distributor("aDistributorValue0", "aDistributorName0")), - ) - val pushProvider2 = FakePushProvider( - index = 1, - name = "aFakePushProvider1", - isAvailable = true, - distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")), - ) - return FakePushService( - availablePushProviders = listOf(pushProvider1, pushProvider2), - registerWithLambda = registerWithLambda, - ) - } - private fun createAdvancedSettingsPresenter( appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), - matrixClient: MatrixClient = FakeMatrixClient(), - pushService: PushService = FakePushService(), ) = AdvancedSettingsPresenter( appPreferencesStore = appPreferencesStore, sessionPreferencesStore = sessionPreferencesStore, - matrixClient = matrixClient, - pushService = pushService, ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt index ee8c900579..a24fb183a8 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt @@ -19,8 +19,6 @@ package io.element.android.features.preferences.impl.advanced import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.compound.theme.Theme import io.element.android.features.preferences.impl.R @@ -34,7 +32,6 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.junit.runner.RunWith -import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class AdvancedSettingsViewTest { @@ -103,33 +100,6 @@ class AdvancedSettingsViewTest { rule.clickOn(R.string.screen_advanced_settings_share_presence) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on Push notification provider emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( - state = aAdvancedSettingsState( - eventSink = eventsRecorder - ), - ) - rule.clickOn(R.string.screen_advanced_settings_push_provider_android) - eventsRecorder.assertSingle(AdvancedSettingsEvents.ChangePushProvider) - } - - @Test - fun `clicking on a push provider emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( - state = aAdvancedSettingsState( - eventSink = eventsRecorder, - showChangePushProviderDialog = true, - availablePushDistributors = listOf("P1", "P2") - ), - ) - rule.onNodeWithText("P2").performClick() - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetPushProvider(1)) - } } private fun AndroidComposeTestRule.setAdvancedSettingsView( diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt index a6e9c63208..ea3f4657fe 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt @@ -20,11 +20,19 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.test.FakePushService +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.libraries.pushproviders.test.FakePushProvider import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest import org.junit.Test @@ -230,14 +238,94 @@ class NotificationSettingsPresenterTests { } } + @Test + fun `present - change push provider`() = runTest { + val presenter = createNotificationSettingsPresenter( + pushService = createFakePushService(), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + assertThat(initialState.currentPushDistributor).isEqualTo(AsyncAction.Success("aDistributorName0")) + assertThat(initialState.availablePushDistributors).containsExactly("aDistributorName0", "aDistributorName1") + initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + val withDialog = awaitItem() + assertThat(withDialog.showChangePushProviderDialog).isTrue() + // Cancel + withDialog.eventSink(NotificationSettingsEvents.CancelChangePushProvider) + val withoutDialog = awaitItem() + assertThat(withoutDialog.showChangePushProviderDialog).isFalse() + withDialog.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + assertThat(awaitItem().showChangePushProviderDialog).isTrue() + withDialog.eventSink(NotificationSettingsEvents.SetPushProvider(1)) + val withNewProvider = awaitItem() + assertThat(withNewProvider.showChangePushProviderDialog).isFalse() + assertThat(withNewProvider.currentPushDistributor).isEqualTo(AsyncAction.Loading) + val lastItem = awaitItem() + assertThat(lastItem.currentPushDistributor).isEqualTo(AsyncAction.Success("aDistributorName1")) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - change push provider error`() = runTest { + val presenter = createNotificationSettingsPresenter( + pushService = createFakePushService( + registerWithLambda = { _, _, _ -> + Result.failure(Exception("An error")) + }, + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + val withDialog = awaitItem() + assertThat(withDialog.showChangePushProviderDialog).isTrue() + withDialog.eventSink(NotificationSettingsEvents.SetPushProvider(1)) + val withNewProvider = awaitItem() + assertThat(withNewProvider.showChangePushProviderDialog).isFalse() + assertThat(withNewProvider.currentPushDistributor).isEqualTo(AsyncAction.Loading) + val lastItem = awaitItem() + assertThat(lastItem.currentPushDistributor).isInstanceOf(AsyncAction.Failure::class.java) + } + } + + private fun createFakePushService( + registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result = { _, _, _ -> + Result.success(Unit) + } + ): PushService { + val pushProvider1 = FakePushProvider( + index = 0, + name = "aFakePushProvider0", + isAvailable = true, + distributors = listOf(Distributor("aDistributorValue0", "aDistributorName0")), + ) + val pushProvider2 = FakePushProvider( + index = 1, + name = "aFakePushProvider1", + isAvailable = true, + distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")), + ) + return FakePushService( + availablePushProviders = listOf(pushProvider1, pushProvider2), + registerWithLambda = registerWithLambda, + ) + } + private fun createNotificationSettingsPresenter( - notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), + pushService: PushService = FakePushService(), ): NotificationSettingsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) return NotificationSettingsPresenter( notificationSettingsService = notificationSettingsService, userPushStoreFactory = FakeUserPushStoreFactory(), matrixClient = matrixClient, + pushService = pushService, systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(), ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt index 8397d5aef6..58a685719a 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt @@ -19,6 +19,8 @@ package io.element.android.features.preferences.impl.notifications import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -248,6 +250,33 @@ class NotificationSettingsViewTest { ) ) } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on Push notification provider emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_advanced_settings_push_provider_android) + eventsRecorder.assertSingle(NotificationSettingsEvents.ChangePushProvider) + } + + @Test + fun `clicking on a push provider emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder, + showChangePushProviderDialog = true, + availablePushDistributors = listOf("P1", "P2") + ), + ) + rule.onNodeWithText("P2").performClick() + eventsRecorder.assertSingle(NotificationSettingsEvents.SetPushProvider(1)) + } } private fun AndroidComposeTestRule.setNotificationSettingsView(