From 516c3cfda36d7158405dadbb0ff573e7e7db1618 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 12 Aug 2025 17:17:46 +0200 Subject: [PATCH] Provide calculated server names when opening a room from another (#5155) * Provide calculated server names when opening a room from another, based on the most frequently used domain names in the user ids for the users in the room. This helps when following permalinks or navigating to the successor room of a tombstoned one. Previously, the `/summary` endpoint was failing because no server names were used in the `via` parameters. --- .../android/appnav/LoggedInFlowNode.kt | 4 ++-- .../room/joined/JoinedRoomLoadedFlowNode.kt | 6 ++--- .../messages/impl/MessagesNavigator.kt | 2 +- .../features/messages/impl/MessagesNode.kt | 5 +++-- .../features/messages/impl/MessagesView.kt | 2 +- .../messages/impl/timeline/TimelineEvents.kt | 6 ++++- .../impl/timeline/TimelinePresenter.kt | 22 +++++++++++++++++-- .../components/TimelineItemVirtualRow.kt | 2 +- .../messages/impl/FakeMessagesNavigator.kt | 6 ++--- .../messages/impl/MessagesViewTest.kt | 2 +- .../impl/timeline/TimelinePresenterTest.kt | 7 +++--- .../roomdetails/api/RoomDetailsEntryPoint.kt | 2 +- .../roomdetails/impl/RoomDetailsFlowNode.kt | 2 +- .../libraries/matrix/api/core/UserId.kt | 3 +++ .../libraries/matrix/impl/RustMatrixClient.kt | 6 ++--- .../matrix/impl/room/RustRoomFactory.kt | 4 ++-- 16 files changed, 54 insertions(+), 27 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 38751c0a1e..fb9b38352c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -335,8 +335,8 @@ class LoggedInFlowNode @AssistedInject constructor( } is NavTarget.Room -> { val callback = object : JoinedRoomLoadedFlowNode.Callback { - override fun onOpenRoom(roomId: RoomId) { - backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) + override fun onOpenRoom(roomId: RoomId, serverNames: List) { + backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames)) } override fun onForwardedToSingleRoom(roomId: RoomId) { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 3e5947aada..b5d118df3c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -69,7 +69,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( plugins = plugins, ), DaggerComponentOwner { interface Callback : Plugin { - fun onOpenRoom(roomId: RoomId) + fun onOpenRoom(roomId: RoomId, serverNames: List) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun onForwardedToSingleRoom(roomId: RoomId) fun onOpenGlobalNotificationSettings() @@ -121,8 +121,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( callbacks.forEach { it.onOpenGlobalNotificationSettings() } } - override fun onOpenRoom(roomId: RoomId) { - callbacks.forEach { it.onOpenRoom(roomId) } + override fun onOpenRoom(roomId: RoomId, serverNames: List) { + callbacks.forEach { it.onOpenRoom(roomId, serverNames) } } override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt index c66ec1ee51..d3c50bc133 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt @@ -20,5 +20,5 @@ interface MessagesNavigator { fun onReportContentClick(eventId: EventId, senderId: UserId) fun onEditPollClick(eventId: EventId) fun onPreviewAttachment(attachments: ImmutableList) - fun onNavigateToRoom(roomId: RoomId) + fun onNavigateToRoom(roomId: RoomId, serverNames: List) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 84d20fbf94..41b88be821 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -66,6 +66,7 @@ import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -213,11 +214,11 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onPreviewAttachments(attachments) } } - override fun onNavigateToRoom(roomId: RoomId) { + override fun onNavigateToRoom(roomId: RoomId, serverNames: List) { if (roomId == room.roomId) { displaySameRoomToast() } else { - val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias()) + val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), viaParameters = serverNames.toImmutableList()) callbacks.forEach { it.onPermalinkClick(permalinkData) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 7c52cc9bab..fb588b5908 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -278,7 +278,7 @@ fun MessagesView( state = state, onLinkClick = { url, customTab -> onLinkClick(url, customTab) }, onRoomSuccessorClick = { roomId -> - state.timelineState.eventSink(TimelineEvents.NavigateToRoom(roomId = roomId)) + state.timelineState.eventSink(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(roomId = roomId)) }, ) }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index da6837df21..66d8da771b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -31,7 +31,11 @@ sealed interface TimelineEvents { data class ComputeVerifiedUserSendFailure(val event: TimelineItem.Event) : EventFromTimelineItem data class ShowShieldDialog(val messageShield: MessageShield) : EventFromTimelineItem data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem - data class NavigateToRoom(val roomId: RoomId) : EventFromTimelineItem + + /** + * Navigate to the predecessor or successor room of the current room. + */ + data class NavigateToPredecessorOrSuccessorRoom(val roomId: RoomId) : EventFromTimelineItem /** * Events coming from a poll item. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index d8c43e7de7..86dc1dfb1e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -178,8 +178,10 @@ class TimelinePresenter @AssistedInject constructor( is TimelineEvents.ComputeVerifiedUserSendFailure -> { resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(event.event)) } - is TimelineEvents.NavigateToRoom -> { - navigator.onNavigateToRoom(event.roomId) + is TimelineEvents.NavigateToPredecessorOrSuccessorRoom -> { + // Navigate to the predecessor or successor room + val serverNames = calculateServerNamesForRoom(room) + navigator.onNavigateToRoom(event.roomId, serverNames) } } } @@ -353,3 +355,19 @@ private fun FocusRequestState.onFocusEventRender(): FocusRequestState { else -> this } } + +// Workaround for not having the server names available, get possible server names from the user ids of the room members +private fun calculateServerNamesForRoom(room: JoinedRoom): List { + // If we have no room members, return right ahead + val serverNames = room.membersStateFlow.value.roomMembers() ?: return emptyList() + + // Otherwise get the three most common server names from the user ids of the room members + return serverNames + .mapNotNull { it.userId.domainName } + .groupingBy { it } + .eachCount() + .let { map -> + map.keys.sortedByDescending { map[it] } + } + .take(3) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt index 45b167b68f..7c0309aac4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt @@ -47,7 +47,7 @@ fun TimelineItemVirtualRow( roomName = timelineRoomInfo.name, isDm = timelineRoomInfo.isDm, onPredecessorRoomClick = { roomId -> - eventSink(TimelineEvents.NavigateToRoom(roomId)) + eventSink(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(roomId)) }, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt index 32638d7b8d..f6e6fedbc5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt @@ -21,7 +21,7 @@ class FakeMessagesNavigator( private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() }, private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() }, private val onPreviewAttachmentLambda: (attachments: ImmutableList) -> Unit = { _ -> lambdaError() }, - private val onNavigateToRoomLambda: (roomId: RoomId) -> Unit = { _ -> lambdaError() } + private val onNavigateToRoomLambda: (roomId: RoomId, serverNames: List) -> Unit = { _, _ -> lambdaError() } ) : MessagesNavigator { override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { onShowEventDebugInfoClickLambda(eventId, debugInfo) @@ -43,7 +43,7 @@ class FakeMessagesNavigator( onPreviewAttachmentLambda(attachments) } - override fun onNavigateToRoom(roomId: RoomId) { - onNavigateToRoomLambda(roomId) + override fun onNavigateToRoom(roomId: RoomId, serverNames: List) { + onNavigateToRoomLambda(roomId, serverNames) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 51f5640ece..f8a5436ffe 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -579,7 +579,7 @@ class MessagesViewTest { val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) // The bottomsheet subcompose seems to make the node to appear twice rule.onAllNodesWithText(text).onFirst().performClick() - eventsRecorder.assertSingle(TimelineEvents.NavigateToRoom(successorRoomId)) + eventsRecorder.assertSingle(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(successorRoomId)) } @Test diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index f202fd7e6e..9ba2688d45 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -753,18 +753,19 @@ class TimelinePresenterTest { canUserSendMessageResult = { _, _ -> Result.success(true) }, ), ) - val onNavigateToRoomLambda = lambdaRecorder {} + val onNavigateToRoomLambda = lambdaRecorder, Unit> { _, _ -> } val navigator = FakeMessagesNavigator( onNavigateToRoomLambda = onNavigateToRoomLambda ) val presenter = createTimelinePresenter(room = room, messagesNavigator = navigator) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(TimelineEvents.NavigateToRoom(A_ROOM_ID)) + initialState.eventSink(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(A_ROOM_ID)) assert(onNavigateToRoomLambda) .isCalledOnce() .with( - value(A_ROOM_ID) + value(A_ROOM_ID), + value(emptyList()) ) } } diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 7ecde7e139..803002d642 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -34,7 +34,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onOpenGlobalNotificationSettings() - fun onOpenRoom(roomId: RoomId) + fun onOpenRoom(roomId: RoomId, serverNames: List) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun onForwardedToSingleRoom(roomId: RoomId) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 122311040e..a802d5cd0a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -270,7 +270,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( } override fun onStartDM(roomId: RoomId) { - plugins().forEach { it.onOpenRoom(roomId) } + plugins().forEach { it.onOpenRoom(roomId, emptyList()) } } override fun onStartCall(dmRoomId: RoomId) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index 61ac0afe2c..ece3a9a296 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -29,4 +29,7 @@ value class UserId(val value: String) : Serializable { get() = value .removePrefix("@") .substringBefore(":") + + val domainName: String? + get() = value.substringAfter(":").takeIf { it.isNotEmpty() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 50c311e56a..4b546232a3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -283,7 +283,7 @@ class RustMatrixClient( } override suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? = withContext(sessionDispatcher) { - (roomFactory.getJoinedRoomOrPreview(roomId) as? GetRoomResult.Joined)?.joinedRoom + (roomFactory.getJoinedRoomOrPreview(roomId, emptyList()) as? GetRoomResult.Joined)?.joinedRoom } /** @@ -481,7 +481,7 @@ class RustMatrixClient( is RoomIdOrAlias.Alias -> { val roomId = innerClient.resolveRoomAlias(roomIdOrAlias.roomAlias.value)?.roomId?.let { RoomId(it) } - var room = (roomId?.let { roomFactory.getJoinedRoomOrPreview(it) } as? GetRoomResult.NotJoined)?.notJoinedRoom + var room = (roomId?.let { roomFactory.getJoinedRoomOrPreview(it, serverNames) } as? GetRoomResult.NotJoined)?.notJoinedRoom if (room == null) { val preview = innerClient.getRoomPreviewFromRoomAlias(roomIdOrAlias.roomAlias.value) room = NotJoinedRustRoom(sessionId, null, RoomPreviewInfoMapper.map(preview.info())) @@ -489,7 +489,7 @@ class RustMatrixClient( room } is RoomIdOrAlias.Id -> { - var room = (roomFactory.getJoinedRoomOrPreview(roomIdOrAlias.roomId) as? GetRoomResult.NotJoined)?.notJoinedRoom + var room = (roomFactory.getJoinedRoomOrPreview(roomIdOrAlias.roomId, serverNames) as? GetRoomResult.NotJoined)?.notJoinedRoom if (room == null) { val preview = innerClient.getRoomPreviewFromRoomId(roomIdOrAlias.roomId.value, serverNames) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt index c842c25876..dd6a6e974f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt @@ -98,7 +98,7 @@ class RustRoomFactory( ) } - suspend fun getJoinedRoomOrPreview(roomId: RoomId): GetRoomResult? = withContext(dispatcher) { + suspend fun getJoinedRoomOrPreview(roomId: RoomId, serverNames: List): GetRoomResult? = withContext(dispatcher) { mutex.withLock { if (isDestroyed.get()) { Timber.d("Room factory is destroyed, returning null for $roomId") @@ -132,7 +132,7 @@ class RustRoomFactory( ) } else { val preview = try { - sdkRoom.previewRoom(via = emptyList()) + sdkRoom.previewRoom(via = serverNames) } catch (e: Exception) { Timber.e(e, "Failed to get room preview for $roomId") return@withContext null