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.
This commit is contained in:
Jorge Martin Espinosa 2025-08-12 17:17:46 +02:00 committed by GitHub
parent 1a31e49f1e
commit 516c3cfda3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 54 additions and 27 deletions

View file

@ -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<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
}
override fun onForwardedToSingleRoom(roomId: RoomId) {

View file

@ -69,7 +69,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
plugins = plugins,
), DaggerComponentOwner {
interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId)
fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
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<String>) {
callbacks.forEach { it.onOpenRoom(roomId, serverNames) }
}
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {

View file

@ -20,5 +20,5 @@ interface MessagesNavigator {
fun onReportContentClick(eventId: EventId, senderId: UserId)
fun onEditPollClick(eventId: EventId)
fun onPreviewAttachment(attachments: ImmutableList<Attachment>)
fun onNavigateToRoom(roomId: RoomId)
fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>)
}

View file

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

View file

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

View file

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

View file

@ -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<String> {
// 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)
}

View file

@ -47,7 +47,7 @@ fun TimelineItemVirtualRow(
roomName = timelineRoomInfo.name,
isDm = timelineRoomInfo.isDm,
onPredecessorRoomClick = { roomId ->
eventSink(TimelineEvents.NavigateToRoom(roomId))
eventSink(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(roomId))
},
)
}

View file

@ -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<Attachment>) -> Unit = { _ -> lambdaError() },
private val onNavigateToRoomLambda: (roomId: RoomId) -> Unit = { _ -> lambdaError() }
private val onNavigateToRoomLambda: (roomId: RoomId, serverNames: List<String>) -> 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<String>) {
onNavigateToRoomLambda(roomId, serverNames)
}
}

View file

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

View file

@ -753,18 +753,19 @@ class TimelinePresenterTest {
canUserSendMessageResult = { _, _ -> Result.success(true) },
),
)
val onNavigateToRoomLambda = lambdaRecorder<RoomId, Unit> {}
val onNavigateToRoomLambda = lambdaRecorder<RoomId, List<String>, 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<String>())
)
}
}

View file

@ -34,7 +34,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onOpenGlobalNotificationSettings()
fun onOpenRoom(roomId: RoomId)
fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun onForwardedToSingleRoom(roomId: RoomId)
}

View file

@ -270,7 +270,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
override fun onStartDM(roomId: RoomId) {
plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onOpenRoom(roomId, emptyList()) }
}
override fun onStartCall(dmRoomId: RoomId) {

View file

@ -29,4 +29,7 @@ value class UserId(val value: String) : Serializable {
get() = value
.removePrefix("@")
.substringBefore(":")
val domainName: String?
get() = value.substringAfter(":").takeIf { it.isNotEmpty() }
}

View file

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

View file

@ -98,7 +98,7 @@ class RustRoomFactory(
)
}
suspend fun getJoinedRoomOrPreview(roomId: RoomId): GetRoomResult? = withContext(dispatcher) {
suspend fun getJoinedRoomOrPreview(roomId: RoomId, serverNames: List<String>): 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