Implement MSC2530 (#2570)
* Implement MSC2530 * Some layout improvements for images and videos with captions * Update screenshots * Replace `it` in several previews with `isMine` --------- Signed-off-by: Marco Antonio Alvarez <surakin@gmail.com> Co-authored-by: Marco Antonio Alvarez <surakin@gmail.com> Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
2777ba09a3
commit
6aa84d600e
234 changed files with 489 additions and 187 deletions
|
|
@ -615,9 +615,9 @@ private fun MessageEventBubbleContent(
|
|||
}
|
||||
|
||||
val timestampPosition = when (event.content) {
|
||||
is TimelineItemImageContent,
|
||||
is TimelineItemImageContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemVideoContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemStickerContent,
|
||||
is TimelineItemVideoContent,
|
||||
is TimelineItemLocationContent -> TimestampPosition.Overlay
|
||||
is TimelineItemPollContent -> TimestampPosition.Below
|
||||
else -> TimestampPosition.Default
|
||||
|
|
@ -723,10 +723,10 @@ private fun ReplyToContentText(metadata: InReplyToMetadata?) {
|
|||
@Composable
|
||||
internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach {
|
||||
sequenceOf(false, true).forEach { isMine ->
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
isMine = isMine,
|
||||
content = aTimelineItemTextContent().copy(
|
||||
body = "A long text which will be displayed on several lines and" +
|
||||
" hopefully can be manually adjusted to test different behaviors."
|
||||
|
|
@ -736,7 +736,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
|||
)
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
isMine = isMine,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
aspectRatio = 2.5f
|
||||
),
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
|||
),
|
||||
aMessageContent(
|
||||
body = "Video",
|
||||
type = VideoMessageType("Video", MediaSource("url"), null),
|
||||
type = VideoMessageType("Video", null, null, MediaSource("url"), null),
|
||||
),
|
||||
aMessageContent(
|
||||
body = "Audio",
|
||||
|
|
@ -113,7 +113,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
|||
),
|
||||
aMessageContent(
|
||||
body = "Image",
|
||||
type = ImageMessageType("Image", MediaSource("url"), null),
|
||||
type = ImageMessageType("Image", null, null, MediaSource("url"), null),
|
||||
),
|
||||
aMessageContent(
|
||||
body = "Sticker",
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private const val MIN_HEIGHT_IN_DP = 100
|
||||
private const val MAX_HEIGHT_IN_DP = 360
|
||||
const val MIN_HEIGHT_IN_DP = 100
|
||||
const val MAX_HEIGHT_IN_DP = 360
|
||||
private const val DEFAULT_ASPECT_RATIO = 1.33f
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ fun TimelineItemEventContentView(
|
|||
)
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
content = content,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier,
|
||||
)
|
||||
is TimelineItemStickerContent -> TimelineItemStickerView(
|
||||
|
|
@ -85,6 +86,7 @@ fun TimelineItemEventContentView(
|
|||
)
|
||||
is TimelineItemVideoContent -> TimelineItemVideoView(
|
||||
content = content,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemFileContent -> TimelineItemFileView(
|
||||
|
|
|
|||
|
|
@ -16,39 +16,134 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import android.text.SpannedString
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
|
||||
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
|
||||
@Composable
|
||||
fun TimelineItemImageView(
|
||||
content: TimelineItemImageContent,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val description = stringResource(CommonStrings.common_image)
|
||||
TimelineItemAspectRatioBox(
|
||||
aspectRatio = content.aspectRatio,
|
||||
Column(
|
||||
modifier = modifier.semantics { contentDescription = description },
|
||||
) {
|
||||
BlurHashAsyncImage(
|
||||
model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
blurHash = content.blurhash,
|
||||
)
|
||||
val containerModifier = if (content.showCaption) {
|
||||
Modifier
|
||||
.padding(top = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
TimelineItemAspectRatioBox(
|
||||
modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f),
|
||||
aspectRatio = content.aspectRatio,
|
||||
) {
|
||||
var isLoaded by remember { mutableStateOf(false) }
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.Center,
|
||||
contentDescription = description,
|
||||
onState = { isLoaded = it is AsyncImagePainter.State.Success },
|
||||
)
|
||||
}
|
||||
|
||||
if (content.showCaption) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} else {
|
||||
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular
|
||||
) {
|
||||
EditorStyledText(
|
||||
modifier = Modifier
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * content.aspectRatio!!, max = MAX_HEIGHT_IN_DP.dp * content.aspectRatio),
|
||||
text = caption,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
releaseOnDetach = false,
|
||||
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChanged = onContentLayoutChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = ElementPreview {
|
||||
TimelineItemImageView(content)
|
||||
TimelineItemImageView(content, {})
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach { isMine ->
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = isMine,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
filename = "image.jpg",
|
||||
body = "A long caption that may wrap into several lines",
|
||||
aspectRatio = 2.5f,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
|
||||
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||
import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
|
|
|
|||
|
|
@ -16,54 +16,124 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import android.text.SpannedString
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
||||
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
|
||||
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
|
||||
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
|
||||
@Composable
|
||||
fun TimelineItemVideoView(
|
||||
content: TimelineItemVideoContent,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val description = stringResource(CommonStrings.common_image)
|
||||
TimelineItemAspectRatioBox(
|
||||
aspectRatio = content.aspectRatio,
|
||||
modifier = modifier.semantics { contentDescription = description },
|
||||
contentAlignment = Alignment.Center,
|
||||
Column(
|
||||
modifier = modifier.semantics { contentDescription = description }
|
||||
) {
|
||||
BlurHashAsyncImage(
|
||||
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
blurHash = content.blurHash,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.roundedBackground(),
|
||||
val containerModifier = if (content.showCaption) {
|
||||
Modifier.padding(top = 6.dp).clip(RoundedCornerShape(6.dp))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
TimelineItemAspectRatioBox(
|
||||
modifier = containerModifier.blurHashBackground(content.blurHash, alpha = 0.9f),
|
||||
aspectRatio = content.aspectRatio,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_play),
|
||||
colorFilter = ColorFilter.tint(Color.White),
|
||||
var isLoaded by remember { mutableStateOf(false) }
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.Center,
|
||||
contentDescription = description,
|
||||
onState = { isLoaded = it is AsyncImagePainter.State.Success },
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.roundedBackground(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_play),
|
||||
colorFilter = ColorFilter.tint(Color.White),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (content.showCaption) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} else {
|
||||
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular,
|
||||
) {
|
||||
EditorStyledText(
|
||||
modifier = Modifier
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * content.aspectRatio!!, max = MAX_HEIGHT_IN_DP.dp * content.aspectRatio),
|
||||
text = caption,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
releaseOnDetach = false,
|
||||
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChanged = onContentLayoutChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -71,5 +141,25 @@ fun TimelineItemVideoView(
|
|||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreview {
|
||||
TimelineItemVideoView(content)
|
||||
TimelineItemVideoView(content, {})
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach { isMine ->
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = isMine,
|
||||
content = aTimelineItemVideoContent().copy(
|
||||
filename = "video.mp4",
|
||||
body = "A long caption that may wrap into several lines",
|
||||
aspectRatio = 2.5f,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
|||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemImageContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
formatted = messageType.formatted,
|
||||
filename = messageType.filename,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
|
|
@ -91,7 +93,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
|||
height = messageType.info?.height?.toInt(),
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty()
|
||||
)
|
||||
}
|
||||
is StickerMessageType -> {
|
||||
|
|
@ -132,6 +134,8 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
|||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemVideoContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
formatted = messageType.formatted,
|
||||
filename = messageType.filename,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
videoSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
|
|
@ -141,7 +145,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
|||
blurHash = messageType.info?.blurhash,
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty(),
|
||||
)
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ package io.element.android.features.messages.impl.timeline.model.event
|
|||
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
|
||||
data class TimelineItemImageContent(
|
||||
val body: String,
|
||||
val formatted: FormattedBody?,
|
||||
val filename: String?,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
|
|
@ -33,6 +36,9 @@ data class TimelineItemImageContent(
|
|||
) : TimelineItemEventContent {
|
||||
override val type: String = "TimelineItemImageContent"
|
||||
|
||||
val showCaption = filename != null && filename != body
|
||||
val caption = if (showCaption) body else ""
|
||||
|
||||
val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
|
||||
mediaSource
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
|
|||
|
||||
fun aTimelineItemImageContent() = TimelineItemImageContent(
|
||||
body = "a body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
mimeType = MimeTypes.IMAGE_JPEG,
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@
|
|||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class TimelineItemVideoContent(
|
||||
val body: String,
|
||||
val formatted: FormattedBody?,
|
||||
val filename: String?,
|
||||
val duration: Duration,
|
||||
val videoSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
|
@ -33,4 +36,7 @@ data class TimelineItemVideoContent(
|
|||
val fileExtension: String,
|
||||
) : TimelineItemEventContent {
|
||||
override val type: String = "TimelineItemImageContent"
|
||||
|
||||
val showCaption = filename != null && filename != body
|
||||
val caption = if (showCaption) body else ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineI
|
|||
|
||||
fun aTimelineItemVideoContent() = TimelineItemVideoContent(
|
||||
body = "Video.mp4",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
thumbnailSource = null,
|
||||
blurHash = A_BLUR_HASH,
|
||||
aspectRatio = 0.5f,
|
||||
|
|
|
|||
|
|
@ -270,6 +270,8 @@ class MessagesPresenterTest {
|
|||
val mediaMessage = aMessageEvent(
|
||||
content = TimelineItemImageContent(
|
||||
body = "image.jpg",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
mediaSource = MediaSource(AN_AVATAR_URL),
|
||||
thumbnailSource = null,
|
||||
mimeType = MimeTypes.Jpeg,
|
||||
|
|
@ -300,6 +302,8 @@ class MessagesPresenterTest {
|
|||
val mediaMessage = aMessageEvent(
|
||||
content = TimelineItemVideoContent(
|
||||
body = "video.mp4",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
duration = 10.milliseconds,
|
||||
videoSource = MediaSource(AN_AVATAR_URL),
|
||||
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
||||
|
|
|
|||
|
|
@ -227,12 +227,14 @@ class TimelineItemContentMessageFactoryTest {
|
|||
fun `test create VideoMessageType`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = VideoMessageType("body", MediaSource("url"), null)),
|
||||
content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVideoContent(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
duration = Duration.ZERO,
|
||||
videoSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = null,
|
||||
|
|
@ -253,7 +255,9 @@ class TimelineItemContentMessageFactoryTest {
|
|||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = VideoMessageType(
|
||||
body = "body.mp4",
|
||||
body = "body.mp4 caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.mp4",
|
||||
source = MediaSource("url"),
|
||||
info = VideoInfo(
|
||||
duration = 1.minutes,
|
||||
|
|
@ -276,7 +280,9 @@ class TimelineItemContentMessageFactoryTest {
|
|||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemVideoContent(
|
||||
body = "body.mp4",
|
||||
body = "body.mp4 caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.mp4",
|
||||
duration = 1.minutes,
|
||||
videoSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
|
|
@ -420,12 +426,14 @@ class TimelineItemContentMessageFactoryTest {
|
|||
fun `test create ImageMessageType`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(type = ImageMessageType("body", MediaSource("url"), null)),
|
||||
content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)),
|
||||
senderDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemImageContent(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = null,
|
||||
formattedFileSize = "0 Bytes",
|
||||
|
|
@ -470,7 +478,9 @@ class TimelineItemContentMessageFactoryTest {
|
|||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = ImageMessageType(
|
||||
body = "body.jpg",
|
||||
body = "body.jpg caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.jpg",
|
||||
source = MediaSource("url"),
|
||||
info = ImageInfo(
|
||||
height = 10L,
|
||||
|
|
@ -492,7 +502,9 @@ class TimelineItemContentMessageFactoryTest {
|
|||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemImageContent(
|
||||
body = "body.jpg",
|
||||
body = "body.jpg caption",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
filename = "body.jpg",
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
formattedFileSize = "888 Bytes",
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ class InReplyToMetadataKtTest {
|
|||
eventContent = aMessageContent(
|
||||
messageType = ImageMessageType(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
source = aMediaSource(),
|
||||
info = anImageInfo(),
|
||||
)
|
||||
|
|
@ -137,6 +139,8 @@ class InReplyToMetadataKtTest {
|
|||
eventContent = aMessageContent(
|
||||
messageType = VideoMessageType(
|
||||
body = "body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
source = aMediaSource(),
|
||||
info = aVideoInfo(),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue