Merge pull request #1610 from vector-im/dla/feature/custom_room_notification_settings_list
Custom room notification settings list
This commit is contained in:
commit
ef0110d9d7
76 changed files with 1279 additions and 156 deletions
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -16,16 +16,30 @@
|
|||
|
||||
package io.element.android.features.preferences.api
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface PreferencesEntryPoint : FeatureEntryPoint {
|
||||
|
||||
sealed interface InitialTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Root : InitialTarget
|
||||
@Parcelize
|
||||
data object NotificationSettings : InitialTarget
|
||||
}
|
||||
|
||||
data class Params(val initialElement: InitialTarget) : NodeInputs
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
|
@ -33,5 +47,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
|||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
fun onVerifyClicked()
|
||||
fun onOpenRoomNotificationSettings(roomId: RoomId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ class DefaultPreferencesEntryPoint @Inject constructor() : PreferencesEntryPoint
|
|||
return object : PreferencesEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
override fun params(params: PreferencesEntryPoint.Params): PreferencesEntryPoint.NodeBuilder {
|
||||
plugins += params
|
||||
return this
|
||||
}
|
||||
|
||||
override fun callback(callback: PreferencesEntryPoint.Callback): PreferencesEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
|
|
@ -42,3 +47,8 @@ class DefaultPreferencesEntryPoint @Inject constructor() : PreferencesEntryPoint
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PreferencesEntryPoint.InitialTarget.toNavTarget() = when (this) {
|
||||
is PreferencesEntryPoint.InitialTarget.Root -> PreferencesFlowNode.NavTarget.Root
|
||||
is PreferencesEntryPoint.InitialTarget.NotificationSettings -> PreferencesFlowNode.NavTarget.NotificationSettings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.architecture.BackstackNode
|
|||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
) : BackstackNode<PreferencesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -152,8 +153,13 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
createNode<NotificationSettingsNode>(buildContext, listOf(notificationSettingsCallback))
|
||||
}
|
||||
is NavTarget.EditDefaultNotificationSetting -> {
|
||||
val callback = object : EditDefaultNotificationSettingNode.Callback {
|
||||
override fun openRoomNotificationSettings(roomId: RoomId) {
|
||||
plugins<PreferencesEntryPoint.Callback>().forEach { it.onOpenRoomNotificationSettings(roomId) }
|
||||
}
|
||||
}
|
||||
val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne)
|
||||
createNode<EditDefaultNotificationSettingNode>(buildContext, plugins = listOf(input))
|
||||
createNode<EditDefaultNotificationSettingNode>(buildContext, plugins = listOf(input, callback))
|
||||
}
|
||||
NavTarget.AdvancedSettings -> {
|
||||
createNode<AdvancedSettingsNode>(buildContext)
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ sealed interface NotificationSettingsEvents {
|
|||
data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
|
||||
data object FixConfigurationMismatch : NotificationSettingsEvents
|
||||
data object ClearConfigurationMismatchError : NotificationSettingsEvents
|
||||
data object ClearNotificationChangeError : NotificationSettingsEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
|
|
@ -50,6 +52,7 @@ class NotificationSettingsPresenter @Inject constructor(
|
|||
val systemNotificationsEnabled: MutableState<Boolean> = remember {
|
||||
mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled())
|
||||
}
|
||||
val changeNotificationSettingAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val appNotificationsEnabled = userPushStore
|
||||
|
|
@ -67,8 +70,12 @@ class NotificationSettingsPresenter @Inject constructor(
|
|||
|
||||
fun handleEvents(event: NotificationSettingsEvents) {
|
||||
when (event) {
|
||||
is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled)
|
||||
is NotificationSettingsEvents.SetCallNotificationsEnabled -> localCoroutineScope.setCallNotificationsEnabled(event.enabled)
|
||||
is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> {
|
||||
localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled, changeNotificationSettingAction)
|
||||
}
|
||||
is NotificationSettingsEvents.SetCallNotificationsEnabled -> {
|
||||
localCoroutineScope.setCallNotificationsEnabled(event.enabled, changeNotificationSettingAction)
|
||||
}
|
||||
is NotificationSettingsEvents.SetNotificationsEnabled -> localCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled)
|
||||
NotificationSettingsEvents.ClearConfigurationMismatchError -> {
|
||||
matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
|
||||
|
|
@ -77,6 +84,7 @@ class NotificationSettingsPresenter @Inject constructor(
|
|||
NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> {
|
||||
systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled()
|
||||
}
|
||||
NotificationSettingsEvents.ClearNotificationChangeError -> changeNotificationSettingAction.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +94,7 @@ class NotificationSettingsPresenter @Inject constructor(
|
|||
systemNotificationsEnabled = systemNotificationsEnabled.value,
|
||||
appNotificationsEnabled = appNotificationsEnabled.value
|
||||
),
|
||||
changeNotificationSettingAction = changeNotificationSettingAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
@ -154,12 +163,16 @@ class NotificationSettingsPresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean) = launch {
|
||||
notificationSettingsService.setRoomMentionEnabled(enabled)
|
||||
private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean, action: MutableState<Async<Unit>>) = launch {
|
||||
suspend {
|
||||
notificationSettingsService.setRoomMentionEnabled(enabled).getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean) = launch {
|
||||
notificationSettingsService.setCallEnabled(enabled)
|
||||
private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean, action: MutableState<Async<Unit>>) = launch {
|
||||
suspend {
|
||||
notificationSettingsService.setCallEnabled(enabled).getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch {
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@
|
|||
package io.element.android.features.preferences.impl.notifications
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
|
||||
@Immutable
|
||||
data class NotificationSettingsState(
|
||||
val matrixSettings: MatrixSettings,
|
||||
val appSettings: AppSettings,
|
||||
val changeNotificationSettingAction: Async<Unit>,
|
||||
val eventSink: (NotificationSettingsEvents) -> Unit,
|
||||
) {
|
||||
sealed interface MatrixSettings {
|
||||
|
|
|
|||
|
|
@ -17,16 +17,21 @@
|
|||
package io.element.android.features.preferences.impl.notifications
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
|
||||
open class NotificationSettingsStateProvider : PreviewParameterProvider<NotificationSettingsState> {
|
||||
override val values: Sequence<NotificationSettingsState>
|
||||
get() = sequenceOf(
|
||||
aNotificationSettingsState(),
|
||||
aNotificationSettingsState(changeNotificationSettingAction = Async.Loading(Unit)),
|
||||
aNotificationSettingsState(changeNotificationSettingAction = Async.Failure(Throwable("error"))),
|
||||
)
|
||||
}
|
||||
|
||||
fun aNotificationSettingsState() = NotificationSettingsState(
|
||||
fun aNotificationSettingsState(
|
||||
changeNotificationSettingAction: Async<Unit> = Async.Uninitialized,
|
||||
) = NotificationSettingsState(
|
||||
matrixSettings = NotificationSettingsState.MatrixSettings.Valid(
|
||||
atRoomNotificationsEnabled = true,
|
||||
callNotificationsEnabled = true,
|
||||
|
|
@ -37,5 +42,6 @@ fun aNotificationSettingsState() = NotificationSettingsState(
|
|||
systemNotificationsEnabled = false,
|
||||
appNotificationsEnabled = true,
|
||||
),
|
||||
changeNotificationSettingAction = changeNotificationSettingAction,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
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
|
||||
|
|
@ -87,9 +89,23 @@ fun NotificationSettingsView(
|
|||
onGroupChatsClicked = { onOpenEditDefault(false) },
|
||||
onDirectChatsClicked = { onOpenEditDefault(true) },
|
||||
onMentionNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(it)) },
|
||||
// TODO We are removing the call notification toggle until support for call notifications has been added
|
||||
// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
|
||||
)
|
||||
}
|
||||
when (state.changeNotificationSettingAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is Async.Failure -> {
|
||||
ErrorDialog(
|
||||
title = stringResource(CommonStrings.dialog_title_error),
|
||||
content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode),
|
||||
onDismiss = { state.eventSink(NotificationSettingsEvents.ClearNotificationChangeError) },
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +117,7 @@ private fun NotificationSettingsContentView(
|
|||
onGroupChatsClicked: () -> Unit,
|
||||
onDirectChatsClicked: () -> Unit,
|
||||
onMentionNotificationsChanged: (Boolean) -> Unit,
|
||||
// TODO We are removing the call notification toggle until support for call notifications has been added
|
||||
// onCallsNotificationsChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -151,7 +168,7 @@ private fun NotificationSettingsContentView(
|
|||
onCheckedChange = onMentionNotificationsChanged
|
||||
)
|
||||
}
|
||||
// We are removing the call notification toggle until call support has been added
|
||||
// TODO We are removing the call notification toggle until support for call notifications has been added
|
||||
// PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_additional_settings_section_title)) {
|
||||
// PreferenceSwitch(
|
||||
// modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
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.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class EditDefaultNotificationSettingNode @AssistedInject constructor(
|
||||
|
|
@ -35,20 +37,30 @@ class EditDefaultNotificationSettingNode @AssistedInject constructor(
|
|||
presenterFactory: EditDefaultNotificationSettingPresenter.Factory
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun openRoomNotificationSettings(roomId: RoomId)
|
||||
}
|
||||
|
||||
data class Inputs(
|
||||
val isOneToOne: Boolean
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
private val callbacks = plugins<Callback>()
|
||||
private val presenter = presenterFactory.create(inputs.isOneToOne)
|
||||
|
||||
private fun openRoomNotificationSettings(roomId: RoomId) {
|
||||
callbacks.forEach { it.openRoomNotificationSettings(roomId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
EditDefaultNotificationSettingView(
|
||||
state = state,
|
||||
openRoomNotificationSettings = { openRoomNotificationSettings(it) },
|
||||
onBackPressed = ::navigateUp,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,20 +25,28 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
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.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.Collator
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
|
||||
private val notificationSettingsService: NotificationSettingsService,
|
||||
@Assisted private val isOneToOne: Boolean,
|
||||
private val roomListService: RoomListService,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : Presenter<EditDefaultNotificationSettingState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -50,21 +58,34 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
|
|||
val mode: MutableState<RoomNotificationMode?> = remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
|
||||
val changeNotificationSettingAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
|
||||
val roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>> = remember {
|
||||
mutableStateOf(listOf())
|
||||
}
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
fetchSettings(mode)
|
||||
observeNotificationSettings(mode)
|
||||
observeRoomSummaries(roomsWithUserDefinedMode)
|
||||
}
|
||||
|
||||
fun handleEvents(event: EditDefaultNotificationSettingStateEvents) {
|
||||
when (event) {
|
||||
is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> localCoroutineScope.setDefaultNotificationMode(event.mode)
|
||||
is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> {
|
||||
localCoroutineScope.setDefaultNotificationMode(event.mode, changeNotificationSettingAction)
|
||||
}
|
||||
EditDefaultNotificationSettingStateEvents.ClearError -> changeNotificationSettingAction.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
return EditDefaultNotificationSettingState(
|
||||
isOneToOne = isOneToOne,
|
||||
mode = mode.value,
|
||||
roomsWithUserDefinedMode = roomsWithUserDefinedMode.value,
|
||||
changeNotificationSettingAction = changeNotificationSettingAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
@ -83,10 +104,39 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
|
|||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode) = launch {
|
||||
// On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did).
|
||||
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne)
|
||||
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne)
|
||||
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>>) {
|
||||
roomListService.allRooms()
|
||||
.summaries
|
||||
.onEach {
|
||||
updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode)
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.updateRoomsWithUserDefinedMode(
|
||||
summaries: List<RoomSummary>,
|
||||
roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>>
|
||||
) = launch {
|
||||
val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrThrow().toSet()
|
||||
|
||||
val sortedSummaries = summaries
|
||||
.filterIsInstance<RoomSummary.Filled>()
|
||||
.filter {
|
||||
val room = matrixClient.getRoom(it.details.roomId) ?: return@filter false
|
||||
roomWithUserDefinedRules.contains(it.identifier()) && isOneToOne == room.isOneToOne
|
||||
}
|
||||
// locale sensitive sorting
|
||||
.sortedWith(compareBy(Collator.getInstance()){ it.details.name })
|
||||
|
||||
roomsWithUserDefinedMode.value = sortedSummaries
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<Async<Unit>>) = launch {
|
||||
suspend {
|
||||
// On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did).
|
||||
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne).getOrThrow()
|
||||
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne).getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,14 @@
|
|||
|
||||
package io.element.android.features.preferences.impl.notifications.edit
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
|
||||
data class EditDefaultNotificationSettingState(
|
||||
val isOneToOne: Boolean,
|
||||
val mode: RoomNotificationMode?,
|
||||
val roomsWithUserDefinedMode: List<RoomSummary.Filled>,
|
||||
val changeNotificationSettingAction: Async<Unit>,
|
||||
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
|||
|
||||
sealed interface EditDefaultNotificationSettingStateEvents {
|
||||
data class SetNotificationMode(val mode: RoomNotificationMode): EditDefaultNotificationSettingStateEvents
|
||||
data object ClearError: EditDefaultNotificationSettingStateEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.preferences.impl.notifications.edit
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
|
||||
open class EditDefaultNotificationSettingStateProvider: PreviewParameterProvider<EditDefaultNotificationSettingState> {
|
||||
override val values: Sequence<EditDefaultNotificationSettingState>
|
||||
get() = sequenceOf(
|
||||
anEditDefaultNotificationSettingsState(),
|
||||
anEditDefaultNotificationSettingsState(isOneToOne = true),
|
||||
anEditDefaultNotificationSettingsState(changeNotificationSettingAction = Async.Loading(Unit)),
|
||||
anEditDefaultNotificationSettingsState(changeNotificationSettingAction = Async.Failure(Throwable("error"))),
|
||||
)
|
||||
}
|
||||
|
||||
private fun anEditDefaultNotificationSettingsState(
|
||||
isOneToOne: Boolean = false,
|
||||
changeNotificationSettingAction: Async<Unit> = Async.Uninitialized
|
||||
) = EditDefaultNotificationSettingState(
|
||||
isOneToOne = isOneToOne,
|
||||
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||
roomsWithUserDefinedMode = listOf(aRoomSummary()),
|
||||
changeNotificationSettingAction = changeNotificationSettingAction,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
private fun aRoomSummary() = RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
roomId = RoomId("!roomId:domain"),
|
||||
name = "Room",
|
||||
avatarURLString = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||
)
|
||||
)
|
||||
|
|
@ -21,8 +21,21 @@ import androidx.compose.foundation.selection.selectableGroup
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
|
|
@ -33,11 +46,12 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun EditDefaultNotificationSettingView(
|
||||
state: EditDefaultNotificationSettingState,
|
||||
openRoomNotificationSettings:(roomId: RoomId) -> Unit,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
val title = if(state.isOneToOne) {
|
||||
val title = if (state.isOneToOne) {
|
||||
CommonStrings.screen_notification_settings_direct_chats
|
||||
} else {
|
||||
CommonStrings.screen_notification_settings_group_chats
|
||||
|
|
@ -51,7 +65,7 @@ fun EditDefaultNotificationSettingView(
|
|||
// Only ALL_MESSAGES and MENTIONS_AND_KEYWORDS_ONLY are valid global defaults.
|
||||
val validModes = listOf(RoomNotificationMode.ALL_MESSAGES, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||
|
||||
val categoryTitle = if(state.isOneToOne) {
|
||||
val categoryTitle = if (state.isOneToOne) {
|
||||
CommonStrings.screen_notification_settings_edit_screen_direct_section_header
|
||||
} else {
|
||||
CommonStrings.screen_notification_settings_edit_screen_group_section_header
|
||||
|
|
@ -70,6 +84,63 @@ fun EditDefaultNotificationSettingView(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (state.roomsWithUserDefinedMode.isNotEmpty()) {
|
||||
PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_edit_custom_settings_section_title)) {
|
||||
state.roomsWithUserDefinedMode.forEach { summary ->
|
||||
val subtitle = when (summary.details.notificationMode) {
|
||||
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages)
|
||||
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
|
||||
stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords)
|
||||
}
|
||||
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
|
||||
null -> ""
|
||||
}
|
||||
val avatarData = AvatarData(
|
||||
id = summary.identifier(),
|
||||
name = summary.details.name,
|
||||
url = summary.details.avatarURLString,
|
||||
size = AvatarSize.CustomRoomNotificationSetting,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = summary.details.name)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(text = subtitle)
|
||||
},
|
||||
leadingContent = ListItemContent.Custom {
|
||||
Avatar(avatarData = avatarData)
|
||||
},
|
||||
onClick = {
|
||||
openRoomNotificationSettings(summary.details.roomId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
when (state.changeNotificationSettingAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is Async.Failure -> {
|
||||
ErrorDialog(
|
||||
title = stringResource(CommonStrings.dialog_title_error),
|
||||
content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode),
|
||||
onDismiss = { state.eventSink(EditDefaultNotificationSettingStateEvents.ClearError) },
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun EditDefaultNotificationSettingViewPreview(
|
||||
@PreviewParameter(EditDefaultNotificationSettingStateProvider::class) state: EditDefaultNotificationSettingState
|
||||
) = ElementPreview {
|
||||
EditDefaultNotificationSettingView(
|
||||
state = state,
|
||||
openRoomNotificationSettings = {},
|
||||
onBackPressed = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,14 @@ import com.google.common.truth.Truth
|
|||
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter
|
||||
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
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.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -32,7 +39,7 @@ class EditDefaultNotificationSettingsPresenterTests {
|
|||
@Test
|
||||
fun `present - ensures initial state is correct`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false)
|
||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -47,10 +54,32 @@ class EditDefaultNotificationSettingsPresenterTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensure list of rooms with user defined mode`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
val notificationSettingsService = FakeNotificationSettingsService(
|
||||
initialRoomMode = RoomNotificationMode.ALL_MESSAGES,
|
||||
initialRoomModeIsDefault = false
|
||||
)
|
||||
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService).apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService, matrixClient)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
roomListService.postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail(notificationMode = RoomNotificationMode.ALL_MESSAGES))))
|
||||
val loadedState = consumeItemsUntilPredicate { state ->
|
||||
state.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||
}.last()
|
||||
Truth.assertThat(loadedState.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - edit default notification setting`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false)
|
||||
val presenter = createEditDefaultNotificationSettingPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -61,4 +90,39 @@ class EditDefaultNotificationSettingsPresenterTests {
|
|||
Truth.assertThat(loadedState.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - edit default notification setting failed`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
||||
notificationSettingsService.givenSetDefaultNotificationModeError(A_THROWABLE)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES))
|
||||
val errorState = consumeItemsUntilPredicate {
|
||||
it.changeNotificationSettingAction.isFailure()
|
||||
}.last()
|
||||
Truth.assertThat(errorState.changeNotificationSettingAction.isFailure()).isTrue()
|
||||
errorState.eventSink(EditDefaultNotificationSettingStateEvents.ClearError)
|
||||
val clearErrorState = consumeItemsUntilPredicate {
|
||||
it.changeNotificationSettingAction.isUninitialized()
|
||||
}.last()
|
||||
Truth.assertThat(clearErrorState.changeNotificationSettingAction.isUninitialized()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEditDefaultNotificationSettingPresenter(
|
||||
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
|
||||
roomListService: FakeRoomListService = FakeRoomListService(),
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
|
||||
): EditDefaultNotificationSettingPresenter {
|
||||
return EditDefaultNotificationSettingPresenter(
|
||||
notificationSettingsService = notificationSettingsService,
|
||||
isOneToOne = false,
|
||||
roomListService = roomListService,
|
||||
matrixClient = matrixClient
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import app.cash.turbine.test
|
|||
import com.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
||||
import com.google.common.truth.Truth
|
||||
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.tests.testutils.consumeItemsUntilPredicate
|
||||
|
|
@ -187,6 +188,35 @@ class NotificationSettingsPresenterTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - clear notification settings change error`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
|
||||
notificationSettingsService.givenSetAtRoomError(A_THROWABLE)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val loadedState = consumeItemsUntilPredicate {
|
||||
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == false
|
||||
}.last()
|
||||
val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
|
||||
Truth.assertThat(validMatrixState?.atRoomNotificationsEnabled).isFalse()
|
||||
|
||||
loadedState.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(true))
|
||||
val errorState = consumeItemsUntilPredicate {
|
||||
it.changeNotificationSettingAction.isFailure()
|
||||
}.last()
|
||||
Truth.assertThat(errorState.changeNotificationSettingAction.isFailure()).isTrue()
|
||||
errorState.eventSink(NotificationSettingsEvents.ClearNotificationChangeError)
|
||||
|
||||
val clearErrorState = consumeItemsUntilPredicate {
|
||||
it.changeNotificationSettingAction.isUninitialized()
|
||||
}.last()
|
||||
Truth.assertThat(clearErrorState.changeNotificationSettingAction.isUninitialized()).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationSettingsPresenter(
|
||||
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService()
|
||||
) : NotificationSettingsPresenter {
|
||||
|
|
|
|||
|
|
@ -33,9 +33,22 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
|
|||
|
||||
@Parcelize
|
||||
data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomNotificationSettings : InitialTarget
|
||||
}
|
||||
|
||||
data class Inputs(val initialElement: InitialTarget) : NodeInputs
|
||||
data class Params(val initialElement: InitialTarget) : NodeInputs
|
||||
|
||||
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs, plugins: List<Plugin>): Node
|
||||
interface Callback : Plugin {
|
||||
fun onOpenGlobalNotificationSettings()
|
||||
}
|
||||
|
||||
interface NodeBuilder {
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ dependencies {
|
|||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
api(projects.features.roomdetails.api)
|
||||
api(projects.libraries.usersearch.api)
|
||||
api(projects.services.apperror.api)
|
||||
|
|
|
|||
|
|
@ -29,17 +29,30 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
inputs: RoomDetailsEntryPoint.Inputs,
|
||||
plugins: List<Plugin>
|
||||
): Node {
|
||||
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins + inputs)
|
||||
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder {
|
||||
return object : RoomDetailsEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder {
|
||||
plugins += params
|
||||
return this
|
||||
}
|
||||
|
||||
override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun InitialTarget.toNavTarget() = when (this) {
|
||||
is InitialTarget.RoomDetails -> NavTarget.RoomDetails
|
||||
is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId)
|
||||
is InitialTarget.RoomNotificationSettings -> NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.bumble.appyx.core.composable.Children
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -47,7 +48,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
) : BackstackNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Inputs>().first().initialElement.toNavTarget(),
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -68,7 +69,13 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
data object InviteMembers : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomNotificationSettings : NavTarget
|
||||
data class RoomNotificationSettings(
|
||||
/**
|
||||
* When presented from outsite 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 +98,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 +125,14 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
createNode<RoomInviteMembersNode>(buildContext)
|
||||
}
|
||||
|
||||
NavTarget.RoomNotificationSettings -> {
|
||||
createNode<RoomNotificationSettingsNode>(buildContext)
|
||||
is NavTarget.RoomNotificationSettings -> {
|
||||
val input = RoomNotificationSettingsNode.RoomNotificationSettingInput(navTarget.showUserDefinedSettingStyle)
|
||||
val callback = object : RoomNotificationSettingsNode.Callback {
|
||||
override fun openGlobalNotificationSettings() {
|
||||
plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onOpenGlobalNotificationSettings() }
|
||||
}
|
||||
}
|
||||
createNode<RoomNotificationSettingsNode>(buildContext, listOf(input, callback))
|
||||
}
|
||||
|
||||
is NavTarget.RoomMemberDetails -> {
|
||||
|
|
|
|||
|
|
@ -21,4 +21,7 @@ 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 ClearSetNotificationError: RoomNotificationSettingsEvents
|
||||
data object ClearRestoreDefaultError: RoomNotificationSettingsEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,13 @@ import com.bumble.appyx.core.lifecycle.subscribe
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
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
|
||||
|
||||
|
|
@ -33,10 +36,24 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||
class RoomNotificationSettingsNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomNotificationSettingsPresenter,
|
||||
presenterFactory: RoomNotificationSettingsPresenter.Factory,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class RoomNotificationSettingInput(
|
||||
val showUserDefinedSettingStyle: Boolean
|
||||
) : NodeInputs
|
||||
interface Callback : Plugin {
|
||||
fun openGlobalNotificationSettings()
|
||||
}
|
||||
private val inputs = inputs<RoomNotificationSettingInput>()
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
||||
private fun openGlobalNotificationSettings() {
|
||||
callbacks.forEach { it.openGlobalNotificationSettings() }
|
||||
}
|
||||
|
||||
private val presenter = presenterFactory.create(inputs.showUserDefinedSettingStyle)
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
|
|
@ -51,6 +68,7 @@ class RoomNotificationSettingsNode @AssistedInject constructor(
|
|||
RoomNotificationSettingsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onShowGlobalNotifications = this::openGlobalNotificationSettings,
|
||||
onBackPressed = this::navigateUp,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.Column
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
|
||||
@Composable
|
||||
fun RoomNotificationSettingsOptions(
|
||||
selected: RoomNotificationMode?,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {},
|
||||
) {
|
||||
val items = roomNotificationSettingsItems()
|
||||
Column(modifier = modifier.selectableGroup()) {
|
||||
items.forEach { item ->
|
||||
RoomNotificationSettingsOption(
|
||||
roomNotificationSettingsItem = item,
|
||||
isSelected = selected == item.mode,
|
||||
onOptionSelected = onOptionSelected,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,90 +19,176 @@ 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
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
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
|
||||
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
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class RoomNotificationSettingsPresenter @Inject constructor(
|
||||
class RoomNotificationSettingsPresenter @AssistedInject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val notificationSettingsService: NotificationSettingsService,
|
||||
@Assisted private val showUserDefinedSettingStyle: Boolean,
|
||||
) : Presenter<RoomNotificationSettingsState> {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(showUserDefinedSettingStyle: Boolean): RoomNotificationSettingsPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomNotificationSettingsState {
|
||||
val defaultRoomNotificationMode: MutableState<RoomNotificationMode?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val setNotificationSettingAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val restoreDefaultAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
|
||||
val roomNotificationSettings: MutableState<Async<RoomNotificationSettings>> = 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<RoomNotificationMode?> = 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<Boolean?> = remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
getDefaultRoomNotificationMode(defaultRoomNotificationMode)
|
||||
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)
|
||||
localCoroutineScope.setRoomNotificationMode(event.mode, pendingRoomNotificationMode, pendingSetDefault, setNotificationSettingAction)
|
||||
}
|
||||
is RoomNotificationSettingsEvents.SetNotificationMode -> {
|
||||
if (event.isDefault) {
|
||||
localCoroutineScope.restoreDefaultRoomNotificationMode()
|
||||
localCoroutineScope.restoreDefaultRoomNotificationMode(restoreDefaultAction, pendingSetDefault)
|
||||
} else {
|
||||
defaultRoomNotificationMode.value?.let {
|
||||
localCoroutineScope.setRoomNotificationMode(it)
|
||||
localCoroutineScope.setRoomNotificationMode(it, pendingRoomNotificationMode, pendingSetDefault, setNotificationSettingAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
is RoomNotificationSettingsEvents.DeleteCustomNotification -> {
|
||||
localCoroutineScope.restoreDefaultRoomNotificationMode(restoreDefaultAction, pendingSetDefault)
|
||||
}
|
||||
RoomNotificationSettingsEvents.ClearSetNotificationError -> {
|
||||
setNotificationSettingAction.value = Async.Uninitialized
|
||||
}
|
||||
RoomNotificationSettingsEvents.ClearRestoreDefaultError -> {
|
||||
restoreDefaultAction.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RoomNotificationSettingsState(
|
||||
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
|
||||
showUserDefinedSettingStyle = showUserDefinedSettingStyle,
|
||||
roomName = room.displayName,
|
||||
roomNotificationSettings = roomNotificationSettings.value,
|
||||
pendingRoomNotificationMode = pendingRoomNotificationMode.value,
|
||||
pendingSetDefault = pendingSetDefault.value,
|
||||
defaultRoomNotificationMode = defaultRoomNotificationMode.value,
|
||||
setNotificationSettingAction = setNotificationSettingAction.value,
|
||||
restoreDefaultAction = restoreDefaultAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun CoroutineScope.observeNotificationSettings() {
|
||||
private fun CoroutineScope.observeNotificationSettings(
|
||||
pendingModeState: MutableState<RoomNotificationMode?>,
|
||||
roomNotificationSettings: MutableState<Async<RoomNotificationSettings>>
|
||||
) {
|
||||
notificationSettingsService.notificationSettingsChangeFlow
|
||||
.debounce(0.5.seconds)
|
||||
.onEach {
|
||||
room.updateRoomNotificationSettings()
|
||||
fetchNotificationSettings(pendingModeState, roomNotificationSettings)
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.getDefaultRoomNotificationMode(defaultRoomNotificationMode: MutableState<RoomNotificationMode?>) = launch {
|
||||
private fun CoroutineScope.fetchNotificationSettings(
|
||||
pendingModeState: MutableState<RoomNotificationMode?>,
|
||||
roomNotificationSettings: MutableState<Async<RoomNotificationSettings>>
|
||||
) = launch {
|
||||
suspend {
|
||||
pendingModeState.value = null
|
||||
notificationSettingsService.getRoomNotificationSettings(room.roomId, room.isEncrypted, room.isOneToOne).getOrThrow()
|
||||
}.runCatchingUpdatingState(roomNotificationSettings)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.getDefaultRoomNotificationMode(
|
||||
defaultRoomNotificationMode: MutableState<RoomNotificationMode?>
|
||||
) = launch {
|
||||
defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode(
|
||||
room.isEncrypted,
|
||||
room.isOneToOne
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode) = launch {
|
||||
notificationSettingsService.setRoomNotificationMode(room.roomId, mode)
|
||||
private fun CoroutineScope.setRoomNotificationMode(
|
||||
mode: RoomNotificationMode,
|
||||
pendingModeState: MutableState<RoomNotificationMode?>,
|
||||
pendingDefaultState: MutableState<Boolean?>,
|
||||
action: MutableState<Async<Unit>>
|
||||
) = launch {
|
||||
suspend {
|
||||
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() = launch {
|
||||
notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId)
|
||||
private fun CoroutineScope.restoreDefaultRoomNotificationMode(
|
||||
action: MutableState<Async<Unit>>,
|
||||
pendingDefaultState: MutableState<Boolean?>
|
||||
) = launch {
|
||||
suspend {
|
||||
pendingDefaultState.value = true
|
||||
val result = notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId)
|
||||
if (result.isFailure) {
|
||||
pendingDefaultState.value = null
|
||||
}
|
||||
result.getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,26 @@
|
|||
|
||||
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 roomNotificationSettings: RoomNotificationSettings?,
|
||||
val showUserDefinedSettingStyle: Boolean,
|
||||
val roomName: String,
|
||||
val roomNotificationSettings: Async<RoomNotificationSettings>,
|
||||
val pendingRoomNotificationMode: RoomNotificationMode?,
|
||||
val pendingSetDefault: Boolean?,
|
||||
val defaultRoomNotificationMode: RoomNotificationMode?,
|
||||
val setNotificationSettingAction: Async<Unit>,
|
||||
val restoreDefaultAction: Async<Unit>,
|
||||
val eventSink: (RoomNotificationSettingsEvents) -> Unit
|
||||
)
|
||||
|
||||
val RoomNotificationSettingsState.displayNotificationMode: RoomNotificationMode? get() {
|
||||
return pendingRoomNotificationMode ?: roomNotificationSettings.dataOrNull()?.mode
|
||||
}
|
||||
|
||||
val RoomNotificationSettingsState.displayIsDefault: Boolean? get() {
|
||||
return pendingSetDefault ?: roomNotificationSettings.dataOrNull()?.isDefault
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,18 +17,38 @@
|
|||
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
|
||||
|
||||
internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider<RoomNotificationSettingsState> {
|
||||
override val values: Sequence<RoomNotificationSettingsState>
|
||||
get() = sequenceOf(
|
||||
RoomNotificationSettingsState(
|
||||
RoomNotificationSettings(
|
||||
mode = RoomNotificationMode.MUTE,
|
||||
isDefault = true),
|
||||
RoomNotificationMode.ALL_MESSAGES,
|
||||
eventSink = { },
|
||||
),
|
||||
aRoomNotificationSettingsState(),
|
||||
aRoomNotificationSettingsState(isDefault = false),
|
||||
aRoomNotificationSettingsState(setNotificationSettingAction = Async.Loading(Unit)),
|
||||
aRoomNotificationSettingsState(setNotificationSettingAction = Async.Failure(Throwable("error"))),
|
||||
aRoomNotificationSettingsState(restoreDefaultAction = Async.Loading(Unit)),
|
||||
aRoomNotificationSettingsState(restoreDefaultAction = Async.Failure(Throwable("error"))),
|
||||
)
|
||||
|
||||
private fun aRoomNotificationSettingsState(
|
||||
isDefault: Boolean = true,
|
||||
setNotificationSettingAction: Async<Unit> = Async.Uninitialized,
|
||||
restoreDefaultAction: Async<Unit> = Async.Uninitialized,
|
||||
): RoomNotificationSettingsState {
|
||||
return RoomNotificationSettingsState(
|
||||
showUserDefinedSettingStyle = false,
|
||||
roomName = "Room 1",
|
||||
Async.Success(RoomNotificationSettings(
|
||||
mode = RoomNotificationMode.MUTE,
|
||||
isDefault = isDefault)),
|
||||
pendingRoomNotificationMode = null,
|
||||
pendingSetDefault = null,
|
||||
defaultRoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||
setNotificationSettingAction = setNotificationSettingAction,
|
||||
restoreDefaultAction = restoreDefaultAction,
|
||||
eventSink = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,25 +18,29 @@ 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
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.preferences.PreferenceCategory
|
||||
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.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -45,11 +49,35 @@ 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,
|
||||
modifier: Modifier = Modifier,
|
||||
onShowGlobalNotifications: () -> Unit = {},
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
if(state.showUserDefinedSettingStyle) {
|
||||
UserDefinedRoomNotificationSettingsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = onBackPressed,
|
||||
)
|
||||
} else {
|
||||
RoomSpecificNotificationSettingsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onShowGlobalNotifications = onShowGlobalNotifications,
|
||||
onBackPressed = onBackPressed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomSpecificNotificationSettingsView(
|
||||
state: RoomNotificationSettingsState,
|
||||
modifier: Modifier = Modifier,
|
||||
onShowGlobalNotifications: () -> Unit = {},
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -67,41 +95,84 @@ fun RoomNotificationSettingsView(
|
|||
.consumeWindowInsets(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
val subtitle = when (state.defaultRoomNotificationMode) {
|
||||
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_room_notification_settings_mode_all_messages)
|
||||
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = R.string.screen_room_notification_settings_mode_mentions_and_keywords)
|
||||
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
|
||||
null -> ""
|
||||
}
|
||||
|
||||
|
||||
PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) {
|
||||
PreferenceSwitch(
|
||||
isChecked = state.roomNotificationSettings?.isDefault.orTrue(),
|
||||
onCheckedChange = {
|
||||
state.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(it))
|
||||
},
|
||||
title = "Match default setting",
|
||||
subtitle = subtitle,
|
||||
enabled = state.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,
|
||||
)
|
||||
|
||||
if (state.roomNotificationSettings != null) {
|
||||
val roomNotificationSettings = state.roomNotificationSettings.dataOrNull()
|
||||
PreferenceSwitch(
|
||||
isChecked = !state.displayIsDefault.orTrue(),
|
||||
onCheckedChange = {
|
||||
state.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(!it))
|
||||
},
|
||||
title = stringResource(id = R.string.screen_room_notification_settings_allow_custom),
|
||||
subtitle = stringResource(id = R.string.screen_room_notification_settings_allow_custom_footnote),
|
||||
enabled = roomNotificationSettings != null
|
||||
)
|
||||
if (state.displayIsDefault.orTrue()) {
|
||||
PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_default_setting_title)) {
|
||||
val text = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_room_notification_settings_default_setting_footnote,
|
||||
R.string.screen_room_notification_settings_default_setting_footnote_content_link,
|
||||
color = Color.Unspecified,
|
||||
underline = false,
|
||||
bold = true,
|
||||
)
|
||||
ClickableText(
|
||||
text = text,
|
||||
onClick = {
|
||||
onShowGlobalNotifications()
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular
|
||||
.copy(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
)
|
||||
if(state.defaultRoomNotificationMode != null){
|
||||
val defaultModeTitle = when (state.defaultRoomNotificationMode) {
|
||||
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_room_notification_settings_mode_all_messages)
|
||||
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
|
||||
stringResource(id = R.string.screen_room_notification_settings_mode_mentions_and_keywords)
|
||||
}
|
||||
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
|
||||
}
|
||||
RoomNotificationSettingsOption(
|
||||
roomNotificationSettingsItem = RoomNotificationSettingsItem(state.defaultRoomNotificationMode, defaultModeTitle),
|
||||
isSelected = true,
|
||||
onOptionSelected = { },
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) {
|
||||
RoomNotificationSettingsOptions(
|
||||
selected = state.roomNotificationSettings.mode,
|
||||
enabled = !state.roomNotificationSettings.isDefault,
|
||||
selected = state.displayNotificationMode,
|
||||
enabled = !state.displayIsDefault.orTrue(),
|
||||
onOptionSelected = {
|
||||
state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode))
|
||||
},
|
||||
)
|
||||
},)
|
||||
}
|
||||
}
|
||||
|
||||
when (state.setNotificationSettingAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is Async.Failure -> {
|
||||
ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearSetNotificationError)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
when (state.restoreDefaultAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is Async.Failure -> {
|
||||
ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearRestoreDefaultError)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,26 +195,6 @@ private fun RoomNotificationSettingsTopBar(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomNotificationSettingsOptions(
|
||||
selected: RoomNotificationMode?,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {},
|
||||
) {
|
||||
val items = roomNotificationSettingsItems()
|
||||
Column(modifier = modifier.selectableGroup()) {
|
||||
items.forEach { item ->
|
||||
RoomNotificationSettingsOption(
|
||||
roomNotificationSettingsItem = item,
|
||||
isSelected = selected == item.mode,
|
||||
onOptionSelected = onOptionSelected,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomNotificationSettingsPreview(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
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(event) },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.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
|
||||
|
||||
internal class UserDefinedRoomNotificationSettingsStateProvider : PreviewParameterProvider<RoomNotificationSettingsState> {
|
||||
override val values: Sequence<RoomNotificationSettingsState>
|
||||
get() = sequenceOf(
|
||||
RoomNotificationSettingsState(
|
||||
showUserDefinedSettingStyle = false,
|
||||
roomName = "Room 1",
|
||||
Async.Success(
|
||||
RoomNotificationSettings(
|
||||
mode = RoomNotificationMode.MUTE,
|
||||
isDefault = false)
|
||||
),
|
||||
pendingRoomNotificationMode = null,
|
||||
pendingSetDefault = null,
|
||||
defaultRoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||
setNotificationSettingAction = Async.Uninitialized,
|
||||
restoreDefaultAction = Async.Uninitialized,
|
||||
eventSink = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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.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.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.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@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),
|
||||
) {
|
||||
val roomNotificationSettings = state.roomNotificationSettings.dataOrNull()
|
||||
if (roomNotificationSettings != null && state.displayNotificationMode != null) {
|
||||
RoomNotificationSettingsOptions(
|
||||
selected = state.displayNotificationMode,
|
||||
enabled = !state.displayIsDefault.orTrue(),
|
||||
onOptionSelected = {
|
||||
state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_room_notification_settings_edit_remove_setting),
|
||||
icon = ImageVector.vectorResource(CommonDrawables.ic_compound_delete),
|
||||
tintColor = MaterialTheme.colorScheme.error,
|
||||
onClick = {
|
||||
state.eventSink(RoomNotificationSettingsEvents.DeleteCustomNotification)
|
||||
}
|
||||
)
|
||||
|
||||
when (state.setNotificationSettingAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is Async.Failure -> {
|
||||
ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearSetNotificationError)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
when (state.restoreDefaultAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog()
|
||||
}
|
||||
is Async.Failure -> {
|
||||
ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearRestoreDefaultError)
|
||||
}
|
||||
is Async.Success -> {
|
||||
LaunchedEffect(state.restoreDefaultAction) {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun UserDefinedRoomNotificationSettingsTopBar(
|
||||
roomName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = roomName,
|
||||
)
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = onBackPressed) },
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UserDefinedRoomNotificationSettingsPreview(
|
||||
@PreviewParameter(UserDefinedRoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState
|
||||
) = ElementPreview {
|
||||
UserDefinedRoomNotificationSettingsView(state)
|
||||
}
|
||||
|
|
@ -24,6 +24,9 @@ import io.element.android.features.roomdetails.aMatrixRoom
|
|||
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsEvents
|
||||
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsPresenter
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -31,12 +34,12 @@ import org.junit.Test
|
|||
class RoomNotificationSettingsPresenterTests {
|
||||
@Test
|
||||
fun `present - initial state is created from room info`() = runTest {
|
||||
val presenter = aNotificationPresenter
|
||||
val presenter = createRoomNotificationSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.roomNotificationSettings).isNull()
|
||||
Truth.assertThat(initialState.roomNotificationSettings.dataOrNull()).isNull()
|
||||
Truth.assertThat(initialState.defaultRoomNotificationMode).isNull()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
|
@ -44,21 +47,80 @@ class RoomNotificationSettingsPresenterTests {
|
|||
|
||||
@Test
|
||||
fun `present - notification mode changed`() = runTest {
|
||||
val presenter = aNotificationPresenter
|
||||
val presenter = createRoomNotificationSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY))
|
||||
val updatedState = consumeItemsUntilPredicate {
|
||||
it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||
it.roomNotificationSettings.dataOrNull()?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||
}.last()
|
||||
Truth.assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||
Truth.assertThat(updatedState.roomNotificationSettings.dataOrNull()?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - observe notification mode changed`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||
val updatedState = consumeItemsUntilPredicate {
|
||||
it.roomNotificationSettings.dataOrNull()?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||
}.last()
|
||||
Truth.assertThat(updatedState.roomNotificationSettings.dataOrNull()?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - notification settings set custom failed`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
notificationSettingsService.givenSetNotificationModeError(A_THROWABLE)
|
||||
val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(false))
|
||||
val failedState = consumeItemsUntilPredicate {
|
||||
it.setNotificationSettingAction.isFailure()
|
||||
}.last()
|
||||
|
||||
Truth.assertThat(failedState.roomNotificationSettings.dataOrNull()?.isDefault).isTrue()
|
||||
Truth.assertThat(failedState.pendingSetDefault).isNull()
|
||||
Truth.assertThat(failedState.setNotificationSettingAction.isFailure()).isTrue()
|
||||
|
||||
failedState.eventSink(RoomNotificationSettingsEvents.ClearSetNotificationError)
|
||||
|
||||
val errorClearedState = consumeItemsUntilPredicate {
|
||||
it.setNotificationSettingAction.isUninitialized()
|
||||
}.last()
|
||||
Truth.assertThat(errorClearedState.setNotificationSettingAction.isUninitialized()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - notification settings set custom`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(false))
|
||||
val defaultState = consumeItemsUntilPredicate {
|
||||
it.roomNotificationSettings.dataOrNull()?.isDefault == false
|
||||
}.last()
|
||||
Truth.assertThat(defaultState.roomNotificationSettings.dataOrNull()?.isDefault).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - notification settings restore default`() = runTest {
|
||||
val presenter = aNotificationPresenter
|
||||
val presenter = createRoomNotificationSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -66,17 +128,45 @@ class RoomNotificationSettingsPresenterTests {
|
|||
initialState.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY))
|
||||
initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(true))
|
||||
val defaultState = consumeItemsUntilPredicate {
|
||||
it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||
it.roomNotificationSettings.dataOrNull()?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||
}.last()
|
||||
Truth.assertThat(defaultState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||
Truth.assertThat(defaultState.roomNotificationSettings.dataOrNull()?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private val aNotificationPresenter: RoomNotificationSettingsPresenter get() {
|
||||
val room = aMatrixRoom()
|
||||
@Test
|
||||
fun `present - notification settings restore default failed`() = runTest {
|
||||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
notificationSettingsService.givenRestoreDefaultNotificationModeError(A_THROWABLE)
|
||||
val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY))
|
||||
initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(true))
|
||||
val failedState = consumeItemsUntilPredicate {
|
||||
it.restoreDefaultAction.isFailure()
|
||||
}.last()
|
||||
Truth.assertThat(failedState.restoreDefaultAction.isFailure()).isTrue()
|
||||
failedState.eventSink(RoomNotificationSettingsEvents.ClearRestoreDefaultError)
|
||||
|
||||
val errorClearedState = consumeItemsUntilPredicate {
|
||||
it.restoreDefaultAction.isUninitialized()
|
||||
}.last()
|
||||
Truth.assertThat(errorClearedState.restoreDefaultAction.isUninitialized()).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
private fun createRoomNotificationSettingsPresenter(
|
||||
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService()
|
||||
): RoomNotificationSettingsPresenter{
|
||||
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
|
||||
return RoomNotificationSettingsPresenter(
|
||||
room = room,
|
||||
notificationSettingsService = room.notificationSettingsService
|
||||
notificationSettingsService = notificationSettingsService,
|
||||
showUserDefinedSettingStyle = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue