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…"
-