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 -> { is NavTarget.Room -> {
val callback = object : JoinedRoomLoadedFlowNode.Callback { val callback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onOpenRoom(roomId: RoomId) { override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
} }
override fun onForwardedToSingleRoom(roomId: RoomId) { override fun onForwardedToSingleRoom(roomId: RoomId) {
@@ -69,7 +69,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
plugins = plugins, plugins = plugins,
), DaggerComponentOwner { ), DaggerComponentOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId) fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun onForwardedToSingleRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId)
fun onOpenGlobalNotificationSettings() fun onOpenGlobalNotificationSettings()
@@ -121,8 +121,8 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
callbacks.forEach { it.onOpenGlobalNotificationSettings() } callbacks.forEach { it.onOpenGlobalNotificationSettings() }
} }
override fun onOpenRoom(roomId: RoomId) { override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) {
callbacks.forEach { it.onOpenRoom(roomId) } callbacks.forEach { it.onOpenRoom(roomId, serverNames) }
} }
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
@@ -20,5 +20,5 @@ interface MessagesNavigator {
fun onReportContentClick(eventId: EventId, senderId: UserId) fun onReportContentClick(eventId: EventId, senderId: UserId)
fun onEditPollClick(eventId: EventId) fun onEditPollClick(eventId: EventId)
fun onPreviewAttachment(attachments: ImmutableList<Attachment>) 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.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -213,11 +214,11 @@ class MessagesNode @AssistedInject constructor(
callbacks.forEach { it.onPreviewAttachments(attachments) } callbacks.forEach { it.onPreviewAttachments(attachments) }
} }
override fun onNavigateToRoom(roomId: RoomId) { override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) {
if (roomId == room.roomId) { if (roomId == room.roomId) {
displaySameRoomToast() displaySameRoomToast()
} else { } else {
val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias()) val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), viaParameters = serverNames.toImmutableList())
callbacks.forEach { it.onPermalinkClick(permalinkData) } callbacks.forEach { it.onPermalinkClick(permalinkData) }
} }
} }
@@ -278,7 +278,7 @@ fun MessagesView(
state = state, state = state,
onLinkClick = { url, customTab -> onLinkClick(url, customTab) }, onLinkClick = { url, customTab -> onLinkClick(url, customTab) },
onRoomSuccessorClick = { roomId -> 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 ComputeVerifiedUserSendFailure(val event: TimelineItem.Event) : EventFromTimelineItem
data class ShowShieldDialog(val messageShield: MessageShield) : EventFromTimelineItem data class ShowShieldDialog(val messageShield: MessageShield) : EventFromTimelineItem
data class LoadMore(val direction: Timeline.PaginationDirection) : 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. * Events coming from a poll item.
@@ -178,8 +178,10 @@ class TimelinePresenter @AssistedInject constructor(
is TimelineEvents.ComputeVerifiedUserSendFailure -> { is TimelineEvents.ComputeVerifiedUserSendFailure -> {
resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(event.event)) resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(event.event))
} }
is TimelineEvents.NavigateToRoom -> { is TimelineEvents.NavigateToPredecessorOrSuccessorRoom -> {
navigator.onNavigateToRoom(event.roomId) // 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 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, roomName = timelineRoomInfo.name,
isDm = timelineRoomInfo.isDm, isDm = timelineRoomInfo.isDm,
onPredecessorRoomClick = { roomId -> 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 onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() },
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() }, private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onPreviewAttachmentLambda: (attachments: ImmutableList<Attachment>) -> 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 { ) : MessagesNavigator {
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
onShowEventDebugInfoClickLambda(eventId, debugInfo) onShowEventDebugInfoClickLambda(eventId, debugInfo)
@@ -43,7 +43,7 @@ class FakeMessagesNavigator(
onPreviewAttachmentLambda(attachments) onPreviewAttachmentLambda(attachments)
} }
override fun onNavigateToRoom(roomId: RoomId) { override fun onNavigateToRoom(roomId: RoomId, serverNames: List<String>) {
onNavigateToRoomLambda(roomId) onNavigateToRoomLambda(roomId, serverNames)
} }
} }
@@ -579,7 +579,7 @@ class MessagesViewTest {
val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action)
// The bottomsheet subcompose seems to make the node to appear twice // The bottomsheet subcompose seems to make the node to appear twice
rule.onAllNodesWithText(text).onFirst().performClick() rule.onAllNodesWithText(text).onFirst().performClick()
eventsRecorder.assertSingle(TimelineEvents.NavigateToRoom(successorRoomId)) eventsRecorder.assertSingle(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(successorRoomId))
} }
@Test @Test
@@ -753,18 +753,19 @@ class TimelinePresenterTest {
canUserSendMessageResult = { _, _ -> Result.success(true) }, canUserSendMessageResult = { _, _ -> Result.success(true) },
), ),
) )
val onNavigateToRoomLambda = lambdaRecorder<RoomId, Unit> {} val onNavigateToRoomLambda = lambdaRecorder<RoomId, List<String>, Unit> { _, _ -> }
val navigator = FakeMessagesNavigator( val navigator = FakeMessagesNavigator(
onNavigateToRoomLambda = onNavigateToRoomLambda onNavigateToRoomLambda = onNavigateToRoomLambda
) )
val presenter = createTimelinePresenter(room = room, messagesNavigator = navigator) val presenter = createTimelinePresenter(room = room, messagesNavigator = navigator)
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(TimelineEvents.NavigateToRoom(A_ROOM_ID)) initialState.eventSink(TimelineEvents.NavigateToPredecessorOrSuccessorRoom(A_ROOM_ID))
assert(onNavigateToRoomLambda) assert(onNavigateToRoomLambda)
.isCalledOnce() .isCalledOnce()
.with( .with(
value(A_ROOM_ID) value(A_ROOM_ID),
value(emptyList<String>())
) )
} }
} }
@@ -34,7 +34,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenGlobalNotificationSettings() fun onOpenGlobalNotificationSettings()
fun onOpenRoom(roomId: RoomId) fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun onForwardedToSingleRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId)
} }
@@ -270,7 +270,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
} }
override fun onStartDM(roomId: RoomId) { 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) { override fun onStartCall(dmRoomId: RoomId) {
@@ -29,4 +29,7 @@ value class UserId(val value: String) : Serializable {
get() = value get() = value
.removePrefix("@") .removePrefix("@")
.substringBefore(":") .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) { 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 -> { is RoomIdOrAlias.Alias -> {
val roomId = innerClient.resolveRoomAlias(roomIdOrAlias.roomAlias.value)?.roomId?.let { RoomId(it) } 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) { if (room == null) {
val preview = innerClient.getRoomPreviewFromRoomAlias(roomIdOrAlias.roomAlias.value) val preview = innerClient.getRoomPreviewFromRoomAlias(roomIdOrAlias.roomAlias.value)
room = NotJoinedRustRoom(sessionId, null, RoomPreviewInfoMapper.map(preview.info())) room = NotJoinedRustRoom(sessionId, null, RoomPreviewInfoMapper.map(preview.info()))
@@ -489,7 +489,7 @@ class RustMatrixClient(
room room
} }
is RoomIdOrAlias.Id -> { 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) { if (room == null) {
val preview = innerClient.getRoomPreviewFromRoomId(roomIdOrAlias.roomId.value, serverNames) 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 { mutex.withLock {
if (isDestroyed.get()) { if (isDestroyed.get()) {
Timber.d("Room factory is destroyed, returning null for $roomId") Timber.d("Room factory is destroyed, returning null for $roomId")
@@ -132,7 +132,7 @@ class RustRoomFactory(
) )
} else { } else {
val preview = try { val preview = try {
sdkRoom.previewRoom(via = emptyList()) sdkRoom.previewRoom(via = serverNames)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to get room preview for $roomId") Timber.e(e, "Failed to get room preview for $roomId")
return@withContext null return@withContext null