Add room notification settings

This commit is contained in:
yostyle
2023-07-06 18:49:14 +02:00
parent dd192cb647
commit d930f4130e
31 changed files with 1037 additions and 7 deletions

2
changelog.d/506.feature Normal file
View File

@@ -0,0 +1,2 @@
Add a "Mute" shortcut icon and a "Notifications" section in the room details screen

View File

@@ -18,4 +18,7 @@ package io.element.android.features.roomdetails.impl
sealed interface RoomDetailsEvent {
object LeaveRoom : RoomDetailsEvent
object MuteNotification : RoomDetailsEvent
object UnmuteNotification : RoomDetailsEvent
}

View File

@@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
@@ -66,6 +67,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
object InviteMembers : NavTarget
@Parcelize
object RoomNotificationSettings : NavTarget
@Parcelize
data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
}
@@ -85,6 +89,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
override fun openInviteMembers() {
backstack.push(NavTarget.InviteMembers)
}
override fun openRoomNotificationSettings() {
backstack.push(NavTarget.RoomNotificationSettings)
}
}
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
}
@@ -110,6 +118,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
createNode<RoomInviteMembersNode>(buildContext)
}
NavTarget.RoomNotificationSettings -> {
createNode<RoomNotificationSettingsNode>(buildContext)
}
is NavTarget.RoomMemberDetails -> {
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
createNode<RoomMemberDetailsNode>(buildContext, plugins)

View File

@@ -51,6 +51,7 @@ class RoomDetailsNode @AssistedInject constructor(
fun openRoomMemberList()
fun openInviteMembers()
fun editRoomDetails()
fun openRoomNotificationSettings()
}
private val callbacks = plugins<Callback>()
@@ -67,6 +68,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.openRoomMemberList() }
}
private fun openRoomNotificationSettings() {
callbacks.forEach { it.openRoomNotificationSettings() }
}
private fun invitePeople() {
callbacks.forEach { it.openInviteMembers() }
}
@@ -133,6 +138,7 @@ class RoomDetailsNode @AssistedInject constructor(
onShareRoom = ::onShareRoom,
onShareMember = ::onShareMember,
openRoomMemberList = ::openRoomMemberList,
openRoomNotificationSettings = ::openRoomNotificationSettings,
invitePeople = ::invitePeople,
)
}

View File

@@ -24,30 +24,45 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
class RoomDetailsPresenter @Inject constructor(
private val client: MatrixClient,
private val room: MatrixRoom,
private val notificationSettingsService: NotificationSettingsService,
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
private val leaveRoomPresenter: LeaveRoomPresenter,
private val dispatchers: CoroutineDispatchers,
) : Presenter<RoomDetailsState> {
@Composable
override fun present(): RoomDetailsState {
val scope = rememberCoroutineScope()
val leaveRoomState = leaveRoomPresenter.present()
LaunchedEffect(Unit) {
room.updateMembers()
room.updateRoomNotificationSettings()
observeNotificationSettings()
}
val membersState by room.membersStateFlow.collectAsState()
@@ -69,10 +84,22 @@ class RoomDetailsPresenter @Inject constructor(
}
}
val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState()
fun handleEvents(event: RoomDetailsEvent) {
when (event) {
is RoomDetailsEvent.LeaveRoom ->
RoomDetailsEvent.LeaveRoom ->
leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId))
RoomDetailsEvent.MuteNotification -> {
scope.launch(dispatchers.io) {
client.notificationSettingsService().muteRoom(room.roomId)
}
}
RoomDetailsEvent.UnmuteNotification -> {
scope.launch(dispatchers.io) {
client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.joinedMemberCount.toULong())
}
}
}
}
@@ -91,6 +118,7 @@ class RoomDetailsPresenter @Inject constructor(
roomType = roomType,
roomMemberDetailsState = roomMemberDetailsState,
leaveRoomState = leaveRoomState,
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
eventSink = ::handleEvents,
)
}
@@ -122,4 +150,10 @@ class RoomDetailsPresenter @Inject constructor(
private fun getCanSendState(membersState: MatrixRoomMembersState, type: StateEventType) = produceState(false, membersState) {
value = room.canSendState(type).getOrElse { false }
}
private fun CoroutineScope.observeNotificationSettings() {
notificationSettingsService.notificationSettingsChangeFlow.onEach {
room.updateRoomNotificationSettings()
}.launchIn(this)
}
}

View File

@@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
data class RoomDetailsState(
val roomId: String,
@@ -33,6 +34,7 @@ data class RoomDetailsState(
val canEdit: Boolean,
val canInvite: Boolean,
val leaveRoomState: LeaveRoomState,
val roomNotificationSettings: RoomNotificationSettings?,
val eventSink: (RoomDetailsEvent) -> Unit
)

View File

@@ -22,6 +22,8 @@ import io.element.android.features.roomdetails.impl.members.details.aRoomMemberD
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
override val values: Sequence<RoomDetailsState>
@@ -78,6 +80,7 @@ fun aRoomDetailsState() = RoomDetailsState(
roomType = RoomDetailsType.Room,
roomMemberDetailsState = null,
leaveRoomState = LeaveRoomState(),
roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false),
eventSink = {}
)

View File

@@ -26,12 +26,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.NotificationsOff
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.PersonAddAlt
import androidx.compose.material.icons.outlined.Share
@@ -73,6 +76,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@@ -85,6 +89,7 @@ fun RoomDetailsView(
onShareRoom: () -> Unit,
onShareMember: (RoomMember) -> Unit,
openRoomMemberList: () -> Unit,
openRoomNotificationSettings: () -> Unit,
invitePeople: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -118,7 +123,10 @@ fun RoomDetailsView(
roomName = state.roomName,
roomAlias = state.roomAlias
)
MainActionsSection(onShareRoom = onShareRoom)
MainActionsSection(
state = state,
onShareRoom = onShareRoom
)
}
is RoomDetailsType.Dm -> {
@@ -140,6 +148,12 @@ fun RoomDetailsView(
)
}
if (state.roomNotificationSettings != null) {
NotificationSection(
state = state,
openRoomNotificationSettings = openRoomNotificationSettings)
}
if (state.roomType is RoomDetailsType.Room) {
MembersSection(
memberCount = state.memberCount,
@@ -209,8 +223,21 @@ internal fun RoomDetailsTopBar(
}
@Composable
internal fun MainActionsSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) {
internal fun MainActionsSection(state: RoomDetailsState, onShareRoom: () -> Unit, modifier: Modifier = Modifier) {
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
val roomNotificationSettings = state.roomNotificationSettings
if (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)
}
}
@@ -276,6 +303,21 @@ internal fun TopicSection(
}
}
@Composable
internal fun NotificationSection(state: RoomDetailsState, openRoomNotificationSettings: () -> Unit, modifier: Modifier = Modifier) {
state.roomNotificationSettings?.let {
val subtitle = if (it.isDefault) "Default" else "Custom"
PreferenceCategory(modifier = modifier) {
PreferenceText(
title = stringResource(R.string.screen_room_details_notification_title),
subtitle = subtitle,
icon = Icons.Outlined.Notifications,
onClick = openRoomNotificationSettings,
)
}
}
}
@Composable
internal fun MembersSection(
memberCount: Long,
@@ -348,6 +390,7 @@ private fun ContentToPreview(state: RoomDetailsState) {
onShareRoom = {},
onShareMember = {},
openRoomMemberList = {},
openRoomNotificationSettings = {},
invitePeople = {},
)
}

View File

@@ -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
object DefaultNotificationModeSelected: RoomNotificationSettingsEvents
}

View File

@@ -0,0 +1,49 @@
/*
* 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 io.element.android.libraries.matrix.api.room.RoomNotificationMode
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 = "All messages",
)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationSettingsItem(
mode = it,
title = "Mentions and keywords",
)
RoomNotificationMode.MUTE -> RoomNotificationSettingsItem(
mode = it,
title = "Mute",
)
}
}
.toImmutableList()
}

View File

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

View File

@@ -0,0 +1,102 @@
/*
* 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.material3.MaterialTheme
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
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
@Composable
fun RoomNotificationSettingsOption(
roomNotificationSettingsItem: RoomNotificationSettingsItem,
modifier: Modifier = Modifier,
enabled: Boolean = true,
isSelected: Boolean = false,
onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {},
) {
Row(
modifier
.fillMaxWidth()
.selectable(
selected = isSelected,
onClick = { onOptionSelected(roomNotificationSettingsItem) },
role = Role.RadioButton,
)
.padding(8.dp),
) {
Column(
Modifier
.weight(1f)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
) {
Text(
text = roomNotificationSettingsItem.title,
fontSize = 16.sp,
color = enabled.toEnabledColor(),
)
}
RadioButton(
modifier = Modifier
.align(Alignment.CenterVertically)
.size(48.dp),
selected = isSelected,
enabled = enabled,
onClick = null // null recommended for accessibility with screenreaders
)
}
}
@Preview
@Composable
fun RoomPrivacyOptionLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
fun RoomPrivacyOptionDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Column {
RoomNotificationSettingsOption(
roomNotificationSettingsItem = roomNotificationSettingsItems().first(),
isSelected = true,
)
RoomNotificationSettingsOption(
roomNotificationSettingsItem = roomNotificationSettingsItems().last(),
isSelected = false,
enabled = false,
)
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
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)
}
RoomNotificationSettingsEvents.DefaultNotificationModeSelected -> {
localCoroutineScope.restoreDefaultRoomNotificationMode()
}
}
}
return RoomNotificationSettingsState(
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
defaultRoomNotificationMode = defaultRoomNotificationMode.value,
eventSink = ::handleEvents,
)
}
private fun CoroutineScope.observeNotificationSettings() {
notificationSettingsService.notificationSettingsChangeFlow.onEach {
room.updateRoomNotificationSettings()
}.launchIn(this)
}
private fun CoroutineScope.getDefaultRoomNotificationMode(defaultRoomNotificationMode: MutableState<RoomNotificationMode?>) = launch {
defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode(
room.isEncrypted,
room.joinedMemberCount.toULong()
).getOrThrow()
}
private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode) = launch {
notificationSettingsService.setRoomNotificationMode(room.roomId, mode)
}
private fun CoroutineScope.restoreDefaultRoomNotificationMode() = launch {
notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,169 @@
/*
* 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.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.components.CenterAlignedTopAppBar
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RoomNotificationSettingsView(
state: RoomNotificationSettingsState,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
Scaffold(
topBar = {
RoomNotificationSettingsTopBar(
onBackPressed = { onBackPressed() }
)
}
) { padding ->
Column(
modifier = modifier
.fillMaxWidth()
.padding(padding)
.consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// PreferenceSwitch(
// isChecked = state.formState.sendLogs,
// onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
// enabled = isFormEnabled,
// title = stringResource(id = R.string.screen_bug_report_include_logs),
// subtitle = stringResource(id = R.string.screen_bug_report_logs_description),
// )
val subtitle = when(state.defaultRoomNotificationMode) {
RoomNotificationMode.ALL_MESSAGES -> "All messages"
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> "Mentions and keywords"
RoomNotificationMode.MUTE -> "Mute"
null -> ""
}
PreferenceCategory(title = "Notify me in this chat for") {
PreferenceSwitch(
isChecked = state.roomNotificationSettings?.isDefault.orTrue(),
onCheckedChange = {
state.eventSink(RoomNotificationSettingsEvents.DefaultNotificationModeSelected)
},
title = "Match default setting",
subtitle = subtitle,
enabled = state.roomNotificationSettings != null
)
PreferenceText(
title = "Allow custom setting",
subtitle = "Turning this on will override yout default setting",
enabled = state.roomNotificationSettings != null && !state.roomNotificationSettings.isDefault,
)
if (state.roomNotificationSettings != null) {
RoomNotificationSettingsOptions(
modifier = modifier,
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 = {},
) {
CenterAlignedTopAppBar(
modifier = modifier,
title = {
Text(
text = stringResource(R.string.screen_room_details_notification_title),
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
)
},
navigationIcon = { BackButton(onClick = onBackPressed) },
)
}
@Composable
fun RoomNotificationSettingsOptions(
selected: RoomNotificationMode?,
modifier: Modifier = Modifier,
enabled: Boolean,
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
fun RoomNotificationSettingsLightPreview(@PreviewParameter(RoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun RoomNotificationSettingsDarkPreview(@PreviewParameter(RoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: RoomNotificationSettingsState) {
RoomNotificationSettingsView(state)
}

View File

@@ -28,17 +28,25 @@ import androidx.compose.foundation.progressSemantics
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
import io.element.android.libraries.theme.ElementTheme
/**
@@ -48,6 +56,7 @@ import io.element.android.libraries.theme.ElementTheme
fun PreferenceText(
title: String,
modifier: Modifier = Modifier,
enabled: Boolean = true,
subtitle: String? = null,
currentValue: String? = null,
loadingCurrentValue: Boolean = false,
@@ -68,8 +77,9 @@ fun PreferenceText(
) {
PreferenceIcon(
icon = icon,
enabled = enabled,
isVisible = showIconAreaIfNoIcon,
tintColor = tintColor ?: ElementTheme.materialColors.secondary
tintColor = tintColor ?: enabled.toSecondaryEnabledColor(),
)
Column(
modifier = Modifier
@@ -79,13 +89,13 @@ fun PreferenceText(
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = tintColor ?: ElementTheme.materialColors.primary,
color = tintColor ?: enabled.toEnabledColor(),
)
if (subtitle != null) {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = tintColor ?: ElementTheme.materialColors.secondary,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
}
}
@@ -96,7 +106,7 @@ fun PreferenceText(
.padding(start = 16.dp, end = 8.dp),
text = currentValue,
style = ElementTheme.typography.fontBodyXsMedium,
color = ElementTheme.materialColors.secondary,
color = enabled.toSecondaryEnabledColor(),
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
@@ -135,6 +145,13 @@ private fun ContentToPreview() {
icon = Icons.Default.BugReport,
currentValue = "123",
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = Icons.Default.BugReport,
currentValue = "123",
enabled = false,
)
PreferenceText(
title = "Title",
subtitle = "Some content",

View File

@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@@ -49,6 +50,7 @@ interface MatrixClient : Closeable {
fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService
fun notificationService(): NotificationService
fun notificationSettingsService(): NotificationSettingsService
suspend fun getCacheSize(): Long
/**

View File

@@ -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): Result<RoomNotificationSettings>
suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: ULong): 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: ULong): Result<Unit>
}

View File

@@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable
import java.io.File
@@ -56,11 +57,15 @@ interface MatrixRoom : Closeable {
*/
val membersStateFlow: StateFlow<MatrixRoomMembersState>
val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState>
/**
* Try to load the room members and update the membersFlow.
*/
suspend fun updateMembers(): Result<Unit>
suspend fun updateRoomNotificationSettings(): Result<Unit>
val syncUpdateFlow: StateFlow<Long>
val timeline: MatrixTimeline

View File

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

View File

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

View File

@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
@@ -103,6 +105,7 @@ class RustMatrixClient constructor(
}
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
private val notificationSettingsService = RustNotificationSettingsService(client)
private val isLoggingOut = AtomicBoolean(false)
@@ -168,6 +171,7 @@ class RustMatrixClient constructor(
sessionId = sessionId,
roomListItem = roomListItem,
innerRoom = fullRoom,
roomNotificationSettingsService = notificationSettingsService,
sessionCoroutineScope = sessionCoroutineScope,
coroutineDispatchers = dispatchers,
systemClock = clock,
@@ -272,6 +276,8 @@ class RustMatrixClient constructor(
override fun notificationService(): NotificationService = notificationService
override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService
override fun close() {
sessionCoroutineScope.cancel()
client.setDelegate(null)

View File

@@ -22,6 +22,7 @@ import dagger.Provides
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -35,6 +36,13 @@ object SessionMatrixModule {
}
@Provides
@SingleIn(SessionScope::class)
fun providesNotificationSettingsService(matrixClient: MatrixClient): NotificationSettingsService {
return matrixClient.notificationSettingsService()
}
@Provides
@SingleIn(SessionScope::class)
fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver {
return matrixClient.roomMembershipObserver()
}

View File

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

View File

@@ -0,0 +1,94 @@
/*
* 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.core.RoomId
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.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.NotificationSettings
import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
class RustNotificationSettingsService(
private val client: Client,
) : NotificationSettingsService, NotificationSettingsDelegate {
private val notificationSettings: NotificationSettings = client.getNotificationSettings()
private val _notificationSettingsChangeFlow = MutableSharedFlow<Unit>()
override val notificationSettingsChangeFlow: SharedFlow<Unit> = _notificationSettingsChangeFlow.asSharedFlow()
// override val notificationSettingsChangeFlow = callbackFlow {
// val delegate = object:NotificationSettingsDelegate {
// override fun notificationSettingsDidChange() {
// trySendBlocking(Unit)
// }
// }
// send(Unit)
// notificationSettings.setDelegate(delegate)
// awaitClose {
// // notificationSettings.setDelegate(null)
// }
// }.buffer(Channel.UNLIMITED)
init {
notificationSettings.setDelegate(this)
}
override suspend fun getRoomNotificationSettings(roomId: RoomId): Result<RoomNotificationSettings> =
runCatching {
notificationSettings.getRoomNotificationMode(roomId.value).let(RoomNotificationSettingsMapper::map)
}
override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: ULong): Result<RoomNotificationMode> =
runCatching {
notificationSettings.getDefaultRoomNotificationMode(isEncrypted, membersCount).let(RoomNotificationSettingsMapper::mapMode)
}
override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> =
runCatching {
notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode))
}
override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> =
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: ULong) =
runCatching {
notificationSettings.unmuteRoom(roomId.value, isEncrypted, membersCount)
}
override fun notificationSettingsDidChange() {
_notificationSettingsChangeFlow.tryEmit(Unit)
}
}

View File

@@ -33,16 +33,19 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.util.destroyAll
@@ -72,6 +75,7 @@ class RustMatrixRoom(
override val sessionId: SessionId,
private val roomListItem: RoomListItem,
private val innerRoom: Room,
private val roomNotificationSettingsService: RustNotificationSettingsService,
sessionCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
private val systemClock: SystemClock,
@@ -90,6 +94,10 @@ class RustMatrixRoom(
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
private val _syncUpdateFlow = MutableStateFlow(0L)
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
private val _timeline by lazy {
RustMatrixTimeline(
matrixRoom = this,
@@ -197,6 +205,19 @@ 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).getOrThrow()
}.map {
_roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it)
}.onFailure {
_roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Error(prevRoomNotificationSettings = currentRoomNotificationSettings, failure = it)
}
}
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = withContext(roomDispatcher) {
runCatching {
innerRoom.memberAvatarUrl(userId.value)

View File

@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.pushers.FakePushersService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
@@ -50,6 +52,7 @@ class FakeMatrixClient(
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
private val notificationService: FakeNotificationService = FakeNotificationService(),
private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
private val syncService: FakeSyncService = FakeSyncService(),
private val accountManagementUrlString: Result<String?> = Result.success(null),
) : MatrixClient {
@@ -142,6 +145,7 @@ class FakeMatrixClient(
override fun pushersService(): PushersService = pushersService
override fun notificationService(): NotificationService = notificationService
override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService
override fun roomMembershipObserver(): RoomMembershipObserver {
return RoomMembershipObserver()

View File

@@ -24,6 +24,8 @@ import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
const val A_USER_NAME = "alice"
const val A_PASSWORD = "password"
@@ -51,6 +53,7 @@ const val A_HOMESERVER_URL_2 = "matrix-client.org"
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
val A_ROOM_NOTIFICATION_SETTINGS = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false)
const val AN_AVATAR_URL = "mxc://data"

View File

@@ -0,0 +1,59 @@
/*
* 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_SETTINGS
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
class FakeNotificationSettingsService : NotificationSettingsService {
private var _roomNotificationSettingsStateFlow = MutableStateFlow(Unit)
private val muteRoomResult: Result<Unit> = Result.success(Unit)
private val unmuteRoomResult: Result<Unit> = Result.success(Unit)
private val getRoomNotificationSettingsResult: Result<RoomNotificationSettings> = Result.success(A_ROOM_NOTIFICATION_SETTINGS)
override val notificationSettingsChangeFlow: SharedFlow<Unit>
get() = _roomNotificationSettingsStateFlow
override suspend fun getRoomNotificationSettings(roomId: RoomId): Result<RoomNotificationSettings> {
return getRoomNotificationSettingsResult
}
override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: ULong): Result<RoomNotificationMode> {
TODO("Not yet implemented")
}
override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> {
TODO("Not yet implemented")
}
override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> {
TODO("Not yet implemented")
}
override suspend fun muteRoom(roomId: RoomId): Result<Unit> {
return muteRoomResult
}
override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: ULong): Result<Unit> {
return unmuteRoomResult
}
}

View File

@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
@@ -68,6 +69,9 @@ class FakeMatrixRoom(
private var userAvatarUrlResult = Result.success<String?>(null)
private var updateMembersResult: Result<Unit> = Result.success(Unit)
private var joinRoomResult = Result.success(Unit)
private var updateRoomNotificationSettingsResult: Result<Unit> = Result.success(Unit)
private var acceptInviteResult = Result.success(Unit)
private var rejectInviteResult = Result.success(Unit)
private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true)
private var canRedactResult = Result.success(canRedact)
@@ -128,10 +132,17 @@ class FakeMatrixRoom(
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown)
override suspend fun updateMembers(): Result<Unit> = simulateLongTask {
updateMembersResult
}
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
updateRoomNotificationSettingsResult
}
override val syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0L)
override val timeline: MatrixTimeline = matrixTimeline