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 8c0616431b..b0029fe4ee 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 @@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent -import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus @@ -91,7 +90,6 @@ class MessagesPresenter @AssistedInject constructor( private val composerPresenter: MessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, timelinePresenterFactory: TimelinePresenter.Factory, - private val typingNotificationPresenter: TypingNotificationPresenter, private val actionListPresenterFactory: ActionListPresenter.Factory, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, @@ -125,7 +123,6 @@ class MessagesPresenter @AssistedInject constructor( val composerState = composerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present() val timelineState = timelinePresenter.present() - val typingNotificationState = typingNotificationPresenter.present() val actionListState = actionListPresenter.present() val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() @@ -216,7 +213,6 @@ class MessagesPresenter @AssistedInject constructor( userEventPermissions = userEventPermissions, voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, - typingNotificationState = typingNotificationState, actionListState = actionListState, customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, 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 546e558ba8..2c5bae6d3b 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 @@ -15,7 +15,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState -import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -33,7 +32,6 @@ data class MessagesState( val composerState: MessageComposerState, val voiceMessageComposerState: VoiceMessageComposerState, val timelineState: TimelineState, - val typingNotificationState: TypingNotificationState, val actionListState: ActionListState, val customReactionState: CustomReactionState, val reactionSummaryState: ReactionSummaryState, 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 96e55aac91..2c1a487440 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 @@ -26,7 +26,6 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent -import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState @@ -122,7 +121,6 @@ fun aMessagesState( userEventPermissions = userEventPermissions, composerState = composerState, voiceMessageComposerState = voiceMessageComposerState, - typingNotificationState = aTypingNotificationState(), timelineState = timelineState, readReceiptBottomSheetState = readReceiptBottomSheetState, actionListState = actionListState, 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 777412e5e3..25ecbcc758 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 @@ -379,7 +379,6 @@ private fun MessagesViewContent( val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior() TimelineView( state = state.timelineState, - typingNotificationState = state.typingNotificationState, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onMessageClick = onMessageClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt index 8c488d61da..ee5ce58366 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -14,6 +14,8 @@ import io.element.android.features.messages.impl.crypto.sendfailure.resolve.Reso import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState +import io.element.android.features.messages.impl.typing.TypingNotificationPresenter +import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope @@ -25,4 +27,8 @@ interface MessagesModule { @Binds fun bindResolveVerifiedUserSendFailurePresenter(presenter: ResolveVerifiedUserSendFailurePresenter): Presenter + + @Binds + fun bindTypingNotificationPresenter(presenter: TypingNotificationPresenter): Presenter + } 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 8684c16e5a..1e557f84bd 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 @@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -44,6 +45,7 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf @@ -87,7 +89,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor( userHasPermissionToSendReaction = false, isCallOngoing = false, // don't compute this value or the pin icon will be shown - pinnedEventIds = emptyList() + pinnedEventIds = emptyList(), + typingNotificationState = TypingNotificationState( + renderTypingNotifications = false, + typingMembers = persistentListOf(), + reserveSpace = false, + ) ) } 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 85f517b1b0..6a980026b2 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 @@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction @@ -70,6 +71,7 @@ class TimelinePresenter @AssistedInject constructor( private val sessionPreferencesStore: SessionPreferencesStore, private val timelineController: TimelineController, private val resolveVerifiedUserSendFailurePresenter: Presenter, + private val typingNotificationPresenter: Presenter, ) : Presenter { @AssistedFactory interface Factory { @@ -225,7 +227,8 @@ class TimelinePresenter @AssistedInject constructor( .launchIn(this) } - val timelineRoomInfo by remember { + val typingNotificationState = typingNotificationPresenter.present() + val timelineRoomInfo by remember(typingNotificationState) { derivedStateOf { TimelineRoomInfo( name = room.displayName, @@ -234,6 +237,7 @@ class TimelinePresenter @AssistedInject constructor( userHasPermissionToSendReaction = userHasPermissionToSendReaction, isCallOngoing = roomInfo?.hasRoomCall.orFalse(), pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(), + typingNotificationState = typingNotificationState, ) } } 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 cd27959395..cfdf5618d0 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 @@ -11,6 +11,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList @@ -67,5 +68,6 @@ data class TimelineRoomInfo( val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendReaction: Boolean, val isCallOngoing: Boolean, - val pinnedEventIds: List + val pinnedEventIds: List, + val typingNotificationState: TypingNotificationState, ) 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 e0a9d660ca..fb96e75e28 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 @@ -21,6 +21,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel +import io.element.android.features.messages.impl.typing.TypingNotificationState +import io.element.android.features.messages.impl.typing.aTypingNotificationState 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.EventId @@ -241,6 +243,7 @@ internal fun aTimelineRoomInfo( isDm: Boolean = false, userHasPermissionToSendMessage: Boolean = true, pinnedEventIds: List = emptyList(), + typingNotificationState: TypingNotificationState = aTypingNotificationState(), ) = TimelineRoomInfo( isDm = isDm, name = name, @@ -248,4 +251,5 @@ internal fun aTimelineRoomInfo( userHasPermissionToSendReaction = true, isCallOngoing = false, pinnedEventIds = pinnedEventIds, + typingNotificationState = typingNotificationState, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 7abf443c04..9e383facd7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -55,9 +55,6 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider -import io.element.android.features.messages.impl.typing.TypingNotificationState -import io.element.android.features.messages.impl.typing.TypingNotificationView -import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.libraries.designsystem.components.dialogs.AlertDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -73,7 +70,6 @@ import kotlin.math.abs @Composable fun TimelineView( state: TimelineState, - typingNotificationState: TypingNotificationState, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onMessageClick: (TimelineItem.Event) -> Unit, @@ -131,11 +127,6 @@ fun TimelineView( reverseLayout = useReverseLayout, contentPadding = PaddingValues(vertical = 8.dp), ) { - if (state.isLive) { - item { - TypingNotificationView(state = typingNotificationState) - } - } items( items = state.timelineItems, contentType = { timelineItem -> timelineItem.contentType() }, @@ -323,7 +314,6 @@ internal fun TimelineViewPreview( ), focusedEventIndex = 0, ), - typingNotificationState = aTypingNotificationState(), onUserDataClick = {}, onLinkClick = {}, onMessageClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt index 81686ed4d2..8c4e774029 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt @@ -36,7 +36,6 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview { timelineItems = items.toImmutableList(), messageShield = messageShield, ), - typingNotificationState = aTypingNotificationState(), onUserDataClick = {}, onLinkClick = {}, onMessageClick = {}, 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 7ff428223b..9b0fada9db 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 @@ -26,6 +26,8 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel +import io.element.android.features.messages.impl.typing.TypingNotificationView @Composable fun TimelineItemVirtualRow( @@ -46,9 +48,15 @@ fun TimelineItemVirtualRow( latestEventSink(TimelineEvents.LoadMore(virtual.model.direction)) } } + // Empty model trick to avoid timeline jumping during forward pagination. is TimelineItemLastForwardIndicatorModel -> { Spacer(modifier = Modifier) } + is TimelineItemTypingNotificationModel -> { + TypingNotificationView( + state = timelineRoomInfo.typingNotificationState, + ) + } } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt index 8a3c5e2caf..39f1d334fd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt @@ -12,6 +12,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem @@ -39,6 +40,7 @@ class TimelineItemVirtualFactory @Inject constructor( timestamp = inner.timestamp ) is VirtualTimelineItem.LastForwardIndicator -> TimelineItemLastForwardIndicatorModel + VirtualTimelineItem.TypingNotification -> TimelineItemTypingNotificationModel } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTypingNotificationModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTypingNotificationModel.kt new file mode 100644 index 0000000000..a91042e3f7 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTypingNotificationModel.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.model.virtual + +data object TimelineItemTypingNotificationModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemTypingNotificationModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt deleted file mode 100644 index b6c08daf24..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.messages.impl.typing - -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.messages.impl.MessagesView -import io.element.android.features.messages.impl.aMessagesState -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight - -@PreviewsDayNight -@Composable -internal fun MessagesViewWithTypingPreview( - @PreviewParameter(TypingNotificationStateForMessagesProvider::class) typingState: TypingNotificationState -) = ElementPreview { - MessagesView( - state = aMessagesState().copy(typingNotificationState = typingState), - onBackClick = {}, - onRoomDetailsClick = {}, - onEventClick = { false }, - onUserDataClick = {}, - onLinkClick = {}, - onPreviewAttachments = {}, - onSendLocationClick = {}, - onCreatePollClick = {}, - onJoinCallClick = {}, - onViewAllPinnedMessagesClick = {}, - ) -} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index be1b4aa7a8..ddd06f4f2f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -40,7 +40,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent -import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter @@ -1055,11 +1054,6 @@ class MessagesPresenterTest { } } val featureFlagService = FakeFeatureFlagService() - val typingNotificationPresenter = TypingNotificationPresenter( - room = matrixRoom, - sessionPreferencesStore = sessionPreferencesStore, - ) - val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) @@ -1069,7 +1063,6 @@ class MessagesPresenterTest { composerPresenter = messageComposerPresenter, voiceMessageComposerPresenter = voiceMessageComposerPresenter, timelinePresenterFactory = timelinePresenterFactory, - typingNotificationPresenter = typingNotificationPresenter, actionListPresenterFactory = FakeActionListPresenter.Factory, customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index a0712907cc..b82299eb9c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -19,6 +19,7 @@ import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryC import io.element.android.features.messages.impl.timeline.components.aCriticalShield import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline @@ -503,7 +504,7 @@ import kotlin.time.Duration.Companion.seconds assertThat(state.timelineItems).isNotEmpty() } initialState.eventSink.invoke(TimelineEvents.JumpToLive) - skipItems(1) + skipItems(2) awaitItem().also { state -> // Event stays focused assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) @@ -670,7 +671,7 @@ import kotlin.time.Duration.Companion.seconds timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), ): TimelinePresenter { return TimelinePresenter( - timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), + timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(timelineItemIndexer), room = room, dispatchers = testCoroutineDispatchers(), appScope = this, @@ -682,6 +683,7 @@ import kotlin.time.Duration.Companion.seconds timelineItemIndexer = timelineItemIndexer, timelineController = TimelineController(room), resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, + typingNotificationPresenter = { aTypingNotificationState() }, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 4dc61b7b80..48f242d24f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -155,7 +155,6 @@ private fun AndroidComposeTestRule.setTimel setSafeContent { TimelineView( state = state, - typingNotificationState = typingNotificationState, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onMessageClick = onMessageClick, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt index 98d6d9fc76..80d627c7ab 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt @@ -24,4 +24,7 @@ sealed interface VirtualTimelineItem { val direction: Timeline.PaginationDirection, val timestamp: Long, ) : VirtualTimelineItem + + data object TypingNotification : VirtualTimelineItem + } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index e0fd82bfcb..517d83ff80 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTim import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor +import io.element.android.libraries.matrix.impl.timeline.postprocessor.TypingNotificationPostProcessor import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper import io.element.android.libraries.matrix.impl.util.MessageEventContent import io.element.android.services.toolbox.api.systemclock.SystemClock @@ -121,6 +122,7 @@ class RustTimeline( private val roomBeginningPostProcessor = RoomBeginningPostProcessor(mode) private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock) private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode) + private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode) private val backPaginationStatus = MutableStateFlow( Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS) @@ -235,6 +237,9 @@ class RustTimeline( hasMoreToLoadForward = hasMoreToLoadForward ) } + .let { items -> + typingNotificationPostProcessor.process(items = items) + } // Keep lastForwardIndicatorsPostProcessor last .let { items -> lastForwardIndicatorsPostProcessor.process( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TypingNotificationPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TypingNotificationPostProcessor.kt new file mode 100644 index 0000000000..c9aa7e67fb --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TypingNotificationPostProcessor.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem + +/** + * This post processor is responsible for adding a typing notification item to the timeline items when the timeline is in live mode. + */ +class TypingNotificationPostProcessor(private val mode: Timeline.Mode) { + + fun process(items: List): List { + return if (mode == Timeline.Mode.LIVE) { + buildList { + addAll(items) + add( + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("TypingNotification"), + virtual = VirtualTimelineItem.TypingNotification + ) + ) + } + } else { + items + } + } +}