Add room notification settings (#807)
* Add room notification settings --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io> Co-authored-by: Jorge Martín <jorgem@element.io> Co-authored-by: Benoit Marty <benoit@matrix.org> Co-authored-by: David Langley <langley.dave@gmail.com>
This commit is contained in:
parent
a40c9ef002
commit
4a5a01d710
68 changed files with 1381 additions and 70 deletions
2
changelog.d/506.feature
Normal file
2
changelog.d/506.feature
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Add a "Mute" shortcut icon and a "Notifications" section in the room details screen
|
||||
|
||||
|
|
@ -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<Async<Unit>>) = 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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
||||
}
|
||||
|
|
@ -110,6 +118,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
createNode<RoomInviteMembersNode>(buildContext)
|
||||
}
|
||||
|
||||
NavTarget.RoomNotificationSettings -> {
|
||||
createNode<RoomNotificationSettingsNode>(buildContext)
|
||||
}
|
||||
|
||||
is NavTarget.RoomMemberDetails -> {
|
||||
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
|
||||
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
fun openRoomMemberList()
|
||||
fun openInviteMembers()
|
||||
fun editRoomDetails()
|
||||
fun openRoomNotificationSettings()
|
||||
}
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RoomDetailsState> {
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RoomDetailsState> {
|
||||
override val values: Sequence<RoomDetailsState>
|
||||
|
|
@ -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 = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<RoomNotificationSettingsItem> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<Plugin>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RoomNotificationSettingsState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomNotificationSettingsState {
|
||||
val defaultRoomNotificationMode: MutableState<RoomNotificationMode?> = 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<RoomNotificationMode?>) = 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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<RoomNotificationSettingsState> {
|
||||
override val values: Sequence<RoomNotificationSettingsState>
|
||||
get() = sequenceOf(
|
||||
RoomNotificationSettingsState(
|
||||
RoomNotificationSettings(
|
||||
mode = RoomNotificationMode.MUTE,
|
||||
isDefault = true),
|
||||
RoomNotificationMode.ALL_MESSAGES,
|
||||
eventSink = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -23,6 +23,16 @@
|
|||
<string name="screen_room_details_share_room_title">"Partager le salon"</string>
|
||||
<string name="screen_room_details_updating_room">"Mise à jour du salon…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"En attente"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Autoriser les paramètres personnalisés"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Activer cette option remplacera votre paramètre par défaut"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Me notifier dans ce chat pour"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"paramètres généraux"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Paramètre par défaut"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"Une erreur s’est produite lors du chargement des paramètres de notification."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Impossible de restaurer le mode par défaut, veuillez réessayer."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Impossible de régler le mode, veuillez réessayer."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Tous les messages"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions et mots-clés uniquement"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Bloquer"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"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."</string>
|
||||
<string name="screen_dm_details_block_user">"Bloquer l\'utilisateur"</string>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@
|
|||
<string name="screen_room_details_updating_room">"Обновление комнаты…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"В ожидании"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Участники комнаты"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Разрешить пользовательские настройки"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Включение этого параметра отменяет настройки по умолчанию"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Уведомить меня в этом чате"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote">"Вы можете изменить его в своем %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Основные Настройки"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Настройка по умолчанию"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"Произошла ошибка при загрузке настроек уведомлений."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Не удалось восстановить режим по умолчанию, попробуйте еще раз."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Не удалось настроить режим, попробуйте еще раз."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Все сообщения"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Заблокировать"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
|
||||
<string name="screen_dm_details_block_user">"Заблокировать пользователя"</string>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@
|
|||
<string name="screen_room_details_updating_room">"Aktualizácia miestnosti…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Čaká sa"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Členovia miestnosti"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Povoliť vlastné nastavenie"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Zapnutím tohto nastavenia sa prepíše vaše predvolené nastavenie"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Upozorniť ma v tejto konverzácii na"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote">"Môžete to zmeniť vo svojich %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"všeobecných nastaveniach"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Predvolené nastavenie"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"Pri načítavaní nastavení oznámení došlo k chybe."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Nepodarilo sa nastaviť režim, skúste to prosím znova."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Všetky správy"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Iba zmienky a kľúčové slová"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Zablokovať"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"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ť."</string>
|
||||
<string name="screen_dm_details_block_user">"Zablokovať používateľa"</string>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@
|
|||
<string name="screen_room_details_updating_room">"正在更新聊天室…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"待定"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"聊天室成員"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"全域設定"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"預設"</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"無法重設為預設模式,請再試一次。"</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"無法設定模式,請再試一次。"</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"所有訊息"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"只限提及與關鍵字"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"封鎖"</string>
|
||||
<string name="screen_dm_details_block_user">"封鎖使用者"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@
|
|||
<string name="screen_room_details_updating_room">"Updating room…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Pending"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Room members"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Allow custom setting"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Turning this on will override your default setting"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Notify me in this chat for"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote">"You can change it in your %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"global settings"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Default setting"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"An error occurred while loading notification settings."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Failed restoring the default mode, please try again."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Failed setting the mode, please try again."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime."</string>
|
||||
<string name="screen_dm_details_block_user">"Block user"</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() :
|
|||
when (feature) {
|
||||
FeatureFlags.LocationSharing -> true
|
||||
FeatureFlags.Polls -> false
|
||||
FeatureFlags.NotificationSettings -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<Unit>
|
||||
suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result<RoomNotificationSettings>
|
||||
suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result<RoomNotificationMode>
|
||||
suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit>
|
||||
suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit>
|
||||
suspend fun muteRoom(roomId: RoomId): Result<Unit>
|
||||
suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result<Unit>
|
||||
}
|
||||
|
|
@ -56,11 +56,15 @@ interface MatrixRoom : Closeable {
|
|||
*/
|
||||
val membersStateFlow: StateFlow<MatrixRoomMembersState>
|
||||
|
||||
val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState>
|
||||
|
||||
/**
|
||||
* Try to load the room members and update the membersFlow.
|
||||
*/
|
||||
suspend fun updateMembers(): Result<Unit>
|
||||
|
||||
suspend fun updateRoomNotificationSettings(): Result<Unit>
|
||||
|
||||
val syncUpdateFlow: StateFlow<Long>
|
||||
|
||||
val timeline: MatrixTimeline
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
override val notificationSettingsChangeFlow: SharedFlow<Unit> = _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<RoomNotificationSettings> =
|
||||
runCatching {
|
||||
notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne(membersCount)).let(RoomNotificationSettingsMapper::map)
|
||||
}
|
||||
|
||||
override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result<RoomNotificationMode> =
|
||||
runCatching {
|
||||
notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne(membersCount)).let(RoomNotificationSettingsMapper::mapMode)
|
||||
}
|
||||
|
||||
override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
notificationSettings.restoreDefaultRoomNotificationMode(roomId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun muteRoom(roomId: RoomId): Result<Unit> = 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
|
||||
}
|
||||
|
|
@ -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>(MatrixRoomMembersState.Unknown)
|
||||
private val _syncUpdateFlow = MutableStateFlow(0L)
|
||||
|
||||
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
||||
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
||||
|
||||
private val _timeline by lazy {
|
||||
RustMatrixTimeline(
|
||||
matrixRoom = this,
|
||||
|
|
@ -197,6 +205,22 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun updateRoomNotificationSettings(): Result<Unit> = 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<String?> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.memberAvatarUrl(userId.value)
|
||||
|
|
|
|||
|
|
@ -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<String?> = 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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Unit>
|
||||
get() = _roomNotificationSettingsStateFlow
|
||||
|
||||
override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result<RoomNotificationSettings> {
|
||||
return Result.success(RoomNotificationSettings(mode = roomNotificationMode, isDefault = roomNotificationMode == defaultRoomNotificationMode))
|
||||
}
|
||||
|
||||
override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result<RoomNotificationMode> {
|
||||
return Result.success(defaultRoomNotificationMode)
|
||||
}
|
||||
|
||||
override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> {
|
||||
roomNotificationMode = mode
|
||||
_roomNotificationSettingsStateFlow.emit(Unit)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> {
|
||||
roomNotificationMode = defaultRoomNotificationMode
|
||||
_roomNotificationSettingsStateFlow.emit(Unit)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun muteRoom(roomId: RoomId): Result<Unit> {
|
||||
return setRoomNotificationMode(roomId, RoomNotificationMode.MUTE)
|
||||
}
|
||||
|
||||
override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result<Unit> {
|
||||
return restoreDefaultRoomNotificationMode(roomId)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
|
||||
|
||||
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
|
||||
MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown)
|
||||
|
||||
override suspend fun updateMembers(): Result<Unit> = simulateLongTask {
|
||||
updateMembersResult
|
||||
}
|
||||
|
||||
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
|
||||
val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, activeMemberCount).getOrThrow()
|
||||
roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override val syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0L)
|
||||
|
||||
override val timeline: MatrixTimeline = matrixTimeline
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="Use_an_identity_server_to_invite_by_email.__default_Use_the_default____defaultIdentityServerName_s___default__or_manage_in__settings_Settings__settings_._web">"Use an identity server to invite by email. "<default>"Use the default (%(defaultIdentityServerName)s)"</default>" or manage in "<settings>"Settings"</settings>"."</string>
|
||||
<string name="a11y_hide_password">"Hide password"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
|
||||
<string name="a11y_notifications_muted">"Muted"</string>
|
||||
|
|
@ -161,6 +162,10 @@
|
|||
<string name="leave_room_alert_private_subtitle">"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."</string>
|
||||
<string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<plurals name="__count_s_rooms_web">
|
||||
<item quantity="one">"%(count)s room"</item>
|
||||
<item quantity="other">"%(count)s rooms"</item>
|
||||
</plurals>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d member"</item>
|
||||
<item quantity="other">"%1$d members"</item>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d1c7b862db8afe25dd958c9011de82e1439d298a95ec15a86dfbae0457f7937
|
||||
size 36436
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c1900985fb6b9d25eee44af5cdc0c6fcdee994ffc3da8dae5a6e742c3d0b7127
|
||||
size 40016
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62418ebe7afa2c1c7eda2b1251fb189a75527aa7a4289a9478c531fac7dee8bd
|
||||
size 10666
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5a32b1baaaad0bf266e190084a64b3d382a2d6d472a55b1c3653ee948e100c9
|
||||
size 9756
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6edcc452c76049a978ceb97b46776446790dbdbe30eef1693fafda089cdc3ee7
|
||||
size 55527
|
||||
oid sha256:49bd51a32dbd3615364a6039163a36c08d0096a8c511a7084e21b7c1780131db
|
||||
size 52810
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:20e630b0f3bb9fddd5f11e4e9f2a92b5d35c4bbe182662e8d0fde27451297006
|
||||
size 45247
|
||||
oid sha256:2c61e5d0eabb1f9cae83c3df0f73f14545e0c61bd7393d6e2862e0f48b9e3d7a
|
||||
size 51272
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:553b498247e9da02ad037716a67c080a3da2fb07602b0c0c44de8f16a4891eda
|
||||
size 46086
|
||||
oid sha256:adf48ade91c22ce7f98bf2773ec4e14ce0c290f7ba25e8c76d9f722677c46277
|
||||
size 44388
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d91c291503dc745092b132fec62208e73dcd031822ca21a66a7fb1f48ecfa6e1
|
||||
size 49316
|
||||
oid sha256:262ba970549c3c137ab90ffc607279479dfc4fbf86468e18b08ea6c5ef5b0710
|
||||
size 53634
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:efda43a25fdc2a21a4ad3767b14b4c7a308003f9bcb9e67d64520db13c83f0fd
|
||||
size 61061
|
||||
oid sha256:49d92579bffadd59f89e79217b5e6148bbfeda0c5750a06ad7aa36f8125d6d29
|
||||
size 50616
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:31f2d59ad03a4d9ba8efd5f146668162f3c2095ac90e16dd3634d3e817f7263d
|
||||
size 61238
|
||||
oid sha256:b742f507ba11be8bdf733e5e751fc5d9fe1c0cc2eb070abf9abcffb605783ab3
|
||||
size 52151
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:31f2d59ad03a4d9ba8efd5f146668162f3c2095ac90e16dd3634d3e817f7263d
|
||||
size 61238
|
||||
oid sha256:b742f507ba11be8bdf733e5e751fc5d9fe1c0cc2eb070abf9abcffb605783ab3
|
||||
size 52151
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fba2e76faa5915acf1e9d2ede6b58e1ab73e287aefa6e5ffc643be527350d987
|
||||
size 50147
|
||||
oid sha256:467ca1b17857b053881cb9999c3d6b099949c6b4279188da57383fdb9083041f
|
||||
size 53946
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1089845cd143d07e0d6a363a9dc490ab6f5ae4711fc99843c01e4f074dbdeb98
|
||||
size 55780
|
||||
oid sha256:d8e1ede8c0ea34595e90b2031c9607b99e72c3d7d1e9a778bbfbece98eb23875
|
||||
size 53035
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:59054f5692d41c0d6fe65c6f1bc9d0c4e1e3149aedd987e9335fabff7b46d85f
|
||||
size 57237
|
||||
oid sha256:a9f1d200ef2c93d557a67a29700918bd70439c7d7d8d247d60473b26f0c44eb3
|
||||
size 53851
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f65151d983057ca185c53b4045fb33675f12730fee1b40e3df6d269e2290a05
|
||||
size 47290
|
||||
oid sha256:b06e1652745ceff912734b9bf74543c95f4d420eaf3aec4b388dd101ea1428f8
|
||||
size 53838
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:de6a5ac50e8b30364b9e630d2edbe49c18cc3684da080e2167f41bd689f17700
|
||||
size 48369
|
||||
oid sha256:5d4a381a3c99a7f04a11a2e93cd7c4145fa0bd33a1f7461038b3918c5cd94571
|
||||
size 46676
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:edcf03466e38ce3906583f2701f45d2a9538a6fd7e9d1c9d4822a037b833a512
|
||||
size 50185
|
||||
oid sha256:61add95d28096c65f94764df109d253c7b589058cea324d908616312e7dffc3e
|
||||
size 54723
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:670f05e0c2861234708c7dee4d919a977bbd3aec3e965db631505957871f19fa
|
||||
size 62904
|
||||
oid sha256:3197e871e4b4c39dc714610b54e351c4061149bfc0b5511f78a0c73e8ec2236b
|
||||
size 51824
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9553c9ef84cd1f2e890290e093cb0006f03d1a2f36de5e2b7d104d0eb102b2e2
|
||||
size 63045
|
||||
oid sha256:a0cf7d5d0c91aa7946586e5258077b07b329f387f46b0df0a29c49cc211ed584
|
||||
size 54021
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9553c9ef84cd1f2e890290e093cb0006f03d1a2f36de5e2b7d104d0eb102b2e2
|
||||
size 63045
|
||||
oid sha256:a0cf7d5d0c91aa7946586e5258077b07b329f387f46b0df0a29c49cc211ed584
|
||||
size 54021
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7ab533a35fbbba8b45d94bcfb0637d0f021ee2ef71230754ed418702ee18f11d
|
||||
size 51182
|
||||
oid sha256:cd4be4f49a78aba83c4b5f9c350f8b64cfe96d50890df16154cd7b0e20546d25
|
||||
size 55020
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ec34df75877b7050ad874905926f5d35fee29d1f8324437b502d7152ef87cdc
|
||||
size 57494
|
||||
oid sha256:da79bef2bd54301f2724269719d93d1d9212fd17b9dc0d2a7ba935693c6170ac
|
||||
size 54107
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08af9a706d579b4632c70265b6f1529008bd483cb4924c341e3a9f0172cc8cd4
|
||||
size 39251
|
||||
oid sha256:bd05144d4b3527a44e1c9f3fb92d227ccc38fd7afce5436f32b59f14f0b54e27
|
||||
size 39028
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@
|
|||
"includeRegex": [
|
||||
"screen_room_details_.*",
|
||||
"screen_room_member_list_.*",
|
||||
"screen_dm_details_.*"
|
||||
"screen_dm_details_.*",
|
||||
"screen_room_notification_settings_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue