List user define room notification settings

- List user define room notification settings
- Add new user defined style of the room notification settings view
- Add navigation to expose room notification settings ui to the global settings
- Add Progress indicators
- Improve error handing
This commit is contained in:
David Langley 2023-10-17 16:08:08 +01:00
parent a9d87da1ff
commit b634db1772
25 changed files with 375 additions and 70 deletions

View file

@ -42,4 +42,5 @@ class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint
internal fun InitialTarget.toNavTarget() = when (this) {
is InitialTarget.RoomDetails -> NavTarget.RoomDetails
is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId)
is InitialTarget.RoomNotificationSettings -> NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = true)
}

View file

@ -68,7 +68,13 @@ class RoomDetailsFlowNode @AssistedInject constructor(
data object InviteMembers : NavTarget
@Parcelize
object RoomNotificationSettings : NavTarget
data class RoomNotificationSettings(
/**
* When presented from oursite the context of the room, the rooms settings UI is different.
* Figma designs: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=5199-198932&mode=design&t=fTTvpuxYFjewYQOe-0
*/
val showUserDefinedSettingStyle: Boolean
) : NavTarget
@Parcelize
data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
@ -91,7 +97,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
override fun openRoomNotificationSettings() {
backstack.push(NavTarget.RoomNotificationSettings)
backstack.push(NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = false))
}
}
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
@ -118,8 +124,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
createNode<RoomInviteMembersNode>(buildContext)
}
NavTarget.RoomNotificationSettings -> {
createNode<RoomNotificationSettingsNode>(buildContext)
is NavTarget.RoomNotificationSettings -> {
val plugins = listOf(RoomNotificationSettingsNode.RoomNotificationSettingInput(navTarget.showUserDefinedSettingStyle))
createNode<RoomNotificationSettingsNode>(buildContext, plugins)
}
is NavTarget.RoomMemberDetails -> {

View file

@ -21,4 +21,6 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
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
}

View file

@ -26,6 +26,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import io.element.android.services.analytics.api.AnalyticsService
@ -37,6 +39,12 @@ class RoomNotificationSettingsNode @AssistedInject constructor(
private val analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
data class RoomNotificationSettingInput(
val showUserDefinedSettingStyle: Boolean
) : NodeInputs
private val inputs = inputs<RoomNotificationSettingInput>()
init {
lifecycle.subscribe(
onResume = {
@ -48,10 +56,18 @@ class RoomNotificationSettingsNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
RoomNotificationSettingsView(
state = state,
modifier = modifier,
onBackPressed = this::navigateUp,
)
if(inputs.showUserDefinedSettingStyle) {
UserDefinedRoomNotificationSettingsView(
state = state,
modifier = modifier,
onBackPressed = this::navigateUp,
)
} else {
RoomNotificationSettingsView(
state = state,
modifier = modifier,
onBackPressed = this::navigateUp,
)
}
}
}

View file

@ -22,9 +22,12 @@ 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
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
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
@ -42,16 +45,18 @@ class RoomNotificationSettingsPresenter @Inject constructor(
private val room: MatrixRoom,
private val notificationSettingsService: NotificationSettingsService,
) : Presenter<RoomNotificationSettingsState> {
@Composable
override fun present(): RoomNotificationSettingsState {
val defaultRoomNotificationMode: MutableState<RoomNotificationMode?> = rememberSaveable {
mutableStateOf(null)
}
val localCoroutineScope = rememberCoroutineScope()
val changeNotificationSettingAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
val deleteCustomNotificationSettingAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
LaunchedEffect(Unit) {
getDefaultRoomNotificationMode(defaultRoomNotificationMode)
room.updateRoomNotificationSettings()
observeNotificationSettings()
}
@ -60,23 +65,32 @@ class RoomNotificationSettingsPresenter @Inject constructor(
fun handleEvents(event: RoomNotificationSettingsEvents) {
when (event) {
is RoomNotificationSettingsEvents.RoomNotificationModeChanged -> {
localCoroutineScope.setRoomNotificationMode(event.mode)
localCoroutineScope.setRoomNotificationMode(event.mode, changeNotificationSettingAction)
}
is RoomNotificationSettingsEvents.SetNotificationMode -> {
if (event.isDefault) {
localCoroutineScope.restoreDefaultRoomNotificationMode()
localCoroutineScope.restoreDefaultRoomNotificationMode(changeNotificationSettingAction)
} else {
defaultRoomNotificationMode.value?.let {
localCoroutineScope.setRoomNotificationMode(it)
localCoroutineScope.setRoomNotificationMode(it, changeNotificationSettingAction)
}
}
}
is RoomNotificationSettingsEvents.DeleteCustomNotification -> {
localCoroutineScope.restoreDefaultRoomNotificationMode(deleteCustomNotificationSettingAction)
}
RoomNotificationSettingsEvents.ClearError -> {
changeNotificationSettingAction.value = Async.Uninitialized
}
}
}
return RoomNotificationSettingsState(
roomName = room.displayName,
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
defaultRoomNotificationMode = defaultRoomNotificationMode.value,
changeNotificationSettingAction = changeNotificationSettingAction.value,
deleteCustomNotificationSettingAction = deleteCustomNotificationSettingAction.value,
eventSink = ::handleEvents,
)
}
@ -98,11 +112,15 @@ class RoomNotificationSettingsPresenter @Inject constructor(
).getOrThrow()
}
private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode) = launch {
notificationSettingsService.setRoomNotificationMode(room.roomId, mode)
private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode, action: MutableState<Async<Unit>>) = launch {
suspend {
notificationSettingsService.setRoomNotificationMode(room.roomId, mode).getOrThrow()
}.runCatchingUpdatingState(action)
}
private fun CoroutineScope.restoreDefaultRoomNotificationMode() = launch {
notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId)
private fun CoroutineScope.restoreDefaultRoomNotificationMode(action: MutableState<Async<Unit>>) = launch {
suspend {
notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId).getOrThrow()
}.runCatchingUpdatingState(action)
}
}

View file

@ -16,11 +16,15 @@
package io.element.android.features.roomdetails.impl.notificationsettings
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
data class RoomNotificationSettingsState(
val roomName: String,
val roomNotificationSettings: RoomNotificationSettings?,
val defaultRoomNotificationMode: RoomNotificationMode?,
val changeNotificationSettingAction: Async<Unit>,
val deleteCustomNotificationSettingAction: Async<Unit>,
val eventSink: (RoomNotificationSettingsEvents) -> Unit
)

View file

@ -17,6 +17,7 @@
package io.element.android.features.roomdetails.impl.notificationsettings
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
@ -24,10 +25,13 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider<
override val values: Sequence<RoomNotificationSettingsState>
get() = sequenceOf(
RoomNotificationSettingsState(
roomName = "Room 1",
RoomNotificationSettings(
mode = RoomNotificationMode.MUTE,
isDefault = true),
RoomNotificationMode.ALL_MESSAGES,
changeNotificationSettingAction = Async.Uninitialized,
deleteCustomNotificationSettingAction = Async.Uninitialized,
eventSink = { },
),
)

View file

@ -18,7 +18,6 @@ package io.element.android.features.roomdetails.impl.notificationsettings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -30,8 +29,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
@ -45,7 +47,6 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RoomNotificationSettingsView(
state: RoomNotificationSettingsState,
@ -74,7 +75,6 @@ fun RoomNotificationSettingsView(
null -> ""
}
PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) {
PreferenceSwitch(
isChecked = state.roomNotificationSettings?.isDefault.orTrue(),
@ -102,6 +102,16 @@ fun RoomNotificationSettingsView(
)
}
}
when (state.changeNotificationSettingAction) {
is Async.Loading -> {
ProgressDialog()
}
is Async.Failure -> {
ShowChangeNotificationSettingError(state)
}
else -> Unit
}
}
}
}
@ -144,6 +154,15 @@ fun RoomNotificationSettingsOptions(
}
}
@Composable
fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState) {
ErrorDialog(
title = stringResource(CommonStrings.dialog_title_error),
content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode),
onDismiss = { state.eventSink(RoomNotificationSettingsEvents.ClearError) },
)
}
@DayNightPreviews
@Composable
internal fun RoomNotificationSettingsPreview(

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.notificationsettings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.VectorIcons
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@Composable
fun UserDefinedRoomNotificationSettingsView(
state: RoomNotificationSettingsState,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
Scaffold(
modifier = modifier,
topBar = {
UserDefinedRoomNotificationSettingsTopBar(
roomName = state.roomName,
onBackPressed = { onBackPressed() }
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(padding)
.consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
if (state.roomNotificationSettings != null) {
RoomNotificationSettingsOptions(
selected = state.roomNotificationSettings.mode,
enabled = !state.roomNotificationSettings.isDefault,
onOptionSelected = {
state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode))
},
)
}
PreferenceText(
title = stringResource(R.string.screen_room_notification_settings_edit_remove_setting),
icon = ImageVector.vectorResource(VectorIcons.Delete),
tintColor = MaterialTheme.colorScheme.error,
onClick = {
state.eventSink(RoomNotificationSettingsEvents.DeleteCustomNotification)
}
)
when (state.changeNotificationSettingAction) {
is Async.Loading -> {
ProgressDialog()
}
is Async.Failure -> {
ShowChangeNotificationSettingError(state)
}
else -> Unit
}
when (state.deleteCustomNotificationSettingAction) {
is Async.Loading -> {
ProgressDialog()
}
is Async.Failure -> {
ShowChangeNotificationSettingError(state)
}
is Async.Success -> {
LaunchedEffect(state.deleteCustomNotificationSettingAction) {
onBackPressed()
}
}
else -> Unit
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UserDefinedRoomNotificationSettingsTopBar(
roomName: String,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
TopAppBar(
modifier = modifier,
title = {
Text(
text = roomName,
)
},
navigationIcon = { BackButton(onClick = onBackPressed) },
)
}