Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
5f508bd
Support gallery messages
bxdxnn Apr 2, 2026
2ab6f96
Address review
bxdxnn May 26, 2026
38e20af
Merge branch 'develop' into feature/gallery-messages
bxdxnn Jun 1, 2026
c5f0e4b
Remove unused function
bxdxnn Jun 1, 2026
6813a0c
Fix indent
bxdxnn Jun 1, 2026
4715549
Add "Gallery" message prefix
bxdxnn Jun 1, 2026
bf5d36b
Support galleries for image editing
bxdxnn Jun 1, 2026
0f094a1
Fix tapping on a tile opens the last item
bxdxnn Jun 1, 2026
d9a8e13
Fix overflow count
bxdxnn Jun 1, 2026
ef41ca7
Add caption editing to gallery messages
bxdxnn Jun 1, 2026
d353cc1
Use the new gallery string for prefix
bxdxnn Jun 1, 2026
b0c1100
Lock sending galleries behind a feature flag
bxdxnn Jun 1, 2026
348cc98
Merge branch 'develop' into feature/gallery-messages
bxdxnn Jun 9, 2026
6370a7c
Merge branch 'develop' into feature/gallery-messages
bxdxnn Jun 11, 2026
25d0ce2
Fix detekts
bxdxnn Jun 15, 2026
3cac053
Fix
bxdxnn Jun 19, 2026
5d3a7f2
Merge branch 'develop' into feature/gallery-messages
bmarty Jun 23, 2026
6a1ee7d
Ensure image edition is saved when navigating
bmarty Jun 23, 2026
d8bcd7f
Fix sending media broken on edited gallery.
bmarty Jun 23, 2026
0daf2f0
Fix tests
bmarty Jun 23, 2026
510576a
Order imports
bmarty Jun 23, 2026
873e7c4
Remove unused parameters.
bmarty Jun 23, 2026
2d25170
Fix copyright header of new files.
bmarty Jun 23, 2026
7166882
Fix Konsist test
bmarty Jun 23, 2026
b866f0f
Extract new previews to a dedicated file.
bmarty Jun 23, 2026
a6f8230
Sync strings
bmarty Jun 23, 2026
4e4723a
Remove unused import
bmarty Jun 23, 2026
012933e
Update screenshots
ElementBot Jun 23, 2026
8f3d548
Trigger CI
bmarty Jun 24, 2026
82f86c2
Remove parameters with default value.
bmarty Jun 24, 2026
c53f72f
More cleanup
bmarty Jun 24, 2026
07af009
Restore sendAsFile behavior.
bmarty Jun 24, 2026
f524d2f
Improve Preview.
bmarty Jun 24, 2026
0f9a601
Improve Preview.
bmarty Jun 24, 2026
357686f
Improve Preview.
bmarty Jun 24, 2026
6b4ec9c
Fix gallery sending cancel and retry issue
bmarty Jun 24, 2026
ebf6022
Ensure any previous job is cancelled.
bmarty Jun 24, 2026
2d55292
Fix issue in summary message
bmarty Jun 24, 2026
a9a43ff
Gallery feature is disabled by default.
bmarty Jun 24, 2026
6f9d71d
Kotlin convention
bmarty Jun 24, 2026
4220ac3
Remove useless parenthesis
bmarty Jun 24, 2026
8270513
Update screenshots
ElementBot Jun 24, 2026
d8877c6
Fix test
bmarty Jun 25, 2026
8a97120
List -> ImmutableList
bmarty Jun 25, 2026
2994bba
Remove useless code.
bmarty Jun 25, 2026
fd5d067
Render formatted caption for attachment list.
bmarty Jun 25, 2026
d3fbfc3
Replace set of Booleans by an enum
bmarty Jun 25, 2026
c88fa5f
Remove unused model for individual caption in a gallery Event.
bmarty Jun 25, 2026
cd9a532
Fix tests
bmarty Jun 25, 2026
861e59d
Fix tests
bmarty Jun 25, 2026
c110ab8
Rework MediaViewer entry point.
bmarty Jun 25, 2026
0fcf523
Ensure gallery item can be click in the pinned message list
bmarty Jun 26, 2026
870d4b3
Improve code and fix separator color
bmarty Jun 26, 2026
dc31685
React on attachment item click
bmarty Jun 26, 2026
da83bb2
Improve code and support 0 items in gallery.
bmarty Jun 26, 2026
697a46d
Fix click on attachment item not rendering anything.
bmarty Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ import io.element.android.features.messages.impl.threads.list.ThreadsListNode
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.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.GalleryItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent
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.TimelineItemGalleryContent
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.TimelineItemVideoContent
Expand Down Expand Up @@ -86,6 +89,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
import io.element.android.libraries.matrix.ui.messages.RoomNamesCache
import io.element.android.libraries.mediaviewer.api.GalleryInfo
import io.element.android.libraries.mediaviewer.api.GalleryItemData
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater
Expand Down Expand Up @@ -153,7 +158,15 @@ class MessagesFlowNode(
) : NavTarget

@Parcelize
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment, val inReplyToEventId: EventId?) : NavTarget
data class GalleryViewer(
val eventId: EventId?,
val galleryInfo: GalleryInfo,
val canUseOverlay: Boolean,
val galleryItems: List<GalleryItemData> = emptyList(),
) : NavTarget

@Parcelize
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachments: ImmutableList<Attachment>, val inReplyToEventId: EventId?) : NavTarget

@Parcelize
data class LocationViewer(val mode: ShowLocationMode) : NavTarget
Expand Down Expand Up @@ -239,18 +252,34 @@ class MessagesFlowNode(
callback.navigateToRoomDetails()
}

override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
override fun handleEventClick(
timelineMode: Timeline.Mode,
event: TimelineItem.Event,
canUseOverlay: Boolean,
): Boolean {
return processEventClick(
timelineMode = timelineMode,
event = event,
canUseOverlay = canUseOverlay,
)
}

override fun handleGalleryItemClick(
event: TimelineItem.Event,
galleryItemIndex: Int,
canUseOverlay: Boolean,
): Boolean {
return processGalleryEventClick(
event = event,
canUseOverlay = canUseOverlay,
galleryItemIndex = galleryItemIndex,
)
}

override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
backstack.push(
NavTarget.AttachmentPreview(
attachment = attachments.first(),
attachments = attachments,
timelineMode = Timeline.Mode.Live,
inReplyToEventId = inReplyToEventId,
)
Expand Down Expand Up @@ -339,13 +368,43 @@ class MessagesFlowNode(
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
}
is NavTarget.MediaViewer -> {
val params = MediaViewerEntryPoint.Params(
val params = MediaViewerEntryPoint.Params.RoomMedia(
mode = navTarget.mode,
eventId = navTarget.eventId,
mediaInfo = navTarget.mediaInfo,
mediaSource = navTarget.mediaSource,
thumbnailSource = navTarget.thumbnailSource,
canShowInfo = true,
)
val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
if (navTarget.canUseOverlay) {
overlay.hide()
} else {
backstack.pop()
}
}

override fun viewInTimeline(eventId: EventId) {
this@MessagesFlowNode.viewInTimeline(eventId)
}

override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
// Need to go to the parent because of the overlay
callback.forwardEvent(eventId, fromPinnedEvents)
}
}
mediaViewerEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = params,
callback = callback
)
}
is NavTarget.GalleryViewer -> {
val params = MediaViewerEntryPoint.Params.EventGallery(
eventId = navTarget.eventId,
galleryInfo = navTarget.galleryInfo,
galleryItems = navTarget.galleryItems,
)
val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
Expand Down Expand Up @@ -374,7 +433,7 @@ class MessagesFlowNode(
}
is NavTarget.AttachmentPreview -> {
val inputs = AttachmentsPreviewNode.Inputs(
attachment = navTarget.attachment,
attachments = navTarget.attachments,
timelineMode = navTarget.timelineMode,
inReplyToEventId = navTarget.inReplyToEventId,
)
Expand Down Expand Up @@ -455,6 +514,18 @@ class MessagesFlowNode(
)
}

override fun handleGalleryItemClick(
event: TimelineItem.Event,
galleryItemIndex: Int,
canUseOverlay: Boolean,
) {
processGalleryEventClick(
event = event,
galleryItemIndex = galleryItemIndex,
canUseOverlay = canUseOverlay,
)
}

override fun navigateToRoomMemberDetails(userId: UserId) {
callback.navigateToRoomMemberDetails(userId)
}
Expand Down Expand Up @@ -490,18 +561,34 @@ class MessagesFlowNode(
focusedEventId = navTarget.focusedEventId,
)
val callback = object : ThreadedMessagesNode.Callback {
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
override fun handleEventClick(
timelineMode: Timeline.Mode,
event: TimelineItem.Event,
canUseOverlay: Boolean,
): Boolean {
return processEventClick(
timelineMode = timelineMode,
event = event,
canUseOverlay = canUseOverlay,
)
}

override fun handleGalleryItemClick(
event: TimelineItem.Event,
galleryItemIndex: Int,
canUseOverlay: Boolean,
): Boolean {
return processGalleryEventClick(
event = event,
canUseOverlay = canUseOverlay,
galleryItemIndex = galleryItemIndex,
)
}

override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
backstack.push(
NavTarget.AttachmentPreview(
attachment = attachments.first(),
attachments = attachments,
timelineMode = Timeline.Mode.Thread(navTarget.threadRootId),
inReplyToEventId = inReplyToEventId,
)
Expand Down Expand Up @@ -704,6 +791,91 @@ class MessagesFlowNode(
}
}

private fun processGalleryEventClick(
event: TimelineItem.Event,
galleryItemIndex: Int,
canUseOverlay: Boolean,
): Boolean {
val navTarget = when (event.content) {
is TimelineItemGalleryContent -> {
val galleryInfo = GalleryInfo(
caption = event.content.caption,
senderId = event.senderId,
senderName = event.safeSenderName,
senderAvatar = event.senderAvatar.url,
dateSent = dateFormatter.format(
event.sentTimeMillis,
mode = DateFormatterMode.Day,
),
dateSentFull = dateFormatter.format(
timestamp = event.sentTimeMillis,
mode = DateFormatterMode.Full,
),
initialIndex = galleryItemIndex,
isAttachment = false,
)
val galleryItems = event.content.items.map { galleryItem ->
GalleryItemData(
filename = galleryItem.filename,
mimeType = galleryItem.mimeType,
mediaSource = galleryItem.mediaSource,
thumbnailSource = galleryItem.thumbnailSource,
type = galleryItem.type.toMediaViewerType(),
)
}.reversed()
NavTarget.GalleryViewer(
eventId = event.eventId,
galleryInfo = galleryInfo,
canUseOverlay = canUseOverlay,
galleryItems = galleryItems,
)
}
is TimelineItemAttachmentsContent -> {
val galleryInfo = GalleryInfo(
caption = event.content.caption,
senderId = event.senderId,
senderName = event.safeSenderName,
senderAvatar = event.senderAvatar.url,
dateSent = dateFormatter.format(
event.sentTimeMillis,
mode = DateFormatterMode.Day,
),
dateSentFull = dateFormatter.format(
timestamp = event.sentTimeMillis,
mode = DateFormatterMode.Full,
),
initialIndex = galleryItemIndex,
isAttachment = true,
)
val galleryItems = event.content.attachments.map { attachment ->
GalleryItemData(
filename = attachment.filename,
mimeType = attachment.mimeType,
mediaSource = attachment.mediaSource,
thumbnailSource = attachment.thumbnailSource,
type = GalleryItemData.Type.File,
)
}.reversed()
NavTarget.GalleryViewer(
eventId = event.eventId,
galleryInfo = galleryInfo,
canUseOverlay = canUseOverlay,
galleryItems = galleryItems,
)
}
else -> null
}
if (navTarget != null) {
if (canUseOverlay) {
overlay.show(navTarget)
} else {
backstack.push(navTarget)
}
return true
}
return false
}

private fun buildMediaViewerNavTarget(
mode: MediaViewerEntryPoint.MediaViewerMode,
event: TimelineItem.Event,
Expand Down Expand Up @@ -768,3 +940,12 @@ class MessagesFlowNode(
}
}
}

private fun GalleryItem.Type.toMediaViewerType(): GalleryItemData.Type {
return when (this) {
GalleryItem.Type.Image -> GalleryItemData.Type.Image
GalleryItem.Type.Video -> GalleryItemData.Type.Video
GalleryItem.Type.Audio -> GalleryItemData.Type.Audio
GalleryItem.Type.File -> GalleryItemData.Type.File
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class MessagesNode(

interface Callback : Plugin {
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
fun handleGalleryItemClick(event: TimelineItem.Event, galleryItemIndex: Int, canUseOverlay: Boolean): Boolean
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun navigateToRoomMemberDetails(userId: UserId)
fun handlePermalinkClick(data: PermalinkData)
Expand Down Expand Up @@ -289,6 +290,9 @@ class MessagesNode(
}
}
},
onGalleryEventItemClick = { event, index ->
callback.handleGalleryItemClick(event, index, canUseOverlay)
},
onUserDataClick = callback::navigateToRoomMemberDetails,
onLinkClick = { url, customTab ->
onLinkClick(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.captionOrNull
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter
import io.element.android.features.roomcall.api.RoomCallState
Expand Down Expand Up @@ -533,7 +533,7 @@ class MessagesPresenter(
) {
val composerMode = MessageComposerMode.EditCaption(
eventOrTransactionId = targetEvent.eventOrTransactionId,
content = (targetEvent.content as? TimelineItemEventContentWithAttachment)?.caption.orEmpty(),
content = targetEvent.content.captionOrNull().orEmpty(),
)
composerState.eventSink(
MessageComposerEvent.SetMode(composerMode)
Expand Down Expand Up @@ -606,7 +606,7 @@ class MessagesPresenter(
}

private fun handleCopyCaption(event: TimelineItem.Event) {
val content = (event.content as? TimelineItemEventContentWithAttachment)?.caption ?: return
val content = event.content.captionOrNull() ?: return
clipboardHelper.copyPlainText(content)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
Expand Down
Loading