From aff8dd8522ebeab0c931dfa737698d9f50c9e4b6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Jun 2025 16:57:17 +0200 Subject: [PATCH] feature (room upgrade) : start rendering SuccessorRoom and PredecessorRoom banners in timeline --- .../features/messages/impl/MessagesNode.kt | 11 ++ .../messages/impl/MessagesPresenter.kt | 1 + .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 3 + .../features/messages/impl/MessagesView.kt | 138 +++++++++++------- .../MessagesViewWithIdentityChangePreview.kt | 1 + .../list/PinnedMessagesListPresenter.kt | 3 +- .../impl/timeline/TimelinePresenter.kt | 3 +- .../messages/impl/timeline/TimelineState.kt | 2 + .../impl/timeline/TimelineStateProvider.kt | 3 + .../components/TimelineItemVirtualRow.kt | 2 +- .../virtual/TimelineItemRoomBeginningView.kt | 61 ++++++-- .../atomic/molecules/ComposerAlertMolecule.kt | 10 +- .../libraries/matrix/api/room/BaseRoom.kt | 4 +- .../libraries/matrix/api/room/RoomInfo.kt | 1 + .../api/room/tombstone/PredecessorRoom.kt | 31 ++++ .../api/room/{ => tombstone}/SuccessorRoom.kt | 2 +- .../matrix/impl/room/RoomInfoMapper.kt | 8 +- .../matrix/impl/room/RustBaseRoom.kt | 6 + .../impl/room/tombstone/PredecessorRoom.kt | 20 +++ .../impl/room/tombstone/SuccessorRoom.kt | 19 +++ .../matrix/impl/roomlist/RoomListFactory.kt | 9 +- .../matrix/test/room/RoomInfoFixture.kt | 2 +- .../matrix/test/room/RoomSummaryFixture.kt | 2 +- 24 files changed, 255 insertions(+), 89 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/PredecessorRoom.kt rename libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/{ => tombstone}/SuccessorRoom.kt (92%) create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/PredecessorRoom.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/SuccessorRoom.kt 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 fc6d01d3a9..d0ef93b73f 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 @@ -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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 74cb9799f2..aa0d55e29b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -270,6 +270,7 @@ class MessagesPresenter @AssistedInject constructor( pinnedMessagesBannerState = pinnedMessagesBannerState, dmUserVerificationState = dmUserVerificationState, roomMemberModerationState = roomMemberModerationState, + successorRoom = roomInfo.successorRoom, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 8ba2cbd039..5247cb2f5e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -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 ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index c366a1e4cf..14f30091f9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -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, ) 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 ee76562b2c..5b3c0f0246 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 @@ -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 = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index cdd91e86e4..b6ce7a8b7e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -40,6 +40,7 @@ internal fun MessagesViewWithIdentityChangePreview( onCreatePollClick = {}, onJoinCallClick = {}, onViewAllPinnedMessagesClick = {}, + onRoomDataClick = {}, knockRequestsBannerView = {} ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index a7901ff52d..1c9e18c9e6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -106,7 +106,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor( renderTypingNotifications = false, typingMembers = persistentListOf(), reserveSpace = false, - ) + ), + predecessorRoom = room.predecessorRoom(), ) } val timelineProtectionState = timelineProtectionPresenter.present() 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 4684b8cfdd..642c1d035a 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 @@ -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(), ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index af4b7622fe..a3cdb34c94 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -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, val typingNotificationState: TypingNotificationState, + val predecessorRoom: PredecessorRoom?, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index d47379da3d..8bb66b4f60 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -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 = 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, ) 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 2507fb9f01..ba7f25c105 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 @@ -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) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt index 645f36602c..dc017db56f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt @@ -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", ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt index 33706239dc..fb06275460 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt @@ -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 = "Alice’s verified identity has changed. Learn more".toAnnotatedString(), isCritical = isCritical, onSubmitClick = {}, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index 120321bddf..8bd8851fc0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -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 - /** * Destroy the room and release all resources associated to it. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt index 8702218150..13e2ed44da 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt @@ -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 diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/PredecessorRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/PredecessorRoom.kt new file mode 100644 index 0000000000..cd54b5a07b --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/PredecessorRoom.kt @@ -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, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/SuccessorRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/SuccessorRoom.kt similarity index 92% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/SuccessorRoom.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/SuccessorRoom.kt index 4aaf3dd2d2..957585a464 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/SuccessorRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/SuccessorRoom.kt @@ -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 diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt index 2717f6275e..1578d8860b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt @@ -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. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index f86a179af2..09b7ec1285 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -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() { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/PredecessorRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/PredecessorRoom.kt new file mode 100644 index 0000000000..08daee4f19 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/PredecessorRoom.kt @@ -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), + ) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/SuccessorRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/SuccessorRoom.kt new file mode 100644 index 0000000000..afc0ee96af --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/SuccessorRoom.kt @@ -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 + ) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt index 275c55ccc0..53b8127ab8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt @@ -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) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt index 5b6040dee5..61c9beb289 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt @@ -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 diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 11e462f977..ad6269ac07 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -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