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
16 changed files with 54 additions and 27 deletions
@@ -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) {
@@ -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) {
@@ -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>)
}
@@ -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) }
}
}
@@ -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))
},
)
},
@@ -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.
@@ -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)
}
@@ -47,7 +47,7 @@ fun TimelineItemVirtualRow(
roomName = timelineRoomInfo.name,
isDm = timelineRoomInfo.isDm,
onPredecessorRoomClick = { roomId ->
eventSink(TimelineEvents.NavigateToRoom(roomId))
eventSink(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(roomId))
},
)
}
@@ -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)
}
}
@@ -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
@@ -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>())
)
}
}
@@ -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)
}
@@ -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) {
@@ -29,4 +29,7 @@ value class UserId(val value: String) : Serializable {
get() = value
.removePrefix("@")
.substringBefore(":")
val domainName: String?
get() = value.substringAfter(":").takeIf { it.isNotEmpty() }
}
@@ -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)
@@ -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