mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +00:00
Multi file uploads (#4358)
* Allow MediaPickerScreen users to select the media selection mode (single or multiple) * Fix cancellation * Add support for multiple media URLs on the MediaUploadPreviewScreen. * Support processing more URLs on the `MediaUploadingPreprocessor` and sending more on the `MediaUploadPreviewScreen` * Add feature flag for `multipleAttachmentUploadEnabled` * Add a label showing the current preview item index in the MediaUploadPreviewScreen * Add support for dragging and dropping or pasting multiple items at the same time. * Support sharing more than one file through the share extension. * Limit the number of items that can be shared in one go to 5. * Fix unit tests * Fix incorrect fatal error when dealing with single selection media pickers. * Document the `multipleAttachmentUploadEnabled` usage in the context of the MediaPicker. * Use a task group for processing selected media in the photo library picker. * Use a task group for processing multiple selected media in the MediaUploadingPreprocessor * Switch the maximum number of items that can be shared to 10. * Allow multiple items to be pasted at the same time.
This commit is contained in:
@@ -302,11 +302,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (_, .presentReportContent, .reportContent(let itemID, let senderID, _)):
|
||||
presentReportContent(for: itemID, from: senderID)
|
||||
|
||||
case (_, .presentMediaUploadPicker, .mediaUploadPicker(let source, _)):
|
||||
case (_, .presentMediaUploadPicker, .mediaUploadPicker(let mode, _)):
|
||||
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
||||
fatalError("Missing required TimelineController")
|
||||
}
|
||||
presentMediaUploadPickerWithSource(source, timelineController: timelineController, animated: animated)
|
||||
presentMediaUploadPickerWithMode(mode, timelineController: timelineController, animated: animated)
|
||||
|
||||
case (_, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji, _)):
|
||||
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
||||
@@ -406,11 +406,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
presentPollForm(mode: mode, timelineController: timelineController)
|
||||
|
||||
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, _)):
|
||||
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let mediaURLs, _)):
|
||||
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
||||
fatalError("Missing required TimelineController")
|
||||
}
|
||||
presentMediaUploadPreviewScreen(for: fileURL, timelineController: timelineController, animated: animated)
|
||||
|
||||
presentMediaUploadPreviewScreen(for: mediaURLs, timelineController: timelineController, animated: animated)
|
||||
|
||||
case (_, .presentInviteUsersScreen, .inviteUsersScreen):
|
||||
presentInviteUsersScreen()
|
||||
@@ -464,8 +465,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch presentationAction {
|
||||
case .eventFocus(let focusedEvent):
|
||||
roomScreenCoordinator?.focusOnEvent(focusedEvent)
|
||||
case .share(.mediaFile(_, let mediaFile)):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url),
|
||||
case .share(.mediaFiles(_, let mediaFiles)):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url)),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .share(.text(_, let text)):
|
||||
roomScreenCoordinator?.shareText(text)
|
||||
@@ -501,8 +502,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
switch presentationAction {
|
||||
case .share(.mediaFile(_, let mediaFile)):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url),
|
||||
case .share(.mediaFiles(_, let mediaFiles)):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url)),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .share(.text), .eventFocus:
|
||||
break // These are both handled in the coordinator's init.
|
||||
@@ -556,11 +557,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .presentReportContent(let itemID, let senderID):
|
||||
stateMachine.tryEvent(.presentReportContent(itemID: itemID,
|
||||
senderID: senderID))
|
||||
case .presentMediaUploadPicker(let source):
|
||||
stateMachine.tryEvent(.presentMediaUploadPicker(source: source),
|
||||
case .presentMediaUploadPicker(let mode):
|
||||
stateMachine.tryEvent(.presentMediaUploadPicker(mode: mode),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .presentMediaUploadPreviewScreen(let url):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url),
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: url),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .presentEmojiPicker(let itemID, let selectedEmojis):
|
||||
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
|
||||
@@ -645,11 +646,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .presentReportContent(let itemID, let senderID):
|
||||
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
|
||||
case .presentMediaUploadPicker(let source):
|
||||
stateMachine.tryEvent(.presentMediaUploadPicker(source: source),
|
||||
case .presentMediaUploadPicker(let mode):
|
||||
stateMachine.tryEvent(.presentMediaUploadPicker(mode: mode),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .presentMediaUploadPreviewScreen(let url):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url),
|
||||
case .presentMediaUploadPreviewScreen(let mediaURLs):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaURLs),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .presentLocationPicker:
|
||||
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
|
||||
@@ -864,7 +865,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
||||
navigationStackCoordinator: stackCoordinator,
|
||||
userIndicatorController: userIndicatorController,
|
||||
orientationManager: appMediator.windowManager)
|
||||
orientationManager: appMediator.windowManager,
|
||||
appSettings: appSettings)
|
||||
let roomDetailsEditCoordinator = RoomDetailsEditScreenCoordinator(parameters: roomDetailsEditParameters)
|
||||
|
||||
roomDetailsEditCoordinator.actions.sink { [weak self] action in
|
||||
@@ -916,13 +918,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentMediaUploadPickerWithSource(_ source: MediaPickerScreenSource,
|
||||
timelineController: TimelineControllerProtocol,
|
||||
animated: Bool) {
|
||||
private func presentMediaUploadPickerWithMode(_ mode: MediaPickerScreenMode,
|
||||
timelineController: TimelineControllerProtocol,
|
||||
animated: Bool) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController,
|
||||
source: source,
|
||||
|
||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
|
||||
appSettings: appSettings,
|
||||
userIndicatorController: userIndicatorController,
|
||||
orientationManager: appMediator.windowManager) { [weak self] action in
|
||||
guard let self else {
|
||||
return
|
||||
@@ -930,8 +933,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .cancel:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .selectMediaAtURL(let url):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url),
|
||||
case .selectedMediaAtURLs(let urls):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: urls),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
}
|
||||
}
|
||||
@@ -945,13 +948,19 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentMediaUploadPreviewScreen(for url: URL,
|
||||
private func presentMediaUploadPreviewScreen(for mediaURLs: [URL],
|
||||
timelineController: TimelineControllerProtocol,
|
||||
animated: Bool) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let title: String? = if mediaURLs.count == 1 {
|
||||
mediaURLs.first?.lastPathComponent
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
let parameters = MediaUploadPreviewScreenCoordinatorParameters(url: url,
|
||||
title: url.lastPathComponent,
|
||||
let parameters = MediaUploadPreviewScreenCoordinatorParameters(mediaURLs: mediaURLs,
|
||||
title: title,
|
||||
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
|
||||
shouldShowCaptionWarning: appSettings.shouldShowMediaCaptionWarning,
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
||||
|
||||
@@ -57,8 +57,8 @@ extension RoomFlowCoordinator {
|
||||
case roomMemberDetails(userID: String, previousState: State)
|
||||
case userProfile(userID: String, previousState: State)
|
||||
case inviteUsersScreen(previousState: State)
|
||||
case mediaUploadPicker(source: MediaPickerScreenSource, previousState: State)
|
||||
case mediaUploadPreview(fileURL: URL, previousState: State)
|
||||
case mediaUploadPicker(mode: MediaPickerScreenMode, previousState: State)
|
||||
case mediaUploadPreview(mediaURLs: [URL], previousState: State)
|
||||
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, previousState: State)
|
||||
case mapNavigator(previousState: State)
|
||||
case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State)
|
||||
@@ -123,10 +123,10 @@ extension RoomFlowCoordinator {
|
||||
case presentInviteUsersScreen
|
||||
case dismissInviteUsersScreen
|
||||
|
||||
case presentMediaUploadPicker(source: MediaPickerScreenSource)
|
||||
case presentMediaUploadPicker(mode: MediaPickerScreenMode)
|
||||
case dismissMediaUploadPicker
|
||||
|
||||
case presentMediaUploadPreview(fileURL: URL)
|
||||
case presentMediaUploadPreview(mediaURLs: [URL])
|
||||
case dismissMediaUploadPreview
|
||||
|
||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
||||
@@ -185,11 +185,11 @@ extension RoomFlowCoordinator {
|
||||
case (.room, .presentReportContent(let itemID, let senderID)):
|
||||
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
||||
|
||||
case (.room, .presentMediaUploadPicker(let source)):
|
||||
return .mediaUploadPicker(source: source, previousState: fromState)
|
||||
case (.room, .presentMediaUploadPicker(let mode)):
|
||||
return .mediaUploadPicker(mode: mode, previousState: fromState)
|
||||
|
||||
case (.room, .presentMediaUploadPreview(let fileURL)):
|
||||
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState)
|
||||
case (.room, .presentMediaUploadPreview(let mediaURLs)):
|
||||
return .mediaUploadPreview(mediaURLs: mediaURLs, previousState: fromState)
|
||||
|
||||
case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
||||
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
||||
@@ -222,11 +222,11 @@ extension RoomFlowCoordinator {
|
||||
case (.thread, .presentReportContent(let itemID, let senderID)):
|
||||
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
||||
|
||||
case (.thread, .presentMediaUploadPicker(let source)):
|
||||
return .mediaUploadPicker(source: source, previousState: fromState)
|
||||
case (.thread, .presentMediaUploadPicker(let mode)):
|
||||
return .mediaUploadPicker(mode: mode, previousState: fromState)
|
||||
|
||||
case (.thread, .presentMediaUploadPreview(let fileURL)):
|
||||
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState)
|
||||
case (.thread, .presentMediaUploadPreview(let mediaURLs)):
|
||||
return .mediaUploadPreview(mediaURLs: mediaURLs, previousState: fromState)
|
||||
|
||||
case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
||||
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
||||
@@ -362,8 +362,8 @@ extension RoomFlowCoordinator {
|
||||
case (.pollsHistoryForm, .dismissPollForm):
|
||||
return .pollsHistory
|
||||
|
||||
case (.mediaUploadPicker(_, let previousMediaUploadPickerState), .presentMediaUploadPreview(let fileURL)):
|
||||
return .mediaUploadPreview(fileURL: fileURL, previousState: previousMediaUploadPickerState)
|
||||
case (.mediaUploadPicker(_, let previousMediaUploadPickerState), .presentMediaUploadPreview(let mediaURLs)):
|
||||
return .mediaUploadPreview(mediaURLs: mediaURLs, previousState: previousMediaUploadPickerState)
|
||||
|
||||
case (_, .presentInviteUsersScreen):
|
||||
return .inviteUsersScreen(previousState: fromState)
|
||||
|
||||
@@ -171,7 +171,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
mediaProvider: parameters.userSession.mediaProvider,
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: parameters.appSettings),
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
userIndicatorController: parameters.userIndicatorController))
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings))
|
||||
|
||||
navigationStackCoordinator?.push(coordinator)
|
||||
}
|
||||
|
||||
@@ -735,7 +735,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
navigationStackCoordinator: startChatNavigationStackCoordinator,
|
||||
userDiscoveryService: userDiscoveryService,
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings))
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
||||
appSettings: appSettings)
|
||||
|
||||
let coordinator = StartChatScreenCoordinator(parameters: parameters)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
@@ -1039,8 +1040,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
case .confirm(let roomID):
|
||||
let sharePayload = switch sharePayload {
|
||||
case .mediaFile(_, let mediaFile):
|
||||
ShareExtensionPayload.mediaFile(roomID: roomID, mediaFile: mediaFile)
|
||||
case .mediaFiles(_, let mediaFiles):
|
||||
ShareExtensionPayload.mediaFiles(roomID: roomID, mediaFiles: mediaFiles)
|
||||
case .text(_, let text):
|
||||
ShareExtensionPayload.text(roomID: roomID, text: text)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user