diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt index c69896a98b..8b3c25d267 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt @@ -22,5 +22,6 @@ sealed interface RoomNotificationSettingsEvents { data class RoomNotificationModeChanged(val mode: RoomNotificationMode) : RoomNotificationSettingsEvents data class SetNotificationMode(val isDefault: Boolean): RoomNotificationSettingsEvents data object DeleteCustomNotification: RoomNotificationSettingsEvents - data object ClearError: RoomNotificationSettingsEvents + data object ClearSetNotificationError: RoomNotificationSettingsEvents + data object ClearRestoreDefaultError: RoomNotificationSettingsEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index 9fb0c8b1c8..3086d878c4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -19,8 +19,6 @@ package io.element.android.features.roomdetails.impl.notificationsettings 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.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -31,7 +29,7 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce @@ -51,76 +49,136 @@ class RoomNotificationSettingsPresenter @Inject constructor( mutableStateOf(null) } val localCoroutineScope = rememberCoroutineScope() - val changeNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } - val deleteCustomNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val setNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val restoreDefaultAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + + val roomNotificationSettings: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + // We store state of which mode the user has set via the notification service before the new push settings have been updated. + // We show this state immediately to the user and debounce updates to notification settings to hide some invalid states returned + // by the rust sdk during these two events that cause the radio buttons ot toggle quickly back and forth. + // This is a client side work-around until bulk push rule updates are supported. + // ref: https://github.com/matrix-org/matrix-spec-proposals/pull/3934 + val pendingRoomNotificationMode: MutableState = remember { + mutableStateOf(null) + } + + // We store state of whether the user has set the notifications settings to default or custom via the notification service. + // We show this state immediately to the user and debounce updates to notification settings to hide some invalid states returned + // by the rust sdk during these two events that cause the switch ot toggle quickly back and forth. + // This is a client side work-around until bulk push rule updates are supported. + // ref: https://github.com/matrix-org/matrix-spec-proposals/pull/3934 + val pendingSetDefault: MutableState = remember { + mutableStateOf(null) + } LaunchedEffect(Unit) { getDefaultRoomNotificationMode(defaultRoomNotificationMode) - room.updateRoomNotificationSettings() - observeNotificationSettings() + fetchNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) + observeNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) } - val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() - fun handleEvents(event: RoomNotificationSettingsEvents) { when (event) { is RoomNotificationSettingsEvents.RoomNotificationModeChanged -> { - localCoroutineScope.setRoomNotificationMode(event.mode, changeNotificationSettingAction) + localCoroutineScope.setRoomNotificationMode(event.mode, pendingRoomNotificationMode, pendingSetDefault, setNotificationSettingAction) } is RoomNotificationSettingsEvents.SetNotificationMode -> { if (event.isDefault) { - localCoroutineScope.restoreDefaultRoomNotificationMode(changeNotificationSettingAction) + localCoroutineScope.restoreDefaultRoomNotificationMode(restoreDefaultAction, pendingSetDefault) } else { defaultRoomNotificationMode.value?.let { - localCoroutineScope.setRoomNotificationMode(it, changeNotificationSettingAction) + localCoroutineScope.setRoomNotificationMode(it, pendingRoomNotificationMode, pendingSetDefault, setNotificationSettingAction) } } } is RoomNotificationSettingsEvents.DeleteCustomNotification -> { - localCoroutineScope.restoreDefaultRoomNotificationMode(deleteCustomNotificationSettingAction) + localCoroutineScope.restoreDefaultRoomNotificationMode(restoreDefaultAction, pendingSetDefault) } - RoomNotificationSettingsEvents.ClearError -> { - changeNotificationSettingAction.value = Async.Uninitialized + RoomNotificationSettingsEvents.ClearSetNotificationError -> { + setNotificationSettingAction.value = Async.Uninitialized + } + RoomNotificationSettingsEvents.ClearRestoreDefaultError -> { + restoreDefaultAction.value = Async.Uninitialized } } } return RoomNotificationSettingsState( roomName = room.displayName, - roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), + roomNotificationSettings = roomNotificationSettings.value, + pendingRoomNotificationMode = pendingRoomNotificationMode.value, + pendingSetDefault = pendingSetDefault.value, defaultRoomNotificationMode = defaultRoomNotificationMode.value, - changeNotificationSettingAction = changeNotificationSettingAction.value, - deleteCustomNotificationSettingAction = deleteCustomNotificationSettingAction.value, + setNotificationSettingAction = setNotificationSettingAction.value, + restoreDefaultAction = restoreDefaultAction.value, eventSink = ::handleEvents, ) } @OptIn(FlowPreview::class) - private fun CoroutineScope.observeNotificationSettings() { + private fun CoroutineScope.observeNotificationSettings( + pendingModeState: MutableState, + roomNotificationSettings: MutableState> + ) { notificationSettingsService.notificationSettingsChangeFlow .debounce(0.5.seconds) .onEach { - room.updateRoomNotificationSettings() + fetchNotificationSettings(pendingModeState, roomNotificationSettings) } .launchIn(this) } - private fun CoroutineScope.getDefaultRoomNotificationMode(defaultRoomNotificationMode: MutableState) = launch { + private fun CoroutineScope.fetchNotificationSettings( + pendingModeState: MutableState, + roomNotificationSettings: MutableState> + ) = launch { + suspend { + pendingModeState.value = null + notificationSettingsService.getRoomNotificationSettings(room.roomId, room.isEncrypted, room.isOneToOne).getOrThrow() + }.runCatchingUpdatingState(roomNotificationSettings) + } + + private fun CoroutineScope.getDefaultRoomNotificationMode( + defaultRoomNotificationMode: MutableState + ) = launch { defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( room.isEncrypted, room.isOneToOne ).getOrThrow() } - private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode, action: MutableState>) = launch { + private fun CoroutineScope.setRoomNotificationMode( + mode: RoomNotificationMode, + pendingModeState: MutableState, + pendingDefaultState: MutableState, + action: MutableState> + ) = launch { suspend { - notificationSettingsService.setRoomNotificationMode(room.roomId, mode).getOrThrow() + pendingModeState.value = mode + pendingDefaultState.value = false + val result = notificationSettingsService.setRoomNotificationMode(room.roomId, mode) + if (result.isFailure) { + pendingModeState.value = null + pendingDefaultState.value = null + } + result.getOrThrow() }.runCatchingUpdatingState(action) } - private fun CoroutineScope.restoreDefaultRoomNotificationMode(action: MutableState>) = launch { + private fun CoroutineScope.restoreDefaultRoomNotificationMode( + action: MutableState>, + pendingDefaultState: MutableState + ) = launch { suspend { - notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId).getOrThrow() + pendingDefaultState.value = true + val result = notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId) + if (result.isFailure) { + pendingDefaultState.value = null + } + result.getOrThrow() }.runCatchingUpdatingState(action) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt index a7c5c3b883..1f8c7e4ce8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt @@ -22,9 +22,19 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings data class RoomNotificationSettingsState( val roomName: String, - val roomNotificationSettings: RoomNotificationSettings?, + val roomNotificationSettings: Async, + val pendingRoomNotificationMode: RoomNotificationMode?, + val pendingSetDefault: Boolean?, val defaultRoomNotificationMode: RoomNotificationMode?, - val changeNotificationSettingAction: Async, - val deleteCustomNotificationSettingAction: Async, + val setNotificationSettingAction: Async, + val restoreDefaultAction: Async, val eventSink: (RoomNotificationSettingsEvents) -> Unit ) + +val RoomNotificationSettingsState.displayNotificationMode: RoomNotificationMode? get() { + return pendingRoomNotificationMode ?: roomNotificationSettings.dataOrNull()?.mode +} + +val RoomNotificationSettingsState.displayIsDefault: Boolean? get() { + return pendingSetDefault ?: roomNotificationSettings.dataOrNull()?.isDefault +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt index 220d82f6b5..961909f933 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -26,12 +26,14 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider< get() = sequenceOf( RoomNotificationSettingsState( roomName = "Room 1", - RoomNotificationSettings( + Async.Success(RoomNotificationSettings( mode = RoomNotificationMode.MUTE, - isDefault = true), - RoomNotificationMode.ALL_MESSAGES, - changeNotificationSettingAction = Async.Uninitialized, - deleteCustomNotificationSettingAction = Async.Uninitialized, + isDefault = true)), + pendingRoomNotificationMode = null, + pendingSetDefault = null, + defaultRoomNotificationMode = RoomNotificationMode.ALL_MESSAGES, + setNotificationSettingAction = Async.Uninitialized, + restoreDefaultAction = Async.Uninitialized, eventSink = { }, ), ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index 92bfe0c084..6f440958b2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -75,27 +75,29 @@ fun RoomNotificationSettingsView( null -> "" } + val roomNotificationSettings = state.roomNotificationSettings.dataOrNull() + PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) { PreferenceSwitch( - isChecked = state.roomNotificationSettings?.isDefault.orTrue(), + isChecked = state.displayIsDefault.orTrue(), onCheckedChange = { state.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(it)) }, title = "Match default setting", subtitle = subtitle, - enabled = state.roomNotificationSettings != null + enabled = roomNotificationSettings != null ) PreferenceText( title = stringResource(id = R.string.screen_room_notification_settings_allow_custom), subtitle = stringResource(id = R.string.screen_room_notification_settings_allow_custom_footnote), - enabled = state.roomNotificationSettings != null && !state.roomNotificationSettings.isDefault, + enabled = !state.displayIsDefault.orTrue(), ) - if (state.roomNotificationSettings != null) { + if (roomNotificationSettings != null && state.displayNotificationMode != null) { RoomNotificationSettingsOptions( - selected = state.roomNotificationSettings.mode, - enabled = !state.roomNotificationSettings.isDefault, + selected = state.displayNotificationMode, + enabled = !state.displayIsDefault.orTrue(), onOptionSelected = { state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) }, @@ -103,12 +105,22 @@ fun RoomNotificationSettingsView( } } - when (state.changeNotificationSettingAction) { + when (state.setNotificationSettingAction) { is Async.Loading -> { ProgressDialog() } is Async.Failure -> { - ShowChangeNotificationSettingError(state) + ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearSetNotificationError) + } + else -> Unit + } + + when (state.restoreDefaultAction) { + is Async.Loading -> { + ProgressDialog() + } + is Async.Failure -> { + ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearRestoreDefaultError) } else -> Unit } @@ -155,11 +167,11 @@ fun RoomNotificationSettingsOptions( } @Composable -fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState) { +fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState, event: RoomNotificationSettingsEvents) { ErrorDialog( title = stringResource(CommonStrings.dialog_title_error), content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode), - onDismiss = { state.eventSink(RoomNotificationSettingsEvents.ClearError) }, + onDismiss = { state.eventSink(event) }, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt index 6435e64497..aa8a2a739d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt @@ -62,10 +62,11 @@ fun UserDefinedRoomNotificationSettingsView( .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - if (state.roomNotificationSettings != null) { + val roomNotificationSettings = state.roomNotificationSettings.dataOrNull() + if (roomNotificationSettings != null && state.displayNotificationMode != null) { RoomNotificationSettingsOptions( - selected = state.roomNotificationSettings.mode, - enabled = !state.roomNotificationSettings.isDefault, + selected = state.displayNotificationMode, + enabled = roomNotificationSettings.isDefault, onOptionSelected = { state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) }, @@ -81,7 +82,7 @@ fun UserDefinedRoomNotificationSettingsView( } ) - when (state.changeNotificationSettingAction) { + when (state.setNotificationSettingAction) { is Async.Loading -> { ProgressDialog() } @@ -91,7 +92,7 @@ fun UserDefinedRoomNotificationSettingsView( else -> Unit } - when (state.deleteCustomNotificationSettingAction) { + when (state.restoreDefaultAction) { is Async.Loading -> { ProgressDialog() } @@ -99,7 +100,7 @@ fun UserDefinedRoomNotificationSettingsView( ShowChangeNotificationSettingError(state) } is Async.Success -> { - LaunchedEffect(state.deleteCustomNotificationSettingAction) { + LaunchedEffect(state.restoreDefaultAction) { onBackPressed() } }