From c02436d3f0c8a6505ccfc48749647cca958a2e4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Feb 2025 16:45:05 +0100 Subject: [PATCH] Update Matrix Room API and allow media swipe on pinned event only. --- .../messages/impl/MessagesFlowNode.kt | 28 +++-- .../features/messages/impl/MessagesNode.kt | 6 +- .../features/messages/impl/MessagesView.kt | 6 +- .../MessagesViewWithIdentityChangePreview.kt | 2 +- .../pinned/PinnedEventsTimelineProvider.kt | 2 +- .../impl/timeline/TimelineController.kt | 2 +- .../messages/impl/MessagesViewTest.kt | 11 +- .../PinnedMessagesBannerPresenterTest.kt | 8 +- .../list/PinnedMessagesListPresenterTest.kt | 14 +-- .../impl/timeline/TimelineControllerTest.kt | 10 +- .../impl/timeline/TimelinePresenterTest.kt | 4 +- .../libraries/matrix/api/room/MatrixRoom.kt | 24 ++-- .../matrix/impl/room/RustMatrixRoom.kt | 114 ++++++++---------- .../matrix/test/room/FakeMatrixRoom.kt | 20 ++- .../mediaviewer/api/MediaViewerEntryPoint.kt | 16 ++- ...edTimelineMediaGalleryDataSourceFactory.kt | 3 + .../impl/datasource/MediaTimeline.kt | 10 +- .../impl/gallery/root/MediaGalleryRootNode.kt | 5 +- .../impl/viewer/MediaViewerDataSource.kt | 4 +- .../impl/viewer/MediaViewerNode.kt | 45 +++++-- .../impl/viewer/MediaViewerPresenter.kt | 4 +- ...melineMediaGalleryDataSourceFactoryTest.kt | 1 + .../datasource/FocusedMediaTimelineTest.kt | 28 ++++- .../impl/datasource/LiveMediaTimelineTest.kt | 9 +- .../TimelineMediaGalleryDataSourceTest.kt | 12 +- .../impl/gallery/MediaGalleryPresenterTest.kt | 12 +- .../impl/viewer/MediaViewerDataSourceTest.kt | 6 +- .../impl/viewer/MediaViewerPresenterTest.kt | 16 +-- .../tests/testutils/EnsureCalledOnce.kt | 21 ++++ .../tests/testutils/EnsureNeverCalled.kt | 6 + 30 files changed, 268 insertions(+), 181 deletions(-) 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 747fc847cc..c2dcef22a2 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 @@ -109,7 +109,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, @@ -140,7 +140,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() } @@ -535,7 +535,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..6e3ce308cd 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 @@ -104,7 +104,7 @@ class PinnedEventsTimelineProvider @Inject constructor( is AsyncData.Uninitialized, is AsyncData.Failure -> { timelineStateFlow.emit(AsyncData.Loading()) withContext(dispatchers.io) { - room.pinnedEventsTimeline() + room.createTimeline(onlyPinnedEvents = true) } .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..a4916fed31 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 @@ -64,7 +64,7 @@ class TimelineController @Inject constructor( } suspend fun focusOnEvent(eventId: EventId): Result { - return room.timelineFocusedOnEvent(eventId) + return room.createTimeline(focusedOnEventId = 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..b25cb1968a 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..77f4b3b65f 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..3d98266d40 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..f6f30e5415 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/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 1d8d151716..c13cce2b11 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,17 @@ 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 focusedOnEventId The event to focus on, if any. Note: if not null, and for regular timeline, + * this method should not be used directly, see `TimelineController` to manage the various timelines. + * @param onlyPinnedEvents True to get the timeline for pinned events only. + * @param onlyMedia True to get the timeline for media events only. */ - 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( + focusedOnEventId: EventId? = null, + onlyPinnedEvents: Boolean = false, + onlyMedia: Boolean = false, + ): 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..a2f7580ae9 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 @@ -214,80 +214,72 @@ 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( + focusedOnEventId: EventId?, + onlyPinnedEvents: Boolean, + onlyMedia: Boolean, ): Result = withContext(roomDispatcher) { - val focus = if (eventId != null) { + val focus = if (onlyPinnedEvents) { + TimelineFocus.PinnedEvents( + maxEventsToLoad = 100u, + maxConcurrentRequests = 10u, + ) + } else if (focusedOnEventId != null) { TimelineFocus.Event( - eventId = eventId.value, + eventId = focusedOnEventId.value, numContextEvents = 50u, ) } else { TimelineFocus.Live } + val allowedMessageTypes = if (onlyMedia) { + AllowedMessageTypes.Only( + types = listOf( + RoomMessageEventMessageType.FILE, + RoomMessageEventMessageType.IMAGE, + RoomMessageEventMessageType.VIDEO, + RoomMessageEventMessageType.AUDIO, + ) + ) + } else { + AllowedMessageTypes.All + } + val internalIdPrefix = if (onlyPinnedEvents) { + "pinned_events" + } else if (focusedOnEventId != null) { + "focus_$focusedOnEventId" + } else if (onlyMedia) { + "MediaGallery_" + } else { + "live" + } + val dateDividerMode = if (onlyMedia) { + DateDividerMode.MONTHLY + } else { + DateDividerMode.DAILY + } + val mode = when { + onlyPinnedEvents -> Timeline.Mode.PINNED_EVENTS + focusedOnEventId != null -> Timeline.Mode.FOCUSED_ON_EVENT + onlyMedia -> Timeline.Mode.MEDIA + else -> Timeline.Mode.LIVE + } 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) + createTimeline(inner, mode = mode) + } + }.mapFailure { + if (focusedOnEventId != null) { + it.toFocusEventException() + } else { + 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..f643a644be 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 @@ -138,9 +138,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: (EventId?, Boolean, Boolean) -> 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 +218,12 @@ 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( + focusedOnEventId: EventId?, + onlyPinnedEvents: Boolean, + onlyMedia: Boolean, + ): Result = simulateLongTask { + createTimelineResult(focusedOnEventId, onlyPinnedEvents, onlyMedia) } 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..3ecc5e622b 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 @@ -44,7 +44,7 @@ class LiveMediaTimeline @Inject constructor( override suspend fun getTimeline(): Result = mutex.withLock { val currentTimeline = timeline if (currentTimeline == null) { - room.mediaTimeline(null) + room.createTimeline(onlyMedia = true) .onSuccess { timeline = it } } else { Result.success(currentTimeline) @@ -58,14 +58,20 @@ 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( + focusedOnEventId = eventId, + onlyPinnedEvents = onlyPinnedEvents, + onlyMedia = true, + ) } 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..7e548c7477 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 @@ -75,11 +75,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 +87,38 @@ class FocusedMediaTimelineTest { ) val timeline = sut.getTimeline() assertThat(timeline.isSuccess).isTrue() - mediaTimelineResult.assertions().isCalledOnce().with(value(AN_EVENT_ID)) + createTimelineResult.assertions().isCalledOnce() + .with(value(AN_EVENT_ID), value(false), value(true)) + } + + @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(AN_EVENT_ID), value(true), value(true)) } 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..fdec7eb7d5 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 @@ -28,22 +28,23 @@ 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(null), value(false), value(true)) 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..b87f37cc70 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..eec0b4368b 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") + } +}