From 566b0c6f86fabde8e62536e0088fceaeda1393dc Mon Sep 17 00:00:00 2001 From: bxdxnn <267911624+bxdxnn@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:01:21 +0000 Subject: [PATCH 1/4] Add item count text as the placeholder for in reply to box for galleries --- .../ui/messages/reply/InReplyToMetadata.kt | 42 ++++ .../messages/reply/InReplyToMetadataKtTest.kt | 222 ++++++++++++++++++ .../src/main/res/values/temporary.xml | 6 + 3 files changed, 270 insertions(+) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt index 9e5e468cd9..2da5184389 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt @@ -10,12 +10,15 @@ package io.element.android.libraries.matrix.ui.messages.reply import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent @@ -35,6 +38,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.messages.reply.InReplyToMetadata.Text import io.element.android.libraries.matrix.ui.messages.reply.InReplyToMetadata.Thumbnail +import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonStrings @Immutable @@ -104,6 +108,44 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad type = AttachmentThumbnailType.Voice, ) ) + is GalleryMessageType -> { + val caption = textContent?.takeIf { it.isNotBlank() } + val isMediaGallery = type.items.all { it is GalleryItemType.Image || it is GalleryItemType.Video } + val countPlural = if (isMediaGallery) { + CommonPlurals.common_gallery_reply_media_items + } else { + CommonPlurals.common_gallery_reply_attachments + } + val text = caption ?: pluralStringResource(countPlural, type.items.size, type.items.size) + if (isMediaGallery) { + val firstMediaItem = type.items.firstOrNull { it is GalleryItemType.Image || it is GalleryItemType.Video } + val thumbnailSource = when (firstMediaItem) { + is GalleryItemType.Image -> (firstMediaItem.content.info?.thumbnailSource ?: firstMediaItem.content.source).takeUnless { hideImage } + is GalleryItemType.Video -> firstMediaItem.content.info?.thumbnailSource?.takeUnless { hideImage } + else -> null + } + val blurHash = when (firstMediaItem) { + is GalleryItemType.Image -> firstMediaItem.content.info?.blurhash + is GalleryItemType.Video -> firstMediaItem.content.info?.blurhash + else -> null + } + Thumbnail( + AttachmentThumbnailInfo( + thumbnailSource = thumbnailSource, + textContent = text, + type = AttachmentThumbnailType.Image, + blurHash = blurHash, + ) + ) + } else { + Thumbnail( + AttachmentThumbnailInfo( + textContent = text, + type = AttachmentThumbnailType.File, + ) + ) + } + } else -> Text(textContent ?: eventContent.body) } is StickerContent -> Thumbnail( diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt index 0847315867..5b6df661f3 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt @@ -24,6 +24,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType +import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherState @@ -407,6 +409,226 @@ class InReplyToMetadataKtTest : RobolectricTest() { } } + @Test + fun `a gallery message with all media items shows media count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "", + formatted = null, + items = listOf( + GalleryItemType.Image( + ImageMessageType( + filename = "image1.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + GalleryItemType.Image( + ImageMessageType( + filename = "image2.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + ), + ) + ), + textContent = "", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = aMediaSource(), + textContent = "2 media items…", + type = AttachmentThumbnailType.Image, + blurHash = A_BLUR_HASH, + ) + ) + ) + } + } + } + + @Test + fun `a gallery message with attachment items shows attachment count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "", + formatted = null, + items = listOf( + GalleryItemType.File( + FileMessageType( + filename = "doc1.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + GalleryItemType.File( + FileMessageType( + filename = "doc2.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + ), + ) + ), + textContent = "", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "2 attachments…", + type = AttachmentThumbnailType.File, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a gallery message with caption shows caption instead of count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "My vacation photos", + formatted = null, + items = listOf( + GalleryItemType.Image( + ImageMessageType( + filename = "image1.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + GalleryItemType.Image( + ImageMessageType( + filename = "image2.jpg", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = anImageInfo(), + ) + ), + ), + ) + ), + textContent = "My vacation photos", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = aMediaSource(), + textContent = "My vacation photos", + type = AttachmentThumbnailType.Image, + blurHash = A_BLUR_HASH, + ) + ) + ) + } + } + } + + @Test + fun `a gallery message with attachment items and caption shows caption instead of count`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + anInReplyToDetailsReady( + eventContent = aMessageContent( + messageType = GalleryMessageType( + body = "My documents", + formatted = null, + items = listOf( + GalleryItemType.File( + FileMessageType( + filename = "doc1.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + GalleryItemType.File( + FileMessageType( + filename = "doc2.pdf", + caption = null, + formattedCaption = null, + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = null, + ), + ) + ), + ), + ) + ), + textContent = "My documents", + ).metadata(hideImage = false) + } + }.test { + awaitItem().let { + assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "My documents", + type = AttachmentThumbnailType.File, + blurHash = null, + ) + ) + ) + } + } + } + @Test fun `a poll content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { diff --git a/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml index fd1edb0759..47d2007517 100644 --- a/libraries/ui-strings/src/main/res/values/temporary.xml +++ b/libraries/ui-strings/src/main/res/values/temporary.xml @@ -8,4 +8,10 @@ "Mark as read" "Jump to first unread message" + + "%1$d media items…" + + + "%1$d attachments…" + From 754eb3d0c17689b245d55f9f5311a07461d9b882 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2026 15:13:36 +0200 Subject: [PATCH 2/4] Improve code and use type Video when necessary --- .../ui/messages/reply/InReplyToMetadata.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt index 2da5184389..3e3f46ffb5 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt @@ -111,13 +111,12 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad is GalleryMessageType -> { val caption = textContent?.takeIf { it.isNotBlank() } val isMediaGallery = type.items.all { it is GalleryItemType.Image || it is GalleryItemType.Video } - val countPlural = if (isMediaGallery) { - CommonPlurals.common_gallery_reply_media_items - } else { - CommonPlurals.common_gallery_reply_attachments - } - val text = caption ?: pluralStringResource(countPlural, type.items.size, type.items.size) if (isMediaGallery) { + val text = caption ?: pluralStringResource( + CommonPlurals.common_gallery_reply_media_items, + type.items.size, + type.items.size, + ) val firstMediaItem = type.items.firstOrNull { it is GalleryItemType.Image || it is GalleryItemType.Video } val thumbnailSource = when (firstMediaItem) { is GalleryItemType.Image -> (firstMediaItem.content.info?.thumbnailSource ?: firstMediaItem.content.source).takeUnless { hideImage } @@ -129,15 +128,24 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad is GalleryItemType.Video -> firstMediaItem.content.info?.blurhash else -> null } + val type = when (firstMediaItem) { + is GalleryItemType.Video -> AttachmentThumbnailType.Video + else -> AttachmentThumbnailType.Image + } Thumbnail( AttachmentThumbnailInfo( thumbnailSource = thumbnailSource, textContent = text, - type = AttachmentThumbnailType.Image, + type = type, blurHash = blurHash, ) ) } else { + val text = caption ?: pluralStringResource( + CommonPlurals.common_gallery_reply_attachments, + type.items.size, + type.items.size, + ) Thumbnail( AttachmentThumbnailInfo( textContent = text, From 7f13bb95cc5227971049667748a8735ed339e8e3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2026 15:16:38 +0200 Subject: [PATCH 3/4] Ignore large test class. --- .../matrix/ui/messages/reply/InReplyToMetadataKtTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt index 5b6df661f3..3fcb2f8f18 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt @@ -54,6 +54,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.time.Duration.Companion.minutes +@Suppress("LargeClass") class InReplyToMetadataKtTest : RobolectricTest() { @Test fun `any message content`() = runTest { From 07ea371806e980eb3bbedf522f658f39a45b05bf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2026 15:24:14 +0200 Subject: [PATCH 4/4] Move strings to Localazy --- libraries/ui-strings/src/main/res/values/localazy.xml | 8 ++++++++ libraries/ui-strings/src/main/res/values/temporary.xml | 6 ------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 82acfd14b3..b032c41d4b 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -268,6 +268,14 @@ Reason: %1$s." "Forward message" "Frequently used" "Gallery" + + "%1$d attachment…" + "%1$d attachments…" + + + "%1$d media item…" + "%1$d media items…" + "GIF" "Group call in progress" "Image" diff --git a/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml index 47d2007517..fd1edb0759 100644 --- a/libraries/ui-strings/src/main/res/values/temporary.xml +++ b/libraries/ui-strings/src/main/res/values/temporary.xml @@ -8,10 +8,4 @@ "Mark as read" "Jump to first unread message" - - "%1$d media items…" - - - "%1$d attachments…" -