Support sending gallery messages behind a feature flag

Adds the ability to send a gallery message (multiple attachments in one message)
via the media upload preview, wired through the timeline controller/proxy to the
SDK's sendGallery. Gated by a `galleryEnabled` developer flag (default off),
matching Element X Android — with the flag off, attachments are sent as
individual messages as before.

Extends the community PR #5601, rebased onto develop. Tracking:
element-hq/element-meta#3201

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>
This commit is contained in:
Mauro Romito
2026-07-21 11:34:44 +02:00
parent 9354d9d0c2
commit 9a61d870d6
14 changed files with 376 additions and 41 deletions
@@ -22,6 +22,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
private var processingTask: Task<Result<[MediaInfo], MediaUploadingPreprocessorError>, Never>
private var requestHandle: SendAttachmentJoinHandleProtocol?
private let clientProxy: ClientProxyProtocol
private let galleryEnabled: Bool
private var actionsSubject: PassthroughSubject<MediaUploadPreviewScreenViewModelAction, Never> = .init()
@@ -36,12 +37,14 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
mediaUploadingPreprocessor: MediaUploadingPreprocessor,
timelineController: TimelineControllerProtocol,
clientProxy: ClientProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
userIndicatorController: UserIndicatorControllerProtocol,
galleryEnabled: Bool) {
self.mediaURLs = mediaURLs
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
self.timelineController = timelineController
self.clientProxy = clientProxy
self.userIndicatorController = userIndicatorController
self.galleryEnabled = galleryEnabled
// Start processing the media whilst the user is reviewing it/adding a caption.
processingTask = Self.processMedia(at: mediaURLs, preprocessor: mediaUploadingPreprocessor, clientProxy: clientProxy)
@@ -54,7 +57,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
override func process(viewAction: MediaUploadPreviewScreenViewAction) {
// Get the current caption before all the processing starts.
var caption = state.bindings.caption.nonBlankString
let caption = state.bindings.caption.nonBlankString
switch viewAction {
case .send:
@@ -65,14 +68,25 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
switch await processingTask.value {
case .success(let mediaInfos):
for mediaInfo in mediaInfos {
switch await sendAttachment(mediaInfo: mediaInfo, caption: caption) {
if galleryEnabled, mediaInfos.count > 1 {
switch await sendGallery(mediaInfos: mediaInfos, caption: caption) {
case .success:
caption = nil // Set the caption only on the first uploaded file.
break
case .failure(let error):
MXLog.error("Failed sending media with error: \(error)")
MXLog.error("Failed sending gallery with error: \(error)")
showError(label: L10n.screenMediaUploadPreviewErrorFailedSending)
}
} else {
var perItemCaption = caption
for mediaInfo in mediaInfos {
switch await sendAttachment(mediaInfo: mediaInfo, caption: perItemCaption) {
case .success:
perItemCaption = nil // Set the caption only on the first uploaded file.
case .failure(let error):
MXLog.error("Failed sending media with error: \(error)")
showError(label: L10n.screenMediaUploadPreviewErrorFailedSending)
}
}
}
actionsSubject.send(.dismiss)
@@ -107,6 +121,40 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
}
}
private func sendGallery(mediaInfos: [MediaInfo], caption: String?) async -> Result<Void, TimelineControllerError> {
let itemInfos: [GalleryItemInfo] = mediaInfos.map { mediaInfo in
switch mediaInfo {
case let .image(imageURL, thumbnailURL, imageInfo):
return .image(imageInfo: imageInfo,
source: .file(filename: imageURL.path(percentEncoded: false)),
caption: nil,
formattedCaption: nil,
thumbnailSource: .file(filename: thumbnailURL.path(percentEncoded: false)))
case let .video(videoURL, thumbnailURL, videoInfo):
return .video(videoInfo: videoInfo,
source: .file(filename: videoURL.path(percentEncoded: false)),
caption: nil,
formattedCaption: nil,
thumbnailSource: .file(filename: thumbnailURL.path(percentEncoded: false)))
case let .audio(audioURL, audioInfo):
return .audio(audioInfo: audioInfo,
source: .file(filename: audioURL.path(percentEncoded: false)),
caption: nil,
formattedCaption: nil)
case let .file(fileURL, fileInfo):
return .file(fileInfo: fileInfo,
source: .file(filename: fileURL.path(percentEncoded: false)),
caption: nil,
formattedCaption: nil)
}
}
return await timelineController.sendGallery(itemInfos: itemInfos,
caption: caption,
formattedCaption: nil,
inReplyToEventID: nil)
}
func stopProcessing() {
processingTask.cancel()
}