Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,10 @@ class LoggedInFlowNode(
}
is NavTarget.Room -> {
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onDone() {
backstack.pop()
}

override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) {
lifecycleScope.launch {
attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = clearBackStack)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class JoinedRoomLoadedFlowNode(
plugins = plugins,
), DependencyInjectionGraphOwner {
interface Callback : Plugin {
fun onDone()
fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean = false)
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun navigateToGlobalNotificationSettings()
Expand Down Expand Up @@ -142,6 +143,10 @@ class JoinedRoomLoadedFlowNode(

private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {
val callback = object : RoomDetailsEntryPoint.Callback {
override fun onDone() {
callback.onDone()
}

override fun navigateToGlobalNotificationSettings() {
callback.navigateToGlobalNotificationSettings()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.tests.testutils.lambda.lambdaError

class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback {
override fun onDone() = lambdaError()
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) = lambdaError()
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
override fun navigateToGlobalNotificationSettings() = lambdaError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
data class Params(val initialElement: InitialTarget) : NodeInputs

interface Callback : Plugin {
fun onDone()
fun navigateToGlobalNotificationSettings()
fun navigateToDeveloperSettings()
fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean = false)
Expand Down
2 changes: 2 additions & 0 deletions features/roomdetails/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -69,6 +70,7 @@ dependencies {
testImplementation(projects.libraries.mediaviewer.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.features.call.test)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ class RoomDetailsFlowNode(
return when (navTarget) {
NavTarget.RoomDetails -> {
val roomDetailsCallback = object : RoomDetailsNode.Callback {
override fun navigateBack() {
callback.onDone()
}

override fun navigateToRoomMemberList() {
backstack.push(NavTarget.RoomMemberList)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.roomdetails.impl

interface RoomDetailsNavigator {
fun onDone()
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ import io.element.android.libraries.androidutils.R as AndroidUtilsR
class RoomDetailsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: RoomDetailsPresenter,
presenterFactory: RoomDetailsPresenter.Factory,
private val room: BaseRoom,
private val analyticsService: AnalyticsService,
private val leaveRoomRenderer: LeaveRoomRenderer,
) : Node(buildContext, plugins = plugins) {
) : Node(buildContext, plugins = plugins), RoomDetailsNavigator {
interface Callback : Plugin {
fun navigateBack()
fun navigateToRoomMemberList()
fun navigateToInviteMembers()
fun navigateToRoomDetailsEdit()
Expand All @@ -65,6 +66,7 @@ class RoomDetailsNode(
fun navigateToSelectNewOwnersWhenLeaving()
}

private val presenter = presenterFactory.create(this)
private val callback: Callback = callback()

init {
Expand Down Expand Up @@ -144,4 +146,8 @@ class RoomDetailsNode(
}
)
}

override fun onDone() {
callback.navigateBack()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.api.knockRequestPermissions
Expand All @@ -44,19 +46,24 @@ 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

@Inject
@AssistedInject
class RoomDetailsPresenter(
@Assisted private val navigator: RoomDetailsNavigator,
private val client: MatrixClient,
private val room: JoinedRoom,
private val notificationSettingsService: NotificationSettingsService,
Expand All @@ -67,7 +74,16 @@ class RoomDetailsPresenter(
private val analyticsService: AnalyticsService,
private val clipboardHelper: ClipboardHelper,
private val appPreferencesStore: AppPreferencesStore,
private val sessionPreferencesStore: SessionPreferencesStore,
private val notificationCleaner: NotificationCleaner,
) : Presenter<RoomDetailsState> {
@AssistedFactory
interface Factory {
fun create(
navigator: RoomDetailsNavigator,
): RoomDetailsPresenter
}

@Composable
override fun present(): RoomDetailsState {
val scope = rememberCoroutineScope()
Expand All @@ -79,6 +95,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 } }

Expand Down Expand Up @@ -145,6 +169,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()
}
}

Expand Down Expand Up @@ -188,6 +214,7 @@ class RoomDetailsPresenter(
showDebugInfo = isDeveloperModeEnabled,
roomVersion = roomInfo.roomVersion,
roomHistoryVisibility = roomInfo.historyVisibility,
hasNewContent = hasNewContent,
eventSink = ::handleEvent,
)
}
Expand Down Expand Up @@ -241,4 +268,26 @@ 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)
navigator.onDone()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -154,6 +155,7 @@ fun aRoomDetailsState(
showDebugInfo = showDebugInfo,
roomVersion = "12",
roomHistoryVisibility = roomHistoryVisibility,
hasNewContent = hasNewContent,
eventSink = eventSink,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions features/roomdetails/impl/src/main/res/values/localazy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class DefaultRoomDetailsEntryPointTest {
)
}
val callback = object : RoomDetailsEntryPoint.Callback {
override fun onDone() = lambdaError()
override fun navigateToGlobalNotificationSettings() = lambdaError()
override fun navigateToDeveloperSettings() = lambdaError()
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) = lambdaError()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.roomdetails.impl

import io.element.android.tests.testutils.lambda.lambdaError

class FakeRoomDetailsNavigator(
private val onDoneResult: () -> Unit = { lambdaError() }
) : RoomDetailsNavigator {
override fun onDone() = onDoneResult()
}
Loading
Loading