Merge branch 'develop' into feature/fga/pin_settings

This commit is contained in:
ganfra 2023-10-26 11:13:52 +02:00
commit 5d98f645d2
376 changed files with 6593 additions and 384 deletions

View file

@ -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
}

View file

@ -44,6 +44,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
@ -54,7 +55,7 @@ class PreferencesFlowNode @AssistedInject constructor(
private val lockScreenEntryPoint: LockScreenEntryPoint,
) : BackstackNode<PreferencesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@ -161,8 +162,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)

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 = {}
)

View file

@ -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,

View file

@ -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,
)
}
}

View file

@ -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)
}
}

View file

@ -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,
)

View file

@ -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
}

View file

@ -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,
)
)

View file

@ -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 = {},
)
}

View file

@ -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
)
}
}

View file

@ -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 {