Add "Mark as read", "Mark as unread" in room settings.

#6398
This commit is contained in:
Benoit Marty 2026-05-19 18:06:11 +02:00
parent 5af57906b5
commit 882d5577a7
8 changed files with 89 additions and 1 deletions

View file

@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
implementation(projects.libraries.testtags)
api(projects.features.roomdetails.api)
api(projects.libraries.usersearch.api)

View file

@ -14,4 +14,6 @@ sealed interface RoomDetailsEvent {
data object UnmuteNotification : RoomDetailsEvent
data class CopyToClipboard(val text: String) : RoomDetailsEvent
data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent
data object MarkAsRead : RoomDetailsEvent
data object MarkAsUnread : RoomDetailsEvent
}

View file

@ -44,13 +44,17 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -67,6 +71,8 @@ class RoomDetailsPresenter(
private val analyticsService: AnalyticsService,
private val clipboardHelper: ClipboardHelper,
private val appPreferencesStore: AppPreferencesStore,
private val sessionPreferencesStore: SessionPreferencesStore,
private val notificationCleaner: NotificationCleaner,
) : Presenter<RoomDetailsState> {
@Composable
override fun present(): RoomDetailsState {
@ -79,6 +85,14 @@ class RoomDetailsPresenter(
val roomTopic by remember { derivedStateOf { roomInfo.topic } }
val isFavorite by remember { derivedStateOf { roomInfo.isFavorite } }
val joinRule by remember { derivedStateOf { roomInfo.joinRule } }
val hasNewContent by remember {
derivedStateOf {
roomInfo.numUnreadMessages > 0 ||
roomInfo.numUnreadMentions > 0 ||
roomInfo.numUnreadNotifications > 0 ||
roomInfo.isMarkedUnread
}
}
val pinnedMessagesCount by remember { derivedStateOf { roomInfo.pinnedEventIds.size } }
@ -145,6 +159,8 @@ class RoomDetailsPresenter(
clipboardHelper.copyPlainText(event.text)
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
}
is RoomDetailsEvent.MarkAsRead -> scope.markAsRead()
is RoomDetailsEvent.MarkAsUnread -> scope.markAsUnread()
}
}
@ -188,6 +204,7 @@ class RoomDetailsPresenter(
showDebugInfo = isDeveloperModeEnabled,
roomVersion = roomInfo.roomVersion,
roomHistoryVisibility = roomInfo.historyVisibility,
hasNewContent = hasNewContent,
eventSink = ::handleEvent,
)
}
@ -241,4 +258,25 @@ class RoomDetailsPresenter(
analyticsService.captureInteraction(Interaction.Name.MobileRoomFavouriteToggle)
}
}
private fun CoroutineScope.markAsRead() = launch {
notificationCleaner.clearMessagesForRoom(client.sessionId, room.roomId)
room.setUnreadFlag(isUnread = false)
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
room.markAsRead(receiptType)
.onSuccess {
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
}
}
private fun CoroutineScope.markAsUnread() = launch {
room.setUnreadFlag(isUnread = true)
.onSuccess {
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
}
}
}

View file

@ -52,6 +52,7 @@ data class RoomDetailsState(
val showDebugInfo: Boolean,
val roomVersion: String?,
val roomHistoryVisibility: RoomHistoryVisibility,
val hasNewContent: Boolean,
val eventSink: (RoomDetailsEvent) -> Unit
) {
val roomBadges = buildList {

View file

@ -34,7 +34,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
override val values: Sequence<RoomDetailsState>
get() = sequenceOf(
aRoomDetailsState(displayAdminSettings = true),
aRoomDetailsState(roomTopic = RoomTopicState.Hidden, showDebugInfo = true),
aRoomDetailsState(roomTopic = RoomTopicState.Hidden, showDebugInfo = true, hasNewContent = true),
aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic),
aRoomDetailsState(isEncrypted = false),
aRoomDetailsState(roomAlias = null),
@ -123,6 +123,7 @@ fun aRoomDetailsState(
isTombstoned: Boolean = false,
showDebugInfo: Boolean = false,
roomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Shared,
hasNewContent: Boolean = false,
eventSink: (RoomDetailsEvent) -> Unit = {},
) = RoomDetailsState(
roomId = roomId,
@ -154,6 +155,7 @@ fun aRoomDetailsState(
showDebugInfo = showDebugInfo,
roomVersion = "12",
roomHistoryVisibility = roomHistoryVisibility,
hasNewContent = hasNewContent,
eventSink = eventSink,
)

View file

@ -8,6 +8,7 @@
package io.element.android.features.roomdetails.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@ -188,6 +189,46 @@ fun RoomDetailsView(
)
}
PreferenceCategory {
if (state.hasNewContent) {
ListItem(
headlineContent = {
Text(
text = stringResource(id = R.string.screen_roomlist_mark_as_read),
style = MaterialTheme.typography.bodyLarge,
)
},
onClick = {
state.eventSink(RoomDetailsEvent.MarkAsRead)
},
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(CompoundIcons.MarkAsRead())
),
trailingContent = ListItemContent.Custom {
Box(
modifier = modifier
.size(8.dp)
.clip(CircleShape)
.background(ElementTheme.colors.iconAccentPrimary)
)
},
)
} else {
ListItem(
headlineContent = {
Text(
text = stringResource(id = R.string.screen_roomlist_mark_as_unread),
)
},
onClick = {
state.eventSink(RoomDetailsEvent.MarkAsUnread)
},
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(CompoundIcons.MarkAsUnread())
),
)
}
}
PreferenceCategory {
if (state.roomNotificationSettings != null) {
NotificationItem(

View file

@ -132,6 +132,8 @@
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
<string name="screen_room_roles_and_permissions_title">"Roles &amp; permissions"</string>
<string name="screen_roomlist_mark_as_read">"Mark as read"</string>
<string name="screen_roomlist_mark_as_unread">"Mark as unread"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Add address"</string>
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Anyone in authorised spaces can join, but everyone else must request access."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Everyone must request access."</string>

View file

@ -221,6 +221,7 @@
"screen_notification_settings_mentions_only_disclaimer",
"screen_room_change_.*",
"screen_room_roles_.*",
"screen_roomlist_mark_as_.*",
"screen\\.edit_room_address\\..*",
"screen\\.security_and_privacy\\..*"
]