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:
@@ -16,11 +16,13 @@ import SwiftUI
|
|||||||
protocol CommonSettingsProtocol {
|
protocol CommonSettingsProtocol {
|
||||||
var logLevel: LogLevel { get }
|
var logLevel: LogLevel { get }
|
||||||
var traceLogPacks: Set<TraceLogPack> { get }
|
var traceLogPacks: Set<TraceLogPack> { get }
|
||||||
|
var bugReportRageshakeURL: RemotePreference<RageshakeConfiguration> { get }
|
||||||
|
|
||||||
var enableOnlySignedDeviceIsolationMode: Bool { get }
|
var enableOnlySignedDeviceIsolationMode: Bool { get }
|
||||||
var enableKeyShareOnInvite: Bool { get }
|
var enableKeyShareOnInvite: Bool { get }
|
||||||
var hideQuietNotificationAlerts: Bool { get }
|
|
||||||
var threadsEnabled: Bool { get }
|
var threadsEnabled: Bool { get }
|
||||||
var bugReportRageshakeURL: RemotePreference<RageshakeConfiguration> { get }
|
var hideQuietNotificationAlerts: Bool { get }
|
||||||
|
var multipleAttachmentUploadEnabled: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store Element specific app settings.
|
/// Store Element specific app settings.
|
||||||
@@ -59,6 +61,7 @@ final class AppSettings {
|
|||||||
case threadsEnabled
|
case threadsEnabled
|
||||||
case developerOptionsEnabled
|
case developerOptionsEnabled
|
||||||
case sharePosEnabledV2
|
case sharePosEnabledV2
|
||||||
|
case multipleAttachmentUploadEnabled
|
||||||
|
|
||||||
// Doug's tweaks 🔧
|
// Doug's tweaks 🔧
|
||||||
case hideUnreadMessagesBadge
|
case hideUnreadMessagesBadge
|
||||||
@@ -70,6 +73,16 @@ final class AppSettings {
|
|||||||
/// UserDefaults to be used on reads and writes.
|
/// UserDefaults to be used on reads and writes.
|
||||||
private static var store: UserDefaults! = UserDefaults(suiteName: suiteName)
|
private static var store: UserDefaults! = UserDefaults(suiteName: suiteName)
|
||||||
|
|
||||||
|
/// Whether or not the app is a development build that isn't in production.
|
||||||
|
static var isDevelopmentBuild: Bool = {
|
||||||
|
#if DEBUG
|
||||||
|
true
|
||||||
|
#else
|
||||||
|
let apps = ["io.element.elementx.nightly", "io.element.elementx.pr"]
|
||||||
|
return apps.contains(InfoPlistReader.main.baseBundleIdentifier)
|
||||||
|
#endif
|
||||||
|
}()
|
||||||
|
|
||||||
#if IS_MAIN_APP
|
#if IS_MAIN_APP
|
||||||
|
|
||||||
static func resetAllSettings() {
|
static func resetAllSettings() {
|
||||||
@@ -135,16 +148,6 @@ final class AppSettings {
|
|||||||
|
|
||||||
// MARK: - Application
|
// MARK: - Application
|
||||||
|
|
||||||
/// Whether or not the app is a development build that isn't in production.
|
|
||||||
static var isDevelopmentBuild: Bool = {
|
|
||||||
#if DEBUG
|
|
||||||
true
|
|
||||||
#else
|
|
||||||
let apps = ["io.element.elementx.nightly", "io.element.elementx.pr"]
|
|
||||||
return apps.contains(InfoPlistReader.main.baseBundleIdentifier)
|
|
||||||
#endif
|
|
||||||
}()
|
|
||||||
|
|
||||||
/// The last known version of the app that was launched on this device, which is
|
/// The last known version of the app that was launched on this device, which is
|
||||||
/// used to detect when migrations should be run. When `nil` the app may have been
|
/// used to detect when migrations should be run. When `nil` the app may have been
|
||||||
/// deleted between runs so should clear data in the shared container and keychain.
|
/// deleted between runs so should clear data in the shared container and keychain.
|
||||||
@@ -362,6 +365,8 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.traceLogPacks, defaultValue: [], storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.traceLogPacks, defaultValue: [], storageType: .userDefaults(store))
|
||||||
var traceLogPacks: Set<TraceLogPack>
|
var traceLogPacks: Set<TraceLogPack>
|
||||||
|
|
||||||
|
let bugReportRageshakeURL: RemotePreference<RageshakeConfiguration> = .init(Secrets.rageshakeURL.map { .url(URL(string: $0)!) } ?? .disabled) // swiftlint:disable:this force_unwrapping
|
||||||
|
|
||||||
/// Configuration to enable only signed device isolation mode for crypto. In this mode only devices signed by their owner will be considered in e2ee rooms.
|
/// Configuration to enable only signed device isolation mode for crypto. In this mode only devices signed by their owner will be considered in e2ee rooms.
|
||||||
@UserPreference(key: UserDefaultsKeys.enableOnlySignedDeviceIsolationMode, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.enableOnlySignedDeviceIsolationMode, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var enableOnlySignedDeviceIsolationMode
|
var enableOnlySignedDeviceIsolationMode
|
||||||
@@ -370,13 +375,14 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.enableKeyShareOnInvite, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.enableKeyShareOnInvite, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var enableKeyShareOnInvite
|
var enableKeyShareOnInvite
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.hideQuietNotificationAlerts, defaultValue: false, storageType: .userDefaults(store))
|
|
||||||
var hideQuietNotificationAlerts
|
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var threadsEnabled
|
var threadsEnabled
|
||||||
|
|
||||||
let bugReportRageshakeURL: RemotePreference<RageshakeConfiguration> = .init(Secrets.rageshakeURL.map { .url(URL(string: $0)!) } ?? .disabled) // swiftlint:disable:this force_unwrapping
|
@UserPreference(key: UserDefaultsKeys.hideQuietNotificationAlerts, defaultValue: false, storageType: .userDefaults(store))
|
||||||
|
var hideQuietNotificationAlerts
|
||||||
|
|
||||||
|
@UserPreference(key: UserDefaultsKeys.multipleAttachmentUploadEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store))
|
||||||
|
var multipleAttachmentUploadEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppSettings: CommonSettingsProtocol { }
|
extension AppSettings: CommonSettingsProtocol { }
|
||||||
|
|||||||
@@ -302,11 +302,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case (_, .presentReportContent, .reportContent(let itemID, let senderID, _)):
|
case (_, .presentReportContent, .reportContent(let itemID, let senderID, _)):
|
||||||
presentReportContent(for: itemID, from: 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 {
|
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
||||||
fatalError("Missing required TimelineController")
|
fatalError("Missing required TimelineController")
|
||||||
}
|
}
|
||||||
presentMediaUploadPickerWithSource(source, timelineController: timelineController, animated: animated)
|
presentMediaUploadPickerWithMode(mode, timelineController: timelineController, animated: animated)
|
||||||
|
|
||||||
case (_, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji, _)):
|
case (_, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji, _)):
|
||||||
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
||||||
@@ -406,11 +406,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
presentPollForm(mode: mode, timelineController: timelineController)
|
presentPollForm(mode: mode, timelineController: timelineController)
|
||||||
|
|
||||||
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, _)):
|
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let mediaURLs, _)):
|
||||||
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
||||||
fatalError("Missing required TimelineController")
|
fatalError("Missing required TimelineController")
|
||||||
}
|
}
|
||||||
presentMediaUploadPreviewScreen(for: fileURL, timelineController: timelineController, animated: animated)
|
|
||||||
|
presentMediaUploadPreviewScreen(for: mediaURLs, timelineController: timelineController, animated: animated)
|
||||||
|
|
||||||
case (_, .presentInviteUsersScreen, .inviteUsersScreen):
|
case (_, .presentInviteUsersScreen, .inviteUsersScreen):
|
||||||
presentInviteUsersScreen()
|
presentInviteUsersScreen()
|
||||||
@@ -464,8 +465,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch presentationAction {
|
switch presentationAction {
|
||||||
case .eventFocus(let focusedEvent):
|
case .eventFocus(let focusedEvent):
|
||||||
roomScreenCoordinator?.focusOnEvent(focusedEvent)
|
roomScreenCoordinator?.focusOnEvent(focusedEvent)
|
||||||
case .share(.mediaFile(_, let mediaFile)):
|
case .share(.mediaFiles(_, let mediaFiles)):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url),
|
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url)),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .share(.text(_, let text)):
|
case .share(.text(_, let text)):
|
||||||
roomScreenCoordinator?.shareText(text)
|
roomScreenCoordinator?.shareText(text)
|
||||||
@@ -501,8 +502,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch presentationAction {
|
switch presentationAction {
|
||||||
case .share(.mediaFile(_, let mediaFile)):
|
case .share(.mediaFiles(_, let mediaFiles)):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url),
|
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url)),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .share(.text), .eventFocus:
|
case .share(.text), .eventFocus:
|
||||||
break // These are both handled in the coordinator's init.
|
break // These are both handled in the coordinator's init.
|
||||||
@@ -556,11 +557,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .presentReportContent(let itemID, let senderID):
|
case .presentReportContent(let itemID, let senderID):
|
||||||
stateMachine.tryEvent(.presentReportContent(itemID: itemID,
|
stateMachine.tryEvent(.presentReportContent(itemID: itemID,
|
||||||
senderID: senderID))
|
senderID: senderID))
|
||||||
case .presentMediaUploadPicker(let source):
|
case .presentMediaUploadPicker(let mode):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPicker(source: source),
|
stateMachine.tryEvent(.presentMediaUploadPicker(mode: mode),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .presentMediaUploadPreviewScreen(let url):
|
case .presentMediaUploadPreviewScreen(let url):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url),
|
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: url),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .presentEmojiPicker(let itemID, let selectedEmojis):
|
case .presentEmojiPicker(let itemID, let selectedEmojis):
|
||||||
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
|
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
|
||||||
@@ -645,11 +646,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .presentReportContent(let itemID, let senderID):
|
case .presentReportContent(let itemID, let senderID):
|
||||||
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
|
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
|
||||||
case .presentMediaUploadPicker(let source):
|
case .presentMediaUploadPicker(let mode):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPicker(source: source),
|
stateMachine.tryEvent(.presentMediaUploadPicker(mode: mode),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .presentMediaUploadPreviewScreen(let url):
|
case .presentMediaUploadPreviewScreen(let mediaURLs):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url),
|
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaURLs),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .presentLocationPicker:
|
case .presentLocationPicker:
|
||||||
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
|
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
|
||||||
@@ -864,7 +865,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
||||||
navigationStackCoordinator: stackCoordinator,
|
navigationStackCoordinator: stackCoordinator,
|
||||||
userIndicatorController: userIndicatorController,
|
userIndicatorController: userIndicatorController,
|
||||||
orientationManager: appMediator.windowManager)
|
orientationManager: appMediator.windowManager,
|
||||||
|
appSettings: appSettings)
|
||||||
let roomDetailsEditCoordinator = RoomDetailsEditScreenCoordinator(parameters: roomDetailsEditParameters)
|
let roomDetailsEditCoordinator = RoomDetailsEditScreenCoordinator(parameters: roomDetailsEditParameters)
|
||||||
|
|
||||||
roomDetailsEditCoordinator.actions.sink { [weak self] action in
|
roomDetailsEditCoordinator.actions.sink { [weak self] action in
|
||||||
@@ -916,13 +918,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentMediaUploadPickerWithSource(_ source: MediaPickerScreenSource,
|
private func presentMediaUploadPickerWithMode(_ mode: MediaPickerScreenMode,
|
||||||
timelineController: TimelineControllerProtocol,
|
timelineController: TimelineControllerProtocol,
|
||||||
animated: Bool) {
|
animated: Bool) {
|
||||||
let stackCoordinator = NavigationStackCoordinator()
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController,
|
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
|
||||||
source: source,
|
appSettings: appSettings,
|
||||||
|
userIndicatorController: userIndicatorController,
|
||||||
orientationManager: appMediator.windowManager) { [weak self] action in
|
orientationManager: appMediator.windowManager) { [weak self] action in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@@ -930,8 +933,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
case .selectMediaAtURL(let url):
|
case .selectedMediaAtURLs(let urls):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url),
|
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: urls),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
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,
|
timelineController: TimelineControllerProtocol,
|
||||||
animated: Bool) {
|
animated: Bool) {
|
||||||
let stackCoordinator = NavigationStackCoordinator()
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
|
let title: String? = if mediaURLs.count == 1 {
|
||||||
|
mediaURLs.first?.lastPathComponent
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
let parameters = MediaUploadPreviewScreenCoordinatorParameters(url: url,
|
let parameters = MediaUploadPreviewScreenCoordinatorParameters(mediaURLs: mediaURLs,
|
||||||
title: url.lastPathComponent,
|
title: title,
|
||||||
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
|
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
|
||||||
shouldShowCaptionWarning: appSettings.shouldShowMediaCaptionWarning,
|
shouldShowCaptionWarning: appSettings.shouldShowMediaCaptionWarning,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ extension RoomFlowCoordinator {
|
|||||||
case roomMemberDetails(userID: String, previousState: State)
|
case roomMemberDetails(userID: String, previousState: State)
|
||||||
case userProfile(userID: String, previousState: State)
|
case userProfile(userID: String, previousState: State)
|
||||||
case inviteUsersScreen(previousState: State)
|
case inviteUsersScreen(previousState: State)
|
||||||
case mediaUploadPicker(source: MediaPickerScreenSource, previousState: State)
|
case mediaUploadPicker(mode: MediaPickerScreenMode, previousState: State)
|
||||||
case mediaUploadPreview(fileURL: URL, previousState: State)
|
case mediaUploadPreview(mediaURLs: [URL], previousState: State)
|
||||||
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, previousState: State)
|
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, previousState: State)
|
||||||
case mapNavigator(previousState: State)
|
case mapNavigator(previousState: State)
|
||||||
case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State)
|
case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State)
|
||||||
@@ -123,10 +123,10 @@ extension RoomFlowCoordinator {
|
|||||||
case presentInviteUsersScreen
|
case presentInviteUsersScreen
|
||||||
case dismissInviteUsersScreen
|
case dismissInviteUsersScreen
|
||||||
|
|
||||||
case presentMediaUploadPicker(source: MediaPickerScreenSource)
|
case presentMediaUploadPicker(mode: MediaPickerScreenMode)
|
||||||
case dismissMediaUploadPicker
|
case dismissMediaUploadPicker
|
||||||
|
|
||||||
case presentMediaUploadPreview(fileURL: URL)
|
case presentMediaUploadPreview(mediaURLs: [URL])
|
||||||
case dismissMediaUploadPreview
|
case dismissMediaUploadPreview
|
||||||
|
|
||||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
||||||
@@ -185,11 +185,11 @@ extension RoomFlowCoordinator {
|
|||||||
case (.room, .presentReportContent(let itemID, let senderID)):
|
case (.room, .presentReportContent(let itemID, let senderID)):
|
||||||
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
||||||
|
|
||||||
case (.room, .presentMediaUploadPicker(let source)):
|
case (.room, .presentMediaUploadPicker(let mode)):
|
||||||
return .mediaUploadPicker(source: source, previousState: fromState)
|
return .mediaUploadPicker(mode: mode, previousState: fromState)
|
||||||
|
|
||||||
case (.room, .presentMediaUploadPreview(let fileURL)):
|
case (.room, .presentMediaUploadPreview(let mediaURLs)):
|
||||||
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState)
|
return .mediaUploadPreview(mediaURLs: mediaURLs, previousState: fromState)
|
||||||
|
|
||||||
case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
||||||
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
||||||
@@ -222,11 +222,11 @@ extension RoomFlowCoordinator {
|
|||||||
case (.thread, .presentReportContent(let itemID, let senderID)):
|
case (.thread, .presentReportContent(let itemID, let senderID)):
|
||||||
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
||||||
|
|
||||||
case (.thread, .presentMediaUploadPicker(let source)):
|
case (.thread, .presentMediaUploadPicker(let mode)):
|
||||||
return .mediaUploadPicker(source: source, previousState: fromState)
|
return .mediaUploadPicker(mode: mode, previousState: fromState)
|
||||||
|
|
||||||
case (.thread, .presentMediaUploadPreview(let fileURL)):
|
case (.thread, .presentMediaUploadPreview(let mediaURLs)):
|
||||||
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState)
|
return .mediaUploadPreview(mediaURLs: mediaURLs, previousState: fromState)
|
||||||
|
|
||||||
case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
||||||
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
||||||
@@ -362,8 +362,8 @@ extension RoomFlowCoordinator {
|
|||||||
case (.pollsHistoryForm, .dismissPollForm):
|
case (.pollsHistoryForm, .dismissPollForm):
|
||||||
return .pollsHistory
|
return .pollsHistory
|
||||||
|
|
||||||
case (.mediaUploadPicker(_, let previousMediaUploadPickerState), .presentMediaUploadPreview(let fileURL)):
|
case (.mediaUploadPicker(_, let previousMediaUploadPickerState), .presentMediaUploadPreview(let mediaURLs)):
|
||||||
return .mediaUploadPreview(fileURL: fileURL, previousState: previousMediaUploadPickerState)
|
return .mediaUploadPreview(mediaURLs: mediaURLs, previousState: previousMediaUploadPickerState)
|
||||||
|
|
||||||
case (_, .presentInviteUsersScreen):
|
case (_, .presentInviteUsersScreen):
|
||||||
return .inviteUsersScreen(previousState: fromState)
|
return .inviteUsersScreen(previousState: fromState)
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
mediaProvider: parameters.userSession.mediaProvider,
|
mediaProvider: parameters.userSession.mediaProvider,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: parameters.appSettings),
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: parameters.appSettings),
|
||||||
navigationStackCoordinator: navigationStackCoordinator,
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
userIndicatorController: parameters.userIndicatorController))
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
|
appSettings: parameters.appSettings))
|
||||||
|
|
||||||
navigationStackCoordinator?.push(coordinator)
|
navigationStackCoordinator?.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -735,7 +735,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
navigationStackCoordinator: startChatNavigationStackCoordinator,
|
navigationStackCoordinator: startChatNavigationStackCoordinator,
|
||||||
userDiscoveryService: userDiscoveryService,
|
userDiscoveryService: userDiscoveryService,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings))
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
|
||||||
|
appSettings: appSettings)
|
||||||
|
|
||||||
let coordinator = StartChatScreenCoordinator(parameters: parameters)
|
let coordinator = StartChatScreenCoordinator(parameters: parameters)
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
@@ -1039,8 +1040,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||||
case .confirm(let roomID):
|
case .confirm(let roomID):
|
||||||
let sharePayload = switch sharePayload {
|
let sharePayload = switch sharePayload {
|
||||||
case .mediaFile(_, let mediaFile):
|
case .mediaFiles(_, let mediaFiles):
|
||||||
ShareExtensionPayload.mediaFile(roomID: roomID, mediaFile: mediaFile)
|
ShareExtensionPayload.mediaFiles(roomID: roomID, mediaFiles: mediaFiles)
|
||||||
case .text(_, let text):
|
case .text(_, let text):
|
||||||
ShareExtensionPayload.text(roomID: roomID, text: text)
|
ShareExtensionPayload.text(roomID: roomID, text: text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ enum CreateRoomCoordinatorAction {
|
|||||||
case openRoom(withIdentifier: String)
|
case openRoom(withIdentifier: String)
|
||||||
case deselectUser(UserProfileProxy)
|
case deselectUser(UserProfileProxy)
|
||||||
case updateDetails(CreateRoomFlowParameters)
|
case updateDetails(CreateRoomFlowParameters)
|
||||||
case displayMediaPickerWithSource(MediaPickerScreenSource)
|
case displayMediaPickerWithMode(MediaPickerScreenMode)
|
||||||
case removeImage
|
case removeImage
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
|
|||||||
case .updateDetails(let details):
|
case .updateDetails(let details):
|
||||||
actionsSubject.send(.updateDetails(details))
|
actionsSubject.send(.updateDetails(details))
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
actionsSubject.send(.displayMediaPickerWithSource(.camera))
|
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)))
|
||||||
case .displayMediaPicker:
|
case .displayMediaPicker:
|
||||||
actionsSubject.send(.displayMediaPickerWithSource(.photoLibrary))
|
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)))
|
||||||
case .removeImage:
|
case .removeImage:
|
||||||
actionsSubject.send(.removeImage)
|
actionsSubject.send(.removeImage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ struct CameraPicker: UIViewControllerRepresentable {
|
|||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let callback: (CameraPickerAction) -> Void
|
private let callback: (CameraPickerAction) -> Void
|
||||||
|
|
||||||
init(userIndicatorController: UserIndicatorControllerProtocol, callback: @escaping (CameraPickerAction) -> Void) {
|
init(userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
|
callback: @escaping (CameraPickerAction) -> Void) {
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,29 +8,35 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum DocumentPickerAction {
|
enum DocumentPickerAction {
|
||||||
case selectFile(URL)
|
case selectedMediaAtURLs([URL])
|
||||||
case cancel
|
case cancel
|
||||||
case error(Error)
|
case error(Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DocumentPickerError: Error {
|
|
||||||
case unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DocumentPicker: UIViewControllerRepresentable {
|
struct DocumentPicker: UIViewControllerRepresentable {
|
||||||
|
private let selectionType: MediaPickerScreenSelectionType
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let callback: (DocumentPickerAction) -> Void
|
private let callback: (DocumentPickerAction) -> Void
|
||||||
|
|
||||||
init(userIndicatorController: UserIndicatorControllerProtocol, callback: @escaping (DocumentPickerAction) -> Void) {
|
init(selectionType: MediaPickerScreenSelectionType,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
|
callback: @escaping (DocumentPickerAction) -> Void) {
|
||||||
|
self.selectionType = selectionType
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
|
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
|
||||||
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.data])
|
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.data])
|
||||||
documentPicker.allowsMultipleSelection = false
|
|
||||||
documentPicker.delegate = context.coordinator
|
documentPicker.delegate = context.coordinator
|
||||||
|
|
||||||
|
documentPicker.allowsMultipleSelection = switch selectionType {
|
||||||
|
case .single:
|
||||||
|
false
|
||||||
|
case .multiple:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
return documentPicker
|
return documentPicker
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +62,6 @@ struct DocumentPicker: UIViewControllerRepresentable {
|
|||||||
private static let loadingIndicatorIdentifier = "\(DocumentPicker.self)-Loading"
|
private static let loadingIndicatorIdentifier = "\(DocumentPicker.self)-Loading"
|
||||||
|
|
||||||
func documentPicker(_ picker: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
func documentPicker(_ picker: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
guard let url = urls.first else {
|
|
||||||
documentPicker.callback(.error(DocumentPickerError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
picker.delegate = nil
|
picker.delegate = nil
|
||||||
|
|
||||||
documentPicker.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading))
|
documentPicker.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading))
|
||||||
@@ -68,14 +69,19 @@ struct DocumentPicker: UIViewControllerRepresentable {
|
|||||||
documentPicker.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
documentPicker.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
var selectedURLs = [URL]()
|
||||||
_ = url.startAccessingSecurityScopedResource()
|
for url in urls.prefix(10) {
|
||||||
let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: url)
|
do {
|
||||||
url.stopAccessingSecurityScopedResource()
|
_ = url.startAccessingSecurityScopedResource()
|
||||||
documentPicker.callback(.selectFile(newURL))
|
let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: url)
|
||||||
} catch {
|
url.stopAccessingSecurityScopedResource()
|
||||||
documentPicker.callback(.error(error))
|
selectedURLs.append(newURL)
|
||||||
|
} catch {
|
||||||
|
documentPicker.callback(.error(error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
documentPicker.callback(.selectedMediaAtURLs(selectedURLs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,31 +7,45 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MediaPickerScreenMode: Hashable {
|
||||||
|
let source: MediaPickerScreenSource
|
||||||
|
let selectionType: MediaPickerScreenSelectionType
|
||||||
|
}
|
||||||
|
|
||||||
enum MediaPickerScreenSource {
|
enum MediaPickerScreenSource {
|
||||||
case camera
|
case camera
|
||||||
case photoLibrary
|
case photoLibrary
|
||||||
case documents
|
case documents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MediaPickerScreenSelectionType {
|
||||||
|
case single
|
||||||
|
case multiple
|
||||||
|
}
|
||||||
|
|
||||||
enum MediaPickerScreenCoordinatorAction {
|
enum MediaPickerScreenCoordinatorAction {
|
||||||
case selectMediaAtURL(URL)
|
case selectedMediaAtURLs([URL])
|
||||||
case cancel
|
case cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
||||||
private let orientationManager: OrientationManagerProtocol
|
private let orientationManager: OrientationManagerProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let source: MediaPickerScreenSource
|
private let mode: MediaPickerScreenMode
|
||||||
private let callback: (MediaPickerScreenCoordinatorAction) -> Void
|
private let callback: (MediaPickerScreenCoordinatorAction) -> Void
|
||||||
|
|
||||||
init(userIndicatorController: UserIndicatorControllerProtocol,
|
init(mode: MediaPickerScreenMode,
|
||||||
source: MediaPickerScreenSource,
|
appSettings: AppSettings,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
orientationManager: OrientationManagerProtocol,
|
orientationManager: OrientationManagerProtocol,
|
||||||
callback: @escaping (MediaPickerScreenCoordinatorAction) -> Void) {
|
callback: @escaping (MediaPickerScreenCoordinatorAction) -> Void) {
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.source = source
|
|
||||||
self.orientationManager = orientationManager
|
self.orientationManager = orientationManager
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
|
||||||
|
// The users of the media picker chose their ideal selection type leaving
|
||||||
|
// the feature flag to only be checked and enforced on this level.
|
||||||
|
self.mode = appSettings.multipleAttachmentUploadEnabled ? mode : .init(source: mode.source, selectionType: .single)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
@@ -39,51 +53,47 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
guard source == .camera else {
|
if mode.source == .camera {
|
||||||
return
|
orientationManager.setOrientation(.portrait)
|
||||||
|
orientationManager.lockOrientation(.portrait)
|
||||||
}
|
}
|
||||||
|
|
||||||
orientationManager.setOrientation(.portrait)
|
|
||||||
orientationManager.lockOrientation(.portrait)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
guard source == .camera else {
|
if mode.source == .camera {
|
||||||
return
|
orientationManager.lockOrientation(.all)
|
||||||
}
|
}
|
||||||
|
|
||||||
orientationManager.lockOrientation(.all)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mediaPicker: some View {
|
private var mediaPicker: some View {
|
||||||
switch source {
|
switch mode.source {
|
||||||
case .camera:
|
case .camera:
|
||||||
cameraPicker
|
cameraPicker
|
||||||
case .photoLibrary:
|
case .photoLibrary:
|
||||||
PhotoLibraryPicker(userIndicatorController: userIndicatorController) { [weak self] action in
|
PhotoLibraryPicker(selectionType: mode.selectionType, userIndicatorController: userIndicatorController) { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
self?.callback(.cancel)
|
self?.callback(.cancel)
|
||||||
case .error(let error):
|
case .error(let error):
|
||||||
MXLog.error("Failed selecting media from the photo library with error: \(error)")
|
MXLog.error("Failed selecting media from the photo library with error: \(error)")
|
||||||
self?.showError()
|
self?.showError()
|
||||||
case .selectFile(let url):
|
case .selectedMediaAtURLs(let urls):
|
||||||
self?.callback(.selectMediaAtURL(url))
|
self?.callback(.selectedMediaAtURLs(urls))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .documents:
|
case .documents:
|
||||||
// The document picker automatically dismisses everything on selection
|
// The document picker automatically dismisses everything on selection
|
||||||
// Strongly retain self in the callback to forward actions correctly
|
// Strongly retain self in the callback to forward actions correctly
|
||||||
DocumentPicker(userIndicatorController: userIndicatorController) { action in
|
DocumentPicker(selectionType: mode.selectionType, userIndicatorController: userIndicatorController) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
self.callback(.cancel)
|
self.callback(.cancel)
|
||||||
case .error(let error):
|
case .error(let error):
|
||||||
MXLog.error("Failed selecting media from the document picker with error: \(error)")
|
MXLog.error("Failed selecting media from the document picker with error: \(error)")
|
||||||
self.showError()
|
self.showError()
|
||||||
case .selectFile(let url):
|
case .selectedMediaAtURLs(let urls):
|
||||||
self.callback(.selectMediaAtURL(url))
|
self.callback(.selectedMediaAtURLs(urls))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +108,7 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
|||||||
MXLog.error("Failed selecting media from the camera picker with error: \(error)")
|
MXLog.error("Failed selecting media from the camera picker with error: \(error)")
|
||||||
self?.showError()
|
self?.showError()
|
||||||
case .selectFile(let url):
|
case .selectFile(let url):
|
||||||
self?.callback(.selectMediaAtURL(url))
|
self?.callback(.selectedMediaAtURLs([url]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(.black, ignoresSafeAreaEdges: .bottom)
|
.background(.black, ignoresSafeAreaEdges: .bottom)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import PhotosUI
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum PhotoLibraryPickerAction {
|
enum PhotoLibraryPickerAction {
|
||||||
case selectFile(URL)
|
case selectedMediaAtURLs([URL])
|
||||||
case cancel
|
case cancel
|
||||||
case error(PhotoLibraryPickerError)
|
case error(PhotoLibraryPickerError)
|
||||||
}
|
}
|
||||||
@@ -20,17 +20,27 @@ enum PhotoLibraryPickerError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
||||||
|
private let selectionType: MediaPickerScreenSelectionType
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let callback: (PhotoLibraryPickerAction) -> Void
|
private let callback: (PhotoLibraryPickerAction) -> Void
|
||||||
|
|
||||||
init(userIndicatorController: UserIndicatorControllerProtocol, callback: @escaping (PhotoLibraryPickerAction) -> Void) {
|
init(selectionType: MediaPickerScreenSelectionType,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
|
callback: @escaping (PhotoLibraryPickerAction) -> Void) {
|
||||||
|
self.selectionType = selectionType
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||||||
configuration.selectionLimit = 1
|
|
||||||
|
configuration.selectionLimit = switch selectionType {
|
||||||
|
case .single:
|
||||||
|
1
|
||||||
|
case .multiple:
|
||||||
|
10
|
||||||
|
}
|
||||||
|
|
||||||
let pickerViewController = PHPickerViewController(configuration: configuration)
|
let pickerViewController = PHPickerViewController(configuration: configuration)
|
||||||
pickerViewController.delegate = context.coordinator
|
pickerViewController.delegate = context.coordinator
|
||||||
@@ -56,38 +66,73 @@ struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
|||||||
private static let loadingIndicatorIdentifier = "\(PhotoLibraryPicker.self)-Loading"
|
private static let loadingIndicatorIdentifier = "\(PhotoLibraryPicker.self)-Loading"
|
||||||
|
|
||||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
guard let provider = results.first?.itemProvider,
|
guard !results.isEmpty else {
|
||||||
let contentType = provider.preferredContentType else {
|
|
||||||
photoLibraryPicker.callback(.cancel)
|
photoLibraryPicker.callback(.cancel)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
picker.delegate = nil
|
picker.delegate = nil
|
||||||
|
|
||||||
photoLibraryPicker.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading))
|
photoLibraryPicker.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
type: .modal,
|
||||||
|
title: L10n.commonLoading))
|
||||||
defer {
|
defer {
|
||||||
photoLibraryPicker.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
photoLibraryPicker.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.loadFileRepresentation(forTypeIdentifier: contentType.type.identifier) { [weak self] url, error in
|
Task {
|
||||||
guard let url else {
|
let selectedURLs = await withTaskGroup { taskGroup in
|
||||||
Task { @MainActor in
|
for result in results {
|
||||||
self?.photoLibraryPicker.callback(.error(.failedLoadingFileRepresentation(error)))
|
taskGroup.addTask { await self.processResult(result) }
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
var selectedURLs = [URL]()
|
||||||
|
for await url in taskGroup {
|
||||||
|
if let url {
|
||||||
|
selectedURLs.append(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedURLs
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
photoLibraryPicker.callback(.selectedMediaAtURLs(selectedURLs))
|
||||||
_ = url.startAccessingSecurityScopedResource()
|
}
|
||||||
let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: url)
|
}
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
|
// MARK: - Private
|
||||||
Task { @MainActor in
|
|
||||||
self?.photoLibraryPicker.callback(.selectFile(newURL))
|
func processResult(_ result: PHPickerResult) async -> URL? {
|
||||||
|
let provider = result.itemProvider
|
||||||
|
|
||||||
|
guard let contentType = provider.preferredContentType else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
provider.loadFileRepresentation(forTypeIdentifier: contentType.type.identifier) { [weak self] url, error in
|
||||||
|
guard let url else {
|
||||||
|
Task { @MainActor in
|
||||||
|
self?.photoLibraryPicker.callback(.error(.failedLoadingFileRepresentation(error)))
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resume(returning: nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
Task { @MainActor in
|
do {
|
||||||
self?.photoLibraryPicker.callback(.error(.failedCopyingFile))
|
_ = url.startAccessingSecurityScopedResource()
|
||||||
|
let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: url)
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
continuation.resume(returning: newURL)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Task { @MainActor in
|
||||||
|
self?.photoLibraryPicker.callback(.error(.failedCopyingFile))
|
||||||
|
continuation.resume(returning: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MediaUploadPreviewScreenCoordinatorParameters {
|
struct MediaUploadPreviewScreenCoordinatorParameters {
|
||||||
let url: URL
|
let mediaURLs: [URL]
|
||||||
let title: String?
|
let title: String?
|
||||||
let isRoomEncrypted: Bool
|
let isRoomEncrypted: Bool
|
||||||
let shouldShowCaptionWarning: Bool
|
let shouldShowCaptionWarning: Bool
|
||||||
@@ -33,7 +33,7 @@ final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(parameters: MediaUploadPreviewScreenCoordinatorParameters) {
|
init(parameters: MediaUploadPreviewScreenCoordinatorParameters) {
|
||||||
viewModel = MediaUploadPreviewScreenViewModel(url: parameters.url,
|
viewModel = MediaUploadPreviewScreenViewModel(mediaURLs: parameters.mediaURLs,
|
||||||
title: parameters.title,
|
title: parameters.title,
|
||||||
isRoomEncrypted: parameters.isRoomEncrypted,
|
isRoomEncrypted: parameters.isRoomEncrypted,
|
||||||
shouldShowCaptionWarning: parameters.shouldShowCaptionWarning,
|
shouldShowCaptionWarning: parameters.shouldShowCaptionWarning,
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ enum MediaUploadPreviewScreenViewModelAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct MediaUploadPreviewScreenViewState: BindableState {
|
struct MediaUploadPreviewScreenViewState: BindableState {
|
||||||
let url: URL
|
let mediaURLs: [URL]
|
||||||
let title: String?
|
let title: String?
|
||||||
let shouldShowCaptionWarning: Bool
|
let shouldShowCaptionWarning: Bool
|
||||||
let isRoomEncrypted: Bool
|
let isRoomEncrypted: Bool
|
||||||
|
|||||||
+26
-20
@@ -16,9 +16,9 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
|
|||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
||||||
private let url: URL
|
private var mediaURLs: [URL]
|
||||||
|
|
||||||
private var processingTask: Task<Result<MediaInfo, MediaUploadingPreprocessorError>, Never>
|
private var processingTask: Task<Result<[MediaInfo], MediaUploadingPreprocessorError>, Never>
|
||||||
private var requestHandle: SendAttachmentJoinHandleProtocol?
|
private var requestHandle: SendAttachmentJoinHandleProtocol?
|
||||||
private let clientProxy: ClientProxyProtocol
|
private let clientProxy: ClientProxyProtocol
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
|
|||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(url: URL,
|
init(mediaURLs: [URL],
|
||||||
title: String?,
|
title: String?,
|
||||||
isRoomEncrypted: Bool,
|
isRoomEncrypted: Bool,
|
||||||
shouldShowCaptionWarning: Bool,
|
shouldShowCaptionWarning: Bool,
|
||||||
@@ -36,16 +36,16 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
|
|||||||
timelineController: TimelineControllerProtocol,
|
timelineController: TimelineControllerProtocol,
|
||||||
clientProxy: ClientProxyProtocol,
|
clientProxy: ClientProxyProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.url = url
|
self.mediaURLs = mediaURLs
|
||||||
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
|
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
|
||||||
self.timelineController = timelineController
|
self.timelineController = timelineController
|
||||||
self.clientProxy = clientProxy
|
self.clientProxy = clientProxy
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
// Start processing the media whilst the user is reviewing it/adding a caption.
|
// Start processing the media whilst the user is reviewing it/adding a caption.
|
||||||
processingTask = Self.processMedia(at: url, preprocessor: mediaUploadingPreprocessor, clientProxy: clientProxy)
|
processingTask = Self.processMedia(at: mediaURLs, preprocessor: mediaUploadingPreprocessor, clientProxy: clientProxy)
|
||||||
|
|
||||||
super.init(initialViewState: MediaUploadPreviewScreenViewState(url: url,
|
super.init(initialViewState: MediaUploadPreviewScreenViewState(mediaURLs: mediaURLs,
|
||||||
title: title,
|
title: title,
|
||||||
shouldShowCaptionWarning: shouldShowCaptionWarning,
|
shouldShowCaptionWarning: shouldShowCaptionWarning,
|
||||||
isRoomEncrypted: isRoomEncrypted))
|
isRoomEncrypted: isRoomEncrypted))
|
||||||
@@ -53,7 +53,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
|
|||||||
|
|
||||||
override func process(viewAction: MediaUploadPreviewScreenViewAction) {
|
override func process(viewAction: MediaUploadPreviewScreenViewAction) {
|
||||||
// Get the current caption before all the processing starts.
|
// Get the current caption before all the processing starts.
|
||||||
let caption = state.bindings.caption.nonBlankString
|
var caption = state.bindings.caption.nonBlankString
|
||||||
|
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .send:
|
case .send:
|
||||||
@@ -62,26 +62,32 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
|
|||||||
Task {
|
Task {
|
||||||
defer { stopLoading() }
|
defer { stopLoading() }
|
||||||
|
|
||||||
|
var shouldDismissOnCompletion = true
|
||||||
switch await processingTask.value {
|
switch await processingTask.value {
|
||||||
case .success(let mediaInfo):
|
case .success(let mediaInfos):
|
||||||
switch await sendAttachment(mediaInfo: mediaInfo,
|
for mediaInfo in mediaInfos {
|
||||||
caption: caption) {
|
switch await sendAttachment(mediaInfo: mediaInfo, caption: caption) {
|
||||||
case .success:
|
case .success:
|
||||||
actionsSubject.send(.dismiss)
|
caption = nil // Set the caption only on the first uploaded file.
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
MXLog.error("Failed sending attachment with error: \(error)")
|
MXLog.error("Failed processing media to upload with error: \(error)")
|
||||||
showError(label: L10n.screenMediaUploadPreviewErrorFailedSending)
|
showError(label: L10n.screenMediaUploadPreviewErrorFailedProcessing)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .failure(.maxUploadSizeUnknown):
|
case .failure(.maxUploadSizeUnknown):
|
||||||
showAlert(.maxUploadSizeUnknown)
|
showAlert(.maxUploadSizeUnknown)
|
||||||
|
shouldDismissOnCompletion = false
|
||||||
case .failure(.maxUploadSizeExceeded(let limit)):
|
case .failure(.maxUploadSizeExceeded(let limit)):
|
||||||
showAlert(.maxUploadSizeExceeded(limit: limit))
|
showAlert(.maxUploadSizeExceeded(limit: limit))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
MXLog.error("Failed processing media to upload with error: \(error)")
|
MXLog.error("Failed processing media to upload with error: \(error)")
|
||||||
showError(label: L10n.screenMediaUploadPreviewErrorFailedProcessing)
|
showError(label: L10n.screenMediaUploadPreviewErrorFailedProcessing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldDismissOnCompletion {
|
||||||
|
actionsSubject.send(.dismiss)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .cancel:
|
case .cancel:
|
||||||
requestHandle?.cancel()
|
requestHandle?.cancel()
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
@@ -94,12 +100,12 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private static func processMedia(at url: URL,
|
private static func processMedia(at urls: [URL],
|
||||||
preprocessor: MediaUploadingPreprocessor,
|
preprocessor: MediaUploadingPreprocessor,
|
||||||
clientProxy: ClientProxyProtocol) -> Task<Result<MediaInfo, MediaUploadingPreprocessorError>, Never> {
|
clientProxy: ClientProxyProtocol) -> Task<Result<[MediaInfo], MediaUploadingPreprocessorError>, Never> {
|
||||||
Task {
|
Task {
|
||||||
guard case let .success(maxUploadSize) = await clientProxy.maxMediaUploadSize else { return .failure(.maxUploadSizeUnknown) }
|
guard case let .success(maxUploadSize) = await clientProxy.maxMediaUploadSize else { return .failure(.maxUploadSizeUnknown) }
|
||||||
return await preprocessor.processMedia(at: url, maxUploadSize: maxUploadSize)
|
return await preprocessor.processMedia(at: urls, maxUploadSize: maxUploadSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +170,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
|
|||||||
message: L10n.screenMediaUploadPreviewErrorCouldNotBeUploaded,
|
message: L10n.screenMediaUploadPreviewErrorCouldNotBeUploaded,
|
||||||
primaryButton: .init(title: L10n.actionTryAgain) { [weak self] in
|
primaryButton: .init(title: L10n.actionTryAgain) { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
processingTask = Self.processMedia(at: url, preprocessor: mediaUploadingPreprocessor, clientProxy: clientProxy)
|
processingTask = Self.processMedia(at: mediaURLs, preprocessor: mediaUploadingPreprocessor, clientProxy: clientProxy)
|
||||||
process(viewAction: .send)
|
process(viewAction: .send)
|
||||||
},
|
},
|
||||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel) { })
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel) { })
|
||||||
|
|||||||
+41
-8
@@ -5,6 +5,7 @@
|
|||||||
// Please see LICENSE files in the repository root for full details.
|
// Please see LICENSE files in the repository root for full details.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Compound
|
import Compound
|
||||||
import GameController
|
import GameController
|
||||||
import QuickLook
|
import QuickLook
|
||||||
@@ -16,6 +17,7 @@ struct MediaUploadPreviewScreen: View {
|
|||||||
@Bindable var context: MediaUploadPreviewScreenViewModel.Context
|
@Bindable var context: MediaUploadPreviewScreenViewModel.Context
|
||||||
|
|
||||||
@State private var captionWarningFrame: CGRect = .zero
|
@State private var captionWarningFrame: CGRect = .zero
|
||||||
|
@State private var currentIndex = 0
|
||||||
@FocusState private var isComposerFocussed
|
@FocusState private var isComposerFocussed
|
||||||
|
|
||||||
private var title: String { ProcessInfo.processInfo.isiOSAppOnMac ? context.viewState.title ?? "" : "" }
|
private var title: String { ProcessInfo.processInfo.isiOSAppOnMac ? context.viewState.title ?? "" : "" }
|
||||||
@@ -23,8 +25,15 @@ struct MediaUploadPreviewScreen: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
mainContent
|
mainContent
|
||||||
.id(context.viewState.url)
|
.id(context.viewState.mediaURLs)
|
||||||
.ignoresSafeArea(edges: [.horizontal])
|
.ignoresSafeArea(edges: [.horizontal])
|
||||||
|
.safeAreaInset(edge: .top) {
|
||||||
|
if context.viewState.mediaURLs.count > 1 {
|
||||||
|
Text("\(currentIndex + 1) / \(context.viewState.mediaURLs.count)")
|
||||||
|
.font(.compound.bodySM)
|
||||||
|
.foregroundColor(.compound.textSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||||
composer
|
composer
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
@@ -50,8 +59,9 @@ struct MediaUploadPreviewScreen: View {
|
|||||||
.foregroundColor(.compound.textSecondary)
|
.foregroundColor(.compound.textSecondary)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else {
|
} else {
|
||||||
PreviewView(fileURL: context.viewState.url,
|
PreviewView(mediaURLs: context.viewState.mediaURLs,
|
||||||
title: context.viewState.title)
|
title: context.viewState.title,
|
||||||
|
currentIndex: $currentIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,11 +164,12 @@ struct MediaUploadPreviewScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct PreviewView: UIViewControllerRepresentable {
|
private struct PreviewView: UIViewControllerRepresentable {
|
||||||
let fileURL: URL
|
let mediaURLs: [URL]
|
||||||
let title: String?
|
let title: String?
|
||||||
|
@Binding var currentIndex: Int
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIViewController {
|
func makeUIViewController(context: Context) -> UIViewController {
|
||||||
let previewController = PreviewViewController()
|
let previewController = PreviewViewController(currentIndex: $currentIndex)
|
||||||
previewController.dataSource = context.coordinator
|
previewController.dataSource = context.coordinator
|
||||||
previewController.delegate = context.coordinator
|
previewController.delegate = context.coordinator
|
||||||
|
|
||||||
@@ -185,11 +196,11 @@ private struct PreviewView: UIViewControllerRepresentable {
|
|||||||
// MARK: - QLPreviewControllerDataSource
|
// MARK: - QLPreviewControllerDataSource
|
||||||
|
|
||||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||||
1
|
view.mediaURLs.count
|
||||||
}
|
}
|
||||||
|
|
||||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||||
PreviewItem(previewItemURL: view.fileURL, previewItemTitle: view.title)
|
PreviewItem(previewItemURL: view.mediaURLs[index], previewItemTitle: view.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - QLPreviewControllerDelegate
|
// MARK: - QLPreviewControllerDelegate
|
||||||
@@ -211,6 +222,28 @@ private class PreviewItem: NSObject, QLPreviewItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class PreviewViewController: QLPreviewController {
|
private class PreviewViewController: QLPreviewController {
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
init(currentIndex: Binding<Int>) {
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
// Observation of currentPreviewItem doesn't work, so use the index instead.
|
||||||
|
publisher(for: \.currentPreviewItemIndex)
|
||||||
|
.sink { index in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if index != Int.max { // Because reasons
|
||||||
|
currentIndex.wrappedValue = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillLayoutSubviews() {
|
override func viewWillLayoutSubviews() {
|
||||||
super.viewWillLayoutSubviews()
|
super.viewWillLayoutSubviews()
|
||||||
|
|
||||||
@@ -228,7 +261,7 @@ struct MediaUploadPreviewScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
static let snapshotURL = URL.picturesDirectory
|
static let snapshotURL = URL.picturesDirectory
|
||||||
static let testURL = Bundle.main.url(forResource: "AppIcon60x60@2x", withExtension: "png")
|
static let testURL = Bundle.main.url(forResource: "AppIcon60x60@2x", withExtension: "png")
|
||||||
|
|
||||||
static let viewModel = MediaUploadPreviewScreenViewModel(url: snapshotURL,
|
static let viewModel = MediaUploadPreviewScreenViewModel(mediaURLs: [snapshotURL],
|
||||||
title: "App Icon.png",
|
title: "App Icon.png",
|
||||||
isRoomEncrypted: true,
|
isRoomEncrypted: true,
|
||||||
shouldShowCaptionWarning: true,
|
shouldShowCaptionWarning: true,
|
||||||
|
|||||||
+13
-6
@@ -16,6 +16,7 @@ struct RoomDetailsEditScreenCoordinatorParameters {
|
|||||||
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
let orientationManager: OrientationManagerProtocol
|
let orientationManager: OrientationManagerProtocol
|
||||||
|
let appSettings: AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomDetailsEditScreenCoordinatorAction {
|
enum RoomDetailsEditScreenCoordinatorAction {
|
||||||
@@ -49,9 +50,9 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .cancel, .saveFinished:
|
case .cancel, .saveFinished:
|
||||||
self?.actionsSubject.send(.dismiss)
|
self?.actionsSubject.send(.dismiss)
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
self?.displayMediaPickerWithSource(.camera)
|
self?.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single))
|
||||||
case .displayMediaPicker:
|
case .displayMediaPicker:
|
||||||
self?.displayMediaPickerWithSource(.photoLibrary)
|
self?.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -63,17 +64,23 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
|
private func displayMediaPickerWithMode(_ mode: MediaPickerScreenMode) {
|
||||||
let stackCoordinator = NavigationStackCoordinator()
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController,
|
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
|
||||||
source: source,
|
appSettings: parameters.appSettings,
|
||||||
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
orientationManager: parameters.orientationManager) { [weak self] action in
|
orientationManager: parameters.orientationManager) { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
case .selectMediaAtURL(let url):
|
case .selectedMediaAtURLs(let urls):
|
||||||
|
guard urls.count == 1,
|
||||||
|
let url = urls.first else {
|
||||||
|
fatalError("Received an invalid number of URLs")
|
||||||
|
}
|
||||||
|
|
||||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
viewModel.didSelectMediaUrl(url: url)
|
viewModel.didSelectMediaUrl(url: url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ enum ComposerToolbarViewModelAction {
|
|||||||
case editLastMessage
|
case editLastMessage
|
||||||
case attach(ComposerAttachmentType)
|
case attach(ComposerAttachmentType)
|
||||||
|
|
||||||
case handlePasteOrDrop(provider: NSItemProvider)
|
case handlePasteOrDrop(providers: [NSItemProvider])
|
||||||
|
|
||||||
case composerModeChanged(mode: ComposerMode)
|
case composerModeChanged(mode: ComposerMode)
|
||||||
case composerFocusedChanged(isFocused: Bool)
|
case composerFocusedChanged(isFocused: Bool)
|
||||||
@@ -46,7 +46,7 @@ enum ComposerToolbarViewAction {
|
|||||||
case cancelReply
|
case cancelReply
|
||||||
case cancelEdit
|
case cancelEdit
|
||||||
case attach(ComposerAttachmentType)
|
case attach(ComposerAttachmentType)
|
||||||
case handlePasteOrDrop(provider: NSItemProvider)
|
case handlePasteOrDrop(providers: [NSItemProvider])
|
||||||
case enableTextFormatting
|
case enableTextFormatting
|
||||||
case composerAction(action: ComposerAction)
|
case composerAction(action: ComposerAction)
|
||||||
case selectedSuggestion(_ suggestion: SuggestionItem)
|
case selectedSuggestion(_ suggestion: SuggestionItem)
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
|||||||
case .attach(let attachment):
|
case .attach(let attachment):
|
||||||
state.bindings.composerFocused = false
|
state.bindings.composerFocused = false
|
||||||
actionsSubject.send(.attach(attachment))
|
actionsSubject.send(.attach(attachment))
|
||||||
case .handlePasteOrDrop(let provider):
|
case .handlePasteOrDrop(let providers):
|
||||||
actionsSubject.send(.handlePasteOrDrop(provider: provider))
|
actionsSubject.send(.handlePasteOrDrop(providers: providers))
|
||||||
case .enableTextFormatting:
|
case .enableTextFormatting:
|
||||||
state.bindings.composerFormattingEnabled = true
|
state.bindings.composerFormattingEnabled = true
|
||||||
state.bindings.composerFocused = true
|
state.bindings.composerFocused = true
|
||||||
|
|||||||
@@ -181,8 +181,8 @@ struct ComposerToolbar: View {
|
|||||||
sendMessage()
|
sendMessage()
|
||||||
} editAction: {
|
} editAction: {
|
||||||
context.send(viewAction: .editLastMessage)
|
context.send(viewAction: .editLastMessage)
|
||||||
} pasteAction: { provider in
|
} pasteAction: { providers in
|
||||||
context.send(viewAction: .handlePasteOrDrop(provider: provider))
|
context.send(viewAction: .handlePasteOrDrop(providers: providers))
|
||||||
} cancellationAction: {
|
} cancellationAction: {
|
||||||
switch context.viewState.composerMode {
|
switch context.viewState.composerMode {
|
||||||
case .edit:
|
case .edit:
|
||||||
@@ -259,7 +259,7 @@ struct ComposerToolbar: View {
|
|||||||
viewModel: context.viewState.wysiwygViewModel,
|
viewModel: context.viewState.wysiwygViewModel,
|
||||||
itemProviderHelper: ItemProviderHelper(),
|
itemProviderHelper: ItemProviderHelper(),
|
||||||
keyCommands: context.viewState.keyCommands) { provider in
|
keyCommands: context.viewState.keyCommands) { provider in
|
||||||
context.send(viewAction: .handlePasteOrDrop(provider: provider))
|
context.send(viewAction: .handlePasteOrDrop(providers: [provider]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SwiftUI
|
|||||||
import WysiwygComposer
|
import WysiwygComposer
|
||||||
|
|
||||||
typealias GenericKeyHandler = (_ key: UIKeyboardHIDUsage) -> Void
|
typealias GenericKeyHandler = (_ key: UIKeyboardHIDUsage) -> Void
|
||||||
typealias PasteHandler = (NSItemProvider) -> Void
|
typealias PasteHandler = ([NSItemProvider]) -> Void
|
||||||
|
|
||||||
struct MessageComposer: View {
|
struct MessageComposer: View {
|
||||||
@Binding var plainComposerText: NSAttributedString
|
@Binding var plainComposerText: NSAttributedString
|
||||||
|
|||||||
+10
-10
@@ -190,8 +190,8 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
|||||||
textView.insertText("\n")
|
textView.insertText("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider) {
|
func textView(_ textView: UITextView, didReceivePasteWith providers: [NSItemProvider]) {
|
||||||
pasteHandler(provider)
|
pasteHandler(providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||||
@@ -207,7 +207,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
|||||||
private protocol ElementTextViewDelegate: AnyObject {
|
private protocol ElementTextViewDelegate: AnyObject {
|
||||||
func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView)
|
func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView)
|
||||||
func textViewDidReceiveKeyPress(_ textView: UITextView, key: UIKeyboardHIDUsage)
|
func textViewDidReceiveKeyPress(_ textView: UITextView, key: UIKeyboardHIDUsage)
|
||||||
func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider)
|
func textView(_ textView: UITextView, didReceivePasteWith providers: [NSItemProvider])
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
||||||
@@ -280,19 +280,19 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return UIPasteboard.general.itemProviders.first?.isSupportedForPasteOrDrop ?? false
|
return UIPasteboard.general.itemProviders.filter { !$0.isSupportedForPasteOrDrop }.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
override func paste(_ sender: Any?) {
|
override func paste(_ sender: Any?) {
|
||||||
guard let provider = UIPasteboard.general.itemProviders.first,
|
let providers = UIPasteboard.general.itemProviders
|
||||||
provider.isSupportedForPasteOrDrop else {
|
|
||||||
// If the item is not supported for media upload then
|
// Use the default behavior if there are any unsupported providers
|
||||||
// just try pasting its contents into the textfield
|
guard providers.filter({ !$0.isSupportedForPasteOrDrop }).isEmpty else {
|
||||||
super.paste(sender)
|
super.paste(sender)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
elementDelegate?.textView(self, didReceivePasteWith: provider)
|
elementDelegate?.textView(self, didReceivePasteWith: providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: PillAttachmentViewProviderDelegate
|
// MARK: PillAttachmentViewProviderDelegate
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ struct RoomScreenCoordinatorParameters {
|
|||||||
|
|
||||||
enum RoomScreenCoordinatorAction {
|
enum RoomScreenCoordinatorAction {
|
||||||
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
case presentMediaUploadPicker(MediaPickerScreenSource)
|
case presentMediaUploadPicker(mode: MediaPickerScreenMode)
|
||||||
case presentMediaUploadPreviewScreen(URL)
|
case presentMediaUploadPreviewScreen(mediaURLs: [URL])
|
||||||
case presentRoomDetails
|
case presentRoomDetails
|
||||||
case presentLocationPicker
|
case presentLocationPicker
|
||||||
case presentPollForm(mode: PollFormMode)
|
case presentPollForm(mode: PollFormMode)
|
||||||
@@ -121,19 +121,19 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .displayReportContent(let itemID, let senderID):
|
case .displayReportContent(let itemID, let senderID):
|
||||||
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
actionsSubject.send(.presentMediaUploadPicker(.camera))
|
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .camera, selectionType: .multiple)))
|
||||||
case .displayMediaPicker:
|
case .displayMediaPicker:
|
||||||
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary))
|
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple)))
|
||||||
case .displayDocumentPicker:
|
case .displayDocumentPicker:
|
||||||
actionsSubject.send(.presentMediaUploadPicker(.documents))
|
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents, selectionType: .multiple)))
|
||||||
case .displayMediaPreview(let mediaPreviewViewModel):
|
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||||
roomViewModel.displayMediaPreview(mediaPreviewViewModel)
|
roomViewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||||
case .displayLocationPicker:
|
case .displayLocationPicker:
|
||||||
actionsSubject.send(.presentLocationPicker)
|
actionsSubject.send(.presentLocationPicker)
|
||||||
case .displayPollForm(let mode):
|
case .displayPollForm(let mode):
|
||||||
actionsSubject.send(.presentPollForm(mode: mode))
|
actionsSubject.send(.presentPollForm(mode: mode))
|
||||||
case .displayMediaUploadPreviewScreen(let url):
|
case .displayMediaUploadPreviewScreen(let mediaURLs):
|
||||||
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
|
actionsSubject.send(.presentMediaUploadPreviewScreen(mediaURLs: mediaURLs))
|
||||||
case .displaySenderDetails(userID: let userID):
|
case .displaySenderDetails(userID: let userID):
|
||||||
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
|
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
|
||||||
case .displayMessageForwarding(let forwardingItem):
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
|
|||||||
+9
-5
@@ -38,15 +38,19 @@ enum DeveloperOptionsScreenViewAction {
|
|||||||
protocol DeveloperOptionsProtocol: AnyObject {
|
protocol DeveloperOptionsProtocol: AnyObject {
|
||||||
var logLevel: LogLevel { get set }
|
var logLevel: LogLevel { get set }
|
||||||
var traceLogPacks: Set<TraceLogPack> { get set }
|
var traceLogPacks: Set<TraceLogPack> { get set }
|
||||||
var publicSearchEnabled: Bool { get set }
|
|
||||||
var hideUnreadMessagesBadge: Bool { get set }
|
|
||||||
var fuzzyRoomListSearchEnabled: Bool { get set }
|
|
||||||
var enableOnlySignedDeviceIsolationMode: Bool { get set }
|
var enableOnlySignedDeviceIsolationMode: Bool { get set }
|
||||||
var enableKeyShareOnInvite: Bool { get set }
|
var enableKeyShareOnInvite: Bool { get set }
|
||||||
var elementCallBaseURLOverride: URL? { get set }
|
|
||||||
var knockingEnabled: Bool { get set }
|
|
||||||
var threadsEnabled: Bool { get set }
|
var threadsEnabled: Bool { get set }
|
||||||
var hideQuietNotificationAlerts: Bool { get set }
|
var hideQuietNotificationAlerts: Bool { get set }
|
||||||
|
var multipleAttachmentUploadEnabled: Bool { get set }
|
||||||
|
|
||||||
|
var hideUnreadMessagesBadge: Bool { get set }
|
||||||
|
var elementCallBaseURLOverride: URL? { get set }
|
||||||
|
|
||||||
|
var publicSearchEnabled: Bool { get set }
|
||||||
|
var fuzzyRoomListSearchEnabled: Bool { get set }
|
||||||
|
var knockingEnabled: Bool { get set }
|
||||||
var sharePosEnabled: Bool { get set }
|
var sharePosEnabled: Bool { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -52,6 +52,12 @@ struct DeveloperOptionsScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section("Timeline") {
|
||||||
|
Toggle(isOn: $context.multipleAttachmentUploadEnabled) {
|
||||||
|
Text("Allow selecting multiple attachments for upload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section("Join rules") {
|
Section("Join rules") {
|
||||||
Toggle(isOn: $context.knockingEnabled) {
|
Toggle(isOn: $context.knockingEnabled) {
|
||||||
Text("Knocking")
|
Text("Knocking")
|
||||||
|
|||||||
+15
-6
@@ -15,6 +15,7 @@ struct UserDetailsEditScreenCoordinatorParameters {
|
|||||||
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
||||||
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
let appSettings: AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
final class UserDetailsEditScreenCoordinator: CoordinatorProtocol {
|
final class UserDetailsEditScreenCoordinator: CoordinatorProtocol {
|
||||||
@@ -36,11 +37,11 @@ final class UserDetailsEditScreenCoordinator: CoordinatorProtocol {
|
|||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
self?.displayMediaPickerWithSource(.camera)
|
self?.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single))
|
||||||
case .displayMediaPicker:
|
case .displayMediaPicker:
|
||||||
self?.displayMediaPickerWithSource(.photoLibrary)
|
self?.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single))
|
||||||
case .displayFilePicker:
|
case .displayFilePicker:
|
||||||
self?.displayMediaPickerWithSource(.documents)
|
self?.displayMediaPickerWithMode(.init(source: .documents, selectionType: .single))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -52,15 +53,23 @@ final class UserDetailsEditScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
|
private func displayMediaPickerWithMode(_ mode: MediaPickerScreenMode) {
|
||||||
let stackCoordinator = NavigationStackCoordinator()
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, source: source, orientationManager: parameters.orientationManager) { [weak self] action in
|
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
|
||||||
|
appSettings: parameters.appSettings,
|
||||||
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
|
orientationManager: parameters.orientationManager) { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
case .selectMediaAtURL(let url):
|
case .selectedMediaAtURLs(let urls):
|
||||||
|
guard urls.count == 1,
|
||||||
|
let url = urls.first else {
|
||||||
|
fatalError("Received an invalid number of URLs")
|
||||||
|
}
|
||||||
|
|
||||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
viewModel.didSelectMediaURL(url: url)
|
viewModel.didSelectMediaURL(url: url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct StartChatScreenCoordinatorParameters {
|
|||||||
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
||||||
|
let appSettings: AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StartChatScreenCoordinatorAction {
|
enum StartChatScreenCoordinatorAction {
|
||||||
@@ -124,8 +125,8 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.createRoomParameters.send(details)
|
self.createRoomParameters.send(details)
|
||||||
case .openRoom(let identifier):
|
case .openRoom(let identifier):
|
||||||
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||||
case .displayMediaPickerWithSource(let source):
|
case .displayMediaPickerWithMode(let mode):
|
||||||
self.displayMediaPickerWithSource(source)
|
self.displayMediaPickerWithMode(mode)
|
||||||
case .removeImage:
|
case .removeImage:
|
||||||
var parameters = self.createRoomParameters.value
|
var parameters = self.createRoomParameters.value
|
||||||
parameters.avatarImageMedia = nil
|
parameters.avatarImageMedia = nil
|
||||||
@@ -139,15 +140,23 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
|
private func displayMediaPickerWithMode(_ mode: MediaPickerScreenMode) {
|
||||||
let stackCoordinator = NavigationStackCoordinator()
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, source: source, orientationManager: parameters.orientationManager) { [weak self] action in
|
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
|
||||||
|
appSettings: parameters.appSettings,
|
||||||
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
|
orientationManager: parameters.orientationManager) { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
case .selectMediaAtURL(let url):
|
case .selectedMediaAtURLs(let urls):
|
||||||
|
guard urls.count == 1,
|
||||||
|
let url = urls.first else {
|
||||||
|
fatalError("Received an invalid number of URLs")
|
||||||
|
}
|
||||||
|
|
||||||
processAvatar(from: url)
|
processAvatar(from: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ struct ThreadTimelineScreenCoordinatorParameters {
|
|||||||
|
|
||||||
enum ThreadTimelineScreenCoordinatorAction {
|
enum ThreadTimelineScreenCoordinatorAction {
|
||||||
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
case presentMediaUploadPicker(MediaPickerScreenSource)
|
case presentMediaUploadPicker(mode: MediaPickerScreenMode)
|
||||||
case presentMediaUploadPreviewScreen(url: URL)
|
case presentMediaUploadPreviewScreen(mediaURLs: [URL])
|
||||||
case presentLocationPicker
|
case presentLocationPicker
|
||||||
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
|
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
|
||||||
case presentPollForm(mode: PollFormMode)
|
case presentPollForm(mode: PollFormMode)
|
||||||
@@ -96,11 +96,11 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .displayReportContent(let itemID, let senderID):
|
case .displayReportContent(let itemID, let senderID):
|
||||||
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
actionsSubject.send(.presentMediaUploadPicker(.camera))
|
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .camera, selectionType: .multiple)))
|
||||||
case .displayMediaPicker:
|
case .displayMediaPicker:
|
||||||
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary))
|
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple)))
|
||||||
case .displayDocumentPicker:
|
case .displayDocumentPicker:
|
||||||
actionsSubject.send(.presentMediaUploadPicker(.documents))
|
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents, selectionType: .multiple)))
|
||||||
case .displayMediaPreview(let mediaPreviewViewModel):
|
case .displayMediaPreview(let mediaPreviewViewModel):
|
||||||
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||||
case .displayLocationPicker:
|
case .displayLocationPicker:
|
||||||
@@ -111,8 +111,8 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
description: description))
|
description: description))
|
||||||
case .displayPollForm(let mode):
|
case .displayPollForm(let mode):
|
||||||
actionsSubject.send(.presentPollForm(mode: mode))
|
actionsSubject.send(.presentPollForm(mode: mode))
|
||||||
case .displayMediaUploadPreviewScreen(let url):
|
case .displayMediaUploadPreviewScreen(let mediaURLs):
|
||||||
actionsSubject.send(.presentMediaUploadPreviewScreen(url: url))
|
actionsSubject.send(.presentMediaUploadPreviewScreen(mediaURLs: mediaURLs))
|
||||||
case .displaySenderDetails(userID: let userID):
|
case .displaySenderDetails(userID: let userID):
|
||||||
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
|
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
|
||||||
case .displayMessageForwarding(let forwardingItem):
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ enum TimelineInteractionHandlerAction {
|
|||||||
case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
||||||
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
case displayMessageForwarding(itemID: TimelineItemIdentifier)
|
case displayMessageForwarding(itemID: TimelineItemIdentifier)
|
||||||
case displayMediaUploadPreviewScreen(url: URL)
|
case displayMediaUploadPreviewScreen(mediaURLs: [URL])
|
||||||
case displayPollForm(mode: PollFormMode)
|
case displayPollForm(mode: PollFormMode)
|
||||||
|
|
||||||
case showActionMenu(TimelineItemActionMenuInfo)
|
case showActionMenu(TimelineItemActionMenuInfo)
|
||||||
@@ -277,7 +277,7 @@ class TimelineInteractionHandler {
|
|||||||
|
|
||||||
// MARK: Pasting and dropping
|
// MARK: Pasting and dropping
|
||||||
|
|
||||||
func handlePasteOrDrop(_ provider: NSItemProvider) {
|
func handlePasteOrDrop(_ providers: [NSItemProvider]) {
|
||||||
Task {
|
Task {
|
||||||
let loadingIndicatorIdentifier = UUID().uuidString
|
let loadingIndicatorIdentifier = UUID().uuidString
|
||||||
self.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
self.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
@@ -285,13 +285,19 @@ class TimelineInteractionHandler {
|
|||||||
self.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
self.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let fileURL = await provider.storeData() else {
|
var mediaURLs = [URL]()
|
||||||
MXLog.error("Failed storing NSItemProvider data \(provider)")
|
for provider in providers {
|
||||||
self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
|
if let fileURL = await provider.storeData() {
|
||||||
return
|
mediaURLs.append(fileURL)
|
||||||
|
} else {
|
||||||
|
MXLog.error("Failed storing NSItemProvider data \(provider)")
|
||||||
|
self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.actionsSubject.send(.displayMediaUploadPreviewScreen(url: fileURL))
|
if !mediaURLs.isEmpty {
|
||||||
|
self.actionsSubject.send(.displayMediaUploadPreviewScreen(mediaURLs: mediaURLs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ enum TimelineViewModelAction {
|
|||||||
case displayDocumentPicker
|
case displayDocumentPicker
|
||||||
case displayLocationPicker
|
case displayLocationPicker
|
||||||
case displayPollForm(mode: PollFormMode)
|
case displayPollForm(mode: PollFormMode)
|
||||||
case displayMediaUploadPreviewScreen(url: URL)
|
case displayMediaUploadPreviewScreen(mediaURLs: [URL])
|
||||||
case displaySenderDetails(userID: String)
|
case displaySenderDetails(userID: String)
|
||||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||||
case displayMediaPreview(TimelineMediaPreviewViewModel)
|
case displayMediaPreview(TimelineMediaPreviewViewModel)
|
||||||
@@ -63,7 +63,7 @@ enum TimelineViewAction {
|
|||||||
case displayReadReceipts(itemID: TimelineItemIdentifier)
|
case displayReadReceipts(itemID: TimelineItemIdentifier)
|
||||||
case displayThread(itemID: TimelineItemIdentifier)
|
case displayThread(itemID: TimelineItemIdentifier)
|
||||||
|
|
||||||
case handlePasteOrDrop(provider: NSItemProvider)
|
case handlePasteOrDrop(providers: [NSItemProvider])
|
||||||
case handlePollAction(TimelineViewPollAction)
|
case handlePollAction(TimelineViewPollAction)
|
||||||
case handleAudioPlayerAction(TimelineAudioPlayerAction)
|
case handleAudioPlayerAction(TimelineAudioPlayerAction)
|
||||||
|
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
displayReadReceipts(for: itemID)
|
displayReadReceipts(for: itemID)
|
||||||
case .displayThread(let itemID):
|
case .displayThread(let itemID):
|
||||||
actionsSubject.send(.displayThread(itemID: itemID))
|
actionsSubject.send(.displayThread(itemID: itemID))
|
||||||
case .handlePasteOrDrop(let provider):
|
case .handlePasteOrDrop(let providers):
|
||||||
timelineInteractionHandler.handlePasteOrDrop(provider)
|
timelineInteractionHandler.handlePasteOrDrop(providers)
|
||||||
case .handlePollAction(let pollAction):
|
case .handlePollAction(let pollAction):
|
||||||
handlePollAction(pollAction)
|
handlePollAction(pollAction)
|
||||||
case .handleAudioPlayerAction(let audioPlayerAction):
|
case .handleAudioPlayerAction(let audioPlayerAction):
|
||||||
@@ -231,8 +231,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
editLastMessage()
|
editLastMessage()
|
||||||
case .attach(let attachment):
|
case .attach(let attachment):
|
||||||
attach(attachment)
|
attach(attachment)
|
||||||
case .handlePasteOrDrop(let provider):
|
case .handlePasteOrDrop(let providers):
|
||||||
timelineInteractionHandler.handlePasteOrDrop(provider)
|
timelineInteractionHandler.handlePasteOrDrop(providers)
|
||||||
case .composerModeChanged(mode: let mode):
|
case .composerModeChanged(mode: let mode):
|
||||||
trackComposerMode(mode)
|
trackComposerMode(mode)
|
||||||
case .composerFocusedChanged(isFocused: let isFocused):
|
case .composerFocusedChanged(isFocused: let isFocused):
|
||||||
@@ -480,8 +480,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
actionsSubject.send(.displayPollForm(mode: mode))
|
actionsSubject.send(.displayPollForm(mode: mode))
|
||||||
case .displayReportContent(let itemID, let senderID):
|
case .displayReportContent(let itemID, let senderID):
|
||||||
actionsSubject.send(.displayReportContent(itemID: itemID, senderID: senderID))
|
actionsSubject.send(.displayReportContent(itemID: itemID, senderID: senderID))
|
||||||
case .displayMediaUploadPreviewScreen(let url):
|
case .displayMediaUploadPreviewScreen(let mediaURLs):
|
||||||
actionsSubject.send(.displayMediaUploadPreviewScreen(url: url))
|
actionsSubject.send(.displayMediaUploadPreviewScreen(mediaURLs: mediaURLs))
|
||||||
case .showActionMenu(let actionMenuInfo):
|
case .showActionMenu(let actionMenuInfo):
|
||||||
self.state.bindings.actionMenuInfo = actionMenuInfo
|
self.state.bindings.actionMenuInfo = actionMenuInfo
|
||||||
case .showDebugInfo(let debugInfo):
|
case .showDebugInfo(let debugInfo):
|
||||||
|
|||||||
@@ -55,12 +55,13 @@ struct TimelineView: View {
|
|||||||
.environmentObject(timelineContext)
|
.environmentObject(timelineContext)
|
||||||
}
|
}
|
||||||
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
|
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
|
||||||
guard let provider = providers.first,
|
let supportedProviders = providers.filter(\.isSupportedForPasteOrDrop)
|
||||||
provider.isSupportedForPasteOrDrop else {
|
|
||||||
|
guard !supportedProviders.isEmpty else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
timelineContext.send(viewAction: .handlePasteOrDrop(provider: provider))
|
timelineContext.send(viewAction: .handlePasteOrDrop(providers: supportedProviders))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,33 @@ struct MediaUploadingPreprocessor {
|
|||||||
static let videoThumbnailTime = 5.0 // seconds
|
static let videoThumbnailTime = 5.0 // seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Processes media at the given URLs. It will generate thumbnails for images and videos, convert videos to 1080p mp4, strip GPS locations
|
||||||
|
/// from images and retrieve associated media information
|
||||||
|
/// - Parameter urls: the file URL
|
||||||
|
/// - Returns: a collection of results containing specific type of `MediaInfo` depending on the file type
|
||||||
|
/// and its associated details or any resulting error
|
||||||
|
func processMedia(at urls: [URL], maxUploadSize: UInt) async -> Result<[MediaInfo], MediaUploadingPreprocessorError> {
|
||||||
|
await withTaskGroup { taskGroup in
|
||||||
|
for url in urls {
|
||||||
|
taskGroup.addTask {
|
||||||
|
await processMedia(at: url, maxUploadSize: maxUploadSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaInfos = [MediaInfo]()
|
||||||
|
for await result in taskGroup {
|
||||||
|
switch result {
|
||||||
|
case .success(let mediaInfo):
|
||||||
|
mediaInfos.append(mediaInfo)
|
||||||
|
case .failure(let error):
|
||||||
|
return .failure(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .success(mediaInfos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Processes media at a given URL. It will generate thumbnails for images and videos, convert videos to 1080p mp4, strip GPS locations
|
/// Processes media at a given URL. It will generate thumbnails for images and videos, convert videos to 1080p mp4, strip GPS locations
|
||||||
/// from images and retrieve associated media information
|
/// from images and retrieve associated media information
|
||||||
/// - Parameter url: the file URL
|
/// - Parameter url: the file URL
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ enum ShareExtensionConstants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ShareExtensionPayload: Hashable, Codable {
|
enum ShareExtensionPayload: Hashable, Codable {
|
||||||
case mediaFile(roomID: String?, mediaFile: ShareExtensionMediaFile)
|
case mediaFiles(roomID: String?, mediaFiles: [ShareExtensionMediaFile])
|
||||||
case text(roomID: String?, text: String)
|
case text(roomID: String?, text: String)
|
||||||
|
|
||||||
var roomID: String? {
|
var roomID: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .mediaFile(let roomID, _),
|
case .mediaFiles(let roomID, _),
|
||||||
.text(let roomID, _):
|
.text(let roomID, _):
|
||||||
roomID
|
roomID
|
||||||
}
|
}
|
||||||
@@ -27,14 +27,16 @@ enum ShareExtensionPayload: Hashable, Codable {
|
|||||||
/// system's `temporaryDirectory` returning a modified payload with updated file URLs.
|
/// system's `temporaryDirectory` returning a modified payload with updated file URLs.
|
||||||
func withDefaultTemporaryDirectory() throws -> Self {
|
func withDefaultTemporaryDirectory() throws -> Self {
|
||||||
switch self {
|
switch self {
|
||||||
case .mediaFile(let roomID, let mediaFile):
|
case .mediaFiles(let roomID, let mediaFiles):
|
||||||
let path = mediaFile.url.path.replacing(URL.appGroupTemporaryDirectory.path, with: "").trimmingPrefix("/")
|
return try .mediaFiles(roomID: roomID, mediaFiles: mediaFiles.map { mediaFile in
|
||||||
let newURL = URL.temporaryDirectory.appending(path: path)
|
let path = mediaFile.url.path.replacing(URL.appGroupTemporaryDirectory.path, with: "").trimmingPrefix("/")
|
||||||
|
let newURL = URL.temporaryDirectory.appending(path: path)
|
||||||
try? FileManager.default.removeItem(at: newURL)
|
|
||||||
try FileManager.default.moveItem(at: mediaFile.url, to: newURL)
|
try? FileManager.default.removeItem(at: newURL)
|
||||||
|
try FileManager.default.moveItem(at: mediaFile.url, to: newURL)
|
||||||
return .mediaFile(roomID: roomID, mediaFile: mediaFile.replacingURL(with: newURL))
|
|
||||||
|
return mediaFile.replacingURL(with: newURL)
|
||||||
|
})
|
||||||
case .text:
|
case .text:
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -639,7 +639,8 @@ class MockScreen: Identifiable {
|
|||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
navigationStackCoordinator: navigationStackCoordinator,
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
userDiscoveryService: userDiscoveryMock,
|
userDiscoveryService: userDiscoveryMock,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings))
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
let coordinator = StartChatScreenCoordinator(parameters: parameters)
|
let coordinator = StartChatScreenCoordinator(parameters: parameters)
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
@@ -654,7 +655,8 @@ class MockScreen: Identifiable {
|
|||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
navigationStackCoordinator: navigationStackCoordinator,
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
userDiscoveryService: userDiscoveryMock,
|
userDiscoveryService: userDiscoveryMock,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings)))
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
|
||||||
|
appSettings: ServiceLocator.shared.settings))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .createRoom:
|
case .createRoom:
|
||||||
|
|||||||
@@ -63,23 +63,32 @@ class ShareExtensionViewController: UIViewController {
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func prepareSharePayload() async -> ShareExtensionPayload? {
|
private func prepareSharePayload() async -> ShareExtensionPayload? {
|
||||||
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
|
guard let extensionContext,
|
||||||
let itemProvider = extensionItem.attachments?.first else {
|
let extensionItem = extensionContext.inputItems.first as? NSExtensionItem,
|
||||||
|
let itemProviders = extensionItem.attachments else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier
|
let roomID = (extensionContext.intent as? INSendMessageIntent)?.conversationIdentifier
|
||||||
|
|
||||||
if let fileURL = await itemProvider.storeData(withinAppGroupContainer: true) {
|
var mediaFiles = [ShareExtensionMediaFile]()
|
||||||
return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
|
for itemProvider in itemProviders {
|
||||||
} else if let url = await itemProvider.loadTransferable(type: URL.self) {
|
if let fileURL = await itemProvider.storeData(withinAppGroupContainer: true) {
|
||||||
return .text(roomID: roomID, text: url.absoluteString)
|
mediaFiles.append(.init(url: fileURL, suggestedName: fileURL.lastPathComponent))
|
||||||
} else if let string = await itemProvider.loadString() {
|
} else if let url = await itemProvider.loadTransferable(type: URL.self) {
|
||||||
return .text(roomID: roomID, text: string)
|
return .text(roomID: roomID, text: url.absoluteString)
|
||||||
} else {
|
} else if let string = await itemProvider.loadString() {
|
||||||
MXLog.error("Failed loading NSItemProvider data: \(itemProvider)")
|
return .text(roomID: roomID, text: string)
|
||||||
return nil
|
} else {
|
||||||
|
MXLog.error("Failed loading NSItemProvider data: \(itemProvider)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !mediaFiles.isEmpty {
|
||||||
|
return .mediaFiles(roomID: roomID, mediaFiles: mediaFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openMainApp(payload: ShareExtensionPayload) async {
|
private func openMainApp(payload: ShareExtensionPayload) async {
|
||||||
|
|||||||
@@ -31,15 +31,15 @@
|
|||||||
<key>NSExtensionActivationRule</key>
|
<key>NSExtensionActivationRule</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>10</integer>
|
||||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>10</integer>
|
||||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>10</integer>
|
||||||
<key>NSExtensionActivationSupportsText</key>
|
<key>NSExtensionActivationSupportsText</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>10</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ targets:
|
|||||||
NSExtensionAttributes:
|
NSExtensionAttributes:
|
||||||
IntentsSupported: [INSendMessageIntent]
|
IntentsSupported: [INSendMessageIntent]
|
||||||
NSExtensionActivationRule:
|
NSExtensionActivationRule:
|
||||||
NSExtensionActivationSupportsFileWithMaxCount: 1
|
NSExtensionActivationSupportsFileWithMaxCount: 10
|
||||||
NSExtensionActivationSupportsImageWithMaxCount: 1
|
NSExtensionActivationSupportsImageWithMaxCount: 10
|
||||||
NSExtensionActivationSupportsMovieWithMaxCount: 1
|
NSExtensionActivationSupportsMovieWithMaxCount: 10
|
||||||
NSExtensionActivationSupportsText: true
|
NSExtensionActivationSupportsText: true
|
||||||
NSExtensionActivationSupportsWebURLWithMaxCount: 1
|
NSExtensionActivationSupportsWebURLWithMaxCount: 10
|
||||||
|
|
||||||
entitlements:
|
entitlements:
|
||||||
path: ShareExtension.entitlements
|
path: ShareExtension.entitlements
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
|||||||
clientProxy.underlyingMaxMediaUploadSize = maxUploadSizeResult
|
clientProxy.underlyingMaxMediaUploadSize = maxUploadSizeResult
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel = MediaUploadPreviewScreenViewModel(url: url,
|
viewModel = MediaUploadPreviewScreenViewModel(mediaURLs: [url],
|
||||||
title: "Some File",
|
title: "Some File",
|
||||||
isRoomEncrypted: true,
|
isRoomEncrypted: true,
|
||||||
shouldShowCaptionWarning: true,
|
shouldShowCaptionWarning: true,
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||||
|
|
||||||
let sharePayload: ShareExtensionPayload = .mediaFile(roomID: "1", mediaFile: .init(url: .picturesDirectory, suggestedName: nil))
|
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "1", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||||
try await process(route: .share(sharePayload))
|
try await process(route: .share(sharePayload))
|
||||||
|
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
|||||||
try await process(route: .settings, expectedState: .settingsScreen(roomListSelectedRoomID: nil))
|
try await process(route: .settings, expectedState: .settingsScreen(roomListSelectedRoomID: nil))
|
||||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||||
|
|
||||||
let sharePayload: ShareExtensionPayload = .mediaFile(roomID: nil, mediaFile: .init(url: .picturesDirectory, suggestedName: nil))
|
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: nil, mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||||
try await process(route: .share(sharePayload),
|
try await process(route: .share(sharePayload),
|
||||||
expectedState: .shareExtensionRoomList(sharePayload: sharePayload))
|
expectedState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
|||||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1"))
|
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1"))
|
||||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
|
||||||
let sharePayload: ShareExtensionPayload = .mediaFile(roomID: "2", mediaFile: .init(url: .picturesDirectory, suggestedName: nil))
|
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "2", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||||
try await process(route: .share(sharePayload),
|
try await process(route: .share(sharePayload),
|
||||||
expectedState: .roomList(roomListSelectedRoomID: "2"))
|
expectedState: .roomList(roomListSelectedRoomID: "2"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user