diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 6b84423d44..2a84150540 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -71,6 +71,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.room.joinedRoomMembers +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache @@ -187,8 +188,11 @@ class MessagesFlowNode @AssistedInject constructor( callbacks.forEach { it.onRoomDetailsClick() } } - override fun onEventClick(event: TimelineItem.Event): Boolean { - return processEventClick(event) + override fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean { + return processEventClick( + timelineMode = if (isLive) Timeline.Mode.LIVE else Timeline.Mode.FOCUSED_ON_EVENT, + event = event, + ) } override fun onPreviewAttachments(attachments: ImmutableList) { @@ -316,7 +320,10 @@ class MessagesFlowNode @AssistedInject constructor( NavTarget.PinnedMessagesList -> { val callback = object : PinnedMessagesListNode.Callback { override fun onEventClick(event: TimelineItem.Event) { - processEventClick(event) + processEventClick( + timelineMode = Timeline.Mode.PINNED_EVENTS, + event = event, + ) } override fun onUserDataClick(userId: UserId) { @@ -358,11 +365,14 @@ class MessagesFlowNode @AssistedInject constructor( callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) } } - private fun processEventClick(event: TimelineItem.Event): Boolean { + private fun processEventClick( + timelineMode: Timeline.Mode, + event: TimelineItem.Event, + ): Boolean { val navTarget = when (event.content) { is TimelineItemImageContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -374,7 +384,7 @@ class MessagesFlowNode @AssistedInject constructor( if encrypted on certain bridges */ event.content.preferredMediaSource?.let { preferredMediaSource -> buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode), event = event, content = event.content, mediaSource = preferredMediaSource, @@ -384,7 +394,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemVideoContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -393,7 +403,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemFileContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -402,7 +412,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemAudioContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, 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 551b362e1a..a8076d023c 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 @@ -89,7 +89,7 @@ class MessagesNode @AssistedInject constructor( interface Callback : Plugin { fun onRoomDetailsClick() - fun onEventClick(event: TimelineItem.Event): Boolean + fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean fun onPreviewAttachments(attachments: ImmutableList) fun onUserDataClick(userId: UserId) fun onPermalinkClick(data: PermalinkData) @@ -120,12 +120,12 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onRoomDetailsClick() } } - private fun onEventClick(event: TimelineItem.Event): Boolean { + private fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean { // Note: cannot use `callbacks.all { it.onEventClick(event) }` because: // - if callbacks is empty, it will return true and we want to return false. // - if a callback returns false, the other callback will not be invoked. return callbacks.takeIf { it.isNotEmpty() } - ?.map { it.onEventClick(event) } + ?.map { it.onEventClick(isLive, event) } ?.all { it } .orFalse() } 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 09c92071c4..a7d312b816 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 @@ -111,7 +111,7 @@ fun MessagesView( state: MessagesState, onBackClick: () -> Unit, onRoomDetailsClick: () -> Unit, - onEventContentClick: (event: TimelineItem.Event) -> Boolean, + onEventContentClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean, onUserDataClick: (UserId) -> Unit, onLinkClick: (String, Boolean) -> Unit, onSendLocationClick: () -> Unit, @@ -142,7 +142,7 @@ fun MessagesView( fun onContentClick(event: TimelineItem.Event) { Timber.v("onMessageClick= ${event.id}") - val hideKeyboard = onEventContentClick(event) + val hideKeyboard = onEventContentClick(state.timelineState.isLive, event) if (hideKeyboard) { localView.hideKeyboard() } @@ -544,7 +544,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) state = state, onBackClick = {}, onRoomDetailsClick = {}, - onEventContentClick = { false }, + onEventContentClick = { _, _ -> false }, onUserDataClick = {}, onLinkClick = { _, _ -> }, onSendLocationClick = {}, 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 5b695e1a9e..cdd91e86e4 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 @@ -33,7 +33,7 @@ internal fun MessagesViewWithIdentityChangePreview( ), onBackClick = {}, onRoomDetailsClick = {}, - onEventContentClick = { false }, + onEventContentClick = { _, _ -> false }, onUserDataClick = {}, onLinkClick = { _, _ -> }, onSendLocationClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt index 43b4fef3dd..1135dcb2d6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.timeline.Timeline @@ -104,7 +105,7 @@ class PinnedEventsTimelineProvider @Inject constructor( is AsyncData.Uninitialized, is AsyncData.Failure -> { timelineStateFlow.emit(AsyncData.Loading()) withContext(dispatchers.io) { - room.pinnedEventsTimeline() + room.createTimeline(CreateTimelineParams.PinnedOnly) } .fold( { timelineStateFlow.emit(AsyncData.Success(it)) }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index bfac291432..985d4f6076 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline @@ -64,7 +65,7 @@ class TimelineController @Inject constructor( } suspend fun focusOnEvent(eventId: EventId): Result { - return room.timelineFocusedOnEvent(eventId) + return room.createTimeline(CreateTimelineParams.Focused(eventId)) .onFailure { if (it is CancellationException) { throw it diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 3557d84bd5..a7c76d73e2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -54,11 +54,11 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureCalledOnceWithParam +import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParamsAndResult import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam -import io.element.android.tests.testutils.EnsureNeverCalledWithParamAndResult import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams +import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParamsAndResult import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce @@ -129,8 +129,9 @@ class MessagesViewTest { eventSink = eventsRecorder ) val timelineItem = state.timelineState.timelineItems.first() - val callback = EnsureCalledOnceWithParam( - expectedParam = timelineItem, + val callback = EnsureCalledOnceWithTwoParamsAndResult( + expectedParam1 = true, + expectedParam2 = timelineItem, result = true, ) rule.setMessagesView( @@ -513,7 +514,7 @@ private fun AndroidComposeTestRule.setMessa state: MessagesState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomDetailsClick: () -> Unit = EnsureNeverCalled(), - onEventClick: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(), + onEventClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithTwoParamsAndResult(), onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(), onSendLocationClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt index 8aa7bee779..5f6fa1b164 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt @@ -55,7 +55,7 @@ class PinnedMessagesBannerPresenterTest { @Test fun `present - loading state`() = runTest { val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(FakeTimeline()) } + createTimelineResult = { Result.success(FakeTimeline()) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) } @@ -86,7 +86,7 @@ class PinnedMessagesBannerPresenterTest { ) ) val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) } + createTimelineResult = { Result.success(pinnedEventsTimeline) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2))) } @@ -125,7 +125,7 @@ class PinnedMessagesBannerPresenterTest { ) ) val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) } + createTimelineResult = { Result.success(pinnedEventsTimeline) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2))) } @@ -160,7 +160,7 @@ class PinnedMessagesBannerPresenterTest { @Test fun `present - timeline failed`() = runTest { val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.failure(Exception()) } + createTimelineResult = { Result.failure(Exception()) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 976c7da29c..7fcbfb62eb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -83,7 +83,7 @@ class PinnedMessagesListPresenterTest { @Test fun `present - timeline failure state`() = runTest { val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.failure(RuntimeException()) }, + createTimelineResult = { Result.failure(RuntimeException()) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, @@ -102,7 +102,7 @@ class PinnedMessagesListPresenterTest { @Test fun `present - empty state`() = runTest { val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(FakeTimeline()) }, + createTimelineResult = { Result.success(FakeTimeline()) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, @@ -122,7 +122,7 @@ class PinnedMessagesListPresenterTest { fun `present - filled state`() = runTest { val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, @@ -149,7 +149,7 @@ class PinnedMessagesListPresenterTest { val pinnedEventsTimeline = createPinnedMessagesTimeline() val analyticsService = FakeAnalyticsService() val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, @@ -195,7 +195,7 @@ class PinnedMessagesListPresenterTest { } val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, @@ -224,7 +224,7 @@ class PinnedMessagesListPresenterTest { } val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, @@ -253,7 +253,7 @@ class PinnedMessagesListPresenterTest { } val pinnedEventsTimeline = createPinnedMessagesTimeline() val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt index a9c8f085d8..83cc05eb52 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt @@ -31,7 +31,7 @@ class TimelineControllerTest { val detachedTimeline = FakeTimeline(name = "detached") val matrixRoom = FakeMatrixRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) val sut = TimelineController(matrixRoom) @@ -63,7 +63,7 @@ class TimelineControllerTest { var callNumber = 0 val matrixRoom = FakeMatrixRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { + createTimelineResult = { callNumber++ when (callNumber) { 1 -> Result.success(detachedTimeline1) @@ -117,7 +117,7 @@ class TimelineControllerTest { val detachedTimeline = FakeTimeline(name = "detached") val matrixRoom = FakeMatrixRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) val sut = TimelineController(matrixRoom) sut.activeTimelineFlow().test { @@ -167,7 +167,7 @@ class TimelineControllerTest { } val matrixRoom = FakeMatrixRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) val sut = TimelineController(matrixRoom) sut.activeTimelineFlow().test { @@ -192,7 +192,7 @@ class TimelineControllerTest { val detachedTimeline = FakeTimeline(name = "detached") val matrixRoom = FakeMatrixRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) val sut = TimelineController(matrixRoom) 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 60a5274142..6c81745ce4 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 @@ -483,7 +483,7 @@ import kotlin.time.Duration.Companion.seconds ) val room = FakeMatrixRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) }, + createTimelineResult = { Result.success(detachedTimeline) }, canUserSendMessageResult = { _, _ -> Result.success(true) }, ) val presenter = createTimelinePresenter( @@ -561,7 +561,7 @@ import kotlin.time.Duration.Companion.seconds liveTimeline = FakeTimeline( timelineItems = flowOf(emptyList()), ), - timelineFocusedOnEventResult = { Result.failure(Throwable("An error")) }, + createTimelineResult = { Result.failure(Throwable("An error")) }, canUserSendMessageResult = { _, _ -> Result.success(true) }, ) ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt new file mode 100644 index 0000000000..0bcfb0bf54 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt @@ -0,0 +1,17 @@ +/* + * 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 + +import io.element.android.libraries.matrix.api.core.EventId + +sealed interface CreateTimelineParams { + data class Focused(val focusedEventId: EventId) : CreateTimelineParams + data object MediaOnly : CreateTimelineParams + data class MediaOnlyFocused(val focusedEventId: EventId) : CreateTimelineParams + data object PinnedOnly : CreateTimelineParams +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 1d8d151716..41b25c0dc0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -109,21 +109,12 @@ interface MatrixRoom : Closeable { val liveTimeline: Timeline /** - * Create a new timeline, focused on the provided Event. - * Should not be used directly, see `TimelineController` to manage the various timelines. + * Create a new timeline. + * @param createTimelineParams contains parameters about how to filter the timeline. Will also configure the date separators. */ - suspend fun timelineFocusedOnEvent(eventId: EventId): Result - - /** - * Create a new timeline for the pinned events of the room. - */ - suspend fun pinnedEventsTimeline(): Result - - /** - * Create a new timeline for the media events of the room. - * @param eventId The event to focus on, if any. - */ - suspend fun mediaTimeline(eventId: EventId?): Result + suspend fun createTimeline( + createTimelineParams: CreateTimelineParams, + ): Result fun destroy() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index ef570f5ae3..87c61ca17c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo @@ -214,80 +215,81 @@ class RustMatrixRoom( override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId) - override suspend fun timelineFocusedOnEvent(eventId: EventId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.timelineWithConfiguration( - configuration = TimelineConfiguration( - focus = TimelineFocus.Event( - eventId = eventId.value, - numContextEvents = 50u, - ), - allowedMessageTypes = AllowedMessageTypes.All, - internalIdPrefix = "focus_$eventId", - dateDividerMode = DateDividerMode.DAILY, - ) - ).let { inner -> - createTimeline(inner, mode = Timeline.Mode.FOCUSED_ON_EVENT) - } - }.mapFailure { - it.toFocusEventException() - }.onFailure { - if (it is CancellationException) { - throw it - } - } - } - - override suspend fun pinnedEventsTimeline(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.timelineWithConfiguration( - configuration = TimelineConfiguration( - focus = TimelineFocus.PinnedEvents( - maxEventsToLoad = 100u, - maxConcurrentRequests = 10u, - ), - allowedMessageTypes = AllowedMessageTypes.All, - internalIdPrefix = "pinned_events", - dateDividerMode = DateDividerMode.DAILY, - ) - ).let { inner -> - createTimeline(inner, mode = Timeline.Mode.PINNED_EVENTS) - } - }.onFailure { - if (it is CancellationException) { - throw it - } - } - } - - override suspend fun mediaTimeline( - eventId: EventId?, + override suspend fun createTimeline( + createTimelineParams: CreateTimelineParams, ): Result = withContext(roomDispatcher) { - val focus = if (eventId != null) { - TimelineFocus.Event( - eventId = eventId.value, + val focus = when (createTimelineParams) { + is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents( + maxEventsToLoad = 100u, + maxConcurrentRequests = 10u, + ) + is CreateTimelineParams.MediaOnly -> TimelineFocus.Live + is CreateTimelineParams.Focused -> TimelineFocus.Event( + eventId = createTimelineParams.focusedEventId.value, + numContextEvents = 50u, + ) + is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event( + eventId = createTimelineParams.focusedEventId.value, numContextEvents = 50u, ) - } else { - TimelineFocus.Live } + + val allowedMessageTypes = when (createTimelineParams) { + is CreateTimelineParams.MediaOnly, + is CreateTimelineParams.MediaOnlyFocused -> AllowedMessageTypes.Only( + types = listOf( + RoomMessageEventMessageType.FILE, + RoomMessageEventMessageType.IMAGE, + RoomMessageEventMessageType.VIDEO, + RoomMessageEventMessageType.AUDIO, + ) + ) + is CreateTimelineParams.Focused, + CreateTimelineParams.PinnedOnly -> AllowedMessageTypes.All + } + + val internalIdPrefix = when (createTimelineParams) { + is CreateTimelineParams.PinnedOnly -> "pinned_events" + is CreateTimelineParams.Focused -> "focus_${createTimelineParams.focusedEventId}" + is CreateTimelineParams.MediaOnly -> "MediaGallery_" + is CreateTimelineParams.MediaOnlyFocused -> "MediaGallery_${createTimelineParams.focusedEventId}" + } + + // Note that for TimelineFilter.MediaOnlyFocused, the date separator will be filtered out, + // but there is no way to exclude data separator at the moment. + val dateDividerMode = when (createTimelineParams) { + is CreateTimelineParams.MediaOnly, + is CreateTimelineParams.MediaOnlyFocused -> DateDividerMode.MONTHLY + is CreateTimelineParams.Focused, + CreateTimelineParams.PinnedOnly -> DateDividerMode.DAILY + } + runCatching { innerRoom.timelineWithConfiguration( configuration = TimelineConfiguration( focus = focus, - allowedMessageTypes = AllowedMessageTypes.Only( - types = listOf( - RoomMessageEventMessageType.FILE, - RoomMessageEventMessageType.IMAGE, - RoomMessageEventMessageType.VIDEO, - RoomMessageEventMessageType.AUDIO, - ) - ), - internalIdPrefix = "MediaGallery_", - dateDividerMode = DateDividerMode.MONTHLY, + allowedMessageTypes = allowedMessageTypes, + internalIdPrefix = internalIdPrefix, + dateDividerMode = dateDividerMode, ) ).let { inner -> - createTimeline(inner, mode = if (eventId != null) Timeline.Mode.FOCUSED_ON_EVENT else Timeline.Mode.MEDIA) + val mode = when (createTimelineParams) { + is CreateTimelineParams.Focused -> Timeline.Mode.FOCUSED_ON_EVENT + is CreateTimelineParams.MediaOnly -> Timeline.Mode.MEDIA + is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FOCUSED_ON_EVENT + CreateTimelineParams.PinnedOnly -> Timeline.Mode.PINNED_EVENTS + } + createTimeline( + timeline = inner, + mode = mode, + ) + } + }.mapFailure { + when (createTimelineParams) { + is CreateTimelineParams.Focused, + is CreateTimelineParams.MediaOnlyFocused -> it.toFocusEventException() + CreateTimelineParams.MediaOnly, + CreateTimelineParams.PinnedOnly -> it } }.onFailure { if (it is CancellationException) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index d214f91f2b..04c32b5445 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo @@ -138,9 +139,7 @@ class FakeMatrixRoom( private val leaveRoomLambda: () -> Result = { lambdaError() }, private val updateMembersResult: () -> Unit = { lambdaError() }, private val getMembersResult: (Int) -> Result> = { lambdaError() }, - private val timelineFocusedOnEventResult: (EventId) -> Result = { lambdaError() }, - private val pinnedEventsTimelineResult: () -> Result = { lambdaError() }, - private val mediaTimelineResult: (EventId?) -> Result = { lambdaError() }, + private val createTimelineResult: (CreateTimelineParams) -> Result = { lambdaError() }, private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> }, private val saveComposerDraftLambda: (ComposerDraft) -> Result = { _: ComposerDraft -> Result.success(Unit) }, private val loadComposerDraftLambda: () -> Result = { Result.success(null) }, @@ -220,16 +219,10 @@ class FakeMatrixRoom( _syncUpdateFlow.tryEmit(_syncUpdateFlow.value + 1) } - override suspend fun timelineFocusedOnEvent(eventId: EventId): Result = simulateLongTask { - timelineFocusedOnEventResult(eventId) - } - - override suspend fun pinnedEventsTimeline(): Result = simulateLongTask { - pinnedEventsTimelineResult() - } - - override suspend fun mediaTimeline(eventId: EventId?): Result = simulateLongTask { - mediaTimelineResult(eventId) + override suspend fun createTimeline( + createTimelineParams: CreateTimelineParams, + ): Result = simulateLongTask { + createTimelineResult(createTimelineParams) } override suspend fun subscribeToSync() { diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt index a824fc5540..906283c457 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.mediaviewer.api +import android.os.Parcelable import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -14,6 +15,8 @@ import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.timeline.Timeline +import kotlinx.parcelize.Parcelize interface MediaViewerEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder @@ -39,9 +42,14 @@ interface MediaViewerEntryPoint : FeatureEntryPoint { val canShowInfo: Boolean, ) : NodeInputs - enum class MediaViewerMode { - SingleMedia, - TimelineImagesAndVideos, - TimelineFilesAndAudios, + sealed interface MediaViewerMode : Parcelable { + @Parcelize + data object SingleMedia : MediaViewerMode + + @Parcelize + data class TimelineImagesAndVideos(val timelineMode: Timeline.Mode) : MediaViewerMode + + @Parcelize + data class TimelineFilesAndAudios(val timelineMode: Timeline.Mode) : MediaViewerMode } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt index f7426aa4e9..f776b35016 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt @@ -18,6 +18,7 @@ interface FocusedTimelineMediaGalleryDataSourceFactory { fun createFor( eventId: EventId, mediaItem: MediaItem.Event, + onlyPinnedEvents: Boolean, ): MediaGalleryDataSource } @@ -30,6 +31,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor( override fun createFor( eventId: EventId, mediaItem: MediaItem.Event, + onlyPinnedEvents: Boolean, ): MediaGalleryDataSource { return TimelineMediaGalleryDataSource( room = room, @@ -37,6 +39,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor( room = room, eventId = eventId, initialMediaItem = mediaItem, + onlyPinnedEvents = onlyPinnedEvents, ), timelineMediaItemsFactory = timelineMediaItemsFactory, mediaItemsPostProcessor = mediaItemsPostProcessor, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt index 6dc4136193..2d7739cc32 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn 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.CreateTimelineParams import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems @@ -44,7 +45,7 @@ class LiveMediaTimeline @Inject constructor( override suspend fun getTimeline(): Result = mutex.withLock { val currentTimeline = timeline if (currentTimeline == null) { - room.mediaTimeline(null) + room.createTimeline(CreateTimelineParams.MediaOnly) .onSuccess { timeline = it } } else { Result.success(currentTimeline) @@ -58,14 +59,22 @@ class LiveMediaTimeline @Inject constructor( /** * A class that will provide a media timeline that is focused on a particular event. + * Optionally, the timeline will only contain the pinned events. */ class FocusedMediaTimeline( private val room: MatrixRoom, private val eventId: EventId, + private val onlyPinnedEvents: Boolean, initialMediaItem: MediaItem.Event, ) : MediaTimeline { override suspend fun getTimeline(): Result { - return room.mediaTimeline(eventId) + return room.createTimeline( + createTimelineParams = if (onlyPinnedEvents) { + CreateTimelineParams.PinnedOnly + } else { + CreateTimelineParams.MediaOnlyFocused(eventId) + }, + ) } override val cache = persistentListOf( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt index 7317920005..f862788c30 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint @@ -96,9 +97,9 @@ class MediaGalleryRootNode @AssistedInject constructor( val mode = when (item) { is MediaItem.Audio, is MediaItem.Voice, - is MediaItem.File -> MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios + is MediaItem.File -> MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(Timeline.Mode.MEDIA) is MediaItem.Image, - is MediaItem.Video -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos + is MediaItem.Video -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.MEDIA) } overlay.show( NavTarget.MediaViewer( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index ebaba4cdcd..3fd01cc08e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -52,8 +52,8 @@ class MediaViewerDataSource( private val galleryMode = when (mode) { MediaViewerMode.SingleMedia, - MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images - MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files + is MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images + is MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files } // Map of sourceUrl to local media state diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index 7876641051..d994f374f9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.impl.datasource.FocusedTimelineMediaGalleryDataSourceFactory @@ -69,16 +70,40 @@ class MediaViewerNode @AssistedInject constructor( // Should not happen timelineMediaGalleryDataSource } else { - // Does timelineMediaGalleryDataSource knows the eventId? - val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull() - val isEventKnown = lastData?.hasEvent(eventId) == true - if (isEventKnown) { - timelineMediaGalleryDataSource - } else { - focusedTimelineMediaGalleryDataSourceFactory.createFor( - eventId = eventId, - mediaItem = inputs.toMediaItem(), - ) + // Can we use a specific timeline? + val timelineMode = when (val mode = inputs.mode) { + is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> mode.timelineMode + is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> mode.timelineMode + else -> null + } + when (timelineMode) { + null -> timelineMediaGalleryDataSource + Timeline.Mode.LIVE -> { + // Even if the timelineMediaGalleryDataSource does not know the eventId, the SDK will create the timeline faster + timelineMediaGalleryDataSource + } + Timeline.Mode.FOCUSED_ON_EVENT -> { + // Does timelineMediaGalleryDataSource knows the eventId? + val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull() + val isEventKnown = lastData?.hasEvent(eventId) == true + if (isEventKnown) { + timelineMediaGalleryDataSource + } else { + focusedTimelineMediaGalleryDataSourceFactory.createFor( + eventId = eventId, + mediaItem = inputs.toMediaItem(), + onlyPinnedEvents = false, + ) + } + } + Timeline.Mode.PINNED_EVENTS -> { + focusedTimelineMediaGalleryDataSourceFactory.createFor( + eventId = eventId, + mediaItem = inputs.toMediaItem(), + onlyPinnedEvents = true, + ) + } + Timeline.Mode.MEDIA -> timelineMediaGalleryDataSource } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index b926291461..834edacd76 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -204,8 +204,8 @@ class MediaViewerPresenter @AssistedInject constructor( private fun showNoMoreItemsSnackbar() { val messageResId = when (inputs.mode) { MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show - MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show + is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show + is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show } val message = SnackbarMessage(messageResId) snackbarDispatcher.post(message) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt index a2ab9b162f..7496b01324 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt @@ -25,6 +25,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest { val result = sut.createFor( eventId = AN_EVENT_ID, mediaItem = aMediaItemImage(), + onlyPinnedEvents = false, ) assertThat(result).isInstanceOf(TimelineMediaGalleryDataSource::class.java) } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt index 1d4fd3adbe..5c46ffd910 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -75,11 +76,11 @@ class FocusedMediaTimelineTest { @Test fun `getTimeline returns the timeline provided by the room`() = runTest { - val mediaTimelineResult = lambdaRecorder> { + val createTimelineResult = lambdaRecorder> { Result.success(FakeTimeline()) } val room = FakeMatrixRoom( - mediaTimelineResult = mediaTimelineResult, + createTimelineResult = createTimelineResult, ) val sut = createFocusedMediaTimeline( room = room, @@ -87,16 +88,36 @@ class FocusedMediaTimelineTest { ) val timeline = sut.getTimeline() assertThat(timeline.isSuccess).isTrue() - mediaTimelineResult.assertions().isCalledOnce().with(value(AN_EVENT_ID)) + createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.MediaOnlyFocused(AN_EVENT_ID))) + } + + @Test + fun `getTimeline returns the timeline provided by the room for pinned Events`() = runTest { + val createTimelineResult = lambdaRecorder> { + Result.success(FakeTimeline()) + } + val room = FakeMatrixRoom( + createTimelineResult = createTimelineResult, + ) + val sut = createFocusedMediaTimeline( + room = room, + eventId = AN_EVENT_ID, + onlyPinnedEvent = true, + ) + val timeline = sut.getTimeline() + assertThat(timeline.isSuccess).isTrue() + createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.PinnedOnly)) } private fun createFocusedMediaTimeline( room: MatrixRoom = FakeMatrixRoom(), eventId: EventId = AN_EVENT_ID, initialMediaItem: MediaItem.Event = aMediaItemImage(), + onlyPinnedEvent: Boolean = false, ) = FocusedMediaTimeline( room = room, eventId = eventId, initialMediaItem = initialMediaItem, + onlyPinnedEvents = onlyPinnedEvent, ) } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt index 95b62a5824..839f8798d2 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -28,22 +28,22 @@ class LiveMediaTimelineTest { @Test fun `getTimeline returns the timeline provided by the room, then from cache`() = runTest { - val mediaTimelineResult = lambdaRecorder> { + val createTimelineResult = lambdaRecorder> { Result.success(FakeTimeline()) } val room = FakeMatrixRoom( - mediaTimelineResult = mediaTimelineResult, + createTimelineResult = createTimelineResult, ) val sut = createLiveMediaTimeline( room = room, ) val timeline = sut.getTimeline() assertThat(timeline.isSuccess).isTrue() - mediaTimelineResult.assertions().isCalledOnce().with(value(null)) + createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.MediaOnly)) val timeline2 = sut.getTimeline() assertThat(timeline2.isSuccess).isTrue() // No called another time - mediaTimelineResult.assertions().isCalledOnce() + createTimelineResult.assertions().isCalledOnce() } private fun createLiveMediaTimeline( diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt index 736fd309f3..8e673ce7aa 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt @@ -57,7 +57,7 @@ class TimelineMediaGalleryDataSourceTest { val fakeTimeline = FakeTimeline() val sut = createTimelineMediaGalleryDataSource( room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -75,7 +75,7 @@ class TimelineMediaGalleryDataSourceTest { runTest { val sut = createTimelineMediaGalleryDataSource( room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -112,7 +112,7 @@ class TimelineMediaGalleryDataSourceTest { } val sut = createTimelineMediaGalleryDataSource( room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -135,7 +135,7 @@ class TimelineMediaGalleryDataSourceTest { } val sut = createTimelineMediaGalleryDataSource( room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -154,7 +154,7 @@ class TimelineMediaGalleryDataSourceTest { fun `test - failing to load timeline should emit an error`() = runTest { val sut = createTimelineMediaGalleryDataSource( room = FakeMatrixRoom( - mediaTimelineResult = { Result.failure(AN_EXCEPTION) }, + createTimelineResult = { Result.failure(AN_EXCEPTION) }, roomCoroutineScope = backgroundScope, ) ) @@ -176,7 +176,7 @@ class TimelineMediaGalleryDataSourceTest { ) val sut = createTimelineMediaGalleryDataSource( room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt index 1a43bbacf6..81e270a09f 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt @@ -52,7 +52,7 @@ class MediaGalleryPresenterTest { ), room = FakeMatrixRoom( displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + createTimelineResult = { Result.success(FakeTimeline()) }, ) ) presenter.test { @@ -71,7 +71,7 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter( room = FakeMatrixRoom( displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + createTimelineResult = { Result.success(FakeTimeline()) }, ) ) presenter.test { @@ -101,7 +101,7 @@ class MediaGalleryPresenterTest { room = FakeMatrixRoom( sessionId = A_USER_ID, displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + createTimelineResult = { Result.success(FakeTimeline()) }, canRedactOwnResult = { Result.success(canDeleteOwn) } ) ) @@ -144,7 +144,7 @@ class MediaGalleryPresenterTest { room = FakeMatrixRoom( sessionId = A_USER_ID, displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + createTimelineResult = { Result.success(FakeTimeline()) }, canRedactOtherResult = { Result.success(canDeleteOther) } ) ) @@ -177,7 +177,7 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter( room = FakeMatrixRoom( displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + createTimelineResult = { Result.success(FakeTimeline()) }, ) ) presenter.test { @@ -244,7 +244,7 @@ class MediaGalleryPresenterTest { ) val presenter = createMediaGalleryPresenter( room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(FakeTimeline()) }, + createTimelineResult = { Result.success(FakeTimeline()) }, ), navigator = navigator, ) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt index b49c174d21..6eced261b1 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt @@ -137,7 +137,7 @@ class MediaViewerDataSourceTest { fun `test dataFlow with data galleryMode image`() = runTest { val galleryDataSource = FakeMediaGalleryDataSource() val sut = createMediaViewerDataSource( - mode = MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), galleryDataSource = galleryDataSource, ) sut.dataFlow().test { @@ -159,7 +159,7 @@ class MediaViewerDataSourceTest { fun `test dataFlow with data galleryMode files`() = runTest { val galleryDataSource = FakeMediaGalleryDataSource() val sut = createMediaViewerDataSource( - mode = MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA), galleryDataSource = galleryDataSource, ) sut.dataFlow().test { @@ -265,7 +265,7 @@ class MediaViewerDataSourceTest { } private fun TestScope.createMediaViewerDataSource( - mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos, + mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), galleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(), mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl), diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index 03d66992bf..06418da15e 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -519,7 +519,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items forward images and videos`() { `present - snackbar displayed when there is no more items forward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_media_to_show, ) } @@ -527,7 +527,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items forward files and audio`() { `present - snackbar displayed when there is no more items forward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_files_to_show, ) } @@ -547,7 +547,7 @@ class MediaViewerPresenterTest { awaitFirstItem() mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), @@ -568,7 +568,7 @@ class MediaViewerPresenterTest { // data source claims that there is no more items to load forward mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(anImage, aBackwardLoadingIndicator), @@ -590,7 +590,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items backward images and videos`() { `present - snackbar displayed when there is no more items backward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_media_to_show, ) } @@ -598,7 +598,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items backward files and audio`() { `present - snackbar displayed when there is no more items backward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_files_to_show, ) } @@ -618,7 +618,7 @@ class MediaViewerPresenterTest { awaitFirstItem() mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), @@ -640,7 +640,7 @@ class MediaViewerPresenterTest { // data source claims that there is no more items to load backward mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(aForwardLoadingIndicator, anImage), diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt index a794a2851b..15d09e17a9 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt @@ -84,6 +84,27 @@ class EnsureCalledOnceWithTwoParams( } } +class EnsureCalledOnceWithTwoParamsAndResult( + private val expectedParam1: T, + private val expectedParam2: U, + private val result: R, +) : (T, U) -> R { + private var counter = 0 + override fun invoke(p1: T, p2: U): R { + if (p1 != expectedParam1 || p2 != expectedParam2) { + throw AssertionError("Expected to be called with $expectedParam1 and $expectedParam2, but was called with $p1 and $p2") + } + counter++ + return result + } + + fun assertSuccess() { + if (counter != 1) { + throw AssertionError("Expected to be called once, but was called $counter times") + } + } +} + /** * Shortcut for [ ensureCalledOnceWithParam] with Unit result. */ diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt index 28c57e62f6..689a52c792 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt @@ -32,3 +32,9 @@ class EnsureNeverCalledWithTwoParams : (T, U) -> Unit { lambdaError("Should not be called and is called with $p1 and $p2") } } + +class EnsureNeverCalledWithTwoParamsAndResult : (T, U) -> R { + override fun invoke(p1: T, p2: U): R { + lambdaError("Should not be called and is called with $p1 and $p2") + } +}