Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
87aff40
Support gallery messages (#6519)
bxdxnn Jun 26, 2026
e282212
Update screenshots
ElementBot Jun 26, 2026
a7e6d8a
Better fix to render all items in the gallery. Should be able to rend…
bmarty Jun 26, 2026
b62242c
Fix sharing gallery from pinned event timeline.
bmarty Jun 26, 2026
95e656c
Fix regression on tests.
bmarty Jun 29, 2026
b2a7c65
Add missing test on TimelineItemContentMessageFactory
bmarty Jun 29, 2026
1a5336e
Add missing test on EventItemFactory
bmarty Jun 29, 2026
fb7bc50
Rename test class.
bmarty Jun 29, 2026
dd3beb1
Add unit test on GalleryMediaGalleryDataSource and simplify the code.
bmarty Jun 29, 2026
fd799a3
Add unit test on TimelineEventContentMapper.
bmarty Jun 29, 2026
ad84122
Revert change on TimelineItemVideoView, it was dead code.
bmarty Jun 29, 2026
73ef038
Preview: add video item in the list
bmarty Jun 29, 2026
c80eb12
Remove duplicated fixtures.
bmarty Jun 29, 2026
79163b6
Fix quality issues
bmarty Jun 29, 2026
5f0461b
Update screenshots
ElementBot Jun 29, 2026
5d0d8be
Fix name regarding naming convention
bmarty Jun 29, 2026
f91fd4b
Rename method to sendMedia
bmarty Jun 30, 2026
96e1c93
Introduce derived state editedAttachments
bmarty Jun 30, 2026
9934392
Extract SingleItemPreviewRenderer, make it a class and rename it to S…
bmarty Jun 30, 2026
89b4722
Restore previous code.
bmarty Jun 30, 2026
29743d3
Some renaming for clarity.
bmarty Jun 30, 2026
5ca1435
Remove dead code.
bmarty Jun 30, 2026
16eec4d
Simplify code
bmarty Jun 30, 2026
6cce74d
[Gallery] Set media optimation user choice per media
bmarty Jun 30, 2026
4566712
Take user choice into consideration when pre-processing the media.
bmarty Jul 3, 2026
74f69b7
Optimize code
bmarty Jul 3, 2026
762823e
`SampleMediaRenderer` can be internal.
bmarty Jul 3, 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,16 @@ class MessagesFlowNode(
) : NavTarget

@Parcelize
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment, val inReplyToEventId: EventId?) : NavTarget
data class GalleryViewer(
val fromPinnedMessages: Boolean,
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 +253,36 @@ 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(
timelineMode: Timeline.Mode,
event: TimelineItem.Event,
galleryItemIndex: Int,
canUseOverlay: Boolean,
): Boolean {
return processGalleryEventClick(
timelineMode = timelineMode,
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 +371,44 @@ 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,
fromPinnedMessages = navTarget.fromPinnedMessages,
)
val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
Expand Down Expand Up @@ -374,7 +437,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 +518,19 @@ class MessagesFlowNode(
)
}

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

override fun navigateToRoomMemberDetails(userId: UserId) {
callback.navigateToRoomMemberDetails(userId)
}
Expand Down Expand Up @@ -490,18 +566,36 @@ 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(
timelineMode: Timeline.Mode,
event: TimelineItem.Event,
galleryItemIndex: Int,
canUseOverlay: Boolean,
): Boolean {
return processGalleryEventClick(
timelineMode = timelineMode,
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 +798,92 @@ class MessagesFlowNode(
}
}

private fun processGalleryEventClick(
timelineMode: Timeline.Mode,
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,
)
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,
fromPinnedMessages = timelineMode is Timeline.Mode.PinnedEvents
)
}
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,
)
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,
fromPinnedMessages = timelineMode is Timeline.Mode.PinnedEvents
)
}
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 +948,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(timelineMode: Timeline.Mode, 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,18 @@ class MessagesNode(
}
}
},
onGalleryEventItemClick = { isLive, event, index ->
if (isLive) {
callback.handleGalleryItemClick(timelineController.mainTimelineMode(), event, index, canUseOverlay)
} else {
val detachedTimelineMode = timelineController.detachedTimelineMode()
if (detachedTimelineMode != null) {
callback.handleGalleryItemClick(detachedTimelineMode, event, index, canUseOverlay)
} else {
false
}
}
},
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
Loading