Browse Source

Move setting to change push provider to the `Notifications` setting screen. #2912

Also improve previews of NotificationSettingsView.
pull/2928/head
Benoit Marty 4 months ago committed by Benoit Marty
parent
commit
300ca40af4
  1. 3
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt
  2. 72
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt
  3. 5
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt
  4. 11
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt
  5. 49
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
  6. 3
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt
  7. 71
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt
  8. 4
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt
  9. 18
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt
  10. 61
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt
  11. 90
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt
  12. 30
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt
  13. 90
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt
  14. 29
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt

3
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt

@ -24,7 +24,4 @@ sealed interface AdvancedSettingsEvents { @@ -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
}

72
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt

@ -17,10 +17,8 @@ @@ -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 @@ -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<AdvancedSettingsState> {
@Composable
override fun present(): AdvancedSettingsState {
@ -61,61 +50,6 @@ class AdvancedSettingsPresenter @Inject constructor( @@ -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<String>>(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<PushProvider, Distributor>?
) = 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( @@ -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( @@ -141,9 +72,6 @@ class AdvancedSettingsPresenter @Inject constructor(
isSharePresenceEnabled = isSharePresenceEnabled,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
currentPushDistributor = currentDistributorName,
availablePushDistributors = distributorNames.toImmutableList(),
showChangePushProviderDialog = showChangePushProviderDialog,
eventSink = { handleEvents(it) }
)
}

5
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt

@ -17,16 +17,11 @@ @@ -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<String>,
val availablePushDistributors: ImmutableList<String>,
val showChangePushProviderDialog: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit
)

11
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 @@ -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<AdvancedSettingsState> {
override val values: Sequence<AdvancedSettingsState>
@ -28,9 +26,6 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett @@ -28,9 +26,6 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
aAdvancedSettingsState(isDeveloperModeEnabled = true),
aAdvancedSettingsState(showChangeThemeDialog = true),
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
aAdvancedSettingsState(showChangePushProviderDialog = true),
aAdvancedSettingsState(currentPushDistributor = AsyncAction.Loading),
aAdvancedSettingsState(currentPushDistributor = AsyncAction.Failure(Exception("Failed to change distributor"))),
)
}
@ -38,17 +33,11 @@ fun aAdvancedSettingsState( @@ -38,17 +33,11 @@ fun aAdvancedSettingsState(
isDeveloperModeEnabled: Boolean = false,
isSendPublicReadReceiptsEnabled: Boolean = false,
showChangeThemeDialog: Boolean = false,
currentPushDistributor: AsyncAction<String> = AsyncAction.Success("Firebase"),
availablePushDistributors: List<String> = 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
)

49
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt

@ -16,24 +16,19 @@ @@ -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( @@ -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( @@ -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

3
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt

@ -25,4 +25,7 @@ sealed interface NotificationSettingsEvents { @@ -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
}

71
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt

@ -20,17 +20,24 @@ import androidx.compose.runtime.Composable @@ -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( @@ -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<NotificationSettingsState> {
@Composable
override fun present(): NotificationSettingsState {
@ -68,6 +76,61 @@ class NotificationSettingsPresenter @Inject constructor( @@ -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<String>>(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<PushProvider, Distributor>?
) = 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( @@ -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( @@ -98,6 +164,9 @@ class NotificationSettingsPresenter @Inject constructor(
appNotificationsEnabled = appNotificationsEnabled.value
),
changeNotificationSettingAction = changeNotificationSettingAction.value,
currentPushDistributor = currentDistributorName,
availablePushDistributors = distributorNames.toImmutableList(),
showChangePushProviderDialog = showChangePushProviderDialog,
eventSink = ::handleEvents
)
}

4
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 @@ -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<Unit>,
val currentPushDistributor: AsyncAction<String>,
val availablePushDistributors: ImmutableList<String>,
val showChangePushProviderDialog: Boolean,
val eventSink: (NotificationSettingsEvents) -> Unit,
) {
sealed interface MatrixSettings {

18
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 @@ -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<NotificationSettingsState> {
override val values: Sequence<NotificationSettingsState>
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( @@ -36,7 +42,11 @@ fun aValidNotificationSettingsState(
atRoomNotificationsEnabled: Boolean = true,
callNotificationsEnabled: Boolean = true,
inviteForMeNotificationsEnabled: Boolean = true,
systemNotificationsEnabled: Boolean = true,
appNotificationEnabled: Boolean = true,
currentPushDistributor: AsyncAction<String> = AsyncAction.Success("Firebase"),
availablePushDistributors: List<String> = listOf("Firebase", "ntfy"),
showChangePushProviderDialog: Boolean = false,
eventSink: (NotificationSettingsEvents) -> Unit = {},
) = NotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Valid(
@ -47,10 +57,13 @@ fun aValidNotificationSettingsState( @@ -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( @@ -66,5 +79,8 @@ fun aInvalidNotificationSettingsState(
appNotificationsEnabled = true,
),
changeNotificationSettingAction = AsyncAction.Uninitialized,
currentPushDistributor = AsyncAction.Uninitialized,
availablePushDistributors = persistentListOf(),
showChangePushProviderDialog = false,
eventSink = eventSink,
)

61
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt

@ -16,27 +16,38 @@ @@ -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( @@ -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( @@ -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( @@ -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( @@ -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) },
)
}
}
}

90
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt

@ -21,16 +21,8 @@ import app.cash.molecule.moleculeFlow @@ -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 { @@ -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<Unit> = { _, _, _ ->
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,
)
}

30
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 @@ -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 @@ -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 { @@ -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<AdvancedSettingsEvents>()
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<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView(
state = aAdvancedSettingsState(
eventSink = eventsRecorder,
showChangePushProviderDialog = true,
availablePushDistributors = listOf("P1", "P2")
),
)
rule.onNodeWithText("P2").performClick()
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetPushProvider(1))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAdvancedSettingsView(

90
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt

@ -20,11 +20,19 @@ import app.cash.molecule.RecompositionMode @@ -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 { @@ -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<Unit> = { _, _, _ ->
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(),
)
}

29
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 @@ -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 { @@ -248,6 +250,33 @@ class NotificationSettingsViewTest {
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Push notification provider emits the expected event`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
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<NotificationSettingsEvents>()
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
eventSink = eventsRecorder,
showChangePushProviderDialog = true,
availablePushDistributors = listOf("P1", "P2")
),
)
rule.onNodeWithText("P2").performClick()
eventsRecorder.assertSingle(NotificationSettingsEvents.SetPushProvider(1))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setNotificationSettingsView(

Loading…
Cancel
Save