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.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
@@ -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
@@ -449,6 +451,7 @@ class MessagesFlowNode @AssistedInject constructor(
mode = DateFormatterMode.Full,
),
waveform = (content as? TimelineItemVoiceContent)?.waveform,
duration = content.duration()?.toHumanReadableDuration(),
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,

View File

@@ -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 {
@@ -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
}
}

View File

@@ -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.
@@ -38,3 +39,5 @@ fun Long.toHumanReadableDuration(): String {
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 dateSentFull: String?,
val waveform: List<Float>?,
val duration: String?,
) : Parcelable
fun anImageMediaInfo(
@@ -45,6 +46,7 @@ fun anImageMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)
fun aVideoMediaInfo(
@@ -52,6 +54,7 @@ fun aVideoMediaInfo(
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
duration: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4",
caption = caption,
@@ -64,6 +67,7 @@ fun aVideoMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = duration,
)
fun aPdfMediaInfo(
@@ -84,6 +88,7 @@ fun aPdfMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)
fun anApkMediaInfo(
@@ -103,6 +108,7 @@ fun anApkMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)
fun anAudioMediaInfo(
@@ -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,
@@ -124,6 +131,7 @@ fun anAudioMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = waveForm,
duration = duration,
)
fun aVoiceMediaInfo(
@@ -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,
@@ -145,4 +154,5 @@ fun aVoiceMediaInfo(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = waveForm,
duration = duration,
)

View File

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

View File

@@ -102,6 +102,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
)
@@ -120,6 +121,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
)
@@ -138,6 +140,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
thumbnailSource = null,
@@ -157,6 +160,7 @@ class EventItemFactory @Inject constructor(
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
),
mediaSource = type.source,
thumbnailSource = null,
@@ -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,
@@ -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(),
)
}
}

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.ui.media.MediaRequestData
import io.element.android.libraries.mediaviewer.api.MediaInfo
import kotlinx.collections.immutable.ImmutableList
sealed interface MediaItem {
data class DateSeparator(
@@ -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))
@@ -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(

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.mediaviewer.api.MediaViewerEntryPoint
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.flowOf
class SingleMediaGalleryDataSource(
@@ -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(),
)
}
)
}

View File

@@ -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,
)
}

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.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>
@@ -46,9 +45,9 @@ fun aMediaItemVoice(
mediaInfo = aVoiceMediaInfo(
filename = filename,
caption = caption,
duration = duration,
waveForm = waveform,
),
mediaSource = MediaSource(""),
duration = duration,
waveform = waveform.toImmutableList(),
)
}

View File

@@ -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,
)

View File

@@ -115,7 +115,7 @@ private fun VoiceInfoRow(
}
Spacer(Modifier.width(8.dp))
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,
style = ElementTheme.typography.fontBodyMdMedium,
maxLines = 1,
@@ -128,7 +128,7 @@ private fun VoiceInfoRow(
.height(34.dp),
showCursor = state.showCursor,
playbackProgress = state.progress,
waveform = voice.waveform.toPersistentList(),
waveform = voice.mediaInfo.waveform.orEmpty().toPersistentList(),
onSeek = {
state.eventSink(VoiceMessageEvents.Seek(it))
},

View File

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