feature (room upgrade) : start rendering SuccessorRoom and PredecessorRoom banners in timeline

This commit is contained in:
ganfra
2025-06-04 16:57:17 +02:00
parent ce5d656320
commit aff8dd8522
24 changed files with 255 additions and 89 deletions

View File

@@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.BaseRoom
@@ -143,6 +144,15 @@ class MessagesNode @AssistedInject constructor(
callbacks.forEach { it.onUserDataClick(userId) }
}
private fun onRoomDataClick(
activity: Activity,
eventSink: (TimelineEvents) -> Unit,
roomId: RoomId
) {
val roomLink = PermalinkData.RoomLink(roomId.toRoomIdOrAlias())
handleRoomLinkClick(activity, roomLink, eventSink)
}
private fun onLinkClick(
activity: Activity,
darkTheme: Boolean,
@@ -250,6 +260,7 @@ class MessagesNode @AssistedInject constructor(
onRoomDetailsClick = this::onRoomDetailsClick,
onEventContentClick = this::onEventClick,
onUserDataClick = this::onUserDataClick,
onRoomDataClick = {roomId -> onRoomDataClick(activity, state.timelineState.eventSink, roomId) },
onLinkClick = { url, customTab -> onLinkClick(activity, isDark, url, state.timelineState.eventSink, customTab) },
onSendLocationClick = this::onSendLocationClick,
onCreatePollClick = this::onCreatePollClick,

View File

@@ -270,6 +270,7 @@ class MessagesPresenter @AssistedInject constructor(
pinnedMessagesBannerState = pinnedMessagesBannerState,
dmUserVerificationState = dmUserVerificationState,
roomMemberModerationState = roomMemberModerationState,
successorRoom = roomInfo.successorRoom,
eventSink = { handleEvents(it) }
)
}

View File

@@ -26,6 +26,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import kotlinx.collections.immutable.ImmutableList
@Immutable
@@ -56,5 +57,6 @@ data class MessagesState(
val pinnedMessagesBannerState: PinnedMessagesBannerState,
val dmUserVerificationState: IdentityState?,
val roomMemberModerationState: RoomMemberModerationState,
val successorRoom: SuccessorRoom?,
val eventSink: (MessagesEvents) -> Unit
)

View File

@@ -44,6 +44,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
import kotlinx.collections.immutable.persistentListOf
@@ -119,6 +120,7 @@ fun aMessagesState(
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
dmUserVerificationState: IdentityState? = null,
roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
successorRoom: SuccessorRoom? = null,
eventSink: (MessagesEvents) -> Unit = {},
) = MessagesState(
roomId = RoomId("!id:domain"),
@@ -147,6 +149,7 @@ fun aMessagesState(
pinnedMessagesBannerState = pinnedMessagesBannerState,
dmUserVerificationState = dmUserVerificationState,
roomMemberModerationState = roomMemberModerationState,
successorRoom = successorRoom,
eventSink = eventSink,
)

View File

@@ -82,6 +82,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.androidutils.ui.hideKeyboard
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@@ -90,6 +91,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toAnnotatedString
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
@@ -101,8 +103,10 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.ui.strings.CommonStrings
@@ -119,6 +123,7 @@ fun MessagesView(
onRoomDetailsClick: () -> Unit,
onEventContentClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean,
onUserDataClick: (UserId) -> Unit,
onRoomDataClick: (RoomId) -> Unit,
onLinkClick: (String, Boolean) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
@@ -205,14 +210,15 @@ fun MessagesView(
MessagesViewContent(
state = state,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding),
.padding(padding)
.consumeWindowInsets(padding),
onContentClick = ::onContentClick,
onMessageLongClick = ::onMessageLongClick,
onRoomSuccessorClicked = onRoomDataClick,
onUserDataClick = {
hidingKeyboard {
state.eventSink(MessagesEvents.OnUserClicked(it))
}
state.eventSink(MessagesEvents.OnUserClicked(it))
}
},
onLinkClick = { link, customTab ->
if (customTab) {
@@ -299,6 +305,7 @@ private fun MessagesViewContent(
state: MessagesState,
onContentClick: (TimelineItem.Event) -> Unit,
onUserDataClick: (MatrixUser) -> Unit,
onRoomSuccessorClicked: (RoomId) -> Unit,
onLinkClick: (Link, Boolean) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
@@ -316,9 +323,9 @@ private fun MessagesViewContent(
) {
Box(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding()
.imePadding(),
.fillMaxSize()
.navigationBarsPadding()
.imePadding(),
) {
AttachmentsBottomSheet(
state = state.composerState,
@@ -410,6 +417,7 @@ private fun MessagesViewContent(
MessagesViewComposerBottomSheetContents(
subcomposing = subcomposing,
state = state,
onRoomSuccessorClicked = onRoomSuccessorClicked,
onLinkClick = { url, customTab -> onLinkClick(Link(url), customTab) },
)
},
@@ -424,52 +432,59 @@ private fun MessagesViewContent(
private fun MessagesViewComposerBottomSheetContents(
subcomposing: Boolean,
state: MessagesState,
onRoomSuccessorClicked: (RoomId) -> Unit,
onLinkClick: (String, Boolean) -> Unit,
) {
if (state.userEventPermissions.canSendMessage) {
Column(modifier = Modifier.fillMaxWidth()) {
SuggestionsPickerView(
modifier = Modifier
.heightIn(max = 230.dp)
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available
}
}),
roomId = state.roomId,
roomName = state.roomName.dataOrNull(),
roomAvatarData = state.roomAvatar.dataOrNull(),
suggestions = state.composerState.suggestions,
onSelectSuggestion = {
state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it))
when {
state.successorRoom != null -> {
SuccessorRoomBanner(roomSuccessor = state.successorRoom, onRoomSuccessorClicked = onRoomSuccessorClicked)
}
state.userEventPermissions.canSendMessage -> {
Column(modifier = Modifier.fillMaxWidth()) {
SuggestionsPickerView(
modifier = Modifier
.heightIn(max = 230.dp)
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available
}
}),
roomId = state.roomId,
roomName = state.roomName.dataOrNull(),
roomAvatarData = state.roomAvatar.dataOrNull(),
suggestions = state.composerState.suggestions,
onSelectSuggestion = {
state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it))
}
)
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
if (state.composerState.suggestions.isEmpty() &&
state.composerState.textEditorState is TextEditorState.Markdown) {
IdentityChangeStateView(
state = state.identityChangeState,
onLinkClick = onLinkClick,
)
}
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
it.identityState == IdentityState.VerificationViolation
}
if (verificationViolation != null) {
DisabledComposerView(modifier = Modifier.fillMaxWidth())
} else {
MessageComposerView(
state = state.composerState,
voiceMessageState = state.voiceMessageComposerState,
subcomposing = subcomposing,
enableVoiceMessages = state.enableVoiceMessages,
modifier = Modifier.fillMaxWidth(),
)
}
)
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
if (state.composerState.suggestions.isEmpty() &&
state.composerState.textEditorState is TextEditorState.Markdown) {
IdentityChangeStateView(
state = state.identityChangeState,
onLinkClick = onLinkClick,
)
}
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
it.identityState == IdentityState.VerificationViolation
}
if (verificationViolation != null) {
DisabledComposerView(modifier = Modifier.fillMaxWidth())
} else {
MessageComposerView(
state = state.composerState,
voiceMessageState = state.voiceMessageComposerState,
subcomposing = subcomposing,
enableVoiceMessages = state.enableVoiceMessages,
modifier = Modifier.fillMaxWidth(),
)
}
}
} else {
CantSendMessageBanner()
else -> {
CantSendMessageBanner()
}
}
}
@@ -493,8 +508,8 @@ private fun MessagesViewTopBar(
val roundedCornerShape = RoundedCornerShape(8.dp)
Row(
modifier = Modifier
.clip(roundedCornerShape)
.clickable { onRoomDetailsClick() },
.clip(roundedCornerShape)
.clickable { onRoomDetailsClick() },
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -572,9 +587,9 @@ private fun RoomAvatarAndNameRow(
private fun CantSendMessageBanner() {
Row(
modifier = Modifier
.fillMaxWidth()
.background(ElementTheme.colors.bgSubtleSecondary)
.padding(16.dp),
.fillMaxWidth()
.background(ElementTheme.colors.bgSubtleSecondary)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
@@ -588,6 +603,22 @@ private fun CantSendMessageBanner() {
}
}
@Composable
private fun SuccessorRoomBanner(
modifier: Modifier = Modifier,
roomSuccessor: SuccessorRoom,
onRoomSuccessorClicked: (RoomId) -> Unit
) {
ComposerAlertMolecule(
avatar = null,
content = "This room has been replaced and is no longer active".toAnnotatedString(),
onSubmitClick = { onRoomSuccessorClicked(roomSuccessor.roomId)},
modifier = modifier,
isCritical = false,
submitText = "Jump to new room"
)
}
@PreviewsDayNight
@Composable
internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) = ElementPreview {
@@ -597,6 +628,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onRoomDetailsClick = {},
onEventContentClick = { _, _ -> false },
onUserDataClick = {},
onRoomDataClick = { },
onLinkClick = { _, _ -> },
onSendLocationClick = {},
onCreatePollClick = {},

View File

@@ -40,6 +40,7 @@ internal fun MessagesViewWithIdentityChangePreview(
onCreatePollClick = {},
onJoinCallClick = {},
onViewAllPinnedMessagesClick = {},
onRoomDataClick = {},
knockRequestsBannerView = {}
)
}

View File

@@ -106,7 +106,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
renderTypingNotifications = false,
typingMembers = persistentListOf(),
reserveSpace = false,
)
),
predecessorRoom = room.predecessorRoom(),
)
}
val timelineProtectionState = timelineProtectionPresenter.present()

View File

@@ -257,8 +257,9 @@ class TimelinePresenter @AssistedInject constructor(
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
roomCallState = roomCallState,
pinnedEventIds = roomInfo.pinnedEventIds.orEmpty(),
pinnedEventIds = roomInfo.pinnedEventIds,
typingNotificationState = typingNotificationState,
predecessorRoom = room.predecessorRoom(),
)
}
}

View File

@@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration
@@ -77,4 +78,5 @@ data class TimelineRoomInfo(
val roomCallState: RoomCallState,
val pinnedEventIds: List<EventId>,
val typingNotificationState: TypingNotificationState,
val predecessorRoom: PredecessorRoom?,
)

View File

@@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
@@ -246,6 +247,7 @@ internal fun aTimelineRoomInfo(
userHasPermissionToSendMessage: Boolean = true,
pinnedEventIds: List<EventId> = emptyList(),
typingNotificationState: TypingNotificationState = aTypingNotificationState(),
predecessorRoom: PredecessorRoom? = null,
) = TimelineRoomInfo(
isDm = isDm,
name = name,
@@ -254,4 +256,5 @@ internal fun aTimelineRoomInfo(
roomCallState = aStandByCallState(),
pinnedEventIds = pinnedEventIds,
typingNotificationState = typingNotificationState,
predecessorRoom = predecessorRoom,
)

View File

@@ -41,7 +41,7 @@ fun TimelineItemVirtualRow(
when (virtual.model) {
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model)
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name)
TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(predecessorRoom = timelineRoomInfo.predecessorRoom, roomName = timelineRoomInfo.name)
is TimelineItemLoadingIndicatorModel -> {
TimelineLoadingMoreIndicator(virtual.model.direction)
val latestEventSink by rememberUpdatedState(eventSink)

View File

@@ -7,6 +7,7 @@
package io.element.android.features.messages.impl.timeline.components.virtual
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -19,43 +20,71 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toAnnotatedString
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
@Composable
fun TimelineItemRoomBeginningView(
predecessorRoom: PredecessorRoom?,
roomName: String?,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center,
Column(
modifier =modifier.fillMaxWidth()
) {
val text = if (roomName == null) {
stringResource(id = R.string.screen_room_timeline_beginning_of_room_no_name)
} else {
stringResource(id = R.string.screen_room_timeline_beginning_of_room, roomName)
if(predecessorRoom != null) {
ComposerAlertMolecule(
avatar = null,
content = "This room is a continuation of another room".toAnnotatedString(),
onSubmitClick = { },
modifier = modifier,
isCritical = false,
submitText = "See old messages"
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center,
) {
val text = if (roomName == null) {
stringResource(id = R.string.screen_room_timeline_beginning_of_room_no_name)
} else {
stringResource(id = R.string.screen_room_timeline_beginning_of_room, roomName)
}
Text(
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyMdRegular,
text = text,
textAlign = TextAlign.Center,
)
}
Text(
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyMdRegular,
text = text,
textAlign = TextAlign.Center,
)
}
}
@PreviewsDayNight
@Composable
internal fun TimelineItemRoomBeginningViewPreview() = ElementPreview {
Column {
Column(verticalArrangement = spacedBy(16.dp)){
TimelineItemRoomBeginningView(
predecessorRoom = null,
roomName = null,
)
TimelineItemRoomBeginningView(
predecessorRoom = null,
roomName = "Room Name",
)
TimelineItemRoomBeginningView(
predecessorRoom = PredecessorRoom(RoomId("!roomId:matrix.org"),EventId("\$eventId:matrix.org") ),
roomName = "Room Name",
)
}

View File

@@ -39,7 +39,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun ComposerAlertMolecule(
avatar: AvatarData,
avatar: AvatarData?,
content: AnnotatedString,
onSubmitClick: () -> Unit,
modifier: Modifier = Modifier,
@@ -71,9 +71,9 @@ fun ComposerAlertMolecule(
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Avatar(
avatarData = avatar,
)
if(avatar != null) {
Avatar(avatarData = avatar)
}
Text(
text = content,
modifier = Modifier.weight(1f),
@@ -101,7 +101,7 @@ fun ComposerAlertMolecule(
@Composable
internal fun ComposerAlertMoleculePreview(@PreviewParameter(BooleanProvider::class) isCritical: Boolean) = ElementPreview {
ComposerAlertMolecule(
avatar = anAvatarData(size = AvatarSize.ComposerAlert),
avatar = null,
content = "Alices verified identity has changed. Learn more".toAnnotatedString(),
isCritical = isCritical,
onSubmitClick = {},

View File

@@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import kotlinx.coroutines.CoroutineScope
@@ -55,6 +56,8 @@ interface BaseRoom : Closeable {
*/
fun info(): RoomInfo = roomInfoFlow.value
fun predecessorRoom(): PredecessorRoom?
/**
* A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
@@ -234,7 +237,6 @@ interface BaseRoom : Closeable {
* @param reason - The reason the room is being reported.
*/
suspend fun reportRoom(reason: String?): Result<Unit>
/**
* Destroy the room and release all resources associated to it.
*/

View File

@@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 New Vector 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.libraries.matrix.api.room.tombstone
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
/**
*
* When a room A is tombstoned, it is replaced by a room B. The room A is the
* predecessor of B, and B is the successor of A. This type holds information
* about the predecessor room.
*
* A room is tombstoned if it has received a m.room.tombstone state event.
*
*/
data class PredecessorRoom(
/**
* The ID of the replaced room.
*/
val roomId: RoomId,
/**
* The event ID of the last known event in the predecessor room.
*/
val lastEventId: EventId,
)

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
package io.element.android.libraries.matrix.api.room.tombstone
import io.element.android.libraries.matrix.api.core.RoomId

View File

@@ -14,11 +14,12 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.SuccessorRoom
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.tombstone.map
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
@@ -88,11 +89,6 @@ fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {
RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE
}
fun RustSuccessorRoom.map(): SuccessorRoom = SuccessorRoom(
roomId = RoomId(roomId),
reason = reason,
)
/**
* Map a RoomHero to a MatrixUser. There is not need to create a RoomHero type on the application side.
*/

View File

@@ -24,12 +24,14 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.impl.room.draft.into
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
import io.element.android.libraries.matrix.impl.room.tombstone.map
import io.element.android.libraries.matrix.impl.roomdirectory.map
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
@@ -78,6 +80,10 @@ class RustBaseRoom(
})
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo)
override fun predecessorRoom(): PredecessorRoom? {
return innerRoom.predecessorRoom()?.map()
}
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override suspend fun updateMembers() {

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector 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.libraries.matrix.impl.room.tombstone
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
import org.matrix.rustcomponents.sdk.PredecessorRoom as RustPredecessorRoom
fun RustPredecessorRoom.map(): PredecessorRoom {
return PredecessorRoom(
roomId = RoomId(roomId),
lastEventId = EventId(lastEventId),
)
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 New Vector 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.libraries.matrix.impl.room.tombstone
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import org.matrix.rustcomponents.sdk.SuccessorRoom as RustSuccessorRoom
fun RustSuccessorRoom.map(): SuccessorRoom {
return SuccessorRoom(
roomId = RoomId(roomId),
reason = reason
)
}

View File

@@ -27,6 +27,11 @@ import org.matrix.rustcomponents.sdk.RoomListService
import kotlin.coroutines.CoroutineContext
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
private val ROOM_LIST_RUST_FILTERS = listOf(
RoomListEntriesDynamicFilterKind.NonLeft,
RoomListEntriesDynamicFilterKind.DeduplicateVersions
)
internal class RoomListFactory(
private val innerRoomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope,
@@ -55,11 +60,11 @@ internal class RoomListFactory(
coroutineScope.launch(coroutineContext) {
innerRoomList = innerProvider()
innerRoomList?.let { innerRoomList ->
innerRoomList.let { innerRoomList ->
innerRoomList.entriesFlow(
pageSize = pageSize,
roomListDynamicEvents = dynamicEvents,
initialFilterKind = RoomListEntriesDynamicFilterKind.NonLeft
initialFilterKind = RoomListEntriesDynamicFilterKind.All(ROOM_LIST_RUST_FILTERS),
).onEach { update ->
processor.postUpdate(update)
}.launchIn(this)

View File

@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.SuccessorRoom
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser

View File

@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.SuccessorRoom
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.message.RoomMessage