Merge pull request #4205 from element-hq/feature/bma/mediaNavigation
Add ability to swipe between media when opened from the timeline.
This commit is contained in:
commit
2150eaa504
62 changed files with 1002 additions and 376 deletions
|
|
@ -123,6 +123,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class MediaViewer(
|
data class MediaViewer(
|
||||||
|
val mode: MediaViewerEntryPoint.MediaViewerMode,
|
||||||
val eventId: EventId?,
|
val eventId: EventId?,
|
||||||
val mediaInfo: MediaInfo,
|
val mediaInfo: MediaInfo,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
|
|
@ -248,8 +249,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
is NavTarget.MediaViewer -> {
|
is NavTarget.MediaViewer -> {
|
||||||
val params = MediaViewerEntryPoint.Params(
|
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 = navTarget.mode,
|
||||||
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
|
||||||
eventId = navTarget.eventId,
|
eventId = navTarget.eventId,
|
||||||
mediaInfo = navTarget.mediaInfo,
|
mediaInfo = navTarget.mediaInfo,
|
||||||
mediaSource = navTarget.mediaSource,
|
mediaSource = navTarget.mediaSource,
|
||||||
|
|
@ -362,6 +362,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
val navTarget = when (event.content) {
|
val navTarget = when (event.content) {
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
buildMediaViewerNavTarget(
|
buildMediaViewerNavTarget(
|
||||||
|
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
|
||||||
event = event,
|
event = event,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
mediaSource = event.content.mediaSource,
|
mediaSource = event.content.mediaSource,
|
||||||
|
|
@ -373,6 +374,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
if encrypted on certain bridges */
|
if encrypted on certain bridges */
|
||||||
event.content.preferredMediaSource?.let { preferredMediaSource ->
|
event.content.preferredMediaSource?.let { preferredMediaSource ->
|
||||||
buildMediaViewerNavTarget(
|
buildMediaViewerNavTarget(
|
||||||
|
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
|
||||||
event = event,
|
event = event,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
mediaSource = preferredMediaSource,
|
mediaSource = preferredMediaSource,
|
||||||
|
|
@ -382,6 +384,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
is TimelineItemVideoContent -> {
|
is TimelineItemVideoContent -> {
|
||||||
buildMediaViewerNavTarget(
|
buildMediaViewerNavTarget(
|
||||||
|
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
|
||||||
event = event,
|
event = event,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
mediaSource = event.content.mediaSource,
|
mediaSource = event.content.mediaSource,
|
||||||
|
|
@ -390,6 +393,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
is TimelineItemFileContent -> {
|
is TimelineItemFileContent -> {
|
||||||
buildMediaViewerNavTarget(
|
buildMediaViewerNavTarget(
|
||||||
|
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios,
|
||||||
event = event,
|
event = event,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
mediaSource = event.content.mediaSource,
|
mediaSource = event.content.mediaSource,
|
||||||
|
|
@ -398,6 +402,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
is TimelineItemAudioContent -> {
|
is TimelineItemAudioContent -> {
|
||||||
buildMediaViewerNavTarget(
|
buildMediaViewerNavTarget(
|
||||||
|
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios,
|
||||||
event = event,
|
event = event,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
mediaSource = event.content.mediaSource,
|
mediaSource = event.content.mediaSource,
|
||||||
|
|
@ -426,12 +431,14 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildMediaViewerNavTarget(
|
private fun buildMediaViewerNavTarget(
|
||||||
|
mode: MediaViewerEntryPoint.MediaViewerMode,
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
content: TimelineItemEventContentWithAttachment,
|
content: TimelineItemEventContentWithAttachment,
|
||||||
mediaSource: MediaSource,
|
mediaSource: MediaSource,
|
||||||
thumbnailSource: MediaSource?,
|
thumbnailSource: MediaSource?,
|
||||||
): NavTarget {
|
): NavTarget {
|
||||||
return NavTarget.MediaViewer(
|
return NavTarget.MediaViewer(
|
||||||
|
mode = mode,
|
||||||
eventId = event.eventId,
|
eventId = event.eventId,
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
filename = content.filename,
|
filename = content.filename,
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,9 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new timeline for the media events of the room.
|
* Create a new timeline for the media events of the room.
|
||||||
|
* @param eventId The event to focus on, if any.
|
||||||
*/
|
*/
|
||||||
suspend fun mediaTimeline(): Result<Timeline>
|
suspend fun mediaTimeline(eventId: EventId?): Result<Timeline>
|
||||||
|
|
||||||
fun destroy()
|
fun destroy()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -253,11 +253,21 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun mediaTimeline(): Result<Timeline> = withContext(roomDispatcher) {
|
override suspend fun mediaTimeline(
|
||||||
|
eventId: EventId?,
|
||||||
|
): Result<Timeline> = withContext(roomDispatcher) {
|
||||||
|
val focus = if (eventId != null) {
|
||||||
|
TimelineFocus.Event(
|
||||||
|
eventId = eventId.value,
|
||||||
|
numContextEvents = 50u,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TimelineFocus.Live
|
||||||
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.timelineWithConfiguration(
|
innerRoom.timelineWithConfiguration(
|
||||||
configuration = TimelineConfiguration(
|
configuration = TimelineConfiguration(
|
||||||
focus = TimelineFocus.Live,
|
focus = focus,
|
||||||
allowedMessageTypes = AllowedMessageTypes.Only(
|
allowedMessageTypes = AllowedMessageTypes.Only(
|
||||||
types = listOf(
|
types = listOf(
|
||||||
RoomMessageEventMessageType.FILE,
|
RoomMessageEventMessageType.FILE,
|
||||||
|
|
@ -270,7 +280,7 @@ class RustMatrixRoom(
|
||||||
dateDividerMode = DateDividerMode.MONTHLY,
|
dateDividerMode = DateDividerMode.MONTHLY,
|
||||||
)
|
)
|
||||||
).let { inner ->
|
).let { inner ->
|
||||||
createTimeline(inner, mode = Timeline.Mode.MEDIA)
|
createTimeline(inner, mode = if (eventId != null) Timeline.Mode.FOCUSED_ON_EVENT else Timeline.Mode.MEDIA)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
if (it is CancellationException) {
|
if (it is CancellationException) {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ val A_THREAD_ID = ThreadId("\$aThreadId")
|
||||||
val A_THREAD_ID_2 = ThreadId("\$aThreadId2")
|
val A_THREAD_ID_2 = ThreadId("\$aThreadId2")
|
||||||
val AN_EVENT_ID = EventId("\$anEventId")
|
val AN_EVENT_ID = EventId("\$anEventId")
|
||||||
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
||||||
|
val AN_EVENT_ID_3 = EventId("\$anEventId3")
|
||||||
val A_ROOM_ALIAS = RoomAlias("#alias1:domain")
|
val A_ROOM_ALIAS = RoomAlias("#alias1:domain")
|
||||||
val A_TRANSACTION_ID = TransactionId("aTransactionId")
|
val A_TRANSACTION_ID = TransactionId("aTransactionId")
|
||||||
val A_DEVICE_ID = DeviceId("ILAKNDNASDLK")
|
val A_DEVICE_ID = DeviceId("ILAKNDNASDLK")
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ class FakeMatrixRoom(
|
||||||
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
|
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
|
||||||
private val timelineFocusedOnEventResult: (EventId) -> Result<Timeline> = { lambdaError() },
|
private val timelineFocusedOnEventResult: (EventId) -> Result<Timeline> = { lambdaError() },
|
||||||
private val pinnedEventsTimelineResult: () -> Result<Timeline> = { lambdaError() },
|
private val pinnedEventsTimelineResult: () -> Result<Timeline> = { lambdaError() },
|
||||||
private val mediaTimelineResult: () -> Result<Timeline> = { lambdaError() },
|
private val mediaTimelineResult: (EventId?) -> Result<Timeline> = { lambdaError() },
|
||||||
private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> },
|
private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> },
|
||||||
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
|
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
|
||||||
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
|
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
|
||||||
|
|
@ -215,8 +215,8 @@ class FakeMatrixRoom(
|
||||||
pinnedEventsTimelineResult()
|
pinnedEventsTimelineResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun mediaTimeline(): Result<Timeline> = simulateLongTask {
|
override suspend fun mediaTimeline(eventId: EventId?): Result<Timeline> = simulateLongTask {
|
||||||
mediaTimelineResult()
|
mediaTimelineResult(eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun subscribeToSync() {
|
override suspend fun subscribeToSync() {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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.androidutils.filesize.FileSizeFormatter
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
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.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
|
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
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.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
@ -39,6 +40,7 @@ interface MediaGalleryDataSource {
|
||||||
@ContributesBinding(RoomScope::class)
|
@ContributesBinding(RoomScope::class)
|
||||||
class TimelineMediaGalleryDataSource @Inject constructor(
|
class TimelineMediaGalleryDataSource @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
private val mediaTimeline: MediaTimeline,
|
||||||
private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
|
private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
|
||||||
private val mediaItemsPostProcessor: MediaItemsPostProcessor,
|
private val mediaItemsPostProcessor: MediaItemsPostProcessor,
|
||||||
) : MediaGalleryDataSource {
|
) : MediaGalleryDataSource {
|
||||||
|
|
@ -48,7 +50,9 @@ class TimelineMediaGalleryDataSource @Inject constructor(
|
||||||
|
|
||||||
override fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>> = groupedMediaItemsFlow
|
override fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>> = groupedMediaItemsFlow
|
||||||
|
|
||||||
override fun getLastData(): AsyncData<GroupedMediaItems> = groupedMediaItemsFlow.replayCache.firstOrNull() ?: AsyncData.Uninitialized
|
override fun getLastData(): AsyncData<GroupedMediaItems> = groupedMediaItemsFlow.replayCache.firstOrNull()
|
||||||
|
?: mediaTimeline.cache?.let { AsyncData.Success(it) }
|
||||||
|
?: AsyncData.Uninitialized
|
||||||
|
|
||||||
private val isStarted = AtomicBoolean(false)
|
private val isStarted = AtomicBoolean(false)
|
||||||
|
|
||||||
|
|
@ -58,8 +62,13 @@ class TimelineMediaGalleryDataSource @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
flow {
|
flow {
|
||||||
groupedMediaItemsFlow.emit(AsyncData.Loading())
|
val cache = mediaTimeline.cache
|
||||||
room.mediaTimeline().fold(
|
if (cache != null) {
|
||||||
|
groupedMediaItemsFlow.emit(AsyncData.Success(cache))
|
||||||
|
} else {
|
||||||
|
groupedMediaItemsFlow.emit(AsyncData.Loading())
|
||||||
|
}
|
||||||
|
mediaTimeline.getTimeline().fold(
|
||||||
{
|
{
|
||||||
timeline = it
|
timeline = it
|
||||||
emit(it)
|
emit(it)
|
||||||
|
|
@ -78,6 +87,8 @@ class TimelineMediaGalleryDataSource @Inject constructor(
|
||||||
timelineMediaItemsFactory.timelineItems
|
timelineMediaItemsFactory.timelineItems
|
||||||
}.map { timelineItems ->
|
}.map { timelineItems ->
|
||||||
mediaItemsPostProcessor.process(mediaItems = timelineItems)
|
mediaItemsPostProcessor.process(mediaItems = timelineItems)
|
||||||
|
}.map {
|
||||||
|
mediaTimeline.orCache(it)
|
||||||
}.onEach { groupedMediaItems ->
|
}.onEach { groupedMediaItems ->
|
||||||
groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems))
|
groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems))
|
||||||
}
|
}
|
||||||
|
|
@ -5,8 +5,10 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 kotlinx.collections.immutable.toImmutableList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -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<Timeline>
|
||||||
|
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<Timeline> = 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<Timeline> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,13 +5,14 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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.DefaultDiffCacheInvalidator
|
||||||
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -5,12 +5,13 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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.DateFormatter
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
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.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class VirtualItemFactory @Inject constructor(
|
class VirtualItemFactory @Inject constructor(
|
||||||
|
|
@ -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.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
|
|
||||||
sealed interface MediaGalleryEvents {
|
sealed interface MediaGalleryEvents {
|
||||||
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
|
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
|
||||||
|
|
|
||||||
|
|
@ -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.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
|
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.gallery.di.MediaItemPresenterFactories
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class MediaGalleryNode @AssistedInject constructor(
|
class MediaGalleryNode @AssistedInject constructor(
|
||||||
|
|
|
||||||
|
|
@ -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.matrix.api.room.powerlevels.canRedactOwn
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
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.details.MediaBottomSheetState
|
||||||
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
|
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 io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.mediaviewer.impl.R
|
import io.element.android.libraries.mediaviewer.impl.R
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
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(
|
data class MediaGalleryState(
|
||||||
val roomName: String,
|
val roomName: String,
|
||||||
|
|
@ -22,18 +22,6 @@ data class MediaGalleryState(
|
||||||
val eventSink: (MediaGalleryEvents) -> Unit,
|
val eventSink: (MediaGalleryEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GroupedMediaItems(
|
|
||||||
val imageAndVideoItems: ImmutableList<MediaItem>,
|
|
||||||
val fileItems: ImmutableList<MediaItem>,
|
|
||||||
) {
|
|
||||||
fun getItems(mode: MediaGalleryMode): ImmutableList<MediaItem> {
|
|
||||||
return when (mode) {
|
|
||||||
MediaGalleryMode.Images -> imageAndVideoItems
|
|
||||||
MediaGalleryMode.Files -> fileItems
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class MediaGalleryMode(val stringResource: Int) {
|
enum class MediaGalleryMode(val stringResource: Int) {
|
||||||
Images(R.string.screen_media_browser_list_mode_media),
|
Images(R.string.screen_media_browser_list_mode_media),
|
||||||
Files(R.string.screen_media_browser_list_mode_files),
|
Files(R.string.screen_media_browser_list_mode_files),
|
||||||
|
|
|
||||||
|
|
@ -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.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
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.details.aMediaDetailsBottomSheetState
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemAudio
|
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
|
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 kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryState> {
|
open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryState> {
|
||||||
|
|
|
||||||
|
|
@ -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.ImageItemView
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VideoItemView
|
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.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 io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -108,15 +111,15 @@ fun MediaGalleryView(
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.consumeWindowInsets(paddingValues)
|
.consumeWindowInsets(paddingValues)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
SingleChoiceSegmentedButtonRow(
|
SingleChoiceSegmentedButtonRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
MediaGalleryMode.entries.forEach { mode ->
|
MediaGalleryMode.entries.forEach { mode ->
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
|
|
@ -354,8 +357,8 @@ private fun MediaGalleryImageGrid(
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
columns = GridCells.Adaptive(80.dp),
|
columns = GridCells.Adaptive(80.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
|
@ -426,9 +429,9 @@ private fun LoadingMoreIndicator(
|
||||||
Timeline.PaginationDirection.FORWARDS -> {
|
Timeline.PaginationDirection.FORWARDS -> {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
.height(1.dp)
|
.height(1.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Timeline.PaginationDirection.BACKWARDS -> {
|
Timeline.PaginationDirection.BACKWARDS -> {
|
||||||
|
|
@ -466,9 +469,9 @@ private fun EmptyContent(
|
||||||
OnboardingBackground()
|
OnboardingBackground()
|
||||||
PageTitle(
|
PageTitle(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 44.dp)
|
.padding(top = 44.dp)
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
title = stringResource(titleRes),
|
title = stringResource(titleRes),
|
||||||
iconStyle = BigIcon.Style.Default(icon),
|
iconStyle = BigIcon.Style.Default(icon),
|
||||||
subtitle = stringResource(subtitleRes),
|
subtitle = stringResource(subtitleRes),
|
||||||
|
|
@ -486,9 +489,9 @@ private fun LoadingContent(
|
||||||
OnboardingBackground()
|
OnboardingBackground()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = 48.dp)
|
.padding(top = 48.dp)
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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<GroupedMediaItems> = 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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.VoiceMessageState
|
||||||
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||||
|
|
||||||
import dagger.MapKey
|
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
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import dagger.multibindings.Multibinds
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.
|
* A factory for a [Presenter] associated with a timeline item.
|
||||||
|
|
|
||||||
|
|
@ -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.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
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.MediaGalleryNode
|
||||||
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.gallery.eventId
|
import io.element.android.libraries.mediaviewer.impl.model.eventId
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo
|
import io.element.android.libraries.mediaviewer.impl.model.mediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource
|
import io.element.android.libraries.mediaviewer.impl.model.mediaSource
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource
|
import io.element.android.libraries.mediaviewer.impl.model.thumbnailSource
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
|
||||||
|
|
@ -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.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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
|
@Composable
|
||||||
fun AudioItemView(
|
fun AudioItemView(
|
||||||
|
|
|
||||||
|
|
@ -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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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
|
@Composable
|
||||||
fun DateItemView(
|
fun DateItemView(
|
||||||
|
|
|
||||||
|
|
@ -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.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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
|
@Composable
|
||||||
fun FileItemView(
|
fun FileItemView(
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ import coil.compose.AsyncImagePainter
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,8 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.core.preview.loremIpsum
|
import io.element.android.libraries.core.preview.loremIpsum
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio
|
||||||
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
|
||||||
|
|
||||||
class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
||||||
override val values: Sequence<MediaItem.Audio>
|
override val values: Sequence<MediaItem.Audio>
|
||||||
|
|
@ -27,19 +25,3 @@ class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator
|
||||||
|
|
||||||
class MediaItemDateSeparatorProvider : PreviewParameterProvider<MediaItem.DateSeparator> {
|
class MediaItemDateSeparatorProvider : PreviewParameterProvider<MediaItem.DateSeparator> {
|
||||||
override val values: Sequence<MediaItem.DateSeparator>
|
override val values: Sequence<MediaItem.DateSeparator>
|
||||||
|
|
@ -18,13 +18,3 @@ class MediaItemDateSeparatorProvider : PreviewParameterProvider<MediaItem.DateSe
|
||||||
aMediaItemDateSeparator(formattedDate = "A long date that should be truncated"),
|
aMediaItemDateSeparator(formattedDate = "A long date that should be truncated"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aMediaItemDateSeparator(
|
|
||||||
id: UniqueId = UniqueId("dateId"),
|
|
||||||
formattedDate: String = "October 2024",
|
|
||||||
): MediaItem.DateSeparator {
|
|
||||||
return MediaItem.DateSeparator(
|
|
||||||
id = id,
|
|
||||||
formattedDate = formattedDate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,8 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.core.preview.loremIpsum
|
import io.element.android.libraries.core.preview.loremIpsum
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
|
||||||
import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
|
||||||
|
|
||||||
class MediaItemFileProvider : PreviewParameterProvider<MediaItem.File> {
|
class MediaItemFileProvider : PreviewParameterProvider<MediaItem.File> {
|
||||||
override val values: Sequence<MediaItem.File>
|
override val values: Sequence<MediaItem.File>
|
||||||
|
|
@ -28,20 +25,3 @@ class MediaItemFileProvider : PreviewParameterProvider<MediaItem.File> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -8,10 +8,8 @@
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVideo
|
||||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
|
||||||
|
|
||||||
class MediaItemVideoProvider : PreviewParameterProvider<MediaItem.Video> {
|
class MediaItemVideoProvider : PreviewParameterProvider<MediaItem.Video> {
|
||||||
override val values: Sequence<MediaItem.Video>
|
override val values: Sequence<MediaItem.Video>
|
||||||
|
|
@ -22,19 +20,3 @@ class MediaItemVideoProvider : PreviewParameterProvider<MediaItem.Video> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,8 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.core.preview.loremIpsum
|
import io.element.android.libraries.core.preview.loremIpsum
|
||||||
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice
|
||||||
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
|
|
||||||
|
|
||||||
class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> {
|
class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> {
|
||||||
override val values: Sequence<MediaItem.Voice>
|
override val values: Sequence<MediaItem.Voice>
|
||||||
|
|
@ -31,23 +28,3 @@ class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aMediaItemVoice(
|
|
||||||
id: UniqueId = UniqueId("fileId"),
|
|
||||||
filename: String = "filename.ogg",
|
|
||||||
caption: String? = null,
|
|
||||||
duration: String? = "1:23",
|
|
||||||
waveform: List<Float> = aWaveForm(),
|
|
||||||
): MediaItem.Voice {
|
|
||||||
return MediaItem.Voice(
|
|
||||||
id = id,
|
|
||||||
eventId = null,
|
|
||||||
mediaInfo = aVoiceMediaInfo(
|
|
||||||
filename = filename,
|
|
||||||
caption = caption,
|
|
||||||
duration = duration,
|
|
||||||
waveForm = waveform,
|
|
||||||
),
|
|
||||||
mediaSource = MediaSource(""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ import dagger.assisted.AssistedInject
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.RoomScope
|
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.MediaItemEventContentKey
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactory
|
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.VoiceMessagePresenterFactory
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
|
||||||
|
|
@ -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<MediaItem>,
|
||||||
|
val fileItems: ImmutableList<MediaItem>,
|
||||||
|
) {
|
||||||
|
fun getItems(mode: MediaGalleryMode): ImmutableList<MediaItem> {
|
||||||
|
return when (mode) {
|
||||||
|
MediaGalleryMode.Images -> imageAndVideoItems
|
||||||
|
MediaGalleryMode.Files -> fileItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GroupedMediaItems.hasEvent(eventId: EventId): Boolean {
|
||||||
|
return (fileItems + imageAndVideoItems)
|
||||||
|
.filterIsInstance<MediaItem.Event>()
|
||||||
|
.any { it.eventId() == eventId }
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
|
|
@ -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<Float> = 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
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.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
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.MediaGalleryMode
|
||||||
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.gallery.eventId
|
import io.element.android.libraries.mediaviewer.impl.model.eventId
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo
|
import io.element.android.libraries.mediaviewer.impl.model.mediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource
|
import io.element.android.libraries.mediaviewer.impl.model.mediaSource
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource
|
import io.element.android.libraries.mediaviewer.impl.model.thumbnailSource
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.collections.immutable.PersistentList
|
import kotlinx.collections.immutable.PersistentList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -38,16 +39,23 @@ import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class MediaViewerDataSource(
|
class MediaViewerDataSource(
|
||||||
private val galleryMode: MediaGalleryMode,
|
mode: MediaViewerMode,
|
||||||
private val dispatcher: CoroutineDispatcher,
|
private val dispatcher: CoroutineDispatcher,
|
||||||
private val galleryDataSource: MediaGalleryDataSource,
|
private val galleryDataSource: MediaGalleryDataSource,
|
||||||
private val mediaLoader: MatrixMediaLoader,
|
private val mediaLoader: MatrixMediaLoader,
|
||||||
private val localMediaFactory: LocalMediaFactory,
|
private val localMediaFactory: LocalMediaFactory,
|
||||||
private val systemClock: SystemClock,
|
private val systemClock: SystemClock,
|
||||||
|
private val pagerKeysHandler: PagerKeysHandler,
|
||||||
) {
|
) {
|
||||||
// List of media files that are currently being loaded
|
// List of media files that are currently being loaded
|
||||||
private val mediaFiles: MutableList<MediaFile> = mutableListOf()
|
private val mediaFiles: MutableList<MediaFile> = mutableListOf()
|
||||||
|
|
||||||
|
private val galleryMode = when (mode) {
|
||||||
|
MediaViewerMode.SingleMedia,
|
||||||
|
MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
|
||||||
|
MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
|
||||||
|
}
|
||||||
|
|
||||||
// Map of sourceUrl to local media state
|
// Map of sourceUrl to local media state
|
||||||
private val localMediaStates: MutableMap<String, MutableState<AsyncData<LocalMedia>>> =
|
private val localMediaStates: MutableMap<String, MutableState<AsyncData<LocalMedia>>> =
|
||||||
mutableMapOf()
|
mutableMapOf()
|
||||||
|
|
@ -78,6 +86,7 @@ class MediaViewerDataSource(
|
||||||
MediaViewerPageData.Loading(
|
MediaViewerPageData.Loading(
|
||||||
direction = Timeline.PaginationDirection.BACKWARDS,
|
direction = Timeline.PaginationDirection.BACKWARDS,
|
||||||
timestamp = systemClock.epochMillis(),
|
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).
|
* will be used to render the downloaded media (see [loadMedia] which will update this value).
|
||||||
*/
|
*/
|
||||||
private fun buildMediaViewerPageList(groupedItems: List<MediaItem>) = buildList {
|
private fun buildMediaViewerPageList(groupedItems: List<MediaItem>) = 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) {
|
when (mediaItem) {
|
||||||
is MediaItem.DateSeparator -> Unit
|
is MediaItem.DateSeparator -> Unit
|
||||||
is MediaItem.Event -> {
|
is MediaItem.Event -> {
|
||||||
|
|
@ -123,6 +135,7 @@ class MediaViewerDataSource(
|
||||||
mediaSource = mediaItem.mediaSource(),
|
mediaSource = mediaItem.mediaSource(),
|
||||||
thumbnailSource = mediaItem.thumbnailSource(),
|
thumbnailSource = mediaItem.thumbnailSource(),
|
||||||
downloadedMedia = localMedia,
|
downloadedMedia = localMedia,
|
||||||
|
pagerKey = pagerKeysHandler.getKey(mediaItem),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +143,7 @@ class MediaViewerDataSource(
|
||||||
MediaViewerPageData.Loading(
|
MediaViewerPageData.Loading(
|
||||||
direction = mediaItem.direction,
|
direction = mediaItem.direction,
|
||||||
timestamp = systemClock.epochMillis(),
|
timestamp = systemClock.epochMillis(),
|
||||||
|
pagerKey = pagerKeysHandler.getKey(mediaItem),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
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.datasource.FocusedTimelineMediaGalleryDataSourceFactory
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.SingleMediaGalleryDataSource
|
import io.element.android.libraries.mediaviewer.impl.datasource.TimelineMediaGalleryDataSource
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.TimelineMediaGalleryDataSource
|
import io.element.android.libraries.mediaviewer.impl.model.hasEvent
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -35,10 +35,12 @@ class MediaViewerNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: MediaViewerPresenter.Factory,
|
presenterFactory: MediaViewerPresenter.Factory,
|
||||||
timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource,
|
timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource,
|
||||||
|
focusedTimelineMediaGalleryDataSourceFactory: FocusedTimelineMediaGalleryDataSourceFactory,
|
||||||
mediaLoader: MatrixMediaLoader,
|
mediaLoader: MatrixMediaLoader,
|
||||||
localMediaFactory: LocalMediaFactory,
|
localMediaFactory: LocalMediaFactory,
|
||||||
coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
systemClock: SystemClock,
|
systemClock: SystemClock,
|
||||||
|
pagerKeysHandler: PagerKeysHandler,
|
||||||
) : Node(buildContext, plugins = plugins),
|
) : Node(buildContext, plugins = plugins),
|
||||||
MediaViewerNavigator {
|
MediaViewerNavigator {
|
||||||
private val inputs = inputs<MediaViewerEntryPoint.Params>()
|
private val inputs = inputs<MediaViewerEntryPoint.Params>()
|
||||||
|
|
@ -62,25 +64,36 @@ class MediaViewerNode @AssistedInject constructor(
|
||||||
private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) {
|
private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) {
|
||||||
SingleMediaGalleryDataSource.createFrom(inputs)
|
SingleMediaGalleryDataSource.createFrom(inputs)
|
||||||
} else {
|
} else {
|
||||||
timelineMediaGalleryDataSource
|
val eventId = inputs.eventId
|
||||||
}
|
if (eventId == null) {
|
||||||
|
// Should not happen
|
||||||
private val galleryMode = when (inputs.mode) {
|
timelineMediaGalleryDataSource
|
||||||
MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
} else {
|
||||||
MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
|
// Does timelineMediaGalleryDataSource knows the eventId?
|
||||||
MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
|
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(
|
private val presenter = presenterFactory.create(
|
||||||
inputs = inputs,
|
inputs = inputs,
|
||||||
navigator = this,
|
navigator = this,
|
||||||
dataSource = MediaViewerDataSource(
|
dataSource = MediaViewerDataSource(
|
||||||
|
mode = inputs.mode,
|
||||||
dispatcher = coroutineDispatchers.computation,
|
dispatcher = coroutineDispatchers.computation,
|
||||||
galleryMode = galleryMode,
|
|
||||||
galleryDataSource = mediaGallerySource,
|
galleryDataSource = mediaGallerySource,
|
||||||
mediaLoader = mediaLoader,
|
mediaLoader = mediaLoader,
|
||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
systemClock = systemClock,
|
systemClock = systemClock,
|
||||||
|
pagerKeysHandler = pagerKeysHandler,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,17 @@ data class MediaViewerState(
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface MediaViewerPageData {
|
sealed interface MediaViewerPageData {
|
||||||
|
val pagerKey: Long
|
||||||
|
|
||||||
data class Failure(
|
data class Failure(
|
||||||
val throwable: Throwable,
|
val throwable: Throwable,
|
||||||
|
override val pagerKey: Long = 0,
|
||||||
) : MediaViewerPageData
|
) : MediaViewerPageData
|
||||||
|
|
||||||
data class Loading(
|
data class Loading(
|
||||||
val direction: Timeline.PaginationDirection,
|
val direction: Timeline.PaginationDirection,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
|
override val pagerKey: Long,
|
||||||
) : MediaViewerPageData
|
) : MediaViewerPageData
|
||||||
|
|
||||||
data class MediaViewerData(
|
data class MediaViewerData(
|
||||||
|
|
@ -43,5 +47,6 @@ sealed interface MediaViewerPageData {
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val downloadedMedia: State<AsyncData<LocalMedia>>,
|
val downloadedMedia: State<AsyncData<LocalMedia>>,
|
||||||
|
override val pagerKey: Long,
|
||||||
) : MediaViewerPageData
|
) : MediaViewerPageData
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ fun aMediaViewerPageDataLoading(
|
||||||
return MediaViewerPageData.Loading(
|
return MediaViewerPageData.Loading(
|
||||||
direction = direction,
|
direction = direction,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
|
pagerKey = 0L,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,6 +183,7 @@ fun aMediaViewerPageData(
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
downloadedMedia = mutableStateOf(downloadedMedia),
|
downloadedMedia = mutableStateOf(downloadedMedia),
|
||||||
|
pagerKey = 0L,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aMediaViewerState(
|
fun aMediaViewerState(
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ fun MediaViewerView(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
// Pre-load previous and next pages
|
// Pre-load previous and next pages
|
||||||
beyondViewportPageCount = 1,
|
beyondViewportPageCount = 1,
|
||||||
|
key = { index -> state.listData[index].pagerKey },
|
||||||
) { page ->
|
) { page ->
|
||||||
when (val dataForPage = state.listData[page]) {
|
when (val dataForPage = state.listData[page]) {
|
||||||
is MediaViewerPageData.Failure -> {
|
is MediaViewerPageData.Failure -> {
|
||||||
|
|
|
||||||
|
|
@ -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<MediaItem>,
|
||||||
|
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<MediaItem>) {
|
||||||
|
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<MediaItem.Event>()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<GroupedMediaItems> = 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
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.aStickerContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
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 io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,11 +5,12 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
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 io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
@ -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<EventId?, Result<Timeline>> {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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<GroupedMediaItems?>(sut.cache).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTimeline returns the timeline provided by the room, then from cache`() = runTest {
|
||||||
|
val mediaTimelineResult = lambdaRecorder<EventId?, Result<Timeline>> {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -5,17 +5,19 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
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.model.GroupedMediaItems
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
|
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 kotlinx.collections.immutable.toImmutableList
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
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.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.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -260,18 +262,21 @@ class TimelineMediaGalleryDataSourceTest {
|
||||||
): TimelineMediaGalleryDataSource {
|
): TimelineMediaGalleryDataSource {
|
||||||
return TimelineMediaGalleryDataSource(
|
return TimelineMediaGalleryDataSource(
|
||||||
room = room,
|
room = room,
|
||||||
timelineMediaItemsFactory = TimelineMediaItemsFactory(
|
mediaTimeline = LiveMediaTimeline(room),
|
||||||
dispatchers = testCoroutineDispatchers(),
|
timelineMediaItemsFactory = createTimelineMediaItemsFactory(),
|
||||||
virtualItemFactory = VirtualItemFactory(
|
|
||||||
dateFormatter = FakeDateFormatter(),
|
|
||||||
),
|
|
||||||
eventItemFactory = EventItemFactory(
|
|
||||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
|
||||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
|
||||||
dateFormatter = FakeDateFormatter(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
mediaItemsPostProcessor = MediaItemsPostProcessor(),
|
mediaItemsPostProcessor = MediaItemsPostProcessor(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TestScope.createTimelineMediaItemsFactory() = TimelineMediaItemsFactory(
|
||||||
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
virtualItemFactory = VirtualItemFactory(
|
||||||
|
dateFormatter = FakeDateFormatter(),
|
||||||
|
),
|
||||||
|
eventItemFactory = EventItemFactory(
|
||||||
|
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||||
|
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||||
|
dateFormatter = FakeDateFormatter(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
@ -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.media.FakeMatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
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.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.FakeLocalMediaActions
|
||||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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_EVENT_ID_2
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
|
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.api.local.LocalMediaFactory
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.FakeMediaGalleryDataSource
|
import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource
|
||||||
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.aGroupedMediaItems
|
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.model.aMediaItemDateSeparator
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile
|
||||||
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.impl.gallery.ui.aMediaItemLoadingIndicator
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemLoadingIndicator
|
||||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
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.A_FAKE_TIMESTAMP
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
|
|
@ -122,10 +122,12 @@ class MediaViewerDataSourceTest {
|
||||||
MediaViewerPageData.Loading(
|
MediaViewerPageData.Loading(
|
||||||
direction = Timeline.PaginationDirection.BACKWARDS,
|
direction = Timeline.PaginationDirection.BACKWARDS,
|
||||||
timestamp = A_FAKE_TIMESTAMP,
|
timestamp = A_FAKE_TIMESTAMP,
|
||||||
|
pagerKey = 0L,
|
||||||
),
|
),
|
||||||
MediaViewerPageData.Loading(
|
MediaViewerPageData.Loading(
|
||||||
direction = Timeline.PaginationDirection.FORWARDS,
|
direction = Timeline.PaginationDirection.FORWARDS,
|
||||||
timestamp = A_FAKE_TIMESTAMP,
|
timestamp = A_FAKE_TIMESTAMP,
|
||||||
|
pagerKey = 1L,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +137,7 @@ class MediaViewerDataSourceTest {
|
||||||
fun `test dataFlow with data galleryMode image`() = runTest {
|
fun `test dataFlow with data galleryMode image`() = runTest {
|
||||||
val galleryDataSource = FakeMediaGalleryDataSource()
|
val galleryDataSource = FakeMediaGalleryDataSource()
|
||||||
val sut = createMediaViewerDataSource(
|
val sut = createMediaViewerDataSource(
|
||||||
galleryMode = MediaGalleryMode.Images,
|
mode = MediaViewerMode.TimelineImagesAndVideos,
|
||||||
galleryDataSource = galleryDataSource,
|
galleryDataSource = galleryDataSource,
|
||||||
)
|
)
|
||||||
sut.dataFlow().test {
|
sut.dataFlow().test {
|
||||||
|
|
@ -157,7 +159,7 @@ class MediaViewerDataSourceTest {
|
||||||
fun `test dataFlow with data galleryMode files`() = runTest {
|
fun `test dataFlow with data galleryMode files`() = runTest {
|
||||||
val galleryDataSource = FakeMediaGalleryDataSource()
|
val galleryDataSource = FakeMediaGalleryDataSource()
|
||||||
val sut = createMediaViewerDataSource(
|
val sut = createMediaViewerDataSource(
|
||||||
galleryMode = MediaGalleryMode.Files,
|
mode = MediaViewerMode.TimelineFilesAndAudios,
|
||||||
galleryDataSource = galleryDataSource,
|
galleryDataSource = galleryDataSource,
|
||||||
)
|
)
|
||||||
sut.dataFlow().test {
|
sut.dataFlow().test {
|
||||||
|
|
@ -263,16 +265,17 @@ class MediaViewerDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createMediaViewerDataSource(
|
private fun TestScope.createMediaViewerDataSource(
|
||||||
galleryMode: MediaGalleryMode = MediaGalleryMode.Images,
|
mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos,
|
||||||
galleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(),
|
galleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(),
|
||||||
mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
|
mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
|
||||||
localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
|
localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
|
||||||
) = MediaViewerDataSource(
|
) = MediaViewerDataSource(
|
||||||
galleryMode = galleryMode,
|
mode = mode,
|
||||||
dispatcher = testCoroutineDispatchers().computation,
|
dispatcher = testCoroutineDispatchers().computation,
|
||||||
galleryDataSource = galleryDataSource,
|
galleryDataSource = galleryDataSource,
|
||||||
mediaLoader = mediaLoader,
|
mediaLoader = mediaLoader,
|
||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
systemClock = FakeSystemClock(),
|
systemClock = FakeSystemClock(),
|
||||||
|
pagerKeysHandler = PagerKeysHandler(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.MediaViewerEntryPoint
|
||||||
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.impl.R
|
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.details.MediaBottomSheetState
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.FakeMediaGalleryDataSource
|
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.GroupedMediaItems
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemLoadingIndicator
|
||||||
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.test.FakeLocalMediaActions
|
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
|
||||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
|
|
@ -782,16 +781,13 @@ class MediaViewerPresenterTest {
|
||||||
),
|
),
|
||||||
navigator = mediaViewerNavigator,
|
navigator = mediaViewerNavigator,
|
||||||
dataSource = MediaViewerDataSource(
|
dataSource = MediaViewerDataSource(
|
||||||
galleryMode = when (mode) {
|
mode = mode,
|
||||||
MediaViewerEntryPoint.MediaViewerMode.SingleMedia -> MediaGalleryMode.Images
|
|
||||||
MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
|
|
||||||
MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
|
|
||||||
},
|
|
||||||
dispatcher = testCoroutineDispatchers().computation,
|
dispatcher = testCoroutineDispatchers().computation,
|
||||||
galleryDataSource = mediaGalleryDataSource,
|
galleryDataSource = mediaGalleryDataSource,
|
||||||
mediaLoader = matrixMediaLoader,
|
mediaLoader = matrixMediaLoader,
|
||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
systemClock = FakeSystemClock(),
|
systemClock = FakeSystemClock(),
|
||||||
|
pagerKeysHandler = PagerKeysHandler(),
|
||||||
),
|
),
|
||||||
room = room,
|
room = room,
|
||||||
localMediaActions = localMediaActions,
|
localMediaActions = localMediaActions,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.anApkMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
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.aGroupedMediaItems
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
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 io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -136,6 +136,7 @@ class KonsistClassNameTest {
|
||||||
"Enterprise",
|
"Enterprise",
|
||||||
"Fdroid",
|
"Fdroid",
|
||||||
"FileExtensionExtractor",
|
"FileExtensionExtractor",
|
||||||
|
"LiveMediaTimeline",
|
||||||
"KeyStore",
|
"KeyStore",
|
||||||
"Matrix",
|
"Matrix",
|
||||||
"Noop",
|
"Noop",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue