Provide duration

This commit is contained in:
Benoit Marty 2025-01-17 16:07:36 +01:00 committed by Benoit Marty
parent 24a2458e4a
commit 03523c9567
13 changed files with 79 additions and 73 deletions

View file

@ -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.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent 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.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.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
@ -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.architecture.overlay.operation.show
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.dateformatter.api.toHumanReadableDuration
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -449,6 +451,7 @@ class MessagesFlowNode @AssistedInject constructor(
mode = DateFormatterMode.Full, mode = DateFormatterMode.Full,
), ),
waveform = (content as? TimelineItemVoiceContent)?.waveform, waveform = (content as? TimelineItemVoiceContent)?.waveform,
duration = content.duration()?.toHumanReadableDuration(),
), ),
mediaSource = mediaSource, mediaSource = mediaSource,
thumbnailSource = thumbnailSource, thumbnailSource = thumbnailSource,

View file

@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import kotlin.time.Duration
@Immutable @Immutable
sealed interface TimelineItemEventContent { sealed interface TimelineItemEventContent {
@ -90,3 +91,12 @@ fun TimelineItemEventContent.isEdited(): Boolean = when (this) {
is TimelineItemEventMutableContent -> isEdited is TimelineItemEventMutableContent -> isEdited
else -> false else -> false
} }
fun TimelineItemEventContentWithAttachment.duration(): Duration? {
return when (this) {
is TimelineItemAudioContent -> duration
is TimelineItemVideoContent -> duration
is TimelineItemVoiceContent -> duration
else -> null
}
}

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.dateformatter.api package io.element.android.libraries.dateformatter.api
import java.util.Locale import java.util.Locale
import kotlin.time.Duration
/** /**
* Convert milliseconds to human readable duration. * Convert milliseconds to human readable duration.
@ -38,3 +39,5 @@ fun Long.toHumanReadableDuration(): String {
String.format(Locale.US, "%d:%02d", minutes, seconds) String.format(Locale.US, "%d:%02d", minutes, seconds)
} }
} }
fun Duration.toHumanReadableDuration() = inWholeMilliseconds.toHumanReadableDuration()

View file

@ -25,6 +25,7 @@ data class MediaInfo(
val dateSent: String?, val dateSent: String?,
val dateSentFull: String?, val dateSentFull: String?,
val waveform: List<Float>?, val waveform: List<Float>?,
val duration: String?,
) : Parcelable ) : Parcelable
fun anImageMediaInfo( fun anImageMediaInfo(
@ -45,6 +46,7 @@ fun anImageMediaInfo(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = null,
) )
fun aVideoMediaInfo( fun aVideoMediaInfo(
@ -52,6 +54,7 @@ fun aVideoMediaInfo(
senderName: String? = null, senderName: String? = null,
dateSent: String? = null, dateSent: String? = null,
dateSentFull: String? = null, dateSentFull: String? = null,
duration: String? = null,
): MediaInfo = MediaInfo( ): MediaInfo = MediaInfo(
filename = "a video file.mp4", filename = "a video file.mp4",
caption = caption, caption = caption,
@ -64,6 +67,7 @@ fun aVideoMediaInfo(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = duration,
) )
fun aPdfMediaInfo( fun aPdfMediaInfo(
@ -84,6 +88,7 @@ fun aPdfMediaInfo(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = null,
) )
fun anApkMediaInfo( fun anApkMediaInfo(
@ -103,6 +108,7 @@ fun anApkMediaInfo(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = null,
) )
fun anAudioMediaInfo( fun anAudioMediaInfo(
@ -112,6 +118,7 @@ fun anAudioMediaInfo(
dateSent: String? = null, dateSent: String? = null,
dateSentFull: String? = null, dateSentFull: String? = null,
waveForm: List<Float>? = null, waveForm: List<Float>? = null,
duration: String? = null,
): MediaInfo = MediaInfo( ): MediaInfo = MediaInfo(
filename = filename, filename = filename,
caption = caption, caption = caption,
@ -124,6 +131,7 @@ fun anAudioMediaInfo(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = waveForm, waveform = waveForm,
duration = duration,
) )
fun aVoiceMediaInfo( fun aVoiceMediaInfo(
@ -133,6 +141,7 @@ fun aVoiceMediaInfo(
dateSent: String? = null, dateSent: String? = null,
dateSentFull: String? = null, dateSentFull: String? = null,
waveForm: List<Float>? = null, waveForm: List<Float>? = null,
duration: String? = null,
): MediaInfo = MediaInfo( ): MediaInfo = MediaInfo(
filename = filename, filename = filename,
caption = caption, caption = caption,
@ -145,4 +154,5 @@ fun aVoiceMediaInfo(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = waveForm, waveform = waveForm,
duration = duration,
) )

View file

@ -56,6 +56,7 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
dateSent = null, dateSent = null,
dateSentFull = null, dateSentFull = null,
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = MediaSource(url = avatarUrl), mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null, thumbnailSource = null,

View file

@ -102,6 +102,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = type.source, mediaSource = type.source,
) )
@ -120,6 +121,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = type.source, mediaSource = type.source,
) )
@ -138,6 +140,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = type.source, mediaSource = type.source,
thumbnailSource = null, thumbnailSource = null,
@ -157,6 +160,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = null,
), ),
mediaSource = type.source, mediaSource = type.source,
thumbnailSource = null, thumbnailSource = null,
@ -176,10 +180,10 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = null, waveform = null,
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
), ),
mediaSource = type.source, mediaSource = type.source,
thumbnailSource = type.info?.thumbnailSource, thumbnailSource = type.info?.thumbnailSource,
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
) )
is VoiceMessageType -> MediaItem.Voice( is VoiceMessageType -> MediaItem.Voice(
id = currentTimelineItem.uniqueId, id = currentTimelineItem.uniqueId,
@ -196,10 +200,9 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = type.details?.waveform.orEmpty(), waveform = type.details?.waveform.orEmpty(),
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
), ),
mediaSource = type.source, mediaSource = type.source,
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
waveform = type.details?.waveform ?: persistentListOf(),
) )
} }
} }

View file

@ -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.api.timeline.Timeline
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaInfo
import kotlinx.collections.immutable.ImmutableList
sealed interface MediaItem { sealed interface MediaItem {
data class DateSeparator( data class DateSeparator(
@ -46,7 +45,6 @@ sealed interface MediaItem {
val mediaInfo: MediaInfo, val mediaInfo: MediaInfo,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val duration: String?,
) : Event { ) : Event {
val thumbnailMediaRequestData: MediaRequestData val thumbnailMediaRequestData: MediaRequestData
get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100)) get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
@ -64,8 +62,6 @@ sealed interface MediaItem {
val eventId: EventId?, val eventId: EventId?,
val mediaInfo: MediaInfo, val mediaInfo: MediaInfo,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val duration: String?,
val waveform: ImmutableList<Float>,
) : Event ) : Event
data class File( data class File(

View file

@ -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.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
class SingleMediaGalleryDataSource( class SingleMediaGalleryDataSource(
@ -32,77 +31,54 @@ class SingleMediaGalleryDataSource(
fun createFrom(params: MediaViewerEntryPoint.Params) = SingleMediaGalleryDataSource( fun createFrom(params: MediaViewerEntryPoint.Params) = SingleMediaGalleryDataSource(
data = when { data = when {
params.mediaInfo.mimeType.isMimeTypeImage() -> { params.mediaInfo.mimeType.isMimeTypeImage() -> {
GroupedMediaItems( MediaItem.Image(
imageAndVideoItems = persistentListOf( id = UniqueId("dummy"),
MediaItem.Image( eventId = params.eventId,
id = UniqueId("dummy"), mediaInfo = params.mediaInfo,
eventId = params.eventId, mediaSource = params.mediaSource,
mediaInfo = params.mediaInfo, thumbnailSource = params.thumbnailSource,
mediaSource = params.mediaSource,
thumbnailSource = params.thumbnailSource,
)
),
fileItems = persistentListOf(),
) )
} }
params.mediaInfo.mimeType.isMimeTypeVideo() -> { params.mediaInfo.mimeType.isMimeTypeVideo() -> {
GroupedMediaItems( MediaItem.Video(
imageAndVideoItems = persistentListOf( id = UniqueId("dummy"),
MediaItem.Video( eventId = params.eventId,
id = UniqueId("dummy"), mediaInfo = params.mediaInfo,
eventId = params.eventId, mediaSource = params.mediaSource,
mediaInfo = params.mediaInfo, thumbnailSource = params.thumbnailSource,
mediaSource = params.mediaSource,
thumbnailSource = params.thumbnailSource,
duration = "TODO", // TODO Duration
)
),
fileItems = persistentListOf(),
) )
} }
params.mediaInfo.mimeType.isMimeTypeAudio() -> { params.mediaInfo.mimeType.isMimeTypeAudio() -> {
if (params.mediaInfo.waveform == null) { if (params.mediaInfo.waveform == null) {
GroupedMediaItems( MediaItem.Audio(
imageAndVideoItems = persistentListOf( id = UniqueId("dummy"),
MediaItem.Audio( eventId = params.eventId,
id = UniqueId("dummy"), mediaInfo = params.mediaInfo,
eventId = params.eventId, mediaSource = params.mediaSource,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
)
),
fileItems = persistentListOf(),
) )
} else { } else {
GroupedMediaItems( MediaItem.Voice(
imageAndVideoItems = persistentListOf( id = UniqueId("dummy"),
MediaItem.Voice( eventId = params.eventId,
id = UniqueId("dummy"), mediaInfo = params.mediaInfo,
eventId = params.eventId, mediaSource = params.mediaSource,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
duration = "TODO", // TODO Duration
waveform = params.mediaInfo.waveform.orEmpty().toImmutableList(),
)
),
fileItems = persistentListOf(),
) )
} }
} }
else -> { else -> {
// Always use imageAndVideoItems, in Single mode, this is the data that will be used MediaItem.File(
GroupedMediaItems( id = UniqueId("dummy"),
imageAndVideoItems = persistentListOf( eventId = params.eventId,
MediaItem.File( mediaInfo = params.mediaInfo,
id = UniqueId("dummy"), mediaSource = params.mediaSource,
eventId = params.eventId,
mediaInfo = params.mediaInfo,
mediaSource = params.mediaSource,
)
),
fileItems = persistentListOf(),
) )
} }
}.let { mediaItem ->
GroupedMediaItems(
// Always use imageAndVideoItems, in Single mode, this is the data that will be used
imageAndVideoItems = persistentListOf(mediaItem),
fileItems = persistentListOf(),
)
} }
) )
} }

View file

@ -31,9 +31,10 @@ fun aMediaItemVideo(
return MediaItem.Video( return MediaItem.Video(
id = id, id = id,
eventId = null, eventId = null,
mediaInfo = aVideoMediaInfo(), mediaInfo = aVideoMediaInfo(
duration = duration
),
mediaSource = mediaSource, mediaSource = mediaSource,
thumbnailSource = null, thumbnailSource = null,
duration = duration,
) )
} }

View file

@ -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.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
import kotlinx.collections.immutable.toImmutableList
class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> { class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> {
override val values: Sequence<MediaItem.Voice> override val values: Sequence<MediaItem.Voice>
@ -46,9 +45,9 @@ fun aMediaItemVoice(
mediaInfo = aVoiceMediaInfo( mediaInfo = aVoiceMediaInfo(
filename = filename, filename = filename,
caption = caption, caption = caption,
duration = duration,
waveForm = waveform,
), ),
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
duration = duration,
waveform = waveform.toImmutableList(),
) )
} }

View file

@ -101,10 +101,10 @@ private fun VideoInfoRow(
imageVector = CompoundIcons.VideoCallSolid(), imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null contentDescription = null
) )
if (video.duration != null) { video.mediaInfo.duration?.let { duration ->
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Text( Text(
text = video.duration, text = duration,
style = ElementTheme.typography.fontBodySmMedium, style = ElementTheme.typography.fontBodySmMedium,
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
) )

View file

@ -115,7 +115,7 @@ private fun VoiceInfoRow(
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
text = if (state.progress > 0f) state.time else voice.duration ?: state.time, text = if (state.progress > 0f) state.time else voice.mediaInfo.duration ?: state.time,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyMdMedium, style = ElementTheme.typography.fontBodyMdMedium,
maxLines = 1, maxLines = 1,
@ -128,7 +128,7 @@ private fun VoiceInfoRow(
.height(34.dp), .height(34.dp),
showCursor = state.showCursor, showCursor = state.showCursor,
playbackProgress = state.progress, playbackProgress = state.progress,
waveform = voice.waveform.toPersistentList(), waveform = voice.mediaInfo.waveform.orEmpty().toPersistentList(),
onSeek = { onSeek = {
state.eventSink(VoiceMessageEvents.Seek(it)) state.eventSink(VoiceMessageEvents.Seek(it))
}, },

View file

@ -48,6 +48,7 @@ class AndroidLocalMediaFactory @Inject constructor(
dateSent = mediaInfo.dateSent, dateSent = mediaInfo.dateSent,
dateSentFull = mediaInfo.dateSentFull, dateSentFull = mediaInfo.dateSentFull,
waveform = mediaInfo.waveform, waveform = mediaInfo.waveform,
duration = mediaInfo.duration,
) )
override fun createFromUri( override fun createFromUri(
@ -67,6 +68,7 @@ class AndroidLocalMediaFactory @Inject constructor(
dateSent = null, dateSent = null,
dateSentFull = null, dateSentFull = null,
waveform = null, waveform = null,
duration = null,
) )
private fun createFromUri( private fun createFromUri(
@ -81,6 +83,7 @@ class AndroidLocalMediaFactory @Inject constructor(
dateSent: String?, dateSent: String?,
dateSentFull: String?, dateSentFull: String?,
waveform: List<Float>?, waveform: List<Float>?,
duration: String?,
): LocalMedia { ): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: "" val fileName = name ?: context.getFileName(uri) ?: ""
@ -100,6 +103,7 @@ class AndroidLocalMediaFactory @Inject constructor(
dateSent = dateSent, dateSent = dateSent,
dateSentFull = dateSentFull, dateSentFull = dateSentFull,
waveform = waveform, waveform = waveform,
duration = duration,
) )
) )
} }