Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media navigation with swipe gesture #4161

Merged
merged 38 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
35cc5df
Create MediaGalleryDataSource and extract logic from MediaGalleryPres…
bmarty Jan 15, 2025
cbf7bf0
Let MediaGalleryDataSource be an interface
bmarty Jan 16, 2025
a06607f
Remove RetryLoading (use LoadMedia)
bmarty Jan 17, 2025
32db42a
Suppress Detekt false positive (?)
bmarty Jan 17, 2025
7df65b0
Add support for files navigation (when coming from the gallery)
bmarty Jan 17, 2025
ca5c06f
Open in SingleMedia mode when coming from the timeline
bmarty Jan 17, 2025
8d6550b
If not displayed, make sure to pause the audio / video
bmarty Jan 17, 2025
98a786b
media viewer : create MediaViewerDataSource
ganfra Jan 20, 2025
d26414f
Provide duration
bmarty Jan 17, 2025
bad4566
Remove unused import
bmarty Jan 20, 2025
3248041
media viewer : use collectAsState in the DataSource
ganfra Jan 21, 2025
c0542c8
Fix and write tests
bmarty Jan 21, 2025
d6ebec8
Improve loading state and add preview.
bmarty Jan 21, 2025
6c904a4
Add exception for Konsist
bmarty Jan 21, 2025
aa68a35
sync strings
bmarty Jan 21, 2025
a4cf1d7
Restore caption rendering
bmarty Jan 22, 2025
f286f6d
Small cleanup
bmarty Jan 22, 2025
428b81c
Add test on SingleMediaGalleryDataSource
bmarty Jan 22, 2025
18c3b5b
Add test on MediaViewerDataSource
bmarty Jan 22, 2025
621558a
MediaViewer: add error case in the UI.
bmarty Jan 22, 2025
e496bc3
Introduce MediaViewerFlickToDismiss and extract to its own file
bmarty Jan 22, 2025
dfda13a
Restore overlay when user cancel the dragging
bmarty Jan 22, 2025
bac69c4
Add timestamp to trigger back pagination.
bmarty Jan 22, 2025
b72f4bb
Fix tests.
bmarty Jan 22, 2025
74be5a5
Update screenshots
ElementBot Jan 22, 2025
1580c73
Ensure gallery is paginating to get new items.
bmarty Jan 23, 2025
77887f9
Fix formatting.
bmarty Jan 23, 2025
dff9005
Remove useless parameter
bmarty Jan 23, 2025
54182b0
Simplify with code `coerceAtLeast(0)``
bmarty Jan 23, 2025
2fde015
Simplify
bmarty Jan 23, 2025
afd8161
Add documentation on buildMediaViewerPageList.
bmarty Jan 23, 2025
a5515b0
Add name to argument for clarity.
bmarty Jan 23, 2025
7dd797b
Use Black for code clarity.
bmarty Jan 23, 2025
beb835c
Fix color for media viewer according to Figma.
bmarty Jan 23, 2025
05660fb
Cleanup
bmarty Jan 23, 2025
4d2edfa
Improve code clarity
bmarty Jan 23, 2025
ba0502c
Update screenshots
ElementBot Jan 23, 2025
da22758
Fix pagination restart issue and cover by unit test.
bmarty Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.duration
import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox
Expand All @@ -58,6 +59,7 @@ import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.dateformatter.api.toHumanReadableDuration
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
Expand Down Expand Up @@ -449,6 +451,7 @@ class MessagesFlowNode @AssistedInject constructor(
mode = DateFormatterMode.Full,
),
waveform = (content as? TimelineItemVoiceContent)?.waveform,
duration = content.duration()?.toHumanReadableDuration(),
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.timeline.model.event

import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.media.MediaSource
import kotlin.time.Duration

@Immutable
sealed interface TimelineItemEventContent {
Expand Down Expand Up @@ -90,3 +91,12 @@ fun TimelineItemEventContent.isEdited(): Boolean = when (this) {
is TimelineItemEventMutableContent -> isEdited
else -> false
}

fun TimelineItemEventContentWithAttachment.duration(): Duration? {
return when (this) {
is TimelineItemAudioContent -> duration
is TimelineItemVideoContent -> duration
is TimelineItemVoiceContent -> duration
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package io.element.android.libraries.dateformatter.api

import java.util.Locale
import kotlin.time.Duration

/**
* Convert milliseconds to human readable duration.
Expand Down Expand Up @@ -38,3 +39,5 @@ fun Long.toHumanReadableDuration(): String {
String.format(Locale.US, "%d:%02d", minutes, seconds)
}
}

fun Duration.toHumanReadableDuration() = inWholeMilliseconds.toHumanReadableDuration()
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ data class MediaInfo(
val dateSent: String?,
val dateSentFull: String?,
val waveform: List<Float>?,
val duration: String?,
) : Parcelable

fun anImageMediaInfo(
Expand All @@ -45,13 +46,15 @@ fun anImageMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)

fun aVideoMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
duration: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4",
caption = caption,
Expand All @@ -64,6 +67,7 @@ fun aVideoMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = duration,
)

fun aPdfMediaInfo(
Expand All @@ -84,6 +88,7 @@ fun aPdfMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)

fun anApkMediaInfo(
Expand All @@ -103,6 +108,7 @@ fun anApkMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)

fun anAudioMediaInfo(
Expand All @@ -112,6 +118,7 @@ fun anAudioMediaInfo(
dateSent: String? = null,
dateSentFull: String? = null,
waveForm: List<Float>? = null,
duration: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
caption = caption,
Expand All @@ -124,6 +131,7 @@ fun anAudioMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = waveForm,
duration = duration,
)

fun aVoiceMediaInfo(
Expand All @@ -133,6 +141,7 @@ fun aVoiceMediaInfo(
dateSent: String? = null,
dateSentFull: String? = null,
waveForm: List<Float>? = null,
duration: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
caption = caption,
Expand All @@ -145,4 +154,5 @@ fun aVoiceMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = waveForm,
duration = duration,
)
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
dateSent = null,
dateSentFull = null,
waveform = null,
duration = null,
),
mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
)
Expand All @@ -120,6 +121,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
)
Expand All @@ -138,6 +140,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
thumbnailSource = null,
Expand All @@ -157,6 +160,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
thumbnailSource = null,
Expand All @@ -176,10 +180,10 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
),
mediaSource = type.source,
thumbnailSource = type.info?.thumbnailSource,
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
)
is VoiceMessageType -> MediaItem.Voice(
id = currentTimelineItem.uniqueId,
Expand All @@ -196,10 +200,9 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = type.details?.waveform.orEmpty(),
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
),
mediaSource = type.source,
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
waveform = type.details?.waveform ?: persistentListOf(),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.mediaviewer.api.MediaInfo
import kotlinx.collections.immutable.ImmutableList

sealed interface MediaItem {
data class DateSeparator(
Expand Down Expand Up @@ -46,7 +45,6 @@ sealed interface MediaItem {
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val duration: String?,
) : Event {
val thumbnailMediaRequestData: MediaRequestData
get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
Expand All @@ -64,8 +62,6 @@ sealed interface MediaItem {
val eventId: EventId?,
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val duration: String?,
val waveform: ImmutableList<Float>,
) : Event

data class File(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ 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.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.flowOf

class SingleMediaGalleryDataSource(
Expand All @@ -32,77 +31,54 @@ class SingleMediaGalleryDataSource(
fun createFrom(params: MediaViewerEntryPoint.Params) = SingleMediaGalleryDataSource(
data = when {
params.mediaInfo.mimeType.isMimeTypeImage() -> {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(
MediaItem.Image(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
thumbnailSource = params.thumbnailSource,
)
),
fileItems = persistentListOf(),
MediaItem.Image(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
thumbnailSource = params.thumbnailSource,
)
}
params.mediaInfo.mimeType.isMimeTypeVideo() -> {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(
MediaItem.Video(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
thumbnailSource = params.thumbnailSource,
duration = "TODO", // TODO Duration
)
),
fileItems = persistentListOf(),
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) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(
MediaItem.Audio(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
)
),
fileItems = persistentListOf(),
MediaItem.Audio(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
)
} else {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(
MediaItem.Voice(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
duration = "TODO", // TODO Duration
waveform = params.mediaInfo.waveform.orEmpty().toImmutableList(),
)
),
fileItems = persistentListOf(),
MediaItem.Voice(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
)
}
}
else -> {
// Always use imageAndVideoItems, in Single mode, this is the data that will be used
GroupedMediaItems(
imageAndVideoItems = persistentListOf(
MediaItem.File(
id = UniqueId("dummy"),
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
)
),
fileItems = persistentListOf(),
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(),
)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ fun aMediaItemVideo(
return MediaItem.Video(
id = id,
eventId = null,
mediaInfo = aVideoMediaInfo(),
mediaInfo = aVideoMediaInfo(
duration = duration
),
mediaSource = mediaSource,
thumbnailSource = null,
duration = duration,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
import kotlinx.collections.immutable.toImmutableList

class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> {
override val values: Sequence<MediaItem.Voice>
Expand Down Expand Up @@ -46,9 +45,9 @@ fun aMediaItemVoice(
mediaInfo = aVoiceMediaInfo(
filename = filename,
caption = caption,
duration = duration,
waveForm = waveform,
),
mediaSource = MediaSource(""),
duration = duration,
waveform = waveform.toImmutableList(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ private fun VideoInfoRow(
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null
)
if (video.duration != null) {
video.mediaInfo.duration?.let { duration ->
Spacer(Modifier.weight(1f))
Text(
text = video.duration,
text = duration,
style = ElementTheme.typography.fontBodySmMedium,
color = ElementTheme.colors.textPrimary,
)
Expand Down
Loading