diff --git a/changelog.d/2521.feature b/changelog.d/2521.feature new file mode 100644 index 00000000000..1abcf68fce7 --- /dev/null +++ b/changelog.d/2521.feature @@ -0,0 +1 @@ +Implement MSC2530 (Body field as media caption) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index ffd2925c149..89204fbe72c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -615,9 +615,9 @@ private fun MessageEventBubbleContent( } val timestampPosition = when (event.content) { - is TimelineItemImageContent, + is TimelineItemImageContent -> if (event.content.showCaption) TimestampPosition.Below else TimestampPosition.Overlay + is TimelineItemVideoContent -> if (event.content.showCaption) TimestampPosition.Below else TimestampPosition.Overlay is TimelineItemStickerContent, - is TimelineItemVideoContent, is TimelineItemLocationContent -> TimestampPosition.Overlay is TimelineItemPollContent -> TimestampPosition.Below else -> TimestampPosition.Default diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt index c5cf6dfecd5..20b9a01e3fd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt @@ -101,7 +101,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider ), 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 ), aMessageContent( body = "Image", - type = ImageMessageType("Image", MediaSource("url"), null), + type = ImageMessageType("Image", null, null, MediaSource("url"), null), ), aMessageContent( body = "Sticker", diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt index bf30669a25f..98257584b54 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt @@ -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 diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index d058df9996d..24f31c95157 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -16,19 +16,27 @@ package io.element.android.features.messages.impl.timeline.components.event +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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 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.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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( @@ -36,14 +44,30 @@ fun TimelineItemImageView( 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, - ) + TimelineItemAspectRatioBox( + aspectRatio = content.aspectRatio, + ) { + BlurHashAsyncImage( + model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), + blurHash = content.blurhash, + ) + } + + if (content.showCaption) { + Spacer(modifier = Modifier.height(8.dp)) + Box { + EditorStyledText( + modifier = Modifier + .widthIn(min = MIN_HEIGHT_IN_DP.dp * content.aspectRatio!!, max = MAX_HEIGHT_IN_DP.dp * content.aspectRatio), + text = content.caption, + style = ElementRichTextEditorStyle.textStyle(), + releaseOnDetach = false + ) + } + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index d0174465396..169806826a9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -18,6 +18,10 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable @@ -30,6 +34,7 @@ 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 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 @@ -37,7 +42,9 @@ 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.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( @@ -45,25 +52,41 @@ fun TimelineItemVideoView( 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(), + TimelineItemAspectRatioBox( + aspectRatio = content.aspectRatio, contentAlignment = Alignment.Center, ) { - Image( - Icons.Default.PlayArrow, - contentDescription = stringResource(id = CommonStrings.a11y_play), - colorFilter = ColorFilter.tint(Color.White), + BlurHashAsyncImage( + model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.File(content.body, content.mimeType)), + blurHash = content.blurHash, + contentScale = ContentScale.Crop, ) + 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)) + Box { + EditorStyledText( + modifier = Modifier + .widthIn(min = MIN_HEIGHT_IN_DP.dp * content.aspectRatio!!, max = MAX_HEIGHT_IN_DP.dp * content.aspectRatio), + text = content.caption, + style = ElementRichTextEditorStyle.textStyle(), + releaseOnDetach = false + ) + } } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index b53d7496133..f32277f092c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -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, @@ -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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index 342e0a336b1..b63705b6436 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -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 { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index dbb32bbab20..e24f84eb4ea 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -32,6 +32,8 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider { - ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) + ImageMessageType(type.content.body, type.content.formatted?.map(), type.content.filename, type.content.source.map(), type.content.info?.map()) } is RustMessageType.Notice -> { NoticeMessageType(type.content.body, type.content.formatted?.map()) @@ -110,7 +110,7 @@ class EventMessageMapper { EmoteMessageType(type.content.body, type.content.formatted?.map()) } is RustMessageType.Video -> { - VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) + VideoMessageType(type.content.body, type.content.formatted?.map(), type.content.filename, type.content.source.map(), type.content.info?.map()) } is RustMessageType.Location -> { LocationMessageType(type.content.body, type.content.geoUri, type.content.description) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt index f37990fd681..4e491a0def0 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt @@ -190,7 +190,7 @@ class NotifiableEventResolverTest { createNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, - messageType = VideoMessageType(body = "Video", MediaSource("url"), null) + messageType = VideoMessageType(body = "Video", null, null, MediaSource("url"), null) ) ) ) @@ -224,7 +224,7 @@ class NotifiableEventResolverTest { createNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, - messageType = ImageMessageType("Image", MediaSource("url"), null), + messageType = ImageMessageType("Image", null, null, MediaSource("url"), null), ) ) )