From 4a5a01d7100a209314df84eeeb9bc84a2712c45f Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Thu, 7 Sep 2023 08:24:34 +0000 Subject: [PATCH] Add room notification settings (#807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add room notification settings --------- Co-authored-by: ElementBot Co-authored-by: Jorge Martín Co-authored-by: Benoit Marty Co-authored-by: David Langley --- changelog.d/506.feature | 2 + .../messages/impl/MessagesPresenter.kt | 13 +- .../messages/MessagesPresenterTest.kt | 12 +- features/roomdetails/impl/build.gradle.kts | 2 + .../roomdetails/impl/RoomDetailsEvent.kt | 2 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 12 ++ .../roomdetails/impl/RoomDetailsNode.kt | 6 + .../roomdetails/impl/RoomDetailsPresenter.kt | 46 ++++- .../roomdetails/impl/RoomDetailsState.kt | 3 + .../impl/RoomDetailsStateProvider.kt | 4 + .../roomdetails/impl/RoomDetailsView.kt | 53 +++++- .../RoomNotificationSettingsEvents.kt | 24 +++ .../RoomNotificationSettingsItem.kt | 52 ++++++ .../RoomNotificationSettingsNode.kt | 57 +++++++ .../RoomNotificationSettingsOption.kt | 97 +++++++++++ .../RoomNotificationSettingsPresenter.kt | 108 ++++++++++++ .../RoomNotificationSettingsState.kt | 26 +++ .../RoomNotificationSettingsStateProvider.kt | 34 ++++ .../RoomNotificationSettingsView.kt | 161 ++++++++++++++++++ .../src/main/res/values-fr/translations.xml | 10 ++ .../src/main/res/values-ru/translations.xml | 11 ++ .../src/main/res/values-sk/translations.xml | 11 ++ .../main/res/values-zh-rTW/translations.xml | 6 + .../impl/src/main/res/values/localazy.xml | 11 ++ .../roomdetails/RoomDetailsPresenterTests.kt | 127 ++++++++++++-- .../RoomNotificationSettingsPresenterTests.kt | 83 +++++++++ .../components/preferences/PreferenceText.kt | 19 ++- .../libraries/featureflag/api/FeatureFlags.kt | 6 + .../impl/BuildtimeFeatureFlagProvider.kt | 1 + .../libraries/matrix/api/MatrixClient.kt | 2 + .../NotificationSettingsService.kt | 36 ++++ .../libraries/matrix/api/room/MatrixRoom.kt | 4 + .../MatrixRoomNotificationSettingsState.kt | 33 ++++ .../api/room/RoomNotificationSettings.kt | 26 +++ .../libraries/matrix/impl/RustMatrixClient.kt | 10 ++ .../matrix/impl/di/SessionMatrixModule.kt | 6 + .../RoomNotificationSettingsMapper.kt | 44 +++++ .../RustNotificationSettingsService.kt | 86 ++++++++++ .../matrix/impl/room/RustMatrixRoom.kt | 24 +++ .../libraries/matrix/test/FakeMatrixClient.kt | 4 + .../android/libraries/matrix/test/TestData.kt | 4 + .../FakeNotificationSettingsService.kt | 64 +++++++ .../matrix/test/room/FakeMatrixRoom.kt | 13 ++ .../src/main/res/values/localazy.xml | 5 + ...nSettingsDark_0_null_0,NEXUS_5,1.0,en].png | 3 + ...SettingsLight_0_null_0,NEXUS_5,1.0,en].png | 3 + ...OptionLight-D-0_1_null,NEXUS_5,1.0,en].png | 3 + ...OptionLight-N-0_2_null,NEXUS_5,1.0,en].png | 3 + ...etailsDark--1_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_2,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_4,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_5,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_6,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_7,NEXUS_5,1.0,en].png | 4 +- ...etailsDark--1_1_null_8,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_7,NEXUS_5,1.0,en].png | 4 +- ...tailsLight--0_0_null_8,NEXUS_5,1.0,en].png | 4 +- ..._PreferenceText_0_null,NEXUS_5,1.0,en].png | 4 +- tools/localazy/config.json | 3 +- 68 files changed, 1381 insertions(+), 70 deletions(-) create mode 100644 changelog.d/506.feature create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsItem.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOption.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettings.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RoomNotificationSettingsMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsDark_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsLight_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-D-0_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-N-0_2_null,NEXUS_5,1.0,en].png diff --git a/changelog.d/506.feature b/changelog.d/506.feature new file mode 100644 index 0000000000..1b4d7c50cc --- /dev/null +++ b/changelog.d/506.feature @@ -0,0 +1,2 @@ + Add a "Mute" shortcut icon and a "Notifications" section in the room details screen + \ No newline at end of file diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 5f67fd1f7c..dc6f3304b1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -59,7 +59,6 @@ import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.androidutils.clipboard.ClipboardHelper 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.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -215,7 +214,8 @@ class MessagesPresenter @AssistedInject constructor( } private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState>) = launch(dispatchers.io) { - suspend { + inviteProgress.value = Async.Loading() + runCatching { room.updateMembers() val memberList = when (val memberState = room.membersStateFlow.value) { @@ -228,7 +228,14 @@ class MessagesPresenter @AssistedInject constructor( room.inviteUserById(member.userId).onFailure { t -> Timber.e(t, "Failed to reinvite DM partner") }.getOrThrow() - }.runCatchingUpdatingState(inviteProgress) + }.fold( + onSuccess = { + inviteProgress.value = Async.Success(Unit) + }, + onFailure = { + inviteProgress.value = Async.Failure(it) + } + ) } private suspend fun handleActionRedact(event: TimelineItem.Event) { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 3d98f665b6..48a8480915 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -51,6 +51,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -80,6 +81,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds class MessagesPresenterTest { @@ -380,13 +382,13 @@ class MessagesPresenterTest { assertThat(initialState.showReinvitePrompt).isFalse() // When the input field is focused we show the alert initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true)) - val focusedState = consumeItemsUntilPredicate { state -> + val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> state.showReinvitePrompt }.last() assertThat(focusedState.showReinvitePrompt).isTrue() // If it's dismissed then we stop showing the alert initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) - val dismissedState = consumeItemsUntilPredicate { state -> + val dismissedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> !state.showReinvitePrompt }.last() assertThat(dismissedState.showReinvitePrompt).isFalse() @@ -470,7 +472,9 @@ class MessagesPresenterTest { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) - val loadingState = awaitItem() + val loadingState = consumeItemsUntilPredicate { state -> + state.inviteProgress.isLoading() + }.last() assertThat(loadingState.inviteProgress.isLoading()).isTrue() val newState = awaitItem() assertThat(newState.inviteProgress.isSuccess()).isTrue() @@ -595,7 +599,7 @@ class MessagesPresenterTest { appCoroutineScope = this, room = matrixRoom, mediaPickerProvider = FakePickerProvider(), - featureFlagService = FakeFeatureFlagService(), + featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)), localMediaFactory = FakeLocalMediaFactory(mockMediaUrl), mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom), snackbarDispatcher = SnackbarDispatcher(), diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 0965cf484a..2137b1401d 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) + implementation(projects.libraries.featureflag.api) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) api(projects.services.apperror.api) @@ -59,6 +60,7 @@ dependencies { testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.usersearch.test) + testImplementation(projects.libraries.featureflag.test) testImplementation(projects.tests.testutils) testImplementation(projects.features.leaveroom.fake) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index 7b18d398df..fdad01d83c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -18,4 +18,6 @@ package io.element.android.features.roomdetails.impl sealed interface RoomDetailsEvent { data object LeaveRoom : RoomDetailsEvent + data object MuteNotification : RoomDetailsEvent + data object UnmuteNotification : RoomDetailsEvent } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index b456df9f02..675ef7de60 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode +import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -66,6 +67,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data object InviteMembers : NavTarget + @Parcelize + object RoomNotificationSettings : NavTarget + @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget } @@ -85,6 +89,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openInviteMembers() { backstack.push(NavTarget.InviteMembers) } + + override fun openRoomNotificationSettings() { + backstack.push(NavTarget.RoomNotificationSettings) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -110,6 +118,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( createNode(buildContext) } + NavTarget.RoomNotificationSettings -> { + createNode(buildContext) + } + is NavTarget.RoomMemberDetails -> { val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId)) createNode(buildContext, plugins) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index b74cf7aaf1..65c2ed0f89 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -51,6 +51,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openRoomMemberList() fun openInviteMembers() fun editRoomDetails() + fun openRoomNotificationSettings() } private val callbacks = plugins() @@ -67,6 +68,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openRoomMemberList() } } + private fun openRoomNotificationSettings() { + callbacks.forEach { it.openRoomNotificationSettings() } + } + private fun invitePeople() { callbacks.forEach { it.openInviteMembers() } } @@ -133,6 +138,7 @@ class RoomDetailsNode @AssistedInject constructor( onShareRoom = ::onShareRoom, onShareMember = ::onShareMember, openRoomMemberList = ::openRoomMemberList, + openRoomNotificationSettings = ::openRoomNotificationSettings, invitePeople = ::invitePeople, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 2612c24365..b12d4710a0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -22,31 +22,55 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +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.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canSendState +import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.ui.room.getDirectRoomMember +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( + private val client: MatrixClient, private val room: MatrixRoom, + private val featureFlagService: FeatureFlagService, + private val notificationSettingsService: NotificationSettingsService, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, private val leaveRoomPresenter: LeaveRoomPresenter, + private val dispatchers: CoroutineDispatchers, ) : Presenter { @Composable override fun present(): RoomDetailsState { + val scope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() + val canShowNotificationSettings = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) + if (canShowNotificationSettings.value) { + room.updateRoomNotificationSettings() + observeNotificationSettings() + } room.updateMembers() } @@ -69,10 +93,22 @@ class RoomDetailsPresenter @Inject constructor( } } + val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() + fun handleEvents(event: RoomDetailsEvent) { when (event) { - is RoomDetailsEvent.LeaveRoom -> + RoomDetailsEvent.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId)) + RoomDetailsEvent.MuteNotification -> { + scope.launch(dispatchers.io) { + client.notificationSettingsService().muteRoom(room.roomId) + } + } + RoomDetailsEvent.UnmuteNotification -> { + scope.launch(dispatchers.io) { + client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.activeMemberCount) + } + } } } @@ -88,9 +124,11 @@ class RoomDetailsPresenter @Inject constructor( isEncrypted = room.isEncrypted, canInvite = canInvite, canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, + canShowNotificationSettings = canShowNotificationSettings.value, roomType = roomType, roomMemberDetailsState = roomMemberDetailsState, leaveRoomState = leaveRoomState, + roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), eventSink = ::handleEvents, ) } @@ -122,4 +160,10 @@ class RoomDetailsPresenter @Inject constructor( private fun getCanSendState(membersState: MatrixRoomMembersState, type: StateEventType) = produceState(false, membersState) { value = room.canSendState(type).getOrElse { false } } + + private fun CoroutineScope.observeNotificationSettings() { + notificationSettingsService.notificationSettingsChangeFlow.onEach { + room.updateRoomNotificationSettings() + }.launchIn(this) + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 49aa7f6ce0..8dc6f81bf1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings data class RoomDetailsState( val roomId: String, @@ -32,7 +33,9 @@ data class RoomDetailsState( val roomMemberDetailsState: RoomMemberDetailsState?, val canEdit: Boolean, val canInvite: Boolean, + val canShowNotificationSettings: Boolean, val leaveRoomState: LeaveRoomState, + val roomNotificationSettings: RoomNotificationSettings?, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index cc4c4a6b1b..c580e6d677 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -22,6 +22,8 @@ import io.element.android.features.roomdetails.impl.members.details.aRoomMemberD import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings open class RoomDetailsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -75,9 +77,11 @@ fun aRoomDetailsState() = RoomDetailsState( isEncrypted = true, canInvite = false, canEdit = false, + canShowNotificationSettings = true, roomType = RoomDetailsType.Room, roomMemberDetailsState = null, leaveRoomState = LeaveRoomState(), + roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false), eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 735ed8491a..19aeb7ba0f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -27,12 +27,15 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.NotificationsOff import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.PersonAddAlt import androidx.compose.material.icons.outlined.Share @@ -75,6 +78,7 @@ 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.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -87,6 +91,7 @@ fun RoomDetailsView( onShareRoom: () -> Unit, onShareMember: (RoomMember) -> Unit, openRoomMemberList: () -> Unit, + openRoomNotificationSettings: () -> Unit, invitePeople: () -> Unit, modifier: Modifier = Modifier, ) { @@ -120,7 +125,10 @@ fun RoomDetailsView( roomName = state.roomName, roomAlias = state.roomAlias ) - MainActionsSection(onShareRoom = onShareRoom) + MainActionsSection( + state = state, + onShareRoom = onShareRoom + ) } is RoomDetailsType.Dm -> { @@ -142,6 +150,12 @@ fun RoomDetailsView( ) } + if (state.canShowNotificationSettings && state.roomNotificationSettings != null) { + NotificationSection( + isDefaultMode = state.roomNotificationSettings.isDefault, + openRoomNotificationSettings = openRoomNotificationSettings) + } + if (state.roomType is RoomDetailsType.Room) { MembersSection( memberCount = state.memberCount, @@ -211,8 +225,21 @@ internal fun RoomDetailsTopBar( } @Composable -internal fun MainActionsSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) { +internal fun MainActionsSection(state: RoomDetailsState, onShareRoom: () -> Unit, modifier: Modifier = Modifier) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + val roomNotificationSettings = state.roomNotificationSettings + if (state.canShowNotificationSettings && roomNotificationSettings != null) { + if (roomNotificationSettings.mode == RoomNotificationMode.MUTE) { + MainActionButton(title = stringResource(CommonStrings.common_unmute), icon = Icons.Outlined.NotificationsOff, onClick = { + state.eventSink(RoomDetailsEvent.UnmuteNotification) + }) + } else { + MainActionButton(title = stringResource(CommonStrings.common_mute), icon = Icons.Outlined.Notifications, onClick = { + state.eventSink(RoomDetailsEvent.MuteNotification) + }) + } + } + Spacer(modifier = Modifier.width(20.dp)) MainActionButton(title = stringResource(R.string.screen_room_details_share_room_title), icon = Icons.Outlined.Share, onClick = onShareRoom) } } @@ -280,6 +307,27 @@ internal fun TopicSection( } } +@Composable +internal fun NotificationSection( + isDefaultMode: Boolean, + openRoomNotificationSettings: () -> Unit, + modifier: Modifier = Modifier +) { + val subtitle = if (isDefaultMode) { + stringResource(R.string.screen_room_details_notification_mode_default) + } else { + stringResource(R.string.screen_room_details_notification_mode_custom) + } + PreferenceCategory(modifier = modifier) { + PreferenceText( + title = stringResource(R.string.screen_room_details_notification_title), + subtitle = subtitle, + icon = Icons.Outlined.Notifications, + onClick = openRoomNotificationSettings, + ) + } +} + @Composable internal fun MembersSection( memberCount: Long, @@ -352,6 +400,7 @@ private fun ContentToPreview(state: RoomDetailsState) { onShareRoom = {}, onShareMember = {}, openRoomMemberList = {}, + openRoomNotificationSettings = {}, invitePeople = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt new file mode 100644 index 0000000000..bbe756b154 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt @@ -0,0 +1,24 @@ +/* + * 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 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 +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsItem.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsItem.kt new file mode 100644 index 0000000000..182944c70e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsItem.kt @@ -0,0 +1,52 @@ +/* + * 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.features.roomdetails.impl.R +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +data class RoomNotificationSettingsItem( + val mode: RoomNotificationMode, + val title: String, +) + +@Composable +fun roomNotificationSettingsItems(): ImmutableList { + return RoomNotificationMode.values() + .map { + when (it) { + RoomNotificationMode.ALL_MESSAGES -> RoomNotificationSettingsItem( + mode = it, + title = stringResource(R.string.screen_room_notification_settings_mode_all_messages), + ) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationSettingsItem( + mode = it, + title = stringResource(R.string.screen_room_notification_settings_mode_mentions_and_keywords), + ) + RoomNotificationMode.MUTE -> RoomNotificationSettingsItem( + mode = it, + title = stringResource(CommonStrings.common_mute), + ) + } + } + .toImmutableList() +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt new file mode 100644 index 0000000000..224f850e28 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt @@ -0,0 +1,57 @@ +/* + * 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.Modifier +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 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.di.RoomScope +import io.element.android.services.analytics.api.AnalyticsService + +@ContributesNode(RoomScope::class) +class RoomNotificationSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RoomNotificationSettingsPresenter, + private val analyticsService: AnalyticsService, +) : Node(buildContext, plugins = plugins) { + + init { + lifecycle.subscribe( + onResume = { + analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.RoomNotifications)) + } + ) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + RoomNotificationSettingsView( + state = state, + modifier = modifier, + onBackPressed = this::navigateUp, + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOption.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOption.kt new file mode 100644 index 0000000000..28592b3b7e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOption.kt @@ -0,0 +1,97 @@ +/* + * 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.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.RadioButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.toEnabledColor +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun RoomNotificationSettingsOption( + roomNotificationSettingsItem: RoomNotificationSettingsItem, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isSelected: Boolean = false, + onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + enabled = enabled, + onClick = { onOptionSelected(roomNotificationSettingsItem) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) { + Text( + text = roomNotificationSettingsItem.title, + style = ElementTheme.typography.fontBodyLgRegular, + color = enabled.toEnabledColor(), + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + enabled = enabled, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + +@DayNightPreviews +@Composable +internal fun RoomPrivacyOptionLightPreview() = ElementPreview { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + RoomNotificationSettingsOption( + roomNotificationSettingsItem = roomNotificationSettingsItems().first(), + isSelected = true, + ) + RoomNotificationSettingsOption( + roomNotificationSettingsItem = roomNotificationSettingsItems().last(), + isSelected = false, + enabled = false, + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt new file mode 100644 index 0000000000..fd9f0bc930 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -0,0 +1,108 @@ +/* + * 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.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.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.libraries.architecture.Presenter +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 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( + private val room: MatrixRoom, + private val notificationSettingsService: NotificationSettingsService, +) : Presenter { + + @Composable + override fun present(): RoomNotificationSettingsState { + val defaultRoomNotificationMode: MutableState = rememberSaveable { + mutableStateOf(null) + } + val localCoroutineScope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + getDefaultRoomNotificationMode(defaultRoomNotificationMode) + observeNotificationSettings() + } + + val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() + + fun handleEvents(event: RoomNotificationSettingsEvents) { + when (event) { + is RoomNotificationSettingsEvents.RoomNotificationModeChanged -> { + localCoroutineScope.setRoomNotificationMode(event.mode) + } + is RoomNotificationSettingsEvents.SetNotificationMode -> { + if (event.isDefault) { + localCoroutineScope.restoreDefaultRoomNotificationMode() + } else { + defaultRoomNotificationMode.value?.let { + localCoroutineScope.setRoomNotificationMode(it) + } + } + } + } + } + + return RoomNotificationSettingsState( + roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), + defaultRoomNotificationMode = defaultRoomNotificationMode.value, + eventSink = ::handleEvents, + ) + } + + @OptIn(FlowPreview::class) + private fun CoroutineScope.observeNotificationSettings() { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + room.updateRoomNotificationSettings() + } + .launchIn(this) + } + + private fun CoroutineScope.getDefaultRoomNotificationMode(defaultRoomNotificationMode: MutableState) = launch { + defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( + room.isEncrypted, + room.activeMemberCount + ).getOrThrow() + } + + private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode) = launch { + notificationSettingsService.setRoomNotificationMode(room.roomId, mode) + } + + private fun CoroutineScope.restoreDefaultRoomNotificationMode() = launch { + notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt new file mode 100644 index 0000000000..04742781b5 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt @@ -0,0 +1,26 @@ +/* + * 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 io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings + +data class RoomNotificationSettingsState( + val roomNotificationSettings: RoomNotificationSettings?, + val defaultRoomNotificationMode: RoomNotificationMode?, + val eventSink: (RoomNotificationSettingsEvents) -> Unit +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt new file mode 100644 index 0000000000..df1dd7977b --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -0,0 +1,34 @@ +/* + * 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.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings + +internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + RoomNotificationSettingsState( + RoomNotificationSettings( + mode = RoomNotificationMode.MUTE, + isDefault = true), + RoomNotificationMode.ALL_MESSAGES, + eventSink = { }, + ), + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt new file mode 100644 index 0000000000..3709280477 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -0,0 +1,161 @@ +/* + * 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.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.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +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.core.bool.orTrue +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.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +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 +import io.element.android.libraries.designsystem.theme.components.TopAppBar +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, + onBackPressed: () -> Unit = {}, +) { + Scaffold( + modifier = modifier, + topBar = { + RoomNotificationSettingsTopBar( + onBackPressed = { onBackPressed() } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(padding) + .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) { + RoomNotificationSettingsOptions( + selected = state.roomNotificationSettings.mode, + enabled = !state.roomNotificationSettings.isDefault, + onOptionSelected = { + state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) + }, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomNotificationSettingsTopBar( + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.screen_room_details_notification_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + ) +} + +@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 + ) + } + } +} + +@Preview +@Composable +internal fun RoomNotificationSettingsLightPreview(@PreviewParameter(RoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun RoomNotificationSettingsDarkPreview(@PreviewParameter(RoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomNotificationSettingsState) { + RoomNotificationSettingsView(state) +} diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml index 2696cc99ea..34fae4fea9 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -23,6 +23,16 @@ "Partager le salon" "Mise à jour du salon…" "En attente" + "Autoriser les paramètres personnalisés" + "Activer cette option remplacera votre paramètre par défaut" + "Me notifier dans ce chat pour" + "paramètres généraux" + "Paramètre par défaut" + "Une erreur s’est produite lors du chargement des paramètres de notification." + "Impossible de restaurer le mode par défaut, veuillez réessayer." + "Impossible de régler le mode, veuillez réessayer." + "Tous les messages" + "Mentions et mots-clés uniquement" "Bloquer" "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment." "Bloquer l\'utilisateur" diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml index 4d2664ab30..4154f041ff 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -25,6 +25,17 @@ "Обновление комнаты…" "В ожидании" "Участники комнаты" + "Разрешить пользовательские настройки" + "Включение этого параметра отменяет настройки по умолчанию" + "Уведомить меня в этом чате" + "Вы можете изменить его в своем %1$s." + "Основные Настройки" + "Настройка по умолчанию" + "Произошла ошибка при загрузке настроек уведомлений." + "Не удалось восстановить режим по умолчанию, попробуйте еще раз." + "Не удалось настроить режим, попробуйте еще раз." + "Все сообщения" + "Только упоминания и ключевые слова" "Заблокировать" "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время." "Заблокировать пользователя" diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml index 1d744fba30..687c495ce7 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -25,6 +25,17 @@ "Aktualizácia miestnosti…" "Čaká sa" "Členovia miestnosti" + "Povoliť vlastné nastavenie" + "Zapnutím tohto nastavenia sa prepíše vaše predvolené nastavenie" + "Upozorniť ma v tejto konverzácii na" + "Môžete to zmeniť vo svojich %1$s." + "všeobecných nastaveniach" + "Predvolené nastavenie" + "Pri načítavaní nastavení oznámení došlo k chybe." + "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." + "Nepodarilo sa nastaviť režim, skúste to prosím znova." + "Všetky správy" + "Iba zmienky a kľúčové slová" "Zablokovať" "Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať." "Zablokovať používateľa" diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml index fb7872844a..7b4eb895ea 100644 --- a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml @@ -18,6 +18,12 @@ "正在更新聊天室…" "待定" "聊天室成員" + "全域設定" + "預設" + "無法重設為預設模式,請再試一次。" + "無法設定模式,請再試一次。" + "所有訊息" + "只限提及與關鍵字" "封鎖" "封鎖使用者" "解除封鎖" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index b1f67dab1e..717f503e22 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -24,6 +24,17 @@ "Updating room…" "Pending" "Room members" + "Allow custom setting" + "Turning this on will override your default setting" + "Notify me in this chat for" + "You can change it in your %1$s." + "global settings" + "Default setting" + "An error occurred while loading notification settings." + "Failed restoring the default mode, please try again." + "Failed setting the mode, please try again." + "All messages" + "Mentions and Keywords only" "Block" "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." "Block user" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 275b4888d1..71695094b3 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -29,22 +29,30 @@ import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.features.roomdetails.impl.RoomTopicState import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 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.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds @ExperimentalCoroutinesApi class RoomDetailsPresenterTests { @@ -52,20 +60,37 @@ class RoomDetailsPresenterTests { @Rule @JvmField val warmUpRule = WarmUpRule() + private fun aRoomDetailsPresenter( + room: MatrixRoom, + leaveRoomPresenter: LeaveRoomPresenter = LeaveRoomPresenterFake(), + dispatchers: CoroutineDispatchers, + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() + ): RoomDetailsPresenter { + val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) - private fun aRoomDetailsPresenter(room: MatrixRoom, leaveRoomPresenter: LeaveRoomPresenter = LeaveRoomPresenterFake()): RoomDetailsPresenter { val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMemberId) + return RoomMemberDetailsPresenter(matrixClient, room, roomMemberId) } } - return RoomDetailsPresenter(room, roomMemberDetailsPresenterFactory, leaveRoomPresenter) + val featureFlagService = FakeFeatureFlagService( + mapOf(FeatureFlags.NotificationSettings.key to true) + ) + return RoomDetailsPresenter( + matrixClient, + room, + featureFlagService, + matrixClient.notificationSettingsService(), + roomMemberDetailsPresenterFactory, + leaveRoomPresenter, + dispatchers + ) } @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -84,7 +109,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -106,7 +131,7 @@ class RoomDetailsPresenterTests { val roomMembers = listOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -122,7 +147,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.success(true)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -140,11 +165,13 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { assertThat(awaitItem().canInvite).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -153,11 +180,13 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.failure(Throwable("Whoops"))) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { assertThat(awaitItem().canInvite).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -169,7 +198,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp"))) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -198,7 +227,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -227,7 +256,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -248,7 +277,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -269,12 +298,14 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { // Initially false, and no further events assertThat(awaitItem().canEdit).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -285,12 +316,14 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { // The initial state is "hidden" and no further state changes happen assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden) + + cancelAndIgnoreRemainingEvents() } } @@ -301,7 +334,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room) + val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -319,7 +352,7 @@ class RoomDetailsPresenterTests { fun `present - leave room event is passed on to leave room presenter`() = runTest { val leaveRoomPresenter = LeaveRoomPresenterFake() val room = aMatrixRoom() - val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -330,6 +363,64 @@ class RoomDetailsPresenterTests { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `present - notification mode changes`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val notificationSettingsService = FakeNotificationSettingsService() + val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + + notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + val updatedState = consumeItemsUntilPredicate { + it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + }.last() + assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - mute room notifications`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val notificationSettingsService = FakeNotificationSettingsService(initialMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(RoomDetailsEvent.MuteNotification) + val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { + it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE + }.last() + assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MUTE) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - unmute room notifications`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val notificationSettingsService = FakeNotificationSettingsService( + initialMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + initialDefaultMode = RoomNotificationMode.ALL_MESSAGES + ) + val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) + val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification) + val updatedState = consumeItemsUntilPredicate { + it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES + }.last() + assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + cancelAndIgnoreRemainingEvents() + } + } } fun aMatrixRoom( @@ -341,6 +432,7 @@ fun aMatrixRoom( isEncrypted: Boolean = true, isPublic: Boolean = true, isDirect: Boolean = false, + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() ) = FakeMatrixRoom( roomId = roomId, name = name, @@ -350,5 +442,6 @@ fun aMatrixRoom( isEncrypted = isEncrypted, isPublic = isPublic, isDirect = isDirect, + notificationSettingsService = notificationSettingsService ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt new file mode 100644 index 0000000000..29bec7f622 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt @@ -0,0 +1,83 @@ +/* + * 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.notificationsettings + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +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_NOTIFICATION_MODE +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomNotificationSettingsPresenterTests { + @Test + fun `present - initial state is created from room info`() = runTest { + val presenter = aNotificationPresenter + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.roomNotificationSettings).isNull() + Truth.assertThat(initialState.defaultRoomNotificationMode).isNull() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - notification mode changed`() = runTest { + val presenter = aNotificationPresenter + 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 + }.last() + Truth.assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + } + } + + @Test + fun `present - notification settings restore default`() = runTest { + val presenter = aNotificationPresenter + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)) + initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(true)) + val defaultState = consumeItemsUntilPredicate { + it.roomNotificationSettings?.mode == A_ROOM_NOTIFICATION_MODE + }.last() + Truth.assertThat(defaultState.roomNotificationSettings?.mode).isEqualTo(A_ROOM_NOTIFICATION_MODE) + } + } + + private val aNotificationPresenter: RoomNotificationSettingsPresenter get() { + val room = aMatrixRoom() + return RoomNotificationSettingsPresenter( + room = room, + notificationSettingsService = room.notificationSettingsService + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt index 3f204ee847..9a5f6b9a41 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt @@ -39,6 +39,8 @@ import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.toEnabledColor +import io.element.android.libraries.designsystem.toSecondaryEnabledColor import io.element.android.libraries.theme.ElementTheme /** @@ -48,6 +50,7 @@ import io.element.android.libraries.theme.ElementTheme fun PreferenceText( title: String, modifier: Modifier = Modifier, + enabled: Boolean = true, subtitle: String? = null, currentValue: String? = null, loadingCurrentValue: Boolean = false, @@ -68,8 +71,9 @@ fun PreferenceText( ) { PreferenceIcon( icon = icon, + enabled = enabled, isVisible = showIconAreaIfNoIcon, - tintColor = tintColor ?: ElementTheme.materialColors.secondary + tintColor = tintColor ?: enabled.toSecondaryEnabledColor(), ) Column( modifier = Modifier @@ -79,13 +83,13 @@ fun PreferenceText( Text( style = ElementTheme.typography.fontBodyLgRegular, text = title, - color = tintColor ?: ElementTheme.materialColors.primary, + color = tintColor ?: enabled.toEnabledColor(), ) if (subtitle != null) { Text( style = ElementTheme.typography.fontBodyMdRegular, text = subtitle, - color = tintColor ?: ElementTheme.materialColors.secondary, + color = tintColor ?: enabled.toSecondaryEnabledColor(), ) } } @@ -96,7 +100,7 @@ fun PreferenceText( .padding(start = 16.dp, end = 8.dp), text = currentValue, style = ElementTheme.typography.fontBodyXsMedium, - color = ElementTheme.materialColors.secondary, + color = enabled.toSecondaryEnabledColor(), ) } else if (loadingCurrentValue) { CircularProgressIndicator( @@ -135,6 +139,13 @@ private fun ContentToPreview() { icon = Icons.Default.BugReport, currentValue = "123", ) + PreferenceText( + title = "Title", + subtitle = "Some content", + icon = Icons.Default.BugReport, + currentValue = "123", + enabled = false, + ) PreferenceText( title = "Title", subtitle = "Some content", diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index c28dc28144..6490b4e357 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -30,5 +30,11 @@ enum class FeatureFlags( key = "feature.polls", title = "Polls", description = "Create poll and render poll events in the timeline", + defaultValue = false, + ), + NotificationSettings( + key = "feature.notificationsettings", + title = "Show notification settings", + defaultValue = false, ), } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt index 83913cbac5..0117a4aa22 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt @@ -31,6 +31,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() : when (feature) { FeatureFlags.LocationSharing -> true FeatureFlags.Polls -> false + FeatureFlags.NotificationSettings -> false } } else { false diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 2ca4420145..63093064b6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -49,6 +50,7 @@ interface MatrixClient : Closeable { fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService fun notificationService(): NotificationService + fun notificationSettingsService(): NotificationSettingsService suspend fun getCacheSize(): Long /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt new file mode 100644 index 0000000000..0f82604aba --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt @@ -0,0 +1,36 @@ +/* + * 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.libraries.matrix.api.notificationsettings + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import kotlinx.coroutines.flow.SharedFlow + +interface NotificationSettingsService { + /** + * State of the current room notification settings flow ([MatrixRoomNotificationSettingsState.Unknown] if not started). + */ + val notificationSettingsChangeFlow : SharedFlow + suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result + suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result + suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result + suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result + suspend fun muteRoom(roomId: RoomId): Result + suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 41f105df8e..60c8b1ddd5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -56,11 +56,15 @@ interface MatrixRoom : Closeable { */ val membersStateFlow: StateFlow + val roomNotificationSettingsStateFlow: StateFlow + /** * Try to load the room members and update the membersFlow. */ suspend fun updateMembers(): Result + suspend fun updateRoomNotificationSettings(): Result + val syncUpdateFlow: StateFlow val timeline: MatrixTimeline diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt new file mode 100644 index 0000000000..d98a3a83d2 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt @@ -0,0 +1,33 @@ +/* + * 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.libraries.matrix.api.room + +sealed interface MatrixRoomNotificationSettingsState { + object Unknown : MatrixRoomNotificationSettingsState + data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState + data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState + data class Ready(val roomNotificationSettings: RoomNotificationSettings) : MatrixRoomNotificationSettingsState +} + +fun MatrixRoomNotificationSettingsState.roomNotificationSettings(): RoomNotificationSettings? { + return when (this) { + is MatrixRoomNotificationSettingsState.Ready -> roomNotificationSettings + is MatrixRoomNotificationSettingsState.Pending -> prevRoomNotificationSettings + is MatrixRoomNotificationSettingsState.Error -> prevRoomNotificationSettings + else -> null + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettings.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettings.kt new file mode 100644 index 0000000000..23f2b41797 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettings.kt @@ -0,0 +1,26 @@ +/* + * 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.libraries.matrix.api.room + +data class RoomNotificationSettings( + val mode: RoomNotificationMode, + val isDefault: Boolean, +) + +enum class RoomNotificationMode { + ALL_MESSAGES, MENTIONS_AND_KEYWORDS_ONLY, MUTE +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 54641f785b..22f1e9af3f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService +import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RustMatrixRoom @@ -99,6 +101,7 @@ class RustMatrixClient constructor( client = client, dispatchers = dispatchers, ) + private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService) private val notificationClient = client.notificationClient(notificationProcessSetup) .use { builder -> @@ -106,8 +109,10 @@ class RustMatrixClient constructor( .filterByPushRules() .finish() } + private val notificationSettings = client.getNotificationSettings() private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock) + private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers) private val isLoggingOut = AtomicBoolean(false) @@ -173,6 +178,7 @@ class RustMatrixClient constructor( sessionId = sessionId, roomListItem = roomListItem, innerRoom = fullRoom, + roomNotificationSettingsService = notificationSettingsService, sessionCoroutineScope = sessionCoroutineScope, coroutineDispatchers = dispatchers, systemClock = clock, @@ -277,9 +283,13 @@ class RustMatrixClient constructor( override fun notificationService(): NotificationService = notificationService + override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService + override fun close() { sessionCoroutineScope.cancel() client.setDelegate(null) + notificationSettings.setDelegate(null) + notificationSettings.destroy() verificationService.destroy() syncService.destroy() innerRoomListService.destroy() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index dba1dbd0a3..1159f668e6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -22,6 +22,7 @@ import dagger.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -34,6 +35,11 @@ object SessionMatrixModule { return matrixClient.sessionVerificationService() } + @Provides + fun providesNotificationSettingsService(matrixClient: MatrixClient): NotificationSettingsService { + return matrixClient.notificationSettingsService() + } + @Provides fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver { return matrixClient.roomMembershipObserver() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RoomNotificationSettingsMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RoomNotificationSettingsMapper.kt new file mode 100644 index 0000000000..084a6f973e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RoomNotificationSettingsMapper.kt @@ -0,0 +1,44 @@ +/* + * 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.libraries.matrix.impl.notificationsettings + +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode +import org.matrix.rustcomponents.sdk.RoomNotificationSettings as RustRoomNotificationSettings + +object RoomNotificationSettingsMapper { + fun map(roomNotificationSettings: RustRoomNotificationSettings): RoomNotificationSettings = + RoomNotificationSettings( + mode = mapMode(roomNotificationSettings.mode), + isDefault = roomNotificationSettings.isDefault + ) + + fun mapMode(mode: RustRoomNotificationMode): RoomNotificationMode = + when (mode) { + RustRoomNotificationMode.ALL_MESSAGES -> RoomNotificationMode.ALL_MESSAGES + RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE + } + + fun mapMode(mode: RoomNotificationMode): RustRoomNotificationMode = + when (mode) { + RoomNotificationMode.ALL_MESSAGES -> RustRoomNotificationMode.ALL_MESSAGES + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + RoomNotificationMode.MUTE -> RustRoomNotificationMode.MUTE + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt new file mode 100644 index 0000000000..1b1d51214f --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -0,0 +1,86 @@ +/* + * 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.libraries.matrix.impl.notificationsettings + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.core.RoomId +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.room.RoomNotificationSettings +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.NotificationSettings +import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate + +class RustNotificationSettingsService( + private val notificationSettings: NotificationSettings, + private val dispatchers: CoroutineDispatchers, +) : NotificationSettingsService { + + private val _notificationSettingsChangeFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + override val notificationSettingsChangeFlow: SharedFlow = _notificationSettingsChangeFlow.asSharedFlow() + + private var notificationSettingsDelegate = object : NotificationSettingsDelegate { + override fun settingsDidChange() { + _notificationSettingsChangeFlow.tryEmit(Unit) + } + } + + init { + notificationSettings.setDelegate(notificationSettingsDelegate) + } + + override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result = + runCatching { + notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne(membersCount)).let(RoomNotificationSettingsMapper::map) + } + + override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result = + runCatching { + notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne(membersCount)).let(RoomNotificationSettingsMapper::mapMode) + } + + override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) + } + } + + override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result = withContext(dispatchers.io) { + runCatching { + notificationSettings.restoreDefaultRoomNotificationMode(roomId.value) + } + } + + override suspend fun muteRoom(roomId: RoomId): Result = setRoomNotificationMode(roomId, RoomNotificationMode.MUTE) + + override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long) = withContext(dispatchers.io) { + runCatching { + notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne(membersCount)) + } + } + + /** + * A one-to-one is a room with exactly 2 members. + * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). + * @param membersCount The active members count in a room + */ + private fun isOneToOne(membersCount: Long) = membersCount == 2L +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 71caed8f19..461e43931e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -33,16 +33,19 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.poll.toInner +import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.util.destroyAll @@ -72,6 +75,7 @@ class RustMatrixRoom( override val sessionId: SessionId, private val roomListItem: RoomListItem, private val innerRoom: Room, + private val roomNotificationSettingsService: RustNotificationSettingsService, sessionCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, private val systemClock: SystemClock, @@ -90,6 +94,10 @@ class RustMatrixRoom( private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId") private val _membersStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) private val _syncUpdateFlow = MutableStateFlow(0L) + + private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) + override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow + private val _timeline by lazy { RustMatrixTimeline( matrixRoom = this, @@ -197,6 +205,22 @@ class RustMatrixRoom( } } + override suspend fun updateRoomNotificationSettings(): Result = withContext(coroutineDispatchers.io) { + val currentState = _roomNotificationSettingsStateFlow.value + val currentRoomNotificationSettings = currentState.roomNotificationSettings() + _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) + runCatching { + roomNotificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, activeMemberCount).getOrThrow() + }.map { + _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it) + }.onFailure { + _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Error( + prevRoomNotificationSettings = currentRoomNotificationSettings, + failure = it + ) + } + } + override suspend fun userAvatarUrl(userId: UserId): Result = withContext(roomDispatcher) { runCatching { innerRoom.memberAvatarUrl(userId.value) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 83f7f3ad79..9ef50ecda5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.pushers.FakePushersService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService @@ -50,6 +52,7 @@ class FakeMatrixClient( private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), private val pushersService: FakePushersService = FakePushersService(), private val notificationService: FakeNotificationService = FakeNotificationService(), + private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val syncService: FakeSyncService = FakeSyncService(), private val accountManagementUrlString: Result = Result.success(null), ) : MatrixClient { @@ -142,6 +145,7 @@ class FakeMatrixClient( override fun pushersService(): PushersService = pushersService override fun notificationService(): NotificationService = notificationService + override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService override fun roomMembershipObserver(): RoomMembershipObserver { return RoomMembershipObserver() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index f6b6e1645f..cae5df4dc4 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -24,6 +24,8 @@ import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings const val A_USER_NAME = "alice" const val A_PASSWORD = "password" @@ -59,6 +61,8 @@ const val A_HOMESERVER_URL_2 = "matrix-client.org" val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false) val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true) +val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE +val A_ROOM_NOTIFICATION_SETTINGS = RoomNotificationSettings(mode = A_ROOM_NOTIFICATION_MODE, isDefault = false) const val AN_AVATAR_URL = "mxc://data" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt new file mode 100644 index 0000000000..852496e0b3 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt @@ -0,0 +1,64 @@ +/* + * 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.libraries.matrix.test.notificationsettings + +import io.element.android.libraries.matrix.api.core.RoomId +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.room.RoomNotificationSettings +import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow + +class FakeNotificationSettingsService( + initialMode: RoomNotificationMode = A_ROOM_NOTIFICATION_MODE, + initialDefaultMode: RoomNotificationMode = A_ROOM_NOTIFICATION_MODE +) : NotificationSettingsService { + private var _roomNotificationSettingsStateFlow = MutableStateFlow(Unit) + private var defaultRoomNotificationMode: RoomNotificationMode = initialDefaultMode + private var roomNotificationMode: RoomNotificationMode = initialMode + override val notificationSettingsChangeFlow: SharedFlow + get() = _roomNotificationSettingsStateFlow + + override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result { + return Result.success(RoomNotificationSettings(mode = roomNotificationMode, isDefault = roomNotificationMode == defaultRoomNotificationMode)) + } + + override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result { + return Result.success(defaultRoomNotificationMode) + } + + override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result { + roomNotificationMode = mode + _roomNotificationSettingsStateFlow.emit(Unit) + return Result.success(Unit) + } + + override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result { + roomNotificationMode = defaultRoomNotificationMode + _roomNotificationSettingsStateFlow.emit(Unit) + return Result.success(Unit) + } + + override suspend fun muteRoom(roomId: RoomId): Result { + return setRoomNotificationMode(roomId, RoomNotificationMode.MUTE) + } + + override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result { + return restoreDefaultRoomNotificationMode(roomId) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 7bffb985bb..761e479816 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -27,16 +27,19 @@ import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.delay @@ -58,6 +61,7 @@ class FakeMatrixRoom( override val isDirect: Boolean = false, override val joinedMemberCount: Long = 123L, override val activeMemberCount: Long = 234L, + val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), canRedact: Boolean = false, ) : MatrixRoom { @@ -136,10 +140,19 @@ class FakeMatrixRoom( override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) + override val roomNotificationSettingsStateFlow: MutableStateFlow = + MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) + override suspend fun updateMembers(): Result = simulateLongTask { updateMembersResult } + override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { + val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, activeMemberCount).getOrThrow() + roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings) + return Result.success(Unit) + } + override val syncUpdateFlow: StateFlow = MutableStateFlow(0L) override val timeline: MatrixTimeline = matrixTimeline diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 8a9f1cee2f..d6f9f2cbaf 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -1,5 +1,6 @@ + "Use an identity server to invite by email. ""Use the default (%(defaultIdentityServerName)s)"" or manage in ""Settings""." "Hide password" "Mentions only" "Muted" @@ -161,6 +162,10 @@ "Are you sure that you want to leave this room? This room is not public and you won\'t be able to rejoin without an invite." "Are you sure that you want to leave the room?" "%1$s Android" + + "%(count)s room" + "%(count)s rooms" + "%1$d member" "%1$d members" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsDark_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..352d1a95e1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsDark_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d1c7b862db8afe25dd958c9011de82e1439d298a95ec15a86dfbae0457f7937 +size 36436 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsLight_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4dc3771f2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettingsLight_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1900985fb6b9d25eee44af5cdc0c6fcdee994ffc3da8dae5a6e742c3d0b7127 +size 40016 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-D-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..38c517ea47 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-D-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62418ebe7afa2c1c7eda2b1251fb189a75527aa7a4289a9478c531fac7dee8bd +size 10666 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-N-0_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cecfc09f64 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomPrivacyOptionLight-N-0_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5a32b1baaaad0bf266e190084a64b3d382a2d6d472a55b1c3653ee948e100c9 +size 9756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png index 9ef89a09c3..211bbf07bc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6edcc452c76049a978ceb97b46776446790dbdbe30eef1693fafda089cdc3ee7 -size 55527 +oid sha256:49bd51a32dbd3615364a6039163a36c08d0096a8c511a7084e21b7c1780131db +size 52810 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png index 0dc50f37c2..19f56b887f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20e630b0f3bb9fddd5f11e4e9f2a92b5d35c4bbe182662e8d0fde27451297006 -size 45247 +oid sha256:2c61e5d0eabb1f9cae83c3df0f73f14545e0c61bd7393d6e2862e0f48b9e3d7a +size 51272 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png index ca7b6913d5..ec15315140 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:553b498247e9da02ad037716a67c080a3da2fb07602b0c0c44de8f16a4891eda -size 46086 +oid sha256:adf48ade91c22ce7f98bf2773ec4e14ce0c290f7ba25e8c76d9f722677c46277 +size 44388 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png index 9610377f28..d72802868e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d91c291503dc745092b132fec62208e73dcd031822ca21a66a7fb1f48ecfa6e1 -size 49316 +oid sha256:262ba970549c3c137ab90ffc607279479dfc4fbf86468e18b08ea6c5ef5b0710 +size 53634 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png index 1fe7df1eaa..04edeb3284 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efda43a25fdc2a21a4ad3767b14b4c7a308003f9bcb9e67d64520db13c83f0fd -size 61061 +oid sha256:49d92579bffadd59f89e79217b5e6148bbfeda0c5750a06ad7aa36f8125d6d29 +size 50616 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png index 761666b2e3..2150b91f12 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31f2d59ad03a4d9ba8efd5f146668162f3c2095ac90e16dd3634d3e817f7263d -size 61238 +oid sha256:b742f507ba11be8bdf733e5e751fc5d9fe1c0cc2eb070abf9abcffb605783ab3 +size 52151 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png index 761666b2e3..2150b91f12 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31f2d59ad03a4d9ba8efd5f146668162f3c2095ac90e16dd3634d3e817f7263d -size 61238 +oid sha256:b742f507ba11be8bdf733e5e751fc5d9fe1c0cc2eb070abf9abcffb605783ab3 +size 52151 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png index f7d782f1b8..f94aa99b7b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fba2e76faa5915acf1e9d2ede6b58e1ab73e287aefa6e5ffc643be527350d987 -size 50147 +oid sha256:467ca1b17857b053881cb9999c3d6b099949c6b4279188da57383fdb9083041f +size 53946 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png index 572f6d76e1..dbddc54999 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1089845cd143d07e0d6a363a9dc490ab6f5ae4711fc99843c01e4f074dbdeb98 -size 55780 +oid sha256:d8e1ede8c0ea34595e90b2031c9607b99e72c3d7d1e9a778bbfbece98eb23875 +size 53035 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png index 6620f9dc9f..ca3e37d42a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59054f5692d41c0d6fe65c6f1bc9d0c4e1e3149aedd987e9335fabff7b46d85f -size 57237 +oid sha256:a9f1d200ef2c93d557a67a29700918bd70439c7d7d8d247d60473b26f0c44eb3 +size 53851 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png index 852e18b044..c27ccdc333 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f65151d983057ca185c53b4045fb33675f12730fee1b40e3df6d269e2290a05 -size 47290 +oid sha256:b06e1652745ceff912734b9bf74543c95f4d420eaf3aec4b388dd101ea1428f8 +size 53838 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png index 89ce9d4604..087bd78cc4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de6a5ac50e8b30364b9e630d2edbe49c18cc3684da080e2167f41bd689f17700 -size 48369 +oid sha256:5d4a381a3c99a7f04a11a2e93cd7c4145fa0bd33a1f7461038b3918c5cd94571 +size 46676 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png index 95f7e24521..0ebb4bdfc9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edcf03466e38ce3906583f2701f45d2a9538a6fd7e9d1c9d4822a037b833a512 -size 50185 +oid sha256:61add95d28096c65f94764df109d253c7b589058cea324d908616312e7dffc3e +size 54723 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png index 2f17f119fc..8a3a6dddf4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:670f05e0c2861234708c7dee4d919a977bbd3aec3e965db631505957871f19fa -size 62904 +oid sha256:3197e871e4b4c39dc714610b54e351c4061149bfc0b5511f78a0c73e8ec2236b +size 51824 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png index d0c76cd680..6547257b66 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9553c9ef84cd1f2e890290e093cb0006f03d1a2f36de5e2b7d104d0eb102b2e2 -size 63045 +oid sha256:a0cf7d5d0c91aa7946586e5258077b07b329f387f46b0df0a29c49cc211ed584 +size 54021 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png index d0c76cd680..6547257b66 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9553c9ef84cd1f2e890290e093cb0006f03d1a2f36de5e2b7d104d0eb102b2e2 -size 63045 +oid sha256:a0cf7d5d0c91aa7946586e5258077b07b329f387f46b0df0a29c49cc211ed584 +size 54021 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png index 0e72199eb0..6879d4dfb5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ab533a35fbbba8b45d94bcfb0637d0f021ee2ef71230754ed418702ee18f11d -size 51182 +oid sha256:cd4be4f49a78aba83c4b5f9c350f8b64cfe96d50890df16154cd7b0e20546d25 +size 55020 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png index 159c179d5c..92a37ad86d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_null_RoomDetailsLight--0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ec34df75877b7050ad874905926f5d35fee29d1f8324437b502d7152ef87cdc -size 57494 +oid sha256:da79bef2bd54301f2724269719d93d1d9212fd17b9dc0d2a7ba935693c6170ac +size 54107 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png index 15e7b55fa2..f757da3a86 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08af9a706d579b4632c70265b6f1529008bd483cb4924c341e3a9f0172cc8cd4 -size 39251 +oid sha256:bd05144d4b3527a44e1c9f3fb92d227ccc38fd7afce5436f32b59f14f0b54e27 +size 39028 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 05d1ced2c6..cf29033d88 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -92,7 +92,8 @@ "includeRegex": [ "screen_room_details_.*", "screen_room_member_list_.*", - "screen_dm_details_.*" + "screen_dm_details_.*", + "screen_room_notification_settings_.*" ] }, {