Merge pull request #3979 from element-hq/feature/bma/mediaCaption

MediaViewer: iterate on design
This commit is contained in:
Benoit Marty 2024-12-03 13:03:15 +01:00 committed by GitHub
commit b6a1700449
49 changed files with 429 additions and 227 deletions

View file

@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
@ -329,100 +330,93 @@ class MessagesFlowNode @AssistedInject constructor(
} }
private fun processEventClick(event: TimelineItem.Event): Boolean { private fun processEventClick(event: TimelineItem.Event): Boolean {
return when (event.content) { val navTarget = when (event.content) {
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
val navTarget = NavTarget.MediaViewer( buildMediaViewerNavTarget(
mediaInfo = MediaInfo( event = event,
filename = event.content.filename, content = event.content,
caption = event.content.caption,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
),
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
) )
overlay.show(navTarget)
true
} }
is TimelineItemStickerContent -> { is TimelineItemStickerContent -> {
/* Sticker may have an empty url and no thumbnail /* Sticker may have an empty url and no thumbnail
if encrypted on certain bridges */ if encrypted on certain bridges */
if (event.content.preferredMediaSource != null) { event.content.preferredMediaSource?.let { preferredMediaSource ->
val navTarget = NavTarget.MediaViewer( buildMediaViewerNavTarget(
mediaInfo = MediaInfo( event = event,
filename = event.content.filename, content = event.content,
caption = event.content.caption, mediaSource = preferredMediaSource,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
),
mediaSource = event.content.preferredMediaSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
) )
overlay.show(navTarget)
true
} else {
false
} }
} }
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
val navTarget = NavTarget.MediaViewer( buildMediaViewerNavTarget(
mediaInfo = MediaInfo( event = event,
filename = event.content.filename, content = event.content,
caption = event.content.caption, mediaSource = event.content.mediaSource,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
),
mediaSource = event.content.videoSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
) )
overlay.show(navTarget)
true
} }
is TimelineItemFileContent -> { is TimelineItemFileContent -> {
val navTarget = NavTarget.MediaViewer( buildMediaViewerNavTarget(
mediaInfo = MediaInfo( event = event,
filename = event.content.filename, content = event.content,
caption = event.content.caption, mediaSource = event.content.mediaSource,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
),
mediaSource = event.content.fileSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
) )
overlay.show(navTarget)
true
} }
is TimelineItemAudioContent -> { is TimelineItemAudioContent -> {
val navTarget = NavTarget.MediaViewer( buildMediaViewerNavTarget(
mediaInfo = MediaInfo( event = event,
filename = event.content.filename, content = event.content,
caption = event.content.caption,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
),
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = null, thumbnailSource = null,
) )
overlay.show(navTarget)
true
} }
is TimelineItemLocationContent -> { is TimelineItemLocationContent -> {
val navTarget = NavTarget.LocationViewer( NavTarget.LocationViewer(
location = event.content.location, location = event.content.location,
description = event.content.description, description = event.content.description,
) )
}
else -> null
}
return when (navTarget) {
is NavTarget.MediaViewer -> {
overlay.show(navTarget) overlay.show(navTarget)
true true
} }
is NavTarget.LocationViewer -> {
backstack.push(navTarget)
true
}
else -> false else -> false
} }
} }
private fun buildMediaViewerNavTarget(
event: TimelineItem.Event,
content: TimelineItemEventContentWithAttachment,
mediaSource: MediaSource,
thumbnailSource: MediaSource?,
): NavTarget {
return NavTarget.MediaViewer(
mediaInfo = MediaInfo(
filename = content.filename,
caption = content.caption,
mimeType = content.mimeType,
formattedFileSize = content.formattedFileSize,
fileExtension = content.fileExtension,
senderName = event.safeSenderName,
dateSent = event.sentTime,
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,
)
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
mentionSpanTheme.updateStyles(currentUserId = room.sessionId) mentionSpanTheme.updateStyles(currentUserId = room.sessionId)

View file

@ -11,12 +11,14 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -81,8 +83,9 @@ fun AttachmentsPreviewView(
title = {}, title = {},
) )
} }
) { ) { paddingValues ->
AttachmentPreviewContent( AttachmentPreviewContent(
modifier = Modifier.padding(paddingValues),
state = state, state = state,
localMediaRenderer = localMediaRenderer, localMediaRenderer = localMediaRenderer,
onSendClick = ::postSendAttachment, onSendClick = ::postSendAttachment,
@ -134,14 +137,16 @@ private fun AttachmentPreviewContent(
state: AttachmentsPreviewState, state: AttachmentsPreviewState,
localMediaRenderer: LocalMediaRenderer, localMediaRenderer: LocalMediaRenderer,
onSendClick: () -> Unit, onSendClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Box( Column(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.navigationBarsPadding(), .navigationBarsPadding(),
) { ) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.weight(1f),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (val attachment = state.attachment) { when (val attachment = state.attachment) {
@ -157,7 +162,6 @@ private fun AttachmentPreviewContent(
.fillMaxWidth() .fillMaxWidth()
.background(ElementTheme.colors.bgCanvasDefault) .background(ElementTheme.colors.bgCanvasDefault)
.height(IntrinsicSize.Min) .height(IntrinsicSize.Min)
.align(Alignment.BottomCenter)
.imePadding(), .imePadding(),
) )
} }

View file

@ -147,7 +147,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
isEdited = content.isEdited, isEdited = content.isEdited,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source, mediaSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
width = messageType.info?.width?.toInt(), width = messageType.info?.width?.toInt(),
height = messageType.info?.height?.toInt(), height = messageType.info?.height?.toInt(),
@ -186,6 +186,8 @@ class TimelineItemContentMessageFactory @Inject constructor(
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(), waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(),
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
) )
} }
false -> { false -> {
@ -211,7 +213,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(), formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
isEdited = content.isEdited, isEdited = content.isEdited,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source, mediaSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension), mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtension fileExtension = fileExtension

View file

@ -17,10 +17,10 @@ data class TimelineItemAudioContent(
override val formattedCaption: CharSequence?, override val formattedCaption: CharSequence?,
override val isEdited: Boolean, override val isEdited: Boolean,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, override val mediaSource: MediaSource,
val mimeType: String, override val mimeType: String,
val formattedFileSize: String, override val formattedFileSize: String,
val fileExtension: String, override val fileExtension: String,
) : TimelineItemEventContentWithAttachment { ) : TimelineItemEventContentWithAttachment {
val fileExtensionAndSize = val fileExtensionAndSize =
formatFileExtensionAndSize( formatFileExtensionAndSize(

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.timeline.model.event 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
@Immutable @Immutable
sealed interface TimelineItemEventContent { sealed interface TimelineItemEventContent {
@ -26,6 +27,10 @@ sealed interface TimelineItemEventContentWithAttachment :
val filename: String val filename: String
val caption: String? val caption: String?
val formattedCaption: CharSequence? val formattedCaption: CharSequence?
val mediaSource: MediaSource
val mimeType: String
val formattedFileSize: String
val fileExtension: String
val bestDescription: String val bestDescription: String
get() = caption ?: filename get() = caption ?: filename

View file

@ -15,11 +15,11 @@ data class TimelineItemFileContent(
override val caption: String?, override val caption: String?,
override val formattedCaption: CharSequence?, override val formattedCaption: CharSequence?,
override val isEdited: Boolean, override val isEdited: Boolean,
val fileSource: MediaSource, override val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, override val formattedFileSize: String,
val fileExtension: String, override val fileExtension: String,
val mimeType: String, override val mimeType: String,
) : TimelineItemEventContentWithAttachment { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemFileContent" override val type: String = "TimelineItemFileContent"

View file

@ -31,7 +31,7 @@ fun aTimelineItemFileContent(
formattedCaption = null, formattedCaption = null,
isEdited = false, isEdited = false,
thumbnailSource = null, thumbnailSource = null,
fileSource = MediaSource(url = ""), mediaSource = MediaSource(url = ""),
mimeType = MimeTypes.Pdf, mimeType = MimeTypes.Pdf,
formattedFileSize = "100kB", formattedFileSize = "100kB",
fileExtension = "pdf" fileExtension = "pdf"

View file

@ -18,11 +18,11 @@ data class TimelineItemImageContent(
override val caption: String?, override val caption: String?,
override val formattedCaption: CharSequence?, override val formattedCaption: CharSequence?,
override val isEdited: Boolean, override val isEdited: Boolean,
val mediaSource: MediaSource, override val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, override val formattedFileSize: String,
val fileExtension: String, override val fileExtension: String,
val mimeType: String, override val mimeType: String,
val blurhash: String?, val blurhash: String?,
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,

View file

@ -14,11 +14,11 @@ data class TimelineItemStickerContent(
override val caption: String?, override val caption: String?,
override val formattedCaption: CharSequence?, override val formattedCaption: CharSequence?,
override val isEdited: Boolean, override val isEdited: Boolean,
val mediaSource: MediaSource, override val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, override val formattedFileSize: String,
val fileExtension: String, override val fileExtension: String,
val mimeType: String, override val mimeType: String,
val blurhash: String?, val blurhash: String?,
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,

View file

@ -16,7 +16,7 @@ data class TimelineItemVideoContent(
override val formattedCaption: CharSequence?, override val formattedCaption: CharSequence?,
override val isEdited: Boolean, override val isEdited: Boolean,
val duration: Duration, val duration: Duration,
val videoSource: MediaSource, override val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val aspectRatio: Float?, val aspectRatio: Float?,
val blurHash: String?, val blurHash: String?,
@ -24,9 +24,9 @@ data class TimelineItemVideoContent(
val width: Int?, val width: Int?,
val thumbnailWidth: Int?, val thumbnailWidth: Int?,
val thumbnailHeight: Int?, val thumbnailHeight: Int?,
val mimeType: String, override val mimeType: String,
val formattedFileSize: String, override val formattedFileSize: String,
val fileExtension: String, override val fileExtension: String,
) : TimelineItemEventContentWithAttachment { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemImageContent" override val type: String = "TimelineItemImageContent"

View file

@ -35,7 +35,7 @@ fun aTimelineItemVideoContent(
blurHash = blurhash, blurHash = blurhash,
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
duration = 100.milliseconds, duration = 100.milliseconds,
videoSource = MediaSource(""), mediaSource = MediaSource(""),
width = 150, width = 150,
height = 300, height = 300,
thumbnailWidth = 150, thumbnailWidth = 150,

View file

@ -19,8 +19,10 @@ data class TimelineItemVoiceContent(
override val formattedCaption: CharSequence?, override val formattedCaption: CharSequence?,
override val isEdited: Boolean, override val isEdited: Boolean,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, override val mediaSource: MediaSource,
val mimeType: String, override val formattedFileSize: String,
override val fileExtension: String,
override val mimeType: String,
val waveform: ImmutableList<Float>, val waveform: ImmutableList<Float>,
) : TimelineItemEventContentWithAttachment { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemAudioContent" override val type: String = "TimelineItemAudioContent"

View file

@ -53,4 +53,6 @@ fun aTimelineItemVoiceContent(
mediaSource = mediaSource, mediaSource = mediaSource,
mimeType = mimeType, mimeType = mimeType,
waveform = waveform.toPersistentList(), waveform = waveform.toPersistentList(),
formattedFileSize = "1.0 MB",
fileExtension = "ogg",
) )

View file

@ -371,7 +371,7 @@ class MessagesPresenterTest {
formattedCaption = null, formattedCaption = null,
isEdited = false, isEdited = false,
duration = 10.milliseconds, duration = 10.milliseconds,
videoSource = MediaSource(AN_AVATAR_URL), mediaSource = MediaSource(AN_AVATAR_URL),
thumbnailSource = MediaSource(AN_AVATAR_URL), thumbnailSource = MediaSource(AN_AVATAR_URL),
mimeType = MimeTypes.Mp4, mimeType = MimeTypes.Mp4,
blurHash = null, blurHash = null,
@ -413,7 +413,7 @@ class MessagesPresenterTest {
caption = null, caption = null,
isEdited = false, isEdited = false,
formattedCaption = null, formattedCaption = null,
fileSource = MediaSource(AN_AVATAR_URL), mediaSource = MediaSource(AN_AVATAR_URL),
thumbnailSource = MediaSource(AN_AVATAR_URL), thumbnailSource = MediaSource(AN_AVATAR_URL),
formattedFileSize = "10 MB", formattedFileSize = "10 MB",
mimeType = MimeTypes.Pdf, mimeType = MimeTypes.Pdf,

View file

@ -239,7 +239,7 @@ class TimelineItemContentMessageFactoryTest {
formattedCaption = null, formattedCaption = null,
isEdited = false, isEdited = false,
duration = Duration.ZERO, duration = Duration.ZERO,
videoSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = null, thumbnailSource = null,
aspectRatio = null, aspectRatio = null,
blurHash = null, blurHash = null,
@ -291,7 +291,7 @@ class TimelineItemContentMessageFactoryTest {
formattedCaption = SpannedString("formatted"), formattedCaption = SpannedString("formatted"),
isEdited = true, isEdited = true,
duration = 1.minutes, duration = 1.minutes,
videoSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"), thumbnailSource = MediaSource("url_thumbnail"),
aspectRatio = 3f, aspectRatio = 3f,
blurHash = A_BLUR_HASH, blurHash = A_BLUR_HASH,
@ -380,7 +380,9 @@ class TimelineItemContentMessageFactoryTest {
duration = Duration.ZERO, duration = Duration.ZERO,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.OctetStream, mimeType = MimeTypes.OctetStream,
waveform = emptyList<Float>().toImmutableList() waveform = emptyList<Float>().toImmutableList(),
fileExtension = "",
formattedFileSize = "0 Bytes",
) )
assertThat(result).isEqualTo(expected) assertThat(result).isEqualTo(expected)
} }
@ -419,7 +421,9 @@ class TimelineItemContentMessageFactoryTest {
duration = 1.minutes, duration = 1.minutes,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.Ogg, mimeType = MimeTypes.Ogg,
waveform = persistentListOf(1f, 2f) waveform = persistentListOf(1f, 2f),
fileExtension = "ogg",
formattedFileSize = "123 Bytes",
) )
assertThat(result).isEqualTo(expected) assertThat(result).isEqualTo(expected)
} }
@ -571,7 +575,7 @@ class TimelineItemContentMessageFactoryTest {
caption = null, caption = null,
formattedCaption = null, formattedCaption = null,
isEdited = false, isEdited = false,
fileSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = null, thumbnailSource = null,
formattedFileSize = "0 Bytes", formattedFileSize = "0 Bytes",
fileExtension = "", fileExtension = "",
@ -612,7 +616,7 @@ class TimelineItemContentMessageFactoryTest {
caption = null, caption = null,
formattedCaption = null, formattedCaption = null,
isEdited = true, isEdited = true,
fileSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"), thumbnailSource = MediaSource("url_thumbnail"),
formattedFileSize = "123 Bytes", formattedFileSize = "123 Bytes",
fileExtension = "pdf", fileExtension = "pdf",

View file

@ -18,44 +18,73 @@ data class MediaInfo(
val mimeType: String, val mimeType: String,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
val senderName: String?,
val dateSent: String?,
) : Parcelable ) : Parcelable
fun anImageMediaInfo(): MediaInfo = MediaInfo( fun anImageMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "an image file.jpg", filename = "an image file.jpg",
caption = null, caption = caption,
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB", formattedFileSize = "4MB",
fileExtension = "jpg", fileExtension = "jpg",
senderName = senderName,
dateSent = dateSent,
) )
fun aVideoMediaInfo(): MediaInfo = MediaInfo( fun aVideoMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4", filename = "a video file.mp4",
caption = null, caption = caption,
mimeType = MimeTypes.Mp4, mimeType = MimeTypes.Mp4,
formattedFileSize = "14MB", formattedFileSize = "14MB",
fileExtension = "mp4", fileExtension = "mp4",
senderName = senderName,
dateSent = dateSent,
) )
fun aPdfMediaInfo(): MediaInfo = MediaInfo( fun aPdfMediaInfo(
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "a pdf file.pdf", filename = "a pdf file.pdf",
caption = null, caption = null,
mimeType = MimeTypes.Pdf, mimeType = MimeTypes.Pdf,
formattedFileSize = "23MB", formattedFileSize = "23MB",
fileExtension = "pdf", fileExtension = "pdf",
senderName = senderName,
dateSent = dateSent,
) )
fun anApkMediaInfo(): MediaInfo = MediaInfo( fun anApkMediaInfo(
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "an apk file.apk", filename = "an apk file.apk",
caption = null, caption = null,
mimeType = MimeTypes.Apk, mimeType = MimeTypes.Apk,
formattedFileSize = "50MB", formattedFileSize = "50MB",
fileExtension = "apk", fileExtension = "apk",
senderName = senderName,
dateSent = dateSent,
) )
fun anAudioMediaInfo(): MediaInfo = MediaInfo( fun anAudioMediaInfo(
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "an audio file.mp3", filename = "an audio file.mp3",
caption = null, caption = null,
mimeType = MimeTypes.Mp3, mimeType = MimeTypes.Mp3,
formattedFileSize = "7MB", formattedFileSize = "7MB",
fileExtension = "mp3", fileExtension = "mp3",
senderName = senderName,
dateSent = dateSent,
) )

View file

@ -46,7 +46,9 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
caption = null, caption = null,
mimeType = mimeType, mimeType = mimeType,
formattedFileSize = "", formattedFileSize = "",
fileExtension = "" fileExtension = "",
senderName = null,
dateSent = null,
), ),
mediaSource = MediaSource(url = avatarUrl), mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null, thumbnailSource = null,

View file

@ -41,6 +41,8 @@ class AndroidLocalMediaFactory @Inject constructor(
name = mediaInfo.filename, name = mediaInfo.filename,
caption = mediaInfo.caption, caption = mediaInfo.caption,
formattedFileSize = mediaInfo.formattedFileSize, formattedFileSize = mediaInfo.formattedFileSize,
senderName = mediaInfo.senderName,
dateSent = mediaInfo.dateSent,
) )
override fun createFromUri( override fun createFromUri(
@ -54,6 +56,8 @@ class AndroidLocalMediaFactory @Inject constructor(
name = name, name = name,
caption = null, caption = null,
formattedFileSize = formattedFileSize, formattedFileSize = formattedFileSize,
senderName = null,
dateSent = null,
) )
private fun createFromUri( private fun createFromUri(
@ -61,7 +65,9 @@ class AndroidLocalMediaFactory @Inject constructor(
mimeType: String?, mimeType: String?,
name: String?, name: String?,
caption: String?, caption: String?,
formattedFileSize: String? formattedFileSize: String?,
senderName: String?,
dateSent: 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) ?: ""
@ -74,7 +80,9 @@ class AndroidLocalMediaFactory @Inject constructor(
filename = fileName, filename = fileName,
caption = caption, caption = caption,
formattedFileSize = fileSize, formattedFileSize = fileSize,
fileExtension = fileExtension fileExtension = fileExtension,
senderName = senderName,
dateSent = dateSent,
) )
) )
} }

View file

@ -29,6 +29,7 @@ class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer {
) )
LocalMediaView( LocalMediaView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomPaddingInPixels = 0,
localMedia = localMedia, localMedia = localMedia,
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
onClick = {} onClick = {}

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.mediaviewer.impl.local.video.MediaVideoView
@Composable @Composable
fun LocalMediaView( fun LocalMediaView(
localMedia: LocalMedia?, localMedia: LocalMedia?,
bottomPaddingInPixels: Int,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(), localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
@ -37,6 +38,7 @@ fun LocalMediaView(
) )
mimeType.isMimeTypeVideo() -> MediaVideoView( mimeType.isMimeTypeVideo() -> MediaVideoView(
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
bottomPaddingInPixels = bottomPaddingInPixels,
localMedia = localMedia, localMedia = localMedia,
modifier = modifier, modifier = modifier,
) )

View file

@ -11,10 +11,13 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
@ -22,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -63,8 +67,22 @@ fun MediaPlayerControllerView(
.widthIn(max = 480.dp), .widthIn(max = 480.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
IconButton( val bgColor = if (state.isPlaying) {
onClick = onTogglePlay, ElementTheme.colors.bgCanvasDefault
} else {
ElementTheme.colors.textPrimary
}
Box(
modifier = Modifier
.size(36.dp)
.background(
color = bgColor,
shape = CircleShape,
)
.clip(CircleShape)
.clickable { onTogglePlay() }
.padding(8.dp),
contentAlignment = Alignment.Center,
) { ) {
if (state.isPlaying) { if (state.isPlaying) {
Icon( Icon(
@ -75,7 +93,7 @@ fun MediaPlayerControllerView(
} else { } else {
Icon( Icon(
imageVector = CompoundIcons.PlaySolid(), imageVector = CompoundIcons.PlaySolid(),
tint = ElementTheme.colors.iconPrimary, tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(CommonStrings.a11y_play) contentDescription = stringResource(CommonStrings.a11y_play)
) )
} }

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -37,6 +38,7 @@ import androidx.media3.ui.PlayerView
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.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
@ -51,6 +53,7 @@ import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun MediaVideoView( fun MediaVideoView(
localMediaViewState: LocalMediaViewState, localMediaViewState: LocalMediaViewState,
bottomPaddingInPixels: Int,
localMedia: LocalMedia?, localMedia: LocalMedia?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -66,6 +69,7 @@ fun MediaVideoView(
} }
ExoPlayerMediaVideoView( ExoPlayerMediaVideoView(
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
bottomPaddingInPixels = bottomPaddingInPixels,
exoPlayer = exoPlayer, exoPlayer = exoPlayer,
localMedia = localMedia, localMedia = localMedia,
modifier = modifier, modifier = modifier,
@ -76,6 +80,7 @@ fun MediaVideoView(
@Composable @Composable
private fun ExoPlayerMediaVideoView( private fun ExoPlayerMediaVideoView(
localMediaViewState: LocalMediaViewState, localMediaViewState: LocalMediaViewState,
bottomPaddingInPixels: Int,
exoPlayer: ExoPlayer, exoPlayer: ExoPlayer,
localMedia: LocalMedia?, localMedia: LocalMedia?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -229,7 +234,8 @@ private fun ExoPlayerMediaVideoView(
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter)
.padding(bottom = bottomPaddingInPixels.toDp()),
) )
} }
@ -252,6 +258,7 @@ private fun ExoPlayerMediaVideoView(
internal fun MediaVideoViewPreview() = ElementPreview { internal fun MediaVideoViewPreview() = ElementPreview {
MediaVideoView( MediaVideoView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomPaddingInPixels = 0,
localMediaViewState = rememberLocalMediaViewState(), localMediaViewState = rememberLocalMediaViewState(),
localMedia = null, localMedia = null,
) )

View file

@ -24,52 +24,72 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
aMediaViewerState(), aMediaViewerState(),
aMediaViewerState(AsyncData.Loading()), aMediaViewerState(AsyncData.Loading()),
aMediaViewerState(AsyncData.Failure(IllegalStateException("error"))), aMediaViewerState(AsyncData.Failure(IllegalStateException("error"))),
anImageMediaInfo(
senderName = "Sally Sanderson",
dateSent = "21 NOV, 2024",
caption = "A caption",
).let {
aMediaViewerState( aMediaViewerState(
AsyncData.Success( AsyncData.Success(
LocalMedia(Uri.EMPTY, anImageMediaInfo()) LocalMedia(Uri.EMPTY, it)
),
anImageMediaInfo(),
), ),
it,
)
},
aVideoMediaInfo(
senderName = "Sally Sanderson",
dateSent = "21 NOV, 2024",
caption = "A caption",
).let {
aMediaViewerState( aMediaViewerState(
AsyncData.Success( AsyncData.Success(
LocalMedia(Uri.EMPTY, aVideoMediaInfo()) LocalMedia(Uri.EMPTY, it)
),
aVideoMediaInfo(),
), ),
it,
)
},
aPdfMediaInfo().let {
aMediaViewerState( aMediaViewerState(
AsyncData.Success( AsyncData.Success(
LocalMedia(Uri.EMPTY, aPdfMediaInfo()) LocalMedia(Uri.EMPTY, it)
),
aPdfMediaInfo(),
), ),
it,
)
},
aMediaViewerState( aMediaViewerState(
AsyncData.Loading(), AsyncData.Loading(),
anApkMediaInfo(), anApkMediaInfo(),
), ),
anApkMediaInfo().let {
aMediaViewerState( aMediaViewerState(
AsyncData.Success( AsyncData.Success(
LocalMedia(Uri.EMPTY, anApkMediaInfo()) LocalMedia(Uri.EMPTY, it)
),
anApkMediaInfo(),
), ),
it,
)
},
aMediaViewerState( aMediaViewerState(
AsyncData.Loading(), AsyncData.Loading(),
anAudioMediaInfo(), anAudioMediaInfo(),
), ),
anAudioMediaInfo().let {
aMediaViewerState( aMediaViewerState(
AsyncData.Success( AsyncData.Success(
LocalMedia(Uri.EMPTY, anAudioMediaInfo()) LocalMedia(Uri.EMPTY, it)
),
anAudioMediaInfo(),
), ),
it,
)
},
anImageMediaInfo().let {
aMediaViewerState( aMediaViewerState(
AsyncData.Success( AsyncData.Success(
LocalMedia(Uri.EMPTY, anImageMediaInfo()) LocalMedia(Uri.EMPTY, it)
), ),
anImageMediaInfo(), it,
canDownload = false, canDownload = false,
canShare = false, canShare = false,
), )
},
) )
} }

View file

@ -16,10 +16,14 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -28,6 +32,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
@ -36,21 +41,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
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.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
@ -80,6 +90,8 @@ fun MediaViewerView(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
var showOverlay by remember { mutableStateOf(true) } var showOverlay by remember { mutableStateOf(true) }
val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
BackHandler { onBackClick() } BackHandler { onBackClick() }
Scaffold( Scaffold(
modifier, modifier,
@ -88,6 +100,7 @@ fun MediaViewerView(
) { ) {
MediaViewerPage( MediaViewerPage(
showOverlay = showOverlay, showOverlay = showOverlay,
bottomPaddingInPixels = bottomPaddingInPixels,
state = state, state = state,
onDismiss = { onDismiss = {
onBackClick() onBackClick()
@ -97,14 +110,29 @@ fun MediaViewerView(
} }
) )
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
) {
MediaViewerTopBar( MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is AsyncData.Success, actionsEnabled = state.downloadedMedia is AsyncData.Success,
mimeType = state.mediaInfo.mimeType, senderName = state.mediaInfo.senderName,
dateSent = state.mediaInfo.dateSent,
onBackClick = onBackClick, onBackClick = onBackClick,
canDownload = state.canDownload,
canShare = state.canShare,
eventSink = state.eventSink eventSink = state.eventSink
) )
MediaViewerBottomBar(
modifier = Modifier.align(Alignment.BottomCenter),
actionsEnabled = state.downloadedMedia is AsyncData.Success,
canDownload = state.canDownload,
canShare = state.canShare,
mimeType = state.mediaInfo.mimeType,
caption = state.mediaInfo.caption,
onHeightChange = { bottomPaddingInPixels = it },
eventSink = state.eventSink
)
}
} }
} }
} }
@ -112,6 +140,7 @@ fun MediaViewerView(
@Composable @Composable
private fun MediaViewerPage( private fun MediaViewerPage(
showOverlay: Boolean, showOverlay: Boolean,
bottomPaddingInPixels: Int,
state: MediaViewerState, state: MediaViewerState,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onShowOverlayChange: (Boolean) -> Unit, onShowOverlayChange: (Boolean) -> Unit,
@ -168,6 +197,7 @@ private fun MediaViewerPage(
LocalMediaView( LocalMediaView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomPaddingInPixels = bottomPaddingInPixels,
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
localMedia = state.downloadedMedia.dataOrNull(), localMedia = state.downloadedMedia.dataOrNull(),
mediaInfo = state.mediaInfo, mediaInfo = state.mediaInfo,
@ -246,23 +276,100 @@ private fun rememberShowProgress(downloadedMedia: AsyncData<LocalMedia>): Boolea
return showProgress return showProgress
} }
@Suppress("UNUSED_PARAMETER")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MediaViewerTopBar( private fun MediaViewerTopBar(
actionsEnabled: Boolean, actionsEnabled: Boolean,
canDownload: Boolean, senderName: String?,
canShare: Boolean, dateSent: String?,
mimeType: String,
onBackClick: () -> Unit, onBackClick: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit, eventSink: (MediaViewerEvents) -> Unit,
) { ) {
TopAppBar( TopAppBar(
title = {}, title = {
if (senderName != null && dateSent != null) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(end = 48.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = senderName,
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
)
Text(
text = dateSent,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textPrimary,
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent.copy(0.6f), containerColor = Color.Transparent.copy(0.6f),
), ),
navigationIcon = { BackButton(onClick = onBackClick) }, navigationIcon = { BackButton(onClick = onBackClick) },
actions = { actions = {
// TODO Add action to open infos.
}
)
}
@Composable
private fun MediaViewerBottomBar(
actionsEnabled: Boolean,
canDownload: Boolean,
canShare: Boolean,
mimeType: String,
caption: String?,
onHeightChange: (Int) -> Unit,
eventSink: (MediaViewerEvents) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(Color(0x99101317))
.onSizeChanged {
onHeightChange(it.height)
},
) {
HorizontalDivider()
if (caption != null) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = caption,
maxLines = 5,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyLgRegular,
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (canShare) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.Share)
},
modifier = Modifier.align(Alignment.CenterVertically)
) {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
contentDescription = stringResource(id = CommonStrings.action_share)
)
}
}
Spacer(modifier = Modifier.weight(1f))
IconButton( IconButton(
enabled = actionsEnabled, enabled = actionsEnabled,
onClick = { onClick = {
@ -293,21 +400,8 @@ private fun MediaViewerTopBar(
) )
} }
} }
if (canShare) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.Share)
},
) {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
contentDescription = stringResource(id = CommonStrings.action_share)
)
} }
} }
}
)
} }
@Composable @Composable

View file

@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.media.FakeMediaFile import io.element.android.libraries.matrix.test.media.FakeMediaFile
import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
@ -25,7 +26,10 @@ class AndroidLocalMediaFactoryTest {
@Test @Test
fun `test AndroidLocalMediaFactory`() { fun `test AndroidLocalMediaFactory`() {
val sut = createAndroidLocalMediaFactory() val sut = createAndroidLocalMediaFactory()
val result = sut.createFromMediaFile(aMediaFile(), anImageMediaInfo()) val result = sut.createFromMediaFile(aMediaFile(), anImageMediaInfo(
senderName = A_USER_NAME,
dateSent = "12:34",
))
assertThat(result.uri.toString()).endsWith("aPath") assertThat(result.uri.toString()).endsWith("aPath")
assertThat(result.info).isEqualTo( assertThat(result.info).isEqualTo(
MediaInfo( MediaInfo(
@ -34,6 +38,8 @@ class AndroidLocalMediaFactoryTest {
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB", formattedFileSize = "4MB",
fileExtension = "jpg", fileExtension = "jpg",
senderName = A_USER_NAME,
dateSent = "12:34"
) )
) )
} }

View file

@ -36,7 +36,9 @@ class FakeLocalMediaFactory(
caption = null, caption = null,
mimeType = mimeType ?: fallbackMimeType, mimeType = mimeType ?: fallbackMimeType,
formattedFileSize = formattedFileSize ?: fallbackFileSize, formattedFileSize = formattedFileSize ?: fallbackFileSize,
fileExtension = fileExtensionExtractor.extractFromName(safeName) fileExtension = fileExtensionExtractor.extractFromName(safeName),
senderName = null,
dateSent = null
) )
return aLocalMedia(uri, mediaInfo) return aLocalMedia(uri, mediaInfo)
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7839db8763b29e38ad67380e09101e48fd3a650080967fb5e51a8bf6411d0427 oid sha256:17aa655ef22beec5cef50f5d44cb2ecd8ad3a1979bc072a33079a49a8c4495c5
size 397423 size 397284

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2c40163401ccd77a79d314bd373bb09d9b9bd0bd8d0afe281792c19d6fd593d3 oid sha256:8a0e722772be6b9d38c5f0ef3198502ca6e6ebb2599e3a4ebb4cc95cde907095
size 51465 size 52012

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1cba25287c6d2beee594937c3139b3800630379e1b5a552c5cca886f8646bbfe oid sha256:aa4cae0da657162117538b6839032ebf467542a043f18e308eeaf5ae28ca14e8
size 51429 size 51984

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b4f33aa3545c29c25b3c40e6de90772e4e1f0685095429bdbcc4216e9020161c oid sha256:df6fc9bcb3636549fdfeacb4c9e19cc3c699b7519558bd14f409e7b0c6a84d80
size 89038 size 89846

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:507c7dffae1aabfa687174f1f964e2c40b004183b6bc3a70b56d764e0d308b47 oid sha256:aedd5d3a01457b8ed06b16cd63d08f24ae34568e2b912b82fb892b154c94e6df
size 392923 size 392780

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7839db8763b29e38ad67380e09101e48fd3a650080967fb5e51a8bf6411d0427 oid sha256:17aa655ef22beec5cef50f5d44cb2ecd8ad3a1979bc072a33079a49a8c4495c5
size 397423 size 397284

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e7a7ea1da7e7602cc53cc162c4a685516a7a46ca148eafca6e19a5748630bda5 oid sha256:333a21a41a7d5f8d47946648a7381dadeccf213a1176696b3512c08ae929d4d6
size 7036 size 7819

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bc35254c6962b5b113a2c25c4c9dd0c94449521152ffd73f41a0c36429fabbb0 oid sha256:cba8f49caf65856569266a561206437840a43da2d41ed5f08321ecda99204329
size 7258 size 8236

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9f69e358ddd7e00f7301598f5dd235a2f5b9077f8926c2bfa3a4bca1168900f0 oid sha256:773785bb234c0719c0f27ee91a970c63d7f971518dc0c83298a4b6adb9acc4cf
size 7173 size 8066

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b096093e04eb187fce0a85496c8a5b251afe0d0bccd977abcfb4d1e3dbe32a20 oid sha256:5424e7ce4a5259aab297a7911741352f0fba01e5fdac972768441db9edf41171
size 7436 size 7675

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3b7160dc01dd4704bb07a8c91858bef0d4e99c0f55ca1d7dbdc3e22546a25125 oid sha256:267d4be8b727a0ecb5af1e5f1e69adfd68f50f32f1de78f4f9fde60f635244b5
size 12210 size 13045

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2591b742c64d3d058d4638e5014080f8b3eb13eba3a73951d71dfdc60e7676d9 oid sha256:412476e819f688b5314651fd3d59439fc0e4f53ee777e3a070b82fd19d51700e
size 12455 size 13272

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a6fe771fa6492768239d0caf7a9c10613efb95ff9c2c1b786ee0a6974392af9f oid sha256:9df1a0ff2b3aaab2d1a346cb134b560d65c462286575ffc6124099529562eac1
size 389635 size 389594

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2b286342ff4d46637beac1f980294f77b3e2eb6824d56448cdbdce7b41c911ab oid sha256:4fde3cef34d23c894ae480a1b4c961065f244d845584f8607c4bb21e0a7e5f10
size 388612 size 388615

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7b023a77242de9cab647e87b18edcd86a48a7843aa8fa51c84b443e3e8b41bcb oid sha256:cf07ed00618c0552205b1e726050382369041bb7f2d9742b754a5652eca48265
size 389669 size 389631

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:75ff0cd0cfe594a217d2b58b48c2bb4aedad692a270c2cee8aa3d4bdf6758e1e oid sha256:143285221dd5a1e714a9897293f13c45ca567635e60350738033b315f44f742a
size 94797 size 94735

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0dc5fdbd7d0b3980ad19bf74640067e8bb294def913e3d13d7bc4ad9c822382d oid sha256:a4e1b6a7a3dcc1627aec1767ac8edac95bec483ebdc98d71d65342084e08a4dc
size 389921 size 396792

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:05794effa4c47e3d2b68892e613f18a55c96789a4e65a182dd0bf2ca0c812d44 oid sha256:6c3c2b7b1d64387ec1109ab21304df50de5bd4fac0317c27a2799e7405da3843
size 14474 size 22219

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e6113747e1677c2f28c2a109bc2c7075a753917068c1bbc3a46d5bdef4db4e23 oid sha256:364fce9e921a21dcaae9ef1d7c98efe47c4c9118d23958550ee9eafaf202e2fa
size 5756 size 5751

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f5a6ec7984ceab5acf44431a9bff89c29c9ac27c95804d29b800124addd6e910 oid sha256:769a00d9e84cb2da43d790158f35e66bc5ed6538fdcb7f5e316f5de13d9bd9ad
size 14800 size 14768

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d03b94e2b502b9857b9b4c2527df86d1aa0a194abed794cbe601ec4172422dcc oid sha256:bf1804b21f3d383c94e28714746ac61987643c8ec0480d2850b19eb01991aa4e
size 14987 size 15043

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:56b9d11058f06732364668a5833f52fa4e62541fa8ee48f13996ae0a11e11100 oid sha256:ec58fa08f3c160e1b7d462b580f79b1c05a5644b9f807a79cfeebbeea417ed10
size 13609 size 13502

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:25c1e3cb51b13bb65b281180862ac8fb6faa1e8bd0ce26d8d0e07321c03c116c oid sha256:689081427ece8bc009266a50b9d2a80def581f51f00bf65b35d295b0ea7dcab3
size 13742 size 13736