From 53a54c1b90ee23ad388fadaa25c724cd3ad6b62f Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Sat, 25 Apr 2026 11:46:40 +0300 Subject: [PATCH] Allow the mac app to chose room avatars directly from files instead of the photo library --- .../Localizations/en-US.lproj/Localizable.strings | 1 + .../Localizations/en.lproj/Localizable.strings | 1 + ElementX/Sources/Generated/Strings.swift | 2 ++ ElementX/Sources/Other/AccessibilityIdentifiers.swift | 1 + .../CreateRoomScreenCoordinator.swift | 2 ++ .../CreateRoomScreen/CreateRoomScreenModels.swift | 2 ++ .../CreateRoomScreen/CreateRoomScreenViewModel.swift | 2 ++ .../CreateRoomScreen/View/CreateRoomScreen.swift | 6 ++++++ .../Screens/MediaPickerScreen/DocumentPicker.swift | 6 +++++- .../MediaPickerScreenCoordinator.swift | 11 +++++++---- .../RoomDetailsEditScreenCoordinator.swift | 2 ++ .../RoomDetailsEditScreenModels.swift | 2 ++ .../RoomDetailsEditScreenViewModel.swift | 2 ++ .../View/RoomDetailsEditScreen.swift | 8 ++++++++ .../Screens/RoomScreen/RoomScreenCoordinator.swift | 2 +- .../UserDetailsEditScreenCoordinator.swift | 2 +- .../UserDetailsEditScreenModels.swift | 1 + .../UserDetailsEditScreenViewModel.swift | 2 ++ .../View/UserDetailsEditScreen.swift | 7 +++++++ .../ThreadTimelineScreenCoordinator.swift | 2 +- 20 files changed, 56 insertions(+), 8 deletions(-) diff --git a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings index 31753e3a1..2e3f15c74 100644 --- a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings @@ -63,6 +63,7 @@ "action_call" = "Call"; "action_cancel" = "Cancel"; "action_cancel_for_now" = "Cancel for now"; +"action_choose_file" = "Choose file"; "action_choose_photo" = "Choose photo"; "action_clear" = "Clear"; "action_close" = "Close"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 21b307e6a..439af9a79 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -63,6 +63,7 @@ "action_call" = "Call"; "action_cancel" = "Cancel"; "action_cancel_for_now" = "Cancel for now"; +"action_choose_file" = "Choose file"; "action_choose_photo" = "Choose photo"; "action_clear" = "Clear"; "action_close" = "Close"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 5c1712f65..d15e6da88 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -160,6 +160,8 @@ internal enum L10n { internal static var actionCancel: String { return L10n.tr("Localizable", "action_cancel") } /// Cancel for now internal static var actionCancelForNow: String { return L10n.tr("Localizable", "action_cancel_for_now") } + /// Choose file + internal static var actionChooseFile: String { return L10n.tr("Localizable", "action_choose_file") } /// Choose photo internal static var actionChoosePhoto: String { return L10n.tr("Localizable", "action_choose_photo") } /// Clear diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 062a1a2ac..58ae5ae30 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -281,6 +281,7 @@ enum A11yIdentifiers { let roomName = "create_room-room_name" let roomTopic = "create_room-room_topic" let mediaPicker = "create_room-media_picker" + let filePicker = "create_room-file_picker" } struct PollFormScreen { diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift index 8b6bf76e0..9ad7d2fcb 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift @@ -54,6 +54,8 @@ final class CreateRoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single))) case .displayMediaPicker: actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single))) + case .displayFilePicker: + actionsSubject.send(.displayMediaPickerWithMode(.init(source: .documents(types: [.image]), selectionType: .single))) case .dismiss: actionsSubject.send(.dismiss) } diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift index ccf201fac..1a34c4f0a 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift @@ -21,6 +21,7 @@ enum CreateRoomScreenViewModelAction { case createdRoom(JoinedRoomProxyProtocol, SpaceRoomListProxyProtocol?) case displayMediaPicker case displayCameraPicker + case displayFilePicker case dismiss } @@ -114,6 +115,7 @@ enum CreateRoomScreenViewAction { case createRoom case displayCameraPicker case displayMediaPicker + case displayFilePicker case removeImage case updateRoomName(String) case updateAliasLocalPart(String) diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift index c312ed812..497a0db53 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift @@ -90,6 +90,8 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen actionsSubject.send(.displayCameraPicker) case .displayMediaPicker: actionsSubject.send(.displayMediaPicker) + case .displayFilePicker: + actionsSubject.send(.displayFilePicker) case .removeImage: state.avatarMediaInfo = nil case .updateAliasLocalPart(let aliasLocalPart): diff --git a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift index a3a451ea9..134700f24 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift @@ -139,11 +139,17 @@ struct CreateRoomScreen: View { Button(L10n.actionTakePhoto) { context.send(viewAction: .displayCameraPicker) } + Button(L10n.actionChoosePhoto) { context.send(viewAction: .displayMediaPicker) } .accessibilityIdentifier(A11yIdentifiers.createRoomScreen.mediaPicker) + Button(L10n.actionChooseFile) { + context.send(viewAction: .displayFilePicker) + } + .accessibilityIdentifier(A11yIdentifiers.createRoomScreen.filePicker) + if context.viewState.avatarImage != nil { Button(L10n.actionRemove, role: .destructive) { context.send(viewAction: .removeImage) diff --git a/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift b/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift index 80e60b12b..6af8caf93 100644 --- a/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift +++ b/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift @@ -7,6 +7,7 @@ // import SwiftUI +import UniformTypeIdentifiers enum DocumentPickerAction { case selectedMediaAtURLs([URL]) @@ -16,19 +17,22 @@ enum DocumentPickerAction { struct DocumentPicker: UIViewControllerRepresentable { private let selectionType: MediaPickerScreenSelectionType + private let contentTypes: [UTType] private let userIndicatorController: UserIndicatorControllerProtocol private let callback: (DocumentPickerAction) -> Void init(selectionType: MediaPickerScreenSelectionType, + contentTypes: [UTType], userIndicatorController: UserIndicatorControllerProtocol, callback: @escaping (DocumentPickerAction) -> Void) { self.selectionType = selectionType + self.contentTypes = contentTypes self.userIndicatorController = userIndicatorController self.callback = callback } func makeUIViewController(context: Context) -> UIDocumentPickerViewController { - let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.data]) + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes) documentPicker.delegate = context.coordinator documentPicker.allowsMultipleSelection = switch selectionType { diff --git a/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift b/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift index e9a5d136d..2a357f0a5 100644 --- a/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift @@ -7,16 +7,17 @@ // import SwiftUI +import UniformTypeIdentifiers struct MediaPickerScreenMode: Hashable { let source: MediaPickerScreenSource let selectionType: MediaPickerScreenSelectionType } -enum MediaPickerScreenSource { +enum MediaPickerScreenSource: Hashable { case camera case photoLibrary - case documents + case documents(types: [UTType] = [.data]) } enum MediaPickerScreenSelectionType { @@ -79,10 +80,12 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol { self?.callback(.selectedMediaAtURLs(urls)) } } - case .documents: + case .documents(let types): // The document picker automatically dismisses everything on selection // Strongly retain self in the callback to forward actions correctly - DocumentPicker(selectionType: mode.selectionType, userIndicatorController: userIndicatorController) { action in + DocumentPicker(selectionType: mode.selectionType, + contentTypes: types, + userIndicatorController: userIndicatorController) { action in switch action { case .cancel: self.callback(.cancel) diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift index 89ba1cf61..43d5a9e98 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift @@ -52,6 +52,8 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol { self?.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)) case .displayMediaPicker: self?.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)) + case .displayFilePicker: + self?.displayMediaPickerWithMode(.init(source: .documents(types: [.image]), selectionType: .single)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift index dc7ba1d48..f67987410 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift @@ -13,6 +13,7 @@ enum RoomDetailsEditScreenViewModelAction { case saveFinished case displayCameraPicker case displayMediaPicker + case displayFilePicker } struct RoomDetailsEditScreenViewState: BindableState { @@ -81,5 +82,6 @@ enum RoomDetailsEditScreenViewAction { case presentMediaSource case displayCameraPicker case displayMediaPicker + case displayFilePicker case removeImage } diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift index a7c57bcb5..d5503f7e7 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift @@ -69,6 +69,8 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe actionsSubject.send(.displayCameraPicker) case .displayMediaPicker: actionsSubject.send(.displayMediaPicker) + case .displayFilePicker: + actionsSubject.send(.displayFilePicker) case .removeImage: state.avatarURL = nil state.localMedia = nil diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift index 6c1aba165..6bca24ecb 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift @@ -119,11 +119,19 @@ struct RoomDetailsEditScreen: View { } label: { Text(L10n.actionTakePhoto) } + Button { context.send(viewAction: .displayMediaPicker) } label: { Text(L10n.actionChoosePhoto) } + + Button { + context.send(viewAction: .displayFilePicker) + } label: { + Text(L10n.actionChooseFile) + } + if context.viewState.showDeleteImageAction { Button(role: .destructive) { context.send(viewAction: .removeImage) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index ce6dd7c50..36c1eb19c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -126,7 +126,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { case .displayMediaPicker: actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple))) case .displayDocumentPicker: - actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents, selectionType: .multiple))) + actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents(), selectionType: .multiple))) case .displayMediaPreview(let mediaPreviewViewModel): roomViewModel.displayMediaPreview(mediaPreviewViewModel) case .displayLocationPicker: diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift index 05ffec2aa..7c30fe451 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift @@ -53,7 +53,7 @@ final class UserDetailsEditScreenCoordinator: CoordinatorProtocol { case .displayMediaPicker: displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)) case .displayFilePicker: - displayMediaPickerWithMode(.init(source: .documents, selectionType: .single)) + displayMediaPickerWithMode(.init(source: .documents(types: [.image]), selectionType: .single)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift index 40922c05f..5ec6ccb81 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift @@ -67,5 +67,6 @@ enum UserDetailsEditScreenViewAction { case presentMediaSource case displayCameraPicker case displayMediaPicker + case displayFilePicker case removeImage } diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift index c74c07e36..dc2e7bf42 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift @@ -77,6 +77,8 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe actionsSubject.send(.displayCameraPicker) case .displayMediaPicker: actionsSubject.send(.displayMediaPicker) + case .displayFilePicker: + actionsSubject.send(.displayFilePicker) case .removeImage: state.localMedia = nil state.selectedAvatarURL = nil diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift index 60de80a9e..b79fb74bf 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift @@ -96,12 +96,19 @@ struct UserDetailsEditScreen: View { } label: { Text(L10n.actionTakePhoto) } + Button { context.send(viewAction: .displayMediaPicker) } label: { Text(L10n.actionChoosePhoto) } + Button { + context.send(viewAction: .displayFilePicker) + } label: { + Text(L10n.actionChooseFile) + } + if context.viewState.showDeleteImageAction { Button(role: .destructive) { context.send(viewAction: .removeImage) diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift index fd8413303..eb8920bbf 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift @@ -114,7 +114,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { case .displayMediaPicker: actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple))) case .displayDocumentPicker: - actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents, selectionType: .multiple))) + actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents(), selectionType: .multiple))) case .displayMediaPreview(let mediaPreviewViewModel): viewModel.displayMediaPreview(mediaPreviewViewModel) case .displayLocationPicker: