Skip to content

Commit 87aff40

Browse files
bxdxnnbmartyElementBot
authored
Support gallery messages (#6519)
* Support gallery messages * Address review * Remove unused function * Fix indent * Add "Gallery" message prefix * Support galleries for image editing * Fix tapping on a tile opens the last item * Fix overflow count * Add caption editing to gallery messages * Use the new gallery string for prefix * Lock sending galleries behind a feature flag * Fix detekts * Fix * Ensure image edition is saved when navigating * Fix sending media broken on edited gallery. * Fix tests * Order imports * Remove unused parameters. * Fix copyright header of new files. * Fix Konsist test * Extract new previews to a dedicated file. * Sync strings * Remove unused import * Update screenshots * Trigger CI * Remove parameters with default value. * More cleanup * Restore sendAsFile behavior. * Improve Preview. * Improve Preview. * Improve Preview. * Fix gallery sending cancel and retry issue * Ensure any previous job is cancelled. * Fix issue in summary message * Gallery feature is disabled by default. * Kotlin convention * Remove useless parenthesis * Update screenshots * Fix test * List -> ImmutableList * Remove useless code. * Render formatted caption for attachment list. * Replace set of Booleans by an enum * Remove unused model for individual caption in a gallery Event. * Fix tests * Fix tests * Rework MediaViewer entry point. And ensure that the clicked image from the gallery is displayed first. * Ensure gallery item can be click in the pinned message list Improve the gallery item click handling code. * Improve code and fix separator color Closes #7101 * React on attachment item click Improve code * Improve code and support 0 items in gallery. * Fix click on attachment item not rendering anything. --------- Co-authored-by: Benoit Marty <benoitm@element.io> Co-authored-by: Benoit Marty <benoit@matrix.org> Co-authored-by: ElementBot <android@element.io>
1 parent 9e7ae34 commit 87aff40

180 files changed

Lines changed: 3463 additions & 630 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt

Lines changed: 189 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@ import io.element.android.features.messages.impl.threads.list.ThreadsListNode
4747
import io.element.android.features.messages.impl.timeline.TimelineController
4848
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
4949
import io.element.android.features.messages.impl.timeline.model.TimelineItem
50+
import io.element.android.features.messages.impl.timeline.model.event.GalleryItem
51+
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent
5052
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
5153
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
5254
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
55+
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemGalleryContent
5356
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
5457
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
5558
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
@@ -86,6 +89,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
8689
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
8790
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
8891
import io.element.android.libraries.matrix.ui.messages.RoomNamesCache
92+
import io.element.android.libraries.mediaviewer.api.GalleryInfo
93+
import io.element.android.libraries.mediaviewer.api.GalleryItemData
8994
import io.element.android.libraries.mediaviewer.api.MediaInfo
9095
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
9196
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater
@@ -153,7 +158,15 @@ class MessagesFlowNode(
153158
) : NavTarget
154159

155160
@Parcelize
156-
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachment: Attachment, val inReplyToEventId: EventId?) : NavTarget
161+
data class GalleryViewer(
162+
val eventId: EventId?,
163+
val galleryInfo: GalleryInfo,
164+
val canUseOverlay: Boolean,
165+
val galleryItems: List<GalleryItemData> = emptyList(),
166+
) : NavTarget
167+
168+
@Parcelize
169+
data class AttachmentPreview(val timelineMode: Timeline.Mode, val attachments: ImmutableList<Attachment>, val inReplyToEventId: EventId?) : NavTarget
157170

158171
@Parcelize
159172
data class LocationViewer(val mode: ShowLocationMode) : NavTarget
@@ -239,18 +252,34 @@ class MessagesFlowNode(
239252
callback.navigateToRoomDetails()
240253
}
241254

242-
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
255+
override fun handleEventClick(
256+
timelineMode: Timeline.Mode,
257+
event: TimelineItem.Event,
258+
canUseOverlay: Boolean,
259+
): Boolean {
243260
return processEventClick(
244261
timelineMode = timelineMode,
245262
event = event,
246263
canUseOverlay = canUseOverlay,
247264
)
248265
}
249266

267+
override fun handleGalleryItemClick(
268+
event: TimelineItem.Event,
269+
galleryItemIndex: Int,
270+
canUseOverlay: Boolean,
271+
): Boolean {
272+
return processGalleryEventClick(
273+
event = event,
274+
canUseOverlay = canUseOverlay,
275+
galleryItemIndex = galleryItemIndex,
276+
)
277+
}
278+
250279
override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
251280
backstack.push(
252281
NavTarget.AttachmentPreview(
253-
attachment = attachments.first(),
282+
attachments = attachments,
254283
timelineMode = Timeline.Mode.Live,
255284
inReplyToEventId = inReplyToEventId,
256285
)
@@ -339,13 +368,43 @@ class MessagesFlowNode(
339368
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
340369
}
341370
is NavTarget.MediaViewer -> {
342-
val params = MediaViewerEntryPoint.Params(
371+
val params = MediaViewerEntryPoint.Params.RoomMedia(
343372
mode = navTarget.mode,
344373
eventId = navTarget.eventId,
345374
mediaInfo = navTarget.mediaInfo,
346375
mediaSource = navTarget.mediaSource,
347376
thumbnailSource = navTarget.thumbnailSource,
348-
canShowInfo = true,
377+
)
378+
val callback = object : MediaViewerEntryPoint.Callback {
379+
override fun onDone() {
380+
if (navTarget.canUseOverlay) {
381+
overlay.hide()
382+
} else {
383+
backstack.pop()
384+
}
385+
}
386+
387+
override fun viewInTimeline(eventId: EventId) {
388+
this@MessagesFlowNode.viewInTimeline(eventId)
389+
}
390+
391+
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
392+
// Need to go to the parent because of the overlay
393+
callback.forwardEvent(eventId, fromPinnedEvents)
394+
}
395+
}
396+
mediaViewerEntryPoint.createNode(
397+
parentNode = this,
398+
buildContext = buildContext,
399+
params = params,
400+
callback = callback
401+
)
402+
}
403+
is NavTarget.GalleryViewer -> {
404+
val params = MediaViewerEntryPoint.Params.EventGallery(
405+
eventId = navTarget.eventId,
406+
galleryInfo = navTarget.galleryInfo,
407+
galleryItems = navTarget.galleryItems,
349408
)
350409
val callback = object : MediaViewerEntryPoint.Callback {
351410
override fun onDone() {
@@ -374,7 +433,7 @@ class MessagesFlowNode(
374433
}
375434
is NavTarget.AttachmentPreview -> {
376435
val inputs = AttachmentsPreviewNode.Inputs(
377-
attachment = navTarget.attachment,
436+
attachments = navTarget.attachments,
378437
timelineMode = navTarget.timelineMode,
379438
inReplyToEventId = navTarget.inReplyToEventId,
380439
)
@@ -455,6 +514,18 @@ class MessagesFlowNode(
455514
)
456515
}
457516

517+
override fun handleGalleryItemClick(
518+
event: TimelineItem.Event,
519+
galleryItemIndex: Int,
520+
canUseOverlay: Boolean,
521+
) {
522+
processGalleryEventClick(
523+
event = event,
524+
galleryItemIndex = galleryItemIndex,
525+
canUseOverlay = canUseOverlay,
526+
)
527+
}
528+
458529
override fun navigateToRoomMemberDetails(userId: UserId) {
459530
callback.navigateToRoomMemberDetails(userId)
460531
}
@@ -490,18 +561,34 @@ class MessagesFlowNode(
490561
focusedEventId = navTarget.focusedEventId,
491562
)
492563
val callback = object : ThreadedMessagesNode.Callback {
493-
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
564+
override fun handleEventClick(
565+
timelineMode: Timeline.Mode,
566+
event: TimelineItem.Event,
567+
canUseOverlay: Boolean,
568+
): Boolean {
494569
return processEventClick(
495570
timelineMode = timelineMode,
496571
event = event,
497572
canUseOverlay = canUseOverlay,
498573
)
499574
}
500575

576+
override fun handleGalleryItemClick(
577+
event: TimelineItem.Event,
578+
galleryItemIndex: Int,
579+
canUseOverlay: Boolean,
580+
): Boolean {
581+
return processGalleryEventClick(
582+
event = event,
583+
canUseOverlay = canUseOverlay,
584+
galleryItemIndex = galleryItemIndex,
585+
)
586+
}
587+
501588
override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
502589
backstack.push(
503590
NavTarget.AttachmentPreview(
504-
attachment = attachments.first(),
591+
attachments = attachments,
505592
timelineMode = Timeline.Mode.Thread(navTarget.threadRootId),
506593
inReplyToEventId = inReplyToEventId,
507594
)
@@ -704,6 +791,91 @@ class MessagesFlowNode(
704791
}
705792
}
706793

794+
private fun processGalleryEventClick(
795+
event: TimelineItem.Event,
796+
galleryItemIndex: Int,
797+
canUseOverlay: Boolean,
798+
): Boolean {
799+
val navTarget = when (event.content) {
800+
is TimelineItemGalleryContent -> {
801+
val galleryInfo = GalleryInfo(
802+
caption = event.content.caption,
803+
senderId = event.senderId,
804+
senderName = event.safeSenderName,
805+
senderAvatar = event.senderAvatar.url,
806+
dateSent = dateFormatter.format(
807+
event.sentTimeMillis,
808+
mode = DateFormatterMode.Day,
809+
),
810+
dateSentFull = dateFormatter.format(
811+
timestamp = event.sentTimeMillis,
812+
mode = DateFormatterMode.Full,
813+
),
814+
initialIndex = galleryItemIndex,
815+
isAttachment = false,
816+
)
817+
val galleryItems = event.content.items.map { galleryItem ->
818+
GalleryItemData(
819+
filename = galleryItem.filename,
820+
mimeType = galleryItem.mimeType,
821+
mediaSource = galleryItem.mediaSource,
822+
thumbnailSource = galleryItem.thumbnailSource,
823+
type = galleryItem.type.toMediaViewerType(),
824+
)
825+
}.reversed()
826+
NavTarget.GalleryViewer(
827+
eventId = event.eventId,
828+
galleryInfo = galleryInfo,
829+
canUseOverlay = canUseOverlay,
830+
galleryItems = galleryItems,
831+
)
832+
}
833+
is TimelineItemAttachmentsContent -> {
834+
val galleryInfo = GalleryInfo(
835+
caption = event.content.caption,
836+
senderId = event.senderId,
837+
senderName = event.safeSenderName,
838+
senderAvatar = event.senderAvatar.url,
839+
dateSent = dateFormatter.format(
840+
event.sentTimeMillis,
841+
mode = DateFormatterMode.Day,
842+
),
843+
dateSentFull = dateFormatter.format(
844+
timestamp = event.sentTimeMillis,
845+
mode = DateFormatterMode.Full,
846+
),
847+
initialIndex = galleryItemIndex,
848+
isAttachment = true,
849+
)
850+
val galleryItems = event.content.attachments.map { attachment ->
851+
GalleryItemData(
852+
filename = attachment.filename,
853+
mimeType = attachment.mimeType,
854+
mediaSource = attachment.mediaSource,
855+
thumbnailSource = attachment.thumbnailSource,
856+
type = GalleryItemData.Type.File,
857+
)
858+
}.reversed()
859+
NavTarget.GalleryViewer(
860+
eventId = event.eventId,
861+
galleryInfo = galleryInfo,
862+
canUseOverlay = canUseOverlay,
863+
galleryItems = galleryItems,
864+
)
865+
}
866+
else -> null
867+
}
868+
if (navTarget != null) {
869+
if (canUseOverlay) {
870+
overlay.show(navTarget)
871+
} else {
872+
backstack.push(navTarget)
873+
}
874+
return true
875+
}
876+
return false
877+
}
878+
707879
private fun buildMediaViewerNavTarget(
708880
mode: MediaViewerEntryPoint.MediaViewerMode,
709881
event: TimelineItem.Event,
@@ -768,3 +940,12 @@ class MessagesFlowNode(
768940
}
769941
}
770942
}
943+
944+
private fun GalleryItem.Type.toMediaViewerType(): GalleryItemData.Type {
945+
return when (this) {
946+
GalleryItem.Type.Image -> GalleryItemData.Type.Image
947+
GalleryItem.Type.Video -> GalleryItemData.Type.Video
948+
GalleryItem.Type.Audio -> GalleryItemData.Type.Audio
949+
GalleryItem.Type.File -> GalleryItemData.Type.File
950+
}
951+
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class MessagesNode(
119119

120120
interface Callback : Plugin {
121121
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
122+
fun handleGalleryItemClick(event: TimelineItem.Event, galleryItemIndex: Int, canUseOverlay: Boolean): Boolean
122123
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
123124
fun navigateToRoomMemberDetails(userId: UserId)
124125
fun handlePermalinkClick(data: PermalinkData)
@@ -289,6 +290,9 @@ class MessagesNode(
289290
}
290291
}
291292
},
293+
onGalleryEventItemClick = { event, index ->
294+
callback.handleGalleryItemClick(event, index, canUseOverlay)
295+
},
292296
onUserDataClick = callback::navigateToRoomMemberDetails,
293297
onLinkClick = { url, customTab ->
294298
onLinkClick(

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
4747
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
4848
import io.element.android.features.messages.impl.timeline.model.TimelineItem
4949
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
50-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
5150
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
5251
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
5352
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
53+
import io.element.android.features.messages.impl.timeline.model.event.captionOrNull
5454
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
5555
import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter
5656
import io.element.android.features.roomcall.api.RoomCallState
@@ -533,7 +533,7 @@ class MessagesPresenter(
533533
) {
534534
val composerMode = MessageComposerMode.EditCaption(
535535
eventOrTransactionId = targetEvent.eventOrTransactionId,
536-
content = (targetEvent.content as? TimelineItemEventContentWithAttachment)?.caption.orEmpty(),
536+
content = targetEvent.content.captionOrNull().orEmpty(),
537537
)
538538
composerState.eventSink(
539539
MessageComposerEvent.SetMode(composerMode)
@@ -606,7 +606,7 @@ class MessagesPresenter(
606606
}
607607

608608
private fun handleCopyCaption(event: TimelineItem.Event) {
609-
val content = (event.content as? TimelineItemEventContentWithAttachment)?.caption ?: return
609+
val content = event.content.captionOrNull() ?: return
610610
clipboardHelper.copyPlainText(content)
611611
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
612612
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))

0 commit comments

Comments
 (0)