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 f9d9bbf27f..6b84423d44 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 @@ -123,6 +123,7 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data class MediaViewer( + val mode: MediaViewerEntryPoint.MediaViewerMode, val eventId: EventId?, val mediaInfo: MediaInfo, val mediaSource: MediaSource, @@ -248,8 +249,7 @@ class MessagesFlowNode @AssistedInject constructor( } is NavTarget.MediaViewer -> { val params = MediaViewerEntryPoint.Params( - // TODO When we will be able to load a media timeline from a EventId, change mode here (and use a mixed mode?) - mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia, + mode = navTarget.mode, eventId = navTarget.eventId, mediaInfo = navTarget.mediaInfo, mediaSource = navTarget.mediaSource, @@ -362,6 +362,7 @@ class MessagesFlowNode @AssistedInject constructor( val navTarget = when (event.content) { is TimelineItemImageContent -> { buildMediaViewerNavTarget( + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -373,6 +374,7 @@ class MessagesFlowNode @AssistedInject constructor( if encrypted on certain bridges */ event.content.preferredMediaSource?.let { preferredMediaSource -> buildMediaViewerNavTarget( + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, event = event, content = event.content, mediaSource = preferredMediaSource, @@ -382,6 +384,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemVideoContent -> { buildMediaViewerNavTarget( + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -390,6 +393,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemFileContent -> { buildMediaViewerNavTarget( + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -398,6 +402,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemAudioContent -> { buildMediaViewerNavTarget( + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -426,12 +431,14 @@ class MessagesFlowNode @AssistedInject constructor( } private fun buildMediaViewerNavTarget( + mode: MediaViewerEntryPoint.MediaViewerMode, event: TimelineItem.Event, content: TimelineItemEventContentWithAttachment, mediaSource: MediaSource, thumbnailSource: MediaSource?, ): NavTarget { return NavTarget.MediaViewer( + mode = mode, eventId = event.eventId, mediaInfo = MediaInfo( filename = content.filename, 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 bb86684be0..4d5f8c7a19 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 @@ -118,8 +118,9 @@ interface MatrixRoom : Closeable { /** * Create a new timeline for the media events of the room. + * @param eventId The event to focus on, if any. */ - suspend fun mediaTimeline(): Result + suspend fun mediaTimeline(eventId: EventId?): 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 ab16ac383f..911a62f376 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 @@ -253,11 +253,21 @@ class RustMatrixRoom( } } - override suspend fun mediaTimeline(): Result = withContext(roomDispatcher) { + override suspend fun mediaTimeline( + eventId: EventId?, + ): Result = withContext(roomDispatcher) { + val focus = if (eventId != null) { + TimelineFocus.Event( + eventId = eventId.value, + numContextEvents = 50u, + ) + } else { + TimelineFocus.Live + } runCatching { innerRoom.timelineWithConfiguration( configuration = TimelineConfiguration( - focus = TimelineFocus.Live, + focus = focus, allowedMessageTypes = AllowedMessageTypes.Only( types = listOf( RoomMessageEventMessageType.FILE, @@ -270,7 +280,7 @@ class RustMatrixRoom( dateDividerMode = DateDividerMode.MONTHLY, ) ).let { inner -> - createTimeline(inner, mode = Timeline.Mode.MEDIA) + createTimeline(inner, mode = if (eventId != null) Timeline.Mode.FOCUSED_ON_EVENT else Timeline.Mode.MEDIA) } }.onFailure { if (it is CancellationException) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 1946d2259b..509fac833c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -48,6 +48,7 @@ val A_THREAD_ID = ThreadId("\$aThreadId") val A_THREAD_ID_2 = ThreadId("\$aThreadId2") val AN_EVENT_ID = EventId("\$anEventId") val AN_EVENT_ID_2 = EventId("\$anEventId2") +val AN_EVENT_ID_3 = EventId("\$anEventId3") val A_ROOM_ALIAS = RoomAlias("#alias1:domain") val A_TRANSACTION_ID = TransactionId("aTransactionId") val A_DEVICE_ID = DeviceId("ILAKNDNASDLK") 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 62d19bd66c..1fd95da36b 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 @@ -137,7 +137,7 @@ class FakeMatrixRoom( private val getMembersResult: (Int) -> Result> = { lambdaError() }, private val timelineFocusedOnEventResult: (EventId) -> Result = { lambdaError() }, private val pinnedEventsTimelineResult: () -> Result = { lambdaError() }, - private val mediaTimelineResult: () -> Result = { lambdaError() }, + private val mediaTimelineResult: (EventId?) -> Result = { lambdaError() }, private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> }, private val saveComposerDraftLambda: (ComposerDraft) -> Result = { _: ComposerDraft -> Result.success(Unit) }, private val loadComposerDraftLambda: () -> Result = { Result.success(null) }, @@ -215,8 +215,8 @@ class FakeMatrixRoom( pinnedEventsTimelineResult() } - override suspend fun mediaTimeline(): Result = simulateLongTask { - mediaTimelineResult() + override suspend fun mediaTimeline(eventId: EventId?): Result = simulateLongTask { + mediaTimelineResult(eventId) } override suspend fun subscribeToSync() { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt similarity index 98% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt index 293329c2dd..705921db8b 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.dateformatter.api.DateFormatter @@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import timber.log.Timber import javax.inject.Inject 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 new file mode 100644 index 0000000000..f7426aa4e9 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt @@ -0,0 +1,45 @@ +/* + * 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.mediaviewer.impl.datasource + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import javax.inject.Inject + +interface FocusedTimelineMediaGalleryDataSourceFactory { + fun createFor( + eventId: EventId, + mediaItem: MediaItem.Event, + ): MediaGalleryDataSource +} + +@ContributesBinding(RoomScope::class) +class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor( + private val room: MatrixRoom, + private val timelineMediaItemsFactory: TimelineMediaItemsFactory, + private val mediaItemsPostProcessor: MediaItemsPostProcessor, +) : FocusedTimelineMediaGalleryDataSourceFactory { + override fun createFor( + eventId: EventId, + mediaItem: MediaItem.Event, + ): MediaGalleryDataSource { + return TimelineMediaGalleryDataSource( + room = room, + mediaTimeline = FocusedMediaTimeline( + room = room, + eventId = eventId, + initialMediaItem = mediaItem, + ), + timelineMediaItemsFactory = timelineMediaItemsFactory, + mediaItemsPostProcessor = mediaItemsPostProcessor, + ) + } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt similarity index 83% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryDataSource.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt index 0e12653108..4ad36d8827 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.architecture.AsyncData @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -39,6 +40,7 @@ interface MediaGalleryDataSource { @ContributesBinding(RoomScope::class) class TimelineMediaGalleryDataSource @Inject constructor( private val room: MatrixRoom, + private val mediaTimeline: MediaTimeline, private val timelineMediaItemsFactory: TimelineMediaItemsFactory, private val mediaItemsPostProcessor: MediaItemsPostProcessor, ) : MediaGalleryDataSource { @@ -48,7 +50,9 @@ class TimelineMediaGalleryDataSource @Inject constructor( override fun groupedMediaItemsFlow(): Flow> = groupedMediaItemsFlow - override fun getLastData(): AsyncData = groupedMediaItemsFlow.replayCache.firstOrNull() ?: AsyncData.Uninitialized + override fun getLastData(): AsyncData = groupedMediaItemsFlow.replayCache.firstOrNull() + ?: mediaTimeline.cache?.let { AsyncData.Success(it) } + ?: AsyncData.Uninitialized private val isStarted = AtomicBoolean(false) @@ -58,8 +62,13 @@ class TimelineMediaGalleryDataSource @Inject constructor( return } flow { - groupedMediaItemsFlow.emit(AsyncData.Loading()) - room.mediaTimeline().fold( + val cache = mediaTimeline.cache + if (cache != null) { + groupedMediaItemsFlow.emit(AsyncData.Success(cache)) + } else { + groupedMediaItemsFlow.emit(AsyncData.Loading()) + } + mediaTimeline.getTimeline().fold( { timeline = it emit(it) @@ -78,6 +87,8 @@ class TimelineMediaGalleryDataSource @Inject constructor( timelineMediaItemsFactory.timelineItems }.map { timelineItems -> mediaItemsPostProcessor.process(mediaItems = timelineItems) + }.map { + mediaTimeline.orCache(it) }.onEach { groupedMediaItems -> groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems)) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessor.kt similarity index 92% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessor.kt index 3fb8d81b1f..1d3fd09a6c 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessor.kt @@ -5,8 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject 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 new file mode 100644 index 0000000000..6dc4136193 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt @@ -0,0 +1,97 @@ +/* + * 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.mediaviewer.impl.datasource + +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.core.UniqueId +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 +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.hasEvent +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import javax.inject.Inject + +interface MediaTimeline { + suspend fun getTimeline(): Result + val cache: GroupedMediaItems? + fun orCache(data: GroupedMediaItems): GroupedMediaItems +} + +/** + * A timeline holder that can be used by the gallery and the media viewer. + * When opening the Media Viewer, if the held timeline knows the Event, it will + * be used, else a FocusedMediaTimeline will be used. + */ +@SingleIn(RoomScope::class) +@ContributesBinding(RoomScope::class) +class LiveMediaTimeline @Inject constructor( + private val room: MatrixRoom, +) : MediaTimeline { + private var timeline: Timeline? = null + private val mutex = Mutex() + + override suspend fun getTimeline(): Result = mutex.withLock { + val currentTimeline = timeline + if (currentTimeline == null) { + room.mediaTimeline(null) + .onSuccess { timeline = it } + } else { + Result.success(currentTimeline) + } + } + + // No cache for LiveMediaTimeline + override val cache = null + override fun orCache(data: GroupedMediaItems) = data +} + +/** + * A class that will provide a media timeline that is focused on a particular event. + */ +class FocusedMediaTimeline( + private val room: MatrixRoom, + private val eventId: EventId, + initialMediaItem: MediaItem.Event, +) : MediaTimeline { + override suspend fun getTimeline(): Result { + return room.mediaTimeline(eventId) + } + + override val cache = persistentListOf( + MediaItem.LoadingIndicator( + id = UniqueId("loading_forwards"), + direction = Timeline.PaginationDirection.FORWARDS, + timestamp = 0L, + ), + initialMediaItem, + MediaItem.LoadingIndicator( + id = UniqueId("loading_backwards"), + direction = Timeline.PaginationDirection.BACKWARDS, + timestamp = 0L, + ), + ).let { + GroupedMediaItems( + fileItems = it, + imageAndVideoItems = it, + ) + } + + override fun orCache(data: GroupedMediaItems): GroupedMediaItems { + return if (data.hasEvent(eventId)) { + data + } else { + cache + } + } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaItemsFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt similarity index 96% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaItemsFactory.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt index 0313390ddd..a759ba4d76 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaItemsFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaItemsFactory.kt @@ -5,13 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import io.element.android.libraries.androidutils.diff.DefaultDiffCacheInvalidator import io.element.android.libraries.androidutils.diff.DiffCacheUpdater import io.element.android.libraries.androidutils.diff.MutableListDiffCache import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.Flow diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/VirtualItemFactory.kt similarity index 92% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/VirtualItemFactory.kt index f364b06cb6..0c45edcf37 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/VirtualItemFactory.kt @@ -5,12 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import javax.inject.Inject class VirtualItemFactory @Inject constructor( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt index 8978059a51..df7d82c7b2 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt @@ -11,6 +11,7 @@ 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.MediaInfo +import io.element.android.libraries.mediaviewer.impl.model.MediaItem sealed interface MediaGalleryEvents { data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt index c4519fcc0f..77520763f5 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactories +import io.element.android.libraries.mediaviewer.impl.model.MediaItem @ContributesNode(RoomScope::class) class MediaGalleryNode @AssistedInject constructor( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index e094e1b9fb..6617a8b9cb 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -32,8 +32,14 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory +import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.eventId +import io.element.android.libraries.mediaviewer.impl.model.mediaInfo +import io.element.android.libraries.mediaviewer.impl.model.mediaSource import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt index 750dbe1038..62877368d7 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt @@ -11,7 +11,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.mediaviewer.impl.R import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState -import kotlinx.collections.immutable.ImmutableList +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems data class MediaGalleryState( val roomName: String, @@ -22,18 +22,6 @@ data class MediaGalleryState( val eventSink: (MediaGalleryEvents) -> Unit, ) -data class GroupedMediaItems( - val imageAndVideoItems: ImmutableList, - val fileItems: ImmutableList, -) { - fun getItems(mode: MediaGalleryMode): ImmutableList { - return when (mode) { - MediaGalleryMode.Images -> imageAndVideoItems - MediaGalleryMode.Files -> fileItems - } - } -} - enum class MediaGalleryMode(val stringResource: Int) { Images(R.string.screen_media_browser_list_mode_media), Files(R.string.screen_media_browser_list_mode_files), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt index 5a1b5fcca8..aedea1eb75 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt @@ -13,13 +13,15 @@ import io.element.android.libraries.designsystem.components.media.aWaveForm import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemAudio -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemLoadingIndicator +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVideo +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice import kotlinx.collections.immutable.toImmutableList open class MediaGalleryStateProvider : PreviewParameterProvider { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt index 3f5e1fc107..38eed3252f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt @@ -72,6 +72,9 @@ import io.element.android.libraries.mediaviewer.impl.gallery.ui.FileItemView import io.element.android.libraries.mediaviewer.impl.gallery.ui.ImageItemView import io.element.android.libraries.mediaviewer.impl.gallery.ui.VideoItemView import io.element.android.libraries.mediaviewer.impl.gallery.ui.VoiceItemView +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.id import io.element.android.libraries.voiceplayer.api.VoiceMessageState import kotlinx.collections.immutable.ImmutableList @@ -108,15 +111,15 @@ fun MediaGalleryView( ) { paddingValues -> Column( modifier = Modifier - .padding(paddingValues) - .consumeWindowInsets(paddingValues) - .fillMaxSize(), + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(2.dp), ) { SingleChoiceSegmentedButtonRow( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + .fillMaxWidth() + .padding(horizontal = 16.dp), ) { MediaGalleryMode.entries.forEach { mode -> SegmentedButton( @@ -354,8 +357,8 @@ private fun MediaGalleryImageGrid( ) { LazyVerticalGrid( modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), + .fillMaxSize() + .padding(horizontal = 16.dp), columns = GridCells.Adaptive(80.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp), @@ -426,9 +429,9 @@ private fun LoadingMoreIndicator( Timeline.PaginationDirection.FORWARDS -> { LinearProgressIndicator( modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp) - .height(1.dp) + .fillMaxWidth() + .padding(top = 2.dp) + .height(1.dp) ) } Timeline.PaginationDirection.BACKWARDS -> { @@ -466,9 +469,9 @@ private fun EmptyContent( OnboardingBackground() PageTitle( modifier = Modifier - .fillMaxWidth() - .padding(top = 44.dp) - .padding(24.dp), + .fillMaxWidth() + .padding(top = 44.dp) + .padding(24.dp), title = stringResource(titleRes), iconStyle = BigIcon.Style.Default(icon), subtitle = stringResource(subtitleRes), @@ -486,9 +489,9 @@ private fun LoadingContent( OnboardingBackground() Column( modifier = Modifier - .fillMaxSize() - .padding(top = 48.dp) - .padding(24.dp), + .fillMaxSize() + .padding(top = 48.dp) + .padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/SingleMediaGalleryDataSource.kt deleted file mode 100644 index 8e6b708a7f..0000000000 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/SingleMediaGalleryDataSource.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.mediaviewer.impl.gallery - -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo -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.timeline.Timeline -import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.flowOf - -class SingleMediaGalleryDataSource( - private val data: GroupedMediaItems, -) : MediaGalleryDataSource { - override fun start() = Unit - override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) - override fun getLastData(): AsyncData = AsyncData.Success(data) - override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit - override suspend fun deleteItem(eventId: EventId) = Unit - - companion object { - fun createFrom(params: MediaViewerEntryPoint.Params) = SingleMediaGalleryDataSource( - data = when { - params.mediaInfo.mimeType.isMimeTypeImage() -> { - MediaItem.Image( - id = UniqueId("dummy"), - eventId = params.eventId, - mediaInfo = params.mediaInfo, - mediaSource = params.mediaSource, - thumbnailSource = params.thumbnailSource, - ) - } - params.mediaInfo.mimeType.isMimeTypeVideo() -> { - MediaItem.Video( - id = UniqueId("dummy"), - eventId = params.eventId, - mediaInfo = params.mediaInfo, - mediaSource = params.mediaSource, - thumbnailSource = params.thumbnailSource, - ) - } - params.mediaInfo.mimeType.isMimeTypeAudio() -> { - if (params.mediaInfo.waveform == null) { - MediaItem.Audio( - id = UniqueId("dummy"), - eventId = params.eventId, - mediaInfo = params.mediaInfo, - mediaSource = params.mediaSource, - ) - } else { - MediaItem.Voice( - id = UniqueId("dummy"), - eventId = params.eventId, - mediaInfo = params.mediaInfo, - mediaSource = params.mediaSource, - ) - } - } - else -> { - MediaItem.File( - id = UniqueId("dummy"), - eventId = params.eventId, - mediaInfo = params.mediaInfo, - mediaSource = params.mediaSource, - ) - } - }.let { mediaItem -> - GroupedMediaItems( - // Always use imageAndVideoItems, in Single mode, this is the data that will be used - imageAndVideoItems = persistentListOf(mediaItem), - fileItems = persistentListOf(), - ) - } - ) - } -} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt index bde6a57ad5..7a2094d890 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery.di import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.libraries.voiceplayer.api.aVoiceMessageState diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt index d76a631689..ad2e49f16f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery.di import dagger.MapKey -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlin.reflect.KClass /** diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt index a2f7296135..02c0441d18 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt @@ -15,7 +15,7 @@ import dagger.multibindings.Multibinds import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import javax.inject.Inject /** diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt index 1216a2dea6..9730fb4c05 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery.di import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem /** * A factory for a [Presenter] associated with a timeline item. 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 71a4057f4b..7317920005 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 @@ -31,11 +31,11 @@ import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryNode -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem -import io.element.android.libraries.mediaviewer.impl.gallery.eventId -import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo -import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource -import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.eventId +import io.element.android.libraries.mediaviewer.impl.model.mediaInfo +import io.element.android.libraries.mediaviewer.impl.model.mediaSource +import io.element.android.libraries.mediaviewer.impl.model.thumbnailSource import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt index f25337343d..2cbbe6826c 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt @@ -36,7 +36,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem @Composable fun AudioItemView( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt index c6488b42a5..84fe175fc9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt @@ -18,7 +18,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem @Composable fun DateItemView( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt index a6af3f5ad7..bdb6539ecc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt @@ -35,7 +35,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem @Composable fun FileItemView( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt index 51610a8bd0..8df451abd6 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt @@ -28,7 +28,8 @@ import coil.compose.AsyncImagePainter import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt index 7a0f6282ce..ccbc9079cd 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt @@ -9,10 +9,8 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.core.preview.loremIpsum -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio class MediaItemAudioProvider : PreviewParameterProvider { override val values: Sequence @@ -27,19 +25,3 @@ class MediaItemAudioProvider : PreviewParameterProvider { ), ) } - -fun aMediaItemAudio( - id: UniqueId = UniqueId("fileId"), - filename: String = "filename", - caption: String? = null, -): MediaItem.Audio { - return MediaItem.Audio( - id = id, - eventId = null, - mediaInfo = anAudioMediaInfo( - filename = filename, - caption = caption, - ), - mediaSource = MediaSource(""), - ) -} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt index 77f789310a..13d49a9919 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt @@ -8,8 +8,8 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator class MediaItemDateSeparatorProvider : PreviewParameterProvider { override val values: Sequence @@ -18,13 +18,3 @@ class MediaItemDateSeparatorProvider : PreviewParameterProvider { override val values: Sequence @@ -28,20 +25,3 @@ class MediaItemFileProvider : PreviewParameterProvider { ), ) } - -fun aMediaItemFile( - id: UniqueId = UniqueId("fileId"), - eventId: EventId? = null, - filename: String = "filename", - caption: String? = null, -): MediaItem.File { - return MediaItem.File( - id = id, - eventId = eventId, - mediaInfo = aPdfMediaInfo( - filename = filename, - caption = caption, - ), - mediaSource = MediaSource(""), - ) -} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemImageProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemImageProvider.kt deleted file mode 100644 index ceb934fbe2..0000000000 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemImageProvider.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2024 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.mediaviewer.impl.gallery.ui - -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.core.UserId -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.mediaviewer.api.anImageMediaInfo -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem - -fun aMediaItemImage( - id: UniqueId = UniqueId("imageId"), - eventId: EventId? = null, - senderId: UserId? = null, - mediaSourceUrl: String = "", -): MediaItem.Image { - return MediaItem.Image( - id = id, - eventId = eventId, - mediaInfo = anImageMediaInfo( - senderId = senderId, - ), - mediaSource = MediaSource(mediaSourceUrl), - thumbnailSource = null, - ) -} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt deleted file mode 100644 index 2c78898325..0000000000 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 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.mediaviewer.impl.gallery.ui - -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem - -fun aMediaItemLoadingIndicator( - id: UniqueId = UniqueId("loadingId"), - direction: Timeline.PaginationDirection = Timeline.PaginationDirection.BACKWARDS, -): MediaItem.LoadingIndicator { - return MediaItem.LoadingIndicator( - id = id, - direction = direction, - timestamp = 123, - ) -} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt index 8e59b925b7..34eef52479 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt @@ -8,10 +8,8 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVideo class MediaItemVideoProvider : PreviewParameterProvider { override val values: Sequence @@ -22,19 +20,3 @@ class MediaItemVideoProvider : PreviewParameterProvider { ), ) } - -fun aMediaItemVideo( - id: UniqueId = UniqueId("videoId"), - mediaSource: MediaSource = MediaSource(""), - duration: String? = "1:23", -): MediaItem.Video { - return MediaItem.Video( - id = id, - eventId = null, - mediaInfo = aVideoMediaInfo( - duration = duration - ), - mediaSource = mediaSource, - thumbnailSource = null, - ) -} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt index 43e04491de..8353e5bfca 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt @@ -9,11 +9,8 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.core.preview.loremIpsum -import io.element.android.libraries.designsystem.components.media.aWaveForm -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice class MediaItemVoiceProvider : PreviewParameterProvider { override val values: Sequence @@ -31,23 +28,3 @@ class MediaItemVoiceProvider : PreviewParameterProvider { ), ) } - -fun aMediaItemVoice( - id: UniqueId = UniqueId("fileId"), - filename: String = "filename.ogg", - caption: String? = null, - duration: String? = "1:23", - waveform: List = aWaveForm(), -): MediaItem.Voice { - return MediaItem.Voice( - id = id, - eventId = null, - mediaInfo = aVoiceMediaInfo( - filename = filename, - caption = caption, - duration = duration, - waveForm = waveform, - ), - mediaSource = MediaSource(""), - ) -} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt index 6b394e7c55..f23be0f382 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt @@ -38,7 +38,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt index d34555e175..89f9a30f40 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt @@ -46,7 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents import io.element.android.libraries.voiceplayer.api.VoiceMessageState diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt index 4d215bc197..92acc754fc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt @@ -17,9 +17,9 @@ import dagger.assisted.AssistedInject import dagger.multibindings.IntoMap import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemEventContentKey import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactory +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory import io.element.android.libraries.voiceplayer.api.VoiceMessageState import kotlin.time.Duration diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/GroupedMediaItems.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/GroupedMediaItems.kt new file mode 100644 index 0000000000..448426f374 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/GroupedMediaItems.kt @@ -0,0 +1,30 @@ +/* + * 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.mediaviewer.impl.model + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode +import kotlinx.collections.immutable.ImmutableList + +data class GroupedMediaItems( + val imageAndVideoItems: ImmutableList, + val fileItems: ImmutableList, +) { + fun getItems(mode: MediaGalleryMode): ImmutableList { + return when (mode) { + MediaGalleryMode.Images -> imageAndVideoItems + MediaGalleryMode.Files -> fileItems + } + } +} + +fun GroupedMediaItems.hasEvent(eventId: EventId): Boolean { + return (fileItems + imageAndVideoItems) + .filterIsInstance() + .any { it.eventId() == eventId } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItem.kt similarity index 98% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItem.kt index e1bd4d779a..bf89486964 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItem.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.model import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItemFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItemFactories.kt new file mode 100644 index 0000000000..b04973322a --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItemFactories.kt @@ -0,0 +1,128 @@ +/* + * 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.mediaviewer.impl.model + +import io.element.android.libraries.designsystem.components.media.aWaveForm +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.core.UserId +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.aPdfMediaInfo +import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo +import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo +import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo + +fun aMediaItemImage( + id: UniqueId = UniqueId("imageId"), + eventId: EventId? = null, + senderId: UserId? = null, + mediaSourceUrl: String = "", +): MediaItem.Image { + return MediaItem.Image( + id = id, + eventId = eventId, + mediaInfo = anImageMediaInfo( + senderId = senderId, + ), + mediaSource = MediaSource(mediaSourceUrl), + thumbnailSource = null, + ) +} + +fun aMediaItemVideo( + id: UniqueId = UniqueId("videoId"), + mediaSource: MediaSource = MediaSource(""), + duration: String? = "1:23", +): MediaItem.Video { + return MediaItem.Video( + id = id, + eventId = null, + mediaInfo = aVideoMediaInfo( + duration = duration + ), + mediaSource = mediaSource, + thumbnailSource = null, + ) +} + +fun aMediaItemFile( + id: UniqueId = UniqueId("fileId"), + eventId: EventId? = null, + filename: String = "filename", + caption: String? = null, +): MediaItem.File { + return MediaItem.File( + id = id, + eventId = eventId, + mediaInfo = aPdfMediaInfo( + filename = filename, + caption = caption, + ), + mediaSource = MediaSource(""), + ) +} + +fun aMediaItemAudio( + id: UniqueId = UniqueId("fileId"), + eventId: EventId? = null, + filename: String = "filename", + caption: String? = null, +): MediaItem.Audio { + return MediaItem.Audio( + id = id, + eventId = eventId, + mediaInfo = anAudioMediaInfo( + filename = filename, + caption = caption, + ), + mediaSource = MediaSource(""), + ) +} + +fun aMediaItemVoice( + id: UniqueId = UniqueId("fileId"), + filename: String = "filename.ogg", + caption: String? = null, + duration: String? = "1:23", + waveform: List = aWaveForm(), +): MediaItem.Voice { + return MediaItem.Voice( + id = id, + eventId = null, + mediaInfo = aVoiceMediaInfo( + filename = filename, + caption = caption, + duration = duration, + waveForm = waveform, + ), + mediaSource = MediaSource(""), + ) +} + +fun aMediaItemDateSeparator( + id: UniqueId = UniqueId("dateId"), + formattedDate: String = "October 2024", +): MediaItem.DateSeparator { + return MediaItem.DateSeparator( + id = id, + formattedDate = formattedDate, + ) +} + +fun aMediaItemLoadingIndicator( + id: UniqueId = UniqueId("loadingId"), + direction: Timeline.PaginationDirection = Timeline.PaginationDirection.BACKWARDS, +): MediaItem.LoadingIndicator { + return MediaItem.LoadingIndicator( + id = id, + direction = direction, + timestamp = 123, + ) +} 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 b185834c3e..ebaba4cdcd 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 @@ -18,15 +18,16 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem -import io.element.android.libraries.mediaviewer.impl.gallery.eventId -import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo -import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource -import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.eventId +import io.element.android.libraries.mediaviewer.impl.model.mediaInfo +import io.element.android.libraries.mediaviewer.impl.model.mediaSource +import io.element.android.libraries.mediaviewer.impl.model.thumbnailSource import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -38,16 +39,23 @@ import kotlinx.coroutines.withContext import timber.log.Timber class MediaViewerDataSource( - private val galleryMode: MediaGalleryMode, + mode: MediaViewerMode, private val dispatcher: CoroutineDispatcher, private val galleryDataSource: MediaGalleryDataSource, private val mediaLoader: MatrixMediaLoader, private val localMediaFactory: LocalMediaFactory, private val systemClock: SystemClock, + private val pagerKeysHandler: PagerKeysHandler, ) { // List of media files that are currently being loaded private val mediaFiles: MutableList = mutableListOf() + private val galleryMode = when (mode) { + MediaViewerMode.SingleMedia, + MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images + MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files + } + // Map of sourceUrl to local media state private val localMediaStates: MutableMap>> = mutableMapOf() @@ -78,6 +86,7 @@ class MediaViewerDataSource( MediaViewerPageData.Loading( direction = Timeline.PaginationDirection.BACKWARDS, timestamp = systemClock.epochMillis(), + pagerKey = Long.MIN_VALUE, ) ) } @@ -108,7 +117,10 @@ class MediaViewerDataSource( * will be used to render the downloaded media (see [loadMedia] which will update this value). */ private fun buildMediaViewerPageList(groupedItems: List) = buildList { - groupedItems.forEach { mediaItem -> + // Filter out DateSeparator items, we do not need them for the media viewer + val groupedItemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator } + pagerKeysHandler.accept(groupedItemsNoDateSeparator) + groupedItemsNoDateSeparator.forEach { mediaItem -> when (mediaItem) { is MediaItem.DateSeparator -> Unit is MediaItem.Event -> { @@ -123,6 +135,7 @@ class MediaViewerDataSource( mediaSource = mediaItem.mediaSource(), thumbnailSource = mediaItem.thumbnailSource(), downloadedMedia = localMedia, + pagerKey = pagerKeysHandler.getKey(mediaItem), ) ) } @@ -130,6 +143,7 @@ class MediaViewerDataSource( MediaViewerPageData.Loading( direction = mediaItem.direction, timestamp = systemClock.epochMillis(), + pagerKey = pagerKeysHandler.getKey(mediaItem), ) ) } 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 e06b691520..7876641051 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 @@ -24,9 +24,9 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode -import io.element.android.libraries.mediaviewer.impl.gallery.SingleMediaGalleryDataSource -import io.element.android.libraries.mediaviewer.impl.gallery.TimelineMediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.datasource.FocusedTimelineMediaGalleryDataSourceFactory +import io.element.android.libraries.mediaviewer.impl.datasource.TimelineMediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.model.hasEvent import io.element.android.services.toolbox.api.systemclock.SystemClock @ContributesNode(RoomScope::class) @@ -35,10 +35,12 @@ class MediaViewerNode @AssistedInject constructor( @Assisted plugins: List, presenterFactory: MediaViewerPresenter.Factory, timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource, + focusedTimelineMediaGalleryDataSourceFactory: FocusedTimelineMediaGalleryDataSourceFactory, mediaLoader: MatrixMediaLoader, localMediaFactory: LocalMediaFactory, coroutineDispatchers: CoroutineDispatchers, systemClock: SystemClock, + pagerKeysHandler: PagerKeysHandler, ) : Node(buildContext, plugins = plugins), MediaViewerNavigator { private val inputs = inputs() @@ -62,25 +64,36 @@ class MediaViewerNode @AssistedInject constructor( private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) { SingleMediaGalleryDataSource.createFrom(inputs) } else { - timelineMediaGalleryDataSource - } - - private val galleryMode = when (inputs.mode) { - MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images - MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files + val eventId = inputs.eventId + if (eventId == null) { + // 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(), + ) + } + } } private val presenter = presenterFactory.create( inputs = inputs, navigator = this, dataSource = MediaViewerDataSource( + mode = inputs.mode, dispatcher = coroutineDispatchers.computation, - galleryMode = galleryMode, galleryDataSource = mediaGallerySource, mediaLoader = mediaLoader, localMediaFactory = localMediaFactory, systemClock = systemClock, + pagerKeysHandler = pagerKeysHandler, ) ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt index cf363a70f1..32c22b0470 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt @@ -28,13 +28,17 @@ data class MediaViewerState( ) sealed interface MediaViewerPageData { + val pagerKey: Long + data class Failure( val throwable: Throwable, + override val pagerKey: Long = 0, ) : MediaViewerPageData data class Loading( val direction: Timeline.PaginationDirection, val timestamp: Long, + override val pagerKey: Long, ) : MediaViewerPageData data class MediaViewerData( @@ -43,5 +47,6 @@ sealed interface MediaViewerPageData { val mediaSource: MediaSource, val thumbnailSource: MediaSource?, val downloadedMedia: State>, + override val pagerKey: Long, ) : MediaViewerPageData } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt index 2c54751fed..95557c13d6 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt @@ -169,6 +169,7 @@ fun aMediaViewerPageDataLoading( return MediaViewerPageData.Loading( direction = direction, timestamp = timestamp, + pagerKey = 0L, ) } @@ -182,6 +183,7 @@ fun aMediaViewerPageData( mediaSource = mediaSource, thumbnailSource = null, downloadedMedia = mutableStateOf(downloadedMedia), + pagerKey = 0L, ) fun aMediaViewerState( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index 51476193d0..3ba57cae33 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -114,6 +114,7 @@ fun MediaViewerView( modifier = Modifier, // Pre-load previous and next pages beyondViewportPageCount = 1, + key = { index -> state.listData[index].pagerKey }, ) { page -> when (val dataForPage = state.listData[page]) { is MediaViewerPageData.Failure -> { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt new file mode 100644 index 0000000000..2c73ab2657 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandler.kt @@ -0,0 +1,86 @@ +/* + * 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.mediaviewer.impl.viewer + +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.eventId +import javax.inject.Inject + +/** + * x and y are loading items. + * Capital letters are media items. + * First list emitted + * x F G H y + * indexes will be + * 0 1 2 3 4 + * (keyOffset = 0) + * New items added to the end of the list + * x F G H I J K y + * indexes will be + * 0 1 2 3 4 5 6 7 + * (keyOffset = 0) + * New items added to the beginning of the list + * x D E F G H I J K y + * indexes will be + * -2 -1 0 1 2 3 4 5 6 7 + * (keyOffset = -2) + * loader item vanishes + * D E F G H I J K + * indexes will be + * -1 0 1 2 3 4 5 6 + * (keyOffset = -1) + */ +class PagerKeysHandler @Inject constructor() { + private data class Data( + val mediaItems: List, + val keyOffset: Long, + ) + + // Will store the list of media items and the key offset of the first item in the list + private var cachedData: Data = Data(emptyList(), 0) + + fun accept(mediaItems: List) { + if (cachedData.mediaItems.isEmpty()) { + cachedData = Data(mediaItems, 0) + } else { + // Search a common item in both lists, i.e. an item with the same eventId + val itemInCacheIndex = cachedData.mediaItems.indexOfFirst { mediaItem -> + mediaItem is MediaItem.Event && mediaItems + .filterIsInstance() + .any { mediaItem.eventId() == it.eventId() } + } + cachedData = if (itemInCacheIndex == -1) { + // If the item is not found, start with a new cache + Data(mediaItems, 0) + } else { + val cachedItem = cachedData.mediaItems[itemInCacheIndex] + val eventId = (cachedItem as? MediaItem.Event)?.eventId() + if (eventId == null) { + // Should not happen, but in this case, start with a new cache + Data(mediaItems, 0) + } else { + // Search the index of the item in the new list + val itemIndex = mediaItems.indexOfFirst { mediaItem -> + mediaItem is MediaItem.Event && mediaItem.eventId() == eventId + } + if (itemIndex == -1) { + // If the item is not found, start with a new cache + Data(mediaItems, 0) + } else { + // Update the cache with the new list and the new offset + Data(mediaItems, cachedData.keyOffset + itemInCacheIndex - itemIndex.toLong()) + } + } + } + } + } + + fun getKey(mediaItem: MediaItem): Long { + return cachedData.mediaItems.indexOf(mediaItem) + cachedData.keyOffset + } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt new file mode 100644 index 0000000000..94ac0fea21 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt @@ -0,0 +1,88 @@ +/* + * 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.mediaviewer.impl.viewer + +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo +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.timeline.Timeline +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.flowOf + +class SingleMediaGalleryDataSource( + private val data: GroupedMediaItems, +) : MediaGalleryDataSource { + override fun start() = Unit + override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) + override fun getLastData(): AsyncData = AsyncData.Success(data) + override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit + override suspend fun deleteItem(eventId: EventId) = Unit + + companion object { + fun createFrom(params: MediaViewerEntryPoint.Params) = SingleMediaGalleryDataSource( + data = GroupedMediaItems( + // Always use imageAndVideoItems, in Single mode, this is the data that will be used + imageAndVideoItems = persistentListOf(params.toMediaItem()), + fileItems = persistentListOf(), + ) + ) + } +} + +fun MediaViewerEntryPoint.Params.toMediaItem() = when { + mediaInfo.mimeType.isMimeTypeImage() -> { + MediaItem.Image( + id = UniqueId("dummy"), + eventId = eventId, + mediaInfo = mediaInfo, + mediaSource = mediaSource, + thumbnailSource = thumbnailSource, + ) + } + mediaInfo.mimeType.isMimeTypeVideo() -> { + MediaItem.Video( + id = UniqueId("dummy"), + eventId = eventId, + mediaInfo = mediaInfo, + mediaSource = mediaSource, + thumbnailSource = thumbnailSource, + ) + } + mediaInfo.mimeType.isMimeTypeAudio() -> { + if (mediaInfo.waveform == null) { + MediaItem.Audio( + id = UniqueId("dummy"), + eventId = eventId, + mediaInfo = mediaInfo, + mediaSource = mediaSource, + ) + } else { + MediaItem.Voice( + id = UniqueId("dummy"), + eventId = eventId, + mediaInfo = mediaInfo, + mediaSource = mediaSource, + ) + } + } + else -> { + MediaItem.File( + id = UniqueId("dummy"), + eventId = eventId, + mediaInfo = mediaInfo, + mediaSource = mediaSource, + ) + } +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt similarity index 99% rename from libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt index a0bc4c1f0f..40e6721c10 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter @@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageCo import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList 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 new file mode 100644 index 0000000000..a2ab9b162f --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.datasource + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest { + @Test + fun `createFor should create a TimelineMediaGalleryDataSource`() = runTest { + val sut = DefaultFocusedTimelineMediaGalleryDataSourceFactory( + room = FakeMatrixRoom(), + timelineMediaItemsFactory = createTimelineMediaItemsFactory(), + mediaItemsPostProcessor = MediaItemsPostProcessor(), + ) + val result = sut.createFor( + eventId = AN_EVENT_ID, + mediaItem = aMediaItemImage(), + ) + assertThat(result).isInstanceOf(TimelineMediaGalleryDataSource::class.java) + } +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt similarity index 91% rename from libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryDataSource.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt index 419c2c568a..ea4bdfbad8 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt @@ -5,11 +5,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.tests.testutils.lambda.lambdaError import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow 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 new file mode 100644 index 0000000000..1d4fd3adbe --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt @@ -0,0 +1,102 @@ +/* + * 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.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.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class FocusedMediaTimelineTest { + @Test + fun `check the returned cache data`() { + val media = aMediaItemImage() + val sut = createFocusedMediaTimeline( + initialMediaItem = media, + ) + val cache = sut.cache + assertThat(cache.imageAndVideoItems.size).isEqualTo(3) + assertThat(cache.fileItems.size).isEqualTo(3) + assertThat(cache.imageAndVideoItems[1]).isEqualTo(media) + assertThat(cache.fileItems[1]).isEqualTo(media) + } + + @Test + fun `when event is not found, the cache is returned`() { + val media = aMediaItemImage( + eventId = AN_EVENT_ID, + ) + val sut = createFocusedMediaTimeline( + initialMediaItem = media, + ) + val cache = sut.orCache( + GroupedMediaItems( + imageAndVideoItems = persistentListOf(), + fileItems = persistentListOf(), + ) + ) + assertThat(cache.imageAndVideoItems.size).isEqualTo(3) + assertThat(cache.fileItems.size).isEqualTo(3) + } + + @Test + fun `when event is found, the data is returned`() { + val media = aMediaItemImage( + eventId = AN_EVENT_ID, + ) + val sut = createFocusedMediaTimeline( + initialMediaItem = media, + ) + val cache = sut.orCache( + GroupedMediaItems( + imageAndVideoItems = persistentListOf(media), + fileItems = persistentListOf(), + ) + ) + assertThat(cache.imageAndVideoItems.size).isEqualTo(1) + assertThat(cache.fileItems).isEmpty() + } + + @Test + fun `getTimeline returns the timeline provided by the room`() = runTest { + val mediaTimelineResult = lambdaRecorder> { + Result.success(FakeTimeline()) + } + val room = FakeMatrixRoom( + mediaTimelineResult = mediaTimelineResult, + ) + val sut = createFocusedMediaTimeline( + room = room, + eventId = AN_EVENT_ID, + ) + val timeline = sut.getTimeline() + assertThat(timeline.isSuccess).isTrue() + mediaTimelineResult.assertions().isCalledOnce().with(value(AN_EVENT_ID)) + } + + private fun createFocusedMediaTimeline( + room: MatrixRoom = FakeMatrixRoom(), + eventId: EventId = AN_EVENT_ID, + initialMediaItem: MediaItem.Event = aMediaItemImage(), + ) = FocusedMediaTimeline( + room = room, + eventId = eventId, + initialMediaItem = initialMediaItem, + ) +} 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 new file mode 100644 index 0000000000..95b62a5824 --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt @@ -0,0 +1,54 @@ +/* + * 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.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.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LiveMediaTimelineTest { + @Test + fun `LiveMediaTimeline cache is always null`() = runTest { + val sut = createLiveMediaTimeline() + assertThat(sut.cache).isNull() + } + + @Test + fun `getTimeline returns the timeline provided by the room, then from cache`() = runTest { + val mediaTimelineResult = lambdaRecorder> { + Result.success(FakeTimeline()) + } + val room = FakeMatrixRoom( + mediaTimelineResult = mediaTimelineResult, + ) + val sut = createLiveMediaTimeline( + room = room, + ) + val timeline = sut.getTimeline() + assertThat(timeline.isSuccess).isTrue() + mediaTimelineResult.assertions().isCalledOnce().with(value(null)) + val timeline2 = sut.getTimeline() + assertThat(timeline2.isSuccess).isTrue() + // No called another time + mediaTimelineResult.assertions().isCalledOnce() + } + + private fun createLiveMediaTimeline( + room: MatrixRoom = FakeMatrixRoom(), + ) = LiveMediaTimeline( + room = room, + ) +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessorTest.kt similarity index 88% rename from libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessorTest.kt index 4c823350ce..a8903394b3 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaItemsPostProcessorTest.kt @@ -5,17 +5,19 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemAudio -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemLoadingIndicator +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVideo +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice import kotlinx.collections.immutable.toImmutableList import org.junit.Test diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt similarity index 93% rename from libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaGalleryDataSourceTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt index 2f8cd634ec..736fd309f3 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.datasource import app.cash.turbine.test import com.google.common.truth.Truth.assertThat @@ -34,6 +34,8 @@ import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.mediaviewer.api.MediaInfo +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -260,18 +262,21 @@ class TimelineMediaGalleryDataSourceTest { ): TimelineMediaGalleryDataSource { return TimelineMediaGalleryDataSource( room = room, - timelineMediaItemsFactory = TimelineMediaItemsFactory( - dispatchers = testCoroutineDispatchers(), - virtualItemFactory = VirtualItemFactory( - dateFormatter = FakeDateFormatter(), - ), - eventItemFactory = EventItemFactory( - fileSizeFormatter = FakeFileSizeFormatter(), - fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), - dateFormatter = FakeDateFormatter(), - ), - ), + mediaTimeline = LiveMediaTimeline(room), + timelineMediaItemsFactory = createTimelineMediaItemsFactory(), mediaItemsPostProcessor = MediaItemsPostProcessor(), ) } } + +fun TestScope.createTimelineMediaItemsFactory() = TimelineMediaItemsFactory( + dispatchers = testCoroutineDispatchers(), + virtualItemFactory = VirtualItemFactory( + dateFormatter = FakeDateFormatter(), + ), + eventItemFactory = EventItemFactory( + fileSizeFormatter = FakeFileSizeFormatter(), + fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), + dateFormatter = FakeDateFormatter(), + ), +) 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 0f304f209e..1a43bbacf6 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 @@ -21,8 +21,10 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.tests.testutils.WarmUpRule diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/model/GroupedMediaItemsTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/model/GroupedMediaItemsTest.kt new file mode 100644 index 0000000000..16f0777994 --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/model/GroupedMediaItemsTest.kt @@ -0,0 +1,32 @@ +/* + * 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.mediaviewer.impl.model + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.AN_EVENT_ID_3 +import kotlinx.collections.immutable.persistentListOf +import org.junit.Test + +class GroupedMediaItemsTest { + @Test + fun `hasEvent returns the expected value`() { + val sut = GroupedMediaItems( + imageAndVideoItems = persistentListOf( + aMediaItemImage(eventId = AN_EVENT_ID), + ), + fileItems = persistentListOf( + aMediaItemAudio(eventId = AN_EVENT_ID_2), + ), + ) + assertThat(sut.hasEvent(AN_EVENT_ID)).isTrue() + assertThat(sut.hasEvent(AN_EVENT_ID_2)).isTrue() + assertThat(sut.hasEvent(AN_EVENT_ID_3)).isFalse() + } +} 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 5348eb2aa3..b49c174d21 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 @@ -17,15 +17,15 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader +import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory -import io.element.android.libraries.mediaviewer.impl.gallery.FakeMediaGalleryDataSource -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode +import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.gallery.aGroupedMediaItems -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemLoadingIndicator import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock @@ -122,10 +122,12 @@ class MediaViewerDataSourceTest { MediaViewerPageData.Loading( direction = Timeline.PaginationDirection.BACKWARDS, timestamp = A_FAKE_TIMESTAMP, + pagerKey = 0L, ), MediaViewerPageData.Loading( direction = Timeline.PaginationDirection.FORWARDS, timestamp = A_FAKE_TIMESTAMP, + pagerKey = 1L, ), ) } @@ -135,7 +137,7 @@ class MediaViewerDataSourceTest { fun `test dataFlow with data galleryMode image`() = runTest { val galleryDataSource = FakeMediaGalleryDataSource() val sut = createMediaViewerDataSource( - galleryMode = MediaGalleryMode.Images, + mode = MediaViewerMode.TimelineImagesAndVideos, galleryDataSource = galleryDataSource, ) sut.dataFlow().test { @@ -157,7 +159,7 @@ class MediaViewerDataSourceTest { fun `test dataFlow with data galleryMode files`() = runTest { val galleryDataSource = FakeMediaGalleryDataSource() val sut = createMediaViewerDataSource( - galleryMode = MediaGalleryMode.Files, + mode = MediaViewerMode.TimelineFilesAndAudios, galleryDataSource = galleryDataSource, ) sut.dataFlow().test { @@ -263,16 +265,17 @@ class MediaViewerDataSourceTest { } private fun TestScope.createMediaViewerDataSource( - galleryMode: MediaGalleryMode = MediaGalleryMode.Images, + mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos, galleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(), mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl), ) = MediaViewerDataSource( - galleryMode = galleryMode, + mode = mode, dispatcher = testCoroutineDispatchers().computation, galleryDataSource = galleryDataSource, mediaLoader = mediaLoader, localMediaFactory = localMediaFactory, systemClock = FakeSystemClock(), + pagerKeysHandler = PagerKeysHandler(), ) } 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 8a2d536a78..03d66992bf 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 @@ -29,13 +29,12 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.impl.R +import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState -import io.element.android.libraries.mediaviewer.impl.gallery.FakeMediaGalleryDataSource -import io.element.android.libraries.mediaviewer.impl.gallery.GroupedMediaItems -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator +import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemLoadingIndicator import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.services.toolbox.test.systemclock.FakeSystemClock @@ -782,16 +781,13 @@ class MediaViewerPresenterTest { ), navigator = mediaViewerNavigator, dataSource = MediaViewerDataSource( - galleryMode = when (mode) { - MediaViewerEntryPoint.MediaViewerMode.SingleMedia -> MediaGalleryMode.Images - MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images - MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files - }, + mode = mode, dispatcher = testCoroutineDispatchers().computation, galleryDataSource = mediaGalleryDataSource, mediaLoader = matrixMediaLoader, localMediaFactory = localMediaFactory, systemClock = FakeSystemClock(), + pagerKeysHandler = PagerKeysHandler(), ), room = room, localMediaActions = localMediaActions, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandlerTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandlerTest.kt new file mode 100644 index 0000000000..807c7b51cd --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/PagerKeysHandlerTest.kt @@ -0,0 +1,76 @@ +/* + * 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.mediaviewer.impl.viewer + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemLoadingIndicator +import org.junit.Test + +class PagerKeysHandlerTest { + private val image1 = aMediaItemImage( + eventId = AN_EVENT_ID, + ) + private val image2 = aMediaItemImage( + eventId = AN_EVENT_ID_2, + ) + private val aBackwardLoadingIndicator = aMediaItemLoadingIndicator( + direction = Timeline.PaginationDirection.BACKWARDS + ) + private val aForwardLoadingIndicator = aMediaItemLoadingIndicator( + direction = Timeline.PaginationDirection.FORWARDS + ) + + @Test + fun `when new items are inserted after existing items, keys are not shifted`() { + val sut = PagerKeysHandler() + sut.accept(listOf(aBackwardLoadingIndicator, image1, aForwardLoadingIndicator)) + assertThat(sut.getKey(aBackwardLoadingIndicator)).isEqualTo(0) + assertThat(sut.getKey(image1)).isEqualTo(1) + assertThat(sut.getKey(aForwardLoadingIndicator)).isEqualTo(2) + sut.accept(listOf(aBackwardLoadingIndicator, image1, image2, aForwardLoadingIndicator)) + assertThat(sut.getKey(aBackwardLoadingIndicator)).isEqualTo(0) + assertThat(sut.getKey(image1)).isEqualTo(1) + assertThat(sut.getKey(image2)).isEqualTo(2) + assertThat(sut.getKey(aForwardLoadingIndicator)).isEqualTo(3) + } + + @Test + fun `when new items are inserted before existing items, keys are not shifted`() { + val sut = PagerKeysHandler() + sut.accept(listOf(aBackwardLoadingIndicator, image1, aForwardLoadingIndicator)) + assertThat(sut.getKey(aBackwardLoadingIndicator)).isEqualTo(0) + assertThat(sut.getKey(image1)).isEqualTo(1) + assertThat(sut.getKey(aForwardLoadingIndicator)).isEqualTo(2) + sut.accept(listOf(aBackwardLoadingIndicator, image2, image1, aForwardLoadingIndicator)) + assertThat(sut.getKey(aBackwardLoadingIndicator)).isEqualTo(-1) + assertThat(sut.getKey(image2)).isEqualTo(0) + assertThat(sut.getKey(image1)).isEqualTo(1) + assertThat(sut.getKey(aForwardLoadingIndicator)).isEqualTo(2) + // Accepting the same list should not change the keys + sut.accept(listOf(aBackwardLoadingIndicator, image2, image1, aForwardLoadingIndicator)) + assertThat(sut.getKey(aBackwardLoadingIndicator)).isEqualTo(-1) + assertThat(sut.getKey(image2)).isEqualTo(0) + assertThat(sut.getKey(image1)).isEqualTo(1) + assertThat(sut.getKey(aForwardLoadingIndicator)).isEqualTo(2) + } + + @Test + fun `when loaders are removed, keys are not shifted`() { + val sut = PagerKeysHandler() + sut.accept(listOf(aBackwardLoadingIndicator, image1, aForwardLoadingIndicator)) + assertThat(sut.getKey(aBackwardLoadingIndicator)).isEqualTo(0) + assertThat(sut.getKey(image1)).isEqualTo(1) + assertThat(sut.getKey(aForwardLoadingIndicator)).isEqualTo(2) + sut.accept(listOf(image1)) + assertThat(sut.getKey(image1)).isEqualTo(1) + } +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/SingleMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt similarity index 94% rename from libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/SingleMediaGalleryDataSourceTest.kt rename to libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt index d616322ddc..540f107601 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/SingleMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.mediaviewer.impl.gallery +package io.element.android.libraries.mediaviewer.impl.viewer import app.cash.turbine.test import com.google.common.truth.Truth.assertThat @@ -22,8 +22,10 @@ import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile -import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage +import io.element.android.libraries.mediaviewer.impl.gallery.aGroupedMediaItems +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile +import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index d140d3fea4..b5e75b904d 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -136,6 +136,7 @@ class KonsistClassNameTest { "Enterprise", "Fdroid", "FileExtensionExtractor", + "LiveMediaTimeline", "KeyStore", "Matrix", "Noop",