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
34a2a0f99c
commit
4a870fc4a5
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.androidutils.clipboard.ClipboardHelper
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -215,7 +214,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) {
|
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) {
|
||||||
suspend {
|
inviteProgress.value = Async.Loading()
|
||||||
|
runCatching {
|
||||||
room.updateMembers()
|
room.updateMembers()
|
||||||
|
|
||||||
val memberList = when (val memberState = room.membersStateFlow.value) {
|
val memberList = when (val memberState = room.membersStateFlow.value) {
|
||||||
|
|
@ -228,7 +228,14 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
room.inviteUserById(member.userId).onFailure { t ->
|
room.inviteUserById(member.userId).onFailure { t ->
|
||||||
Timber.e(t, "Failed to reinvite DM partner")
|
Timber.e(t, "Failed to reinvite DM partner")
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}.runCatchingUpdatingState(inviteProgress)
|
}.fold(
|
||||||
|
onSuccess = {
|
||||||
|
inviteProgress.value = Async.Success(Unit)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
inviteProgress.value = Async.Failure(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
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.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
@ -80,6 +81,7 @@ import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class MessagesPresenterTest {
|
class MessagesPresenterTest {
|
||||||
|
|
||||||
|
|
@ -380,13 +382,13 @@ class MessagesPresenterTest {
|
||||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||||
// When the input field is focused we show the alert
|
// When the input field is focused we show the alert
|
||||||
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
|
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
|
||||||
val focusedState = consumeItemsUntilPredicate { state ->
|
val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state ->
|
||||||
state.showReinvitePrompt
|
state.showReinvitePrompt
|
||||||
}.last()
|
}.last()
|
||||||
assertThat(focusedState.showReinvitePrompt).isTrue()
|
assertThat(focusedState.showReinvitePrompt).isTrue()
|
||||||
// If it's dismissed then we stop showing the alert
|
// If it's dismissed then we stop showing the alert
|
||||||
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel))
|
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel))
|
||||||
val dismissedState = consumeItemsUntilPredicate { state ->
|
val dismissedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state ->
|
||||||
!state.showReinvitePrompt
|
!state.showReinvitePrompt
|
||||||
}.last()
|
}.last()
|
||||||
assertThat(dismissedState.showReinvitePrompt).isFalse()
|
assertThat(dismissedState.showReinvitePrompt).isFalse()
|
||||||
|
|
@ -470,7 +472,9 @@ class MessagesPresenterTest {
|
||||||
val initialState = consumeItemsUntilTimeout().last()
|
val initialState = consumeItemsUntilTimeout().last()
|
||||||
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
|
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val loadingState = awaitItem()
|
val loadingState = consumeItemsUntilPredicate { state ->
|
||||||
|
state.inviteProgress.isLoading()
|
||||||
|
}.last()
|
||||||
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
|
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
|
||||||
val newState = awaitItem()
|
val newState = awaitItem()
|
||||||
assertThat(newState.inviteProgress.isSuccess()).isTrue()
|
assertThat(newState.inviteProgress.isSuccess()).isTrue()
|
||||||
|
|
@ -595,7 +599,7 @@ class MessagesPresenterTest {
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
mediaPickerProvider = FakePickerProvider(),
|
mediaPickerProvider = FakePickerProvider(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)),
|
||||||
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
|
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
|
||||||
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
|
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.mediapickers.api)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
|
implementation(projects.libraries.featureflag.api)
|
||||||
api(projects.features.roomdetails.api)
|
api(projects.features.roomdetails.api)
|
||||||
api(projects.libraries.usersearch.api)
|
api(projects.libraries.usersearch.api)
|
||||||
api(projects.services.apperror.api)
|
api(projects.services.apperror.api)
|
||||||
|
|
@ -59,6 +60,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.features.leaveroom.fake)
|
testImplementation(projects.features.leaveroom.fake)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,6 @@ package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
sealed interface RoomDetailsEvent {
|
sealed interface RoomDetailsEvent {
|
||||||
data object LeaveRoom : 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.invite.RoomInviteMembersNode
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
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.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.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
|
@ -66,6 +67,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object InviteMembers : NavTarget
|
data object InviteMembers : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object RoomNotificationSettings : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
|
data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +89,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
override fun openInviteMembers() {
|
override fun openInviteMembers() {
|
||||||
backstack.push(NavTarget.InviteMembers)
|
backstack.push(NavTarget.InviteMembers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openRoomNotificationSettings() {
|
||||||
|
backstack.push(NavTarget.RoomNotificationSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
||||||
}
|
}
|
||||||
|
|
@ -110,6 +118,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
createNode<RoomInviteMembersNode>(buildContext)
|
createNode<RoomInviteMembersNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavTarget.RoomNotificationSettings -> {
|
||||||
|
createNode<RoomNotificationSettingsNode>(buildContext)
|
||||||
|
}
|
||||||
|
|
||||||
is NavTarget.RoomMemberDetails -> {
|
is NavTarget.RoomMemberDetails -> {
|
||||||
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
|
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
|
||||||
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
fun openRoomMemberList()
|
fun openRoomMemberList()
|
||||||
fun openInviteMembers()
|
fun openInviteMembers()
|
||||||
fun editRoomDetails()
|
fun editRoomDetails()
|
||||||
|
fun openRoomNotificationSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callbacks = plugins<Callback>()
|
private val callbacks = plugins<Callback>()
|
||||||
|
|
@ -67,6 +68,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
callbacks.forEach { it.openRoomMemberList() }
|
callbacks.forEach { it.openRoomMemberList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openRoomNotificationSettings() {
|
||||||
|
callbacks.forEach { it.openRoomNotificationSettings() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun invitePeople() {
|
private fun invitePeople() {
|
||||||
callbacks.forEach { it.openInviteMembers() }
|
callbacks.forEach { it.openInviteMembers() }
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +138,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
onShareRoom = ::onShareRoom,
|
onShareRoom = ::onShareRoom,
|
||||||
onShareMember = ::onShareMember,
|
onShareMember = ::onShareMember,
|
||||||
openRoomMemberList = ::openRoomMemberList,
|
openRoomMemberList = ::openRoomMemberList,
|
||||||
|
openRoomNotificationSettings = ::openRoomNotificationSettings,
|
||||||
invitePeople = ::invitePeople,
|
invitePeople = ::invitePeople,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,31 +22,55 @@ import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
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.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
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.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
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.canInvite
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
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 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
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomDetailsPresenter @Inject constructor(
|
class RoomDetailsPresenter @Inject constructor(
|
||||||
|
private val client: MatrixClient,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val notificationSettingsService: NotificationSettingsService,
|
||||||
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
|
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
|
||||||
private val leaveRoomPresenter: LeaveRoomPresenter,
|
private val leaveRoomPresenter: LeaveRoomPresenter,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) : Presenter<RoomDetailsState> {
|
) : Presenter<RoomDetailsState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomDetailsState {
|
override fun present(): RoomDetailsState {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val leaveRoomState = leaveRoomPresenter.present()
|
val leaveRoomState = leaveRoomPresenter.present()
|
||||||
|
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
|
||||||
|
if (canShowNotificationSettings.value) {
|
||||||
|
room.updateRoomNotificationSettings()
|
||||||
|
observeNotificationSettings()
|
||||||
|
}
|
||||||
room.updateMembers()
|
room.updateMembers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,10 +93,22 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState()
|
||||||
|
|
||||||
fun handleEvents(event: RoomDetailsEvent) {
|
fun handleEvents(event: RoomDetailsEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomDetailsEvent.LeaveRoom ->
|
RoomDetailsEvent.LeaveRoom ->
|
||||||
leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId))
|
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,
|
isEncrypted = room.isEncrypted,
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room,
|
canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room,
|
||||||
|
canShowNotificationSettings = canShowNotificationSettings.value,
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
roomMemberDetailsState = roomMemberDetailsState,
|
roomMemberDetailsState = roomMemberDetailsState,
|
||||||
leaveRoomState = leaveRoomState,
|
leaveRoomState = leaveRoomState,
|
||||||
|
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -122,4 +160,10 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
private fun getCanSendState(membersState: MatrixRoomMembersState, type: StateEventType) = produceState(false, membersState) {
|
private fun getCanSendState(membersState: MatrixRoomMembersState, type: StateEventType) = produceState(false, membersState) {
|
||||||
value = room.canSendState(type).getOrElse { false }
|
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.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
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.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
|
|
||||||
data class RoomDetailsState(
|
data class RoomDetailsState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
|
|
@ -32,7 +33,9 @@ data class RoomDetailsState(
|
||||||
val roomMemberDetailsState: RoomMemberDetailsState?,
|
val roomMemberDetailsState: RoomMemberDetailsState?,
|
||||||
val canEdit: Boolean,
|
val canEdit: Boolean,
|
||||||
val canInvite: Boolean,
|
val canInvite: Boolean,
|
||||||
|
val canShowNotificationSettings: Boolean,
|
||||||
val leaveRoomState: LeaveRoomState,
|
val leaveRoomState: LeaveRoomState,
|
||||||
|
val roomNotificationSettings: RoomNotificationSettings?,
|
||||||
val eventSink: (RoomDetailsEvent) -> Unit
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
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.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
|
|
||||||
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
||||||
override val values: Sequence<RoomDetailsState>
|
override val values: Sequence<RoomDetailsState>
|
||||||
|
|
@ -75,9 +77,11 @@ fun aRoomDetailsState() = RoomDetailsState(
|
||||||
isEncrypted = true,
|
isEncrypted = true,
|
||||||
canInvite = false,
|
canInvite = false,
|
||||||
canEdit = false,
|
canEdit = false,
|
||||||
|
canShowNotificationSettings = true,
|
||||||
roomType = RoomDetailsType.Room,
|
roomType = RoomDetailsType.Room,
|
||||||
roomMemberDetailsState = null,
|
roomMemberDetailsState = null,
|
||||||
leaveRoomState = LeaveRoomState(),
|
leaveRoomState = LeaveRoomState(),
|
||||||
|
roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false),
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
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.Person
|
||||||
import androidx.compose.material.icons.outlined.PersonAddAlt
|
import androidx.compose.material.icons.outlined.PersonAddAlt
|
||||||
import androidx.compose.material.icons.outlined.Share
|
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.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
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.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -87,6 +91,7 @@ fun RoomDetailsView(
|
||||||
onShareRoom: () -> Unit,
|
onShareRoom: () -> Unit,
|
||||||
onShareMember: (RoomMember) -> Unit,
|
onShareMember: (RoomMember) -> Unit,
|
||||||
openRoomMemberList: () -> Unit,
|
openRoomMemberList: () -> Unit,
|
||||||
|
openRoomNotificationSettings: () -> Unit,
|
||||||
invitePeople: () -> Unit,
|
invitePeople: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -120,7 +125,10 @@ fun RoomDetailsView(
|
||||||
roomName = state.roomName,
|
roomName = state.roomName,
|
||||||
roomAlias = state.roomAlias
|
roomAlias = state.roomAlias
|
||||||
)
|
)
|
||||||
MainActionsSection(onShareRoom = onShareRoom)
|
MainActionsSection(
|
||||||
|
state = state,
|
||||||
|
onShareRoom = onShareRoom
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is RoomDetailsType.Dm -> {
|
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) {
|
if (state.roomType is RoomDetailsType.Room) {
|
||||||
MembersSection(
|
MembersSection(
|
||||||
memberCount = state.memberCount,
|
memberCount = state.memberCount,
|
||||||
|
|
@ -211,8 +225,21 @@ internal fun RoomDetailsTopBar(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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) {
|
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)
|
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
|
@Composable
|
||||||
internal fun MembersSection(
|
internal fun MembersSection(
|
||||||
memberCount: Long,
|
memberCount: Long,
|
||||||
|
|
@ -352,6 +400,7 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
||||||
onShareRoom = {},
|
onShareRoom = {},
|
||||||
onShareMember = {},
|
onShareMember = {},
|
||||||
openRoomMemberList = {},
|
openRoomMemberList = {},
|
||||||
|
openRoomNotificationSettings = {},
|
||||||
invitePeople = {},
|
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_share_room_title">"Partager le salon"</string>
|
||||||
<string name="screen_room_details_updating_room">"Mise à jour du 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_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_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_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>
|
<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_details_updating_room">"Обновление комнаты…"</string>
|
||||||
<string name="screen_room_member_list_pending_header_title">"В ожидании"</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_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_action">"Заблокировать"</string>
|
||||||
<string name="screen_dm_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
|
<string name="screen_dm_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
|
||||||
<string name="screen_dm_details_block_user">"Заблокировать пользователя"</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_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_pending_header_title">"Čaká sa"</string>
|
||||||
<string name="screen_room_member_list_room_members_header_title">"Členovia miestnosti"</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_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_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>
|
<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_details_updating_room">"正在更新聊天室…"</string>
|
||||||
<string name="screen_room_member_list_pending_header_title">"待定"</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_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_alert_action">"封鎖"</string>
|
||||||
<string name="screen_dm_details_block_user">"封鎖使用者"</string>
|
<string name="screen_dm_details_block_user">"封鎖使用者"</string>
|
||||||
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</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_details_updating_room">"Updating room…"</string>
|
||||||
<string name="screen_room_member_list_pending_header_title">"Pending"</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_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_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_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>
|
<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.RoomTopicState
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
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.api.room.StateEventType
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
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.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class RoomDetailsPresenterTests {
|
class RoomDetailsPresenterTests {
|
||||||
|
|
@ -52,20 +60,37 @@ class RoomDetailsPresenterTests {
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val warmUpRule = WarmUpRule()
|
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 {
|
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
|
||||||
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
|
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
|
@Test
|
||||||
fun `present - initial state is created from room info`() = runTest {
|
fun `present - initial state is created from room info`() = runTest {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -84,7 +109,7 @@ class RoomDetailsPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state with no room name`() = runTest {
|
fun `present - initial state with no room name`() = runTest {
|
||||||
val room = aMatrixRoom(name = null)
|
val room = aMatrixRoom(name = null)
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -106,7 +131,7 @@ class RoomDetailsPresenterTests {
|
||||||
val roomMembers = listOf(myRoomMember, otherRoomMember)
|
val roomMembers = listOf(myRoomMember, otherRoomMember)
|
||||||
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -122,7 +147,7 @@ class RoomDetailsPresenterTests {
|
||||||
val room = aMatrixRoom().apply {
|
val room = aMatrixRoom().apply {
|
||||||
givenCanInviteResult(Result.success(true))
|
givenCanInviteResult(Result.success(true))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -140,11 +165,13 @@ class RoomDetailsPresenterTests {
|
||||||
val room = aMatrixRoom().apply {
|
val room = aMatrixRoom().apply {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(awaitItem().canInvite).isFalse()
|
assertThat(awaitItem().canInvite).isFalse()
|
||||||
|
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,11 +180,13 @@ class RoomDetailsPresenterTests {
|
||||||
val room = aMatrixRoom().apply {
|
val room = aMatrixRoom().apply {
|
||||||
givenCanInviteResult(Result.failure(Throwable("Whoops")))
|
givenCanInviteResult(Result.failure(Throwable("Whoops")))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(awaitItem().canInvite).isFalse()
|
assertThat(awaitItem().canInvite).isFalse()
|
||||||
|
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +198,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp")))
|
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp")))
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -198,7 +227,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
|
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -227,7 +256,7 @@ class RoomDetailsPresenterTests {
|
||||||
|
|
||||||
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
|
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -248,7 +277,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
|
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -269,12 +298,14 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false))
|
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false))
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Initially false, and no further events
|
// Initially false, and no further events
|
||||||
assertThat(awaitItem().canEdit).isFalse()
|
assertThat(awaitItem().canEdit).isFalse()
|
||||||
|
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,12 +316,14 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// The initial state is "hidden" and no further state changes happen
|
// The initial state is "hidden" and no further state changes happen
|
||||||
assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden)
|
assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden)
|
||||||
|
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,7 +334,7 @@ class RoomDetailsPresenterTests {
|
||||||
givenCanInviteResult(Result.success(false))
|
givenCanInviteResult(Result.success(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -319,7 +352,7 @@ class RoomDetailsPresenterTests {
|
||||||
fun `present - leave room event is passed on to leave room presenter`() = runTest {
|
fun `present - leave room event is passed on to leave room presenter`() = runTest {
|
||||||
val leaveRoomPresenter = LeaveRoomPresenterFake()
|
val leaveRoomPresenter = LeaveRoomPresenterFake()
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter)
|
val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers())
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -330,6 +363,64 @@ class RoomDetailsPresenterTests {
|
||||||
cancelAndIgnoreRemainingEvents()
|
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(
|
fun aMatrixRoom(
|
||||||
|
|
@ -341,6 +432,7 @@ fun aMatrixRoom(
|
||||||
isEncrypted: Boolean = true,
|
isEncrypted: Boolean = true,
|
||||||
isPublic: Boolean = true,
|
isPublic: Boolean = true,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
|
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService()
|
||||||
) = FakeMatrixRoom(
|
) = FakeMatrixRoom(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -350,5 +442,6 @@ fun aMatrixRoom(
|
||||||
isEncrypted = isEncrypted,
|
isEncrypted = isEncrypted,
|
||||||
isPublic = isPublic,
|
isPublic = isPublic,
|
||||||
isDirect = isDirect,
|
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.preview.PreviewGroup
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -48,6 +50,7 @@ import io.element.android.libraries.theme.ElementTheme
|
||||||
fun PreferenceText(
|
fun PreferenceText(
|
||||||
title: String,
|
title: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
subtitle: String? = null,
|
subtitle: String? = null,
|
||||||
currentValue: String? = null,
|
currentValue: String? = null,
|
||||||
loadingCurrentValue: Boolean = false,
|
loadingCurrentValue: Boolean = false,
|
||||||
|
|
@ -68,8 +71,9 @@ fun PreferenceText(
|
||||||
) {
|
) {
|
||||||
PreferenceIcon(
|
PreferenceIcon(
|
||||||
icon = icon,
|
icon = icon,
|
||||||
|
enabled = enabled,
|
||||||
isVisible = showIconAreaIfNoIcon,
|
isVisible = showIconAreaIfNoIcon,
|
||||||
tintColor = tintColor ?: ElementTheme.materialColors.secondary
|
tintColor = tintColor ?: enabled.toSecondaryEnabledColor(),
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -79,13 +83,13 @@ fun PreferenceText(
|
||||||
Text(
|
Text(
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
text = title,
|
text = title,
|
||||||
color = tintColor ?: ElementTheme.materialColors.primary,
|
color = tintColor ?: enabled.toEnabledColor(),
|
||||||
)
|
)
|
||||||
if (subtitle != null) {
|
if (subtitle != null) {
|
||||||
Text(
|
Text(
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
text = subtitle,
|
text = subtitle,
|
||||||
color = tintColor ?: ElementTheme.materialColors.secondary,
|
color = tintColor ?: enabled.toSecondaryEnabledColor(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +100,7 @@ fun PreferenceText(
|
||||||
.padding(start = 16.dp, end = 8.dp),
|
.padding(start = 16.dp, end = 8.dp),
|
||||||
text = currentValue,
|
text = currentValue,
|
||||||
style = ElementTheme.typography.fontBodyXsMedium,
|
style = ElementTheme.typography.fontBodyXsMedium,
|
||||||
color = ElementTheme.materialColors.secondary,
|
color = enabled.toSecondaryEnabledColor(),
|
||||||
)
|
)
|
||||||
} else if (loadingCurrentValue) {
|
} else if (loadingCurrentValue) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
|
|
@ -135,6 +139,13 @@ private fun ContentToPreview() {
|
||||||
icon = Icons.Default.BugReport,
|
icon = Icons.Default.BugReport,
|
||||||
currentValue = "123",
|
currentValue = "123",
|
||||||
)
|
)
|
||||||
|
PreferenceText(
|
||||||
|
title = "Title",
|
||||||
|
subtitle = "Some content",
|
||||||
|
icon = Icons.Default.BugReport,
|
||||||
|
currentValue = "123",
|
||||||
|
enabled = false,
|
||||||
|
)
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = "Title",
|
title = "Title",
|
||||||
subtitle = "Some content",
|
subtitle = "Some content",
|
||||||
|
|
|
||||||
|
|
@ -30,5 +30,11 @@ enum class FeatureFlags(
|
||||||
key = "feature.polls",
|
key = "feature.polls",
|
||||||
title = "Polls",
|
title = "Polls",
|
||||||
description = "Create poll and render poll events in the timeline",
|
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) {
|
when (feature) {
|
||||||
FeatureFlags.LocationSharing -> true
|
FeatureFlags.LocationSharing -> true
|
||||||
FeatureFlags.Polls -> false
|
FeatureFlags.Polls -> false
|
||||||
|
FeatureFlags.NotificationSettings -> false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
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.createroom.CreateRoomParameters
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
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.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.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
|
@ -49,6 +50,7 @@ interface MatrixClient : Closeable {
|
||||||
fun sessionVerificationService(): SessionVerificationService
|
fun sessionVerificationService(): SessionVerificationService
|
||||||
fun pushersService(): PushersService
|
fun pushersService(): PushersService
|
||||||
fun notificationService(): NotificationService
|
fun notificationService(): NotificationService
|
||||||
|
fun notificationSettingsService(): NotificationSettingsService
|
||||||
suspend fun getCacheSize(): Long
|
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 membersStateFlow: StateFlow<MatrixRoomMembersState>
|
||||||
|
|
||||||
|
val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to load the room members and update the membersFlow.
|
* Try to load the room members and update the membersFlow.
|
||||||
*/
|
*/
|
||||||
suspend fun updateMembers(): Result<Unit>
|
suspend fun updateMembers(): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun updateRoomNotificationSettings(): Result<Unit>
|
||||||
|
|
||||||
val syncUpdateFlow: StateFlow<Long>
|
val syncUpdateFlow: StateFlow<Long>
|
||||||
|
|
||||||
val timeline: MatrixTimeline
|
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.createroom.RoomVisibility
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
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.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.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
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.mapper.toSessionData
|
||||||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
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.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.pushers.RustPushersService
|
||||||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||||
|
|
@ -99,6 +101,7 @@ class RustMatrixClient constructor(
|
||||||
client = client,
|
client = client,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService)
|
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService)
|
||||||
private val notificationClient = client.notificationClient(notificationProcessSetup)
|
private val notificationClient = client.notificationClient(notificationProcessSetup)
|
||||||
.use { builder ->
|
.use { builder ->
|
||||||
|
|
@ -106,8 +109,10 @@ class RustMatrixClient constructor(
|
||||||
.filterByPushRules()
|
.filterByPushRules()
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
private val notificationSettings = client.getNotificationSettings()
|
||||||
|
|
||||||
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
|
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
|
||||||
|
private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers)
|
||||||
|
|
||||||
private val isLoggingOut = AtomicBoolean(false)
|
private val isLoggingOut = AtomicBoolean(false)
|
||||||
|
|
||||||
|
|
@ -173,6 +178,7 @@ class RustMatrixClient constructor(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
roomListItem = roomListItem,
|
roomListItem = roomListItem,
|
||||||
innerRoom = fullRoom,
|
innerRoom = fullRoom,
|
||||||
|
roomNotificationSettingsService = notificationSettingsService,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
coroutineDispatchers = dispatchers,
|
coroutineDispatchers = dispatchers,
|
||||||
systemClock = clock,
|
systemClock = clock,
|
||||||
|
|
@ -277,9 +283,13 @@ class RustMatrixClient constructor(
|
||||||
|
|
||||||
override fun notificationService(): NotificationService = notificationService
|
override fun notificationService(): NotificationService = notificationService
|
||||||
|
|
||||||
|
override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
sessionCoroutineScope.cancel()
|
sessionCoroutineScope.cancel()
|
||||||
client.setDelegate(null)
|
client.setDelegate(null)
|
||||||
|
notificationSettings.setDelegate(null)
|
||||||
|
notificationSettings.destroy()
|
||||||
verificationService.destroy()
|
verificationService.destroy()
|
||||||
syncService.destroy()
|
syncService.destroy()
|
||||||
innerRoomListService.destroy()
|
innerRoomListService.destroy()
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import dagger.Provides
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
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.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
|
@ -34,6 +35,11 @@ object SessionMatrixModule {
|
||||||
return matrixClient.sessionVerificationService()
|
return matrixClient.sessionVerificationService()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesNotificationSettingsService(matrixClient: MatrixClient): NotificationSettingsService {
|
||||||
|
return matrixClient.notificationSettingsService()
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver {
|
fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver {
|
||||||
return 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.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
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.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.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
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.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
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.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
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.core.toProgressWatcher
|
||||||
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
|
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.media.map
|
||||||
import io.element.android.libraries.matrix.impl.poll.toInner
|
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.room.location.toInner
|
||||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||||
|
|
@ -72,6 +75,7 @@ class RustMatrixRoom(
|
||||||
override val sessionId: SessionId,
|
override val sessionId: SessionId,
|
||||||
private val roomListItem: RoomListItem,
|
private val roomListItem: RoomListItem,
|
||||||
private val innerRoom: Room,
|
private val innerRoom: Room,
|
||||||
|
private val roomNotificationSettingsService: RustNotificationSettingsService,
|
||||||
sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val systemClock: SystemClock,
|
private val systemClock: SystemClock,
|
||||||
|
|
@ -90,6 +94,10 @@ class RustMatrixRoom(
|
||||||
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
|
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
|
||||||
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
|
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
|
||||||
private val _syncUpdateFlow = MutableStateFlow(0L)
|
private val _syncUpdateFlow = MutableStateFlow(0L)
|
||||||
|
|
||||||
|
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
||||||
|
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
||||||
|
|
||||||
private val _timeline by lazy {
|
private val _timeline by lazy {
|
||||||
RustMatrixTimeline(
|
RustMatrixTimeline(
|
||||||
matrixRoom = this,
|
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) {
|
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.memberAvatarUrl(userId.value)
|
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.createroom.CreateRoomParameters
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
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.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.pusher.PushersService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
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.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
|
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.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.pushers.FakePushersService
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
|
@ -50,6 +52,7 @@ class FakeMatrixClient(
|
||||||
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||||
private val pushersService: FakePushersService = FakePushersService(),
|
private val pushersService: FakePushersService = FakePushersService(),
|
||||||
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
||||||
|
private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
private val syncService: FakeSyncService = FakeSyncService(),
|
private val syncService: FakeSyncService = FakeSyncService(),
|
||||||
private val accountManagementUrlString: Result<String?> = Result.success(null),
|
private val accountManagementUrlString: Result<String?> = Result.success(null),
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
|
|
@ -142,6 +145,7 @@ class FakeMatrixClient(
|
||||||
override fun pushersService(): PushersService = pushersService
|
override fun pushersService(): PushersService = pushersService
|
||||||
|
|
||||||
override fun notificationService(): NotificationService = notificationService
|
override fun notificationService(): NotificationService = notificationService
|
||||||
|
override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService
|
||||||
|
|
||||||
override fun roomMembershipObserver(): RoomMembershipObserver {
|
override fun roomMembershipObserver(): RoomMembershipObserver {
|
||||||
return 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.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
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.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_USER_NAME = "alice"
|
||||||
const val A_PASSWORD = "password"
|
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 = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
|
||||||
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
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"
|
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.ImageInfo
|
||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
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.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.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
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.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
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.StateEventType
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
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.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
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.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -58,6 +61,7 @@ class FakeMatrixRoom(
|
||||||
override val isDirect: Boolean = false,
|
override val isDirect: Boolean = false,
|
||||||
override val joinedMemberCount: Long = 123L,
|
override val joinedMemberCount: Long = 123L,
|
||||||
override val activeMemberCount: Long = 234L,
|
override val activeMemberCount: Long = 234L,
|
||||||
|
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||||
canRedact: Boolean = false,
|
canRedact: Boolean = false,
|
||||||
) : MatrixRoom {
|
) : MatrixRoom {
|
||||||
|
|
@ -136,10 +140,19 @@ class FakeMatrixRoom(
|
||||||
|
|
||||||
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
|
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
|
||||||
|
|
||||||
|
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
|
||||||
|
MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown)
|
||||||
|
|
||||||
override suspend fun updateMembers(): Result<Unit> = simulateLongTask {
|
override suspend fun updateMembers(): Result<Unit> = simulateLongTask {
|
||||||
updateMembersResult
|
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 syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0L)
|
||||||
|
|
||||||
override val timeline: MatrixTimeline = matrixTimeline
|
override val timeline: MatrixTimeline = matrixTimeline
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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_hide_password">"Hide password"</string>
|
||||||
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
|
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
|
||||||
<string name="a11y_notifications_muted">"Muted"</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_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="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>
|
<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">
|
<plurals name="common_member_count">
|
||||||
<item quantity="one">"%1$d member"</item>
|
<item quantity="one">"%1$d member"</item>
|
||||||
<item quantity="other">"%1$d members"</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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6edcc452c76049a978ceb97b46776446790dbdbe30eef1693fafda089cdc3ee7
|
oid sha256:49bd51a32dbd3615364a6039163a36c08d0096a8c511a7084e21b7c1780131db
|
||||||
size 55527
|
size 52810
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:20e630b0f3bb9fddd5f11e4e9f2a92b5d35c4bbe182662e8d0fde27451297006
|
oid sha256:2c61e5d0eabb1f9cae83c3df0f73f14545e0c61bd7393d6e2862e0f48b9e3d7a
|
||||||
size 45247
|
size 51272
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:553b498247e9da02ad037716a67c080a3da2fb07602b0c0c44de8f16a4891eda
|
oid sha256:adf48ade91c22ce7f98bf2773ec4e14ce0c290f7ba25e8c76d9f722677c46277
|
||||||
size 46086
|
size 44388
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d91c291503dc745092b132fec62208e73dcd031822ca21a66a7fb1f48ecfa6e1
|
oid sha256:262ba970549c3c137ab90ffc607279479dfc4fbf86468e18b08ea6c5ef5b0710
|
||||||
size 49316
|
size 53634
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:efda43a25fdc2a21a4ad3767b14b4c7a308003f9bcb9e67d64520db13c83f0fd
|
oid sha256:49d92579bffadd59f89e79217b5e6148bbfeda0c5750a06ad7aa36f8125d6d29
|
||||||
size 61061
|
size 50616
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:31f2d59ad03a4d9ba8efd5f146668162f3c2095ac90e16dd3634d3e817f7263d
|
oid sha256:b742f507ba11be8bdf733e5e751fc5d9fe1c0cc2eb070abf9abcffb605783ab3
|
||||||
size 61238
|
size 52151
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:31f2d59ad03a4d9ba8efd5f146668162f3c2095ac90e16dd3634d3e817f7263d
|
oid sha256:b742f507ba11be8bdf733e5e751fc5d9fe1c0cc2eb070abf9abcffb605783ab3
|
||||||
size 61238
|
size 52151
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fba2e76faa5915acf1e9d2ede6b58e1ab73e287aefa6e5ffc643be527350d987
|
oid sha256:467ca1b17857b053881cb9999c3d6b099949c6b4279188da57383fdb9083041f
|
||||||
size 50147
|
size 53946
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1089845cd143d07e0d6a363a9dc490ab6f5ae4711fc99843c01e4f074dbdeb98
|
oid sha256:d8e1ede8c0ea34595e90b2031c9607b99e72c3d7d1e9a778bbfbece98eb23875
|
||||||
size 55780
|
size 53035
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:59054f5692d41c0d6fe65c6f1bc9d0c4e1e3149aedd987e9335fabff7b46d85f
|
oid sha256:a9f1d200ef2c93d557a67a29700918bd70439c7d7d8d247d60473b26f0c44eb3
|
||||||
size 57237
|
size 53851
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6f65151d983057ca185c53b4045fb33675f12730fee1b40e3df6d269e2290a05
|
oid sha256:b06e1652745ceff912734b9bf74543c95f4d420eaf3aec4b388dd101ea1428f8
|
||||||
size 47290
|
size 53838
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:de6a5ac50e8b30364b9e630d2edbe49c18cc3684da080e2167f41bd689f17700
|
oid sha256:5d4a381a3c99a7f04a11a2e93cd7c4145fa0bd33a1f7461038b3918c5cd94571
|
||||||
size 48369
|
size 46676
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:edcf03466e38ce3906583f2701f45d2a9538a6fd7e9d1c9d4822a037b833a512
|
oid sha256:61add95d28096c65f94764df109d253c7b589058cea324d908616312e7dffc3e
|
||||||
size 50185
|
size 54723
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:670f05e0c2861234708c7dee4d919a977bbd3aec3e965db631505957871f19fa
|
oid sha256:3197e871e4b4c39dc714610b54e351c4061149bfc0b5511f78a0c73e8ec2236b
|
||||||
size 62904
|
size 51824
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9553c9ef84cd1f2e890290e093cb0006f03d1a2f36de5e2b7d104d0eb102b2e2
|
oid sha256:a0cf7d5d0c91aa7946586e5258077b07b329f387f46b0df0a29c49cc211ed584
|
||||||
size 63045
|
size 54021
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9553c9ef84cd1f2e890290e093cb0006f03d1a2f36de5e2b7d104d0eb102b2e2
|
oid sha256:a0cf7d5d0c91aa7946586e5258077b07b329f387f46b0df0a29c49cc211ed584
|
||||||
size 63045
|
size 54021
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7ab533a35fbbba8b45d94bcfb0637d0f021ee2ef71230754ed418702ee18f11d
|
oid sha256:cd4be4f49a78aba83c4b5f9c350f8b64cfe96d50890df16154cd7b0e20546d25
|
||||||
size 51182
|
size 55020
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9ec34df75877b7050ad874905926f5d35fee29d1f8324437b502d7152ef87cdc
|
oid sha256:da79bef2bd54301f2724269719d93d1d9212fd17b9dc0d2a7ba935693c6170ac
|
||||||
size 57494
|
size 54107
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:08af9a706d579b4632c70265b6f1529008bd483cb4924c341e3a9f0172cc8cd4
|
oid sha256:bd05144d4b3527a44e1c9f3fb92d227ccc38fd7afce5436f32b59f14f0b54e27
|
||||||
size 39251
|
size 39028
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@
|
||||||
"includeRegex": [
|
"includeRegex": [
|
||||||
"screen_room_details_.*",
|
"screen_room_details_.*",
|
||||||
"screen_room_member_list_.*",
|
"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