Allow the mac app to chose room avatars directly from files instead of the photo library

This commit is contained in:
Stefan Ceriu
2026-04-25 11:46:40 +03:00
parent 0ec114d2f3
commit 53a54c1b90
20 changed files with 56 additions and 8 deletions
@@ -63,6 +63,7 @@
"action_call" = "Call"; "action_call" = "Call";
"action_cancel" = "Cancel"; "action_cancel" = "Cancel";
"action_cancel_for_now" = "Cancel for now"; "action_cancel_for_now" = "Cancel for now";
"action_choose_file" = "Choose file";
"action_choose_photo" = "Choose photo"; "action_choose_photo" = "Choose photo";
"action_clear" = "Clear"; "action_clear" = "Clear";
"action_close" = "Close"; "action_close" = "Close";
@@ -63,6 +63,7 @@
"action_call" = "Call"; "action_call" = "Call";
"action_cancel" = "Cancel"; "action_cancel" = "Cancel";
"action_cancel_for_now" = "Cancel for now"; "action_cancel_for_now" = "Cancel for now";
"action_choose_file" = "Choose file";
"action_choose_photo" = "Choose photo"; "action_choose_photo" = "Choose photo";
"action_clear" = "Clear"; "action_clear" = "Clear";
"action_close" = "Close"; "action_close" = "Close";
+2
View File
@@ -160,6 +160,8 @@ internal enum L10n {
internal static var actionCancel: String { return L10n.tr("Localizable", "action_cancel") } internal static var actionCancel: String { return L10n.tr("Localizable", "action_cancel") }
/// Cancel for now /// Cancel for now
internal static var actionCancelForNow: String { return L10n.tr("Localizable", "action_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 /// Choose photo
internal static var actionChoosePhoto: String { return L10n.tr("Localizable", "action_choose_photo") } internal static var actionChoosePhoto: String { return L10n.tr("Localizable", "action_choose_photo") }
/// Clear /// Clear
@@ -281,6 +281,7 @@ enum A11yIdentifiers {
let roomName = "create_room-room_name" let roomName = "create_room-room_name"
let roomTopic = "create_room-room_topic" let roomTopic = "create_room-room_topic"
let mediaPicker = "create_room-media_picker" let mediaPicker = "create_room-media_picker"
let filePicker = "create_room-file_picker"
} }
struct PollFormScreen { struct PollFormScreen {
@@ -54,6 +54,8 @@ final class CreateRoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single))) actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)))
case .displayMediaPicker: case .displayMediaPicker:
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single))) actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)))
case .displayFilePicker:
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .documents(types: [.image]), selectionType: .single)))
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} }
@@ -21,6 +21,7 @@ enum CreateRoomScreenViewModelAction {
case createdRoom(JoinedRoomProxyProtocol, SpaceRoomListProxyProtocol?) case createdRoom(JoinedRoomProxyProtocol, SpaceRoomListProxyProtocol?)
case displayMediaPicker case displayMediaPicker
case displayCameraPicker case displayCameraPicker
case displayFilePicker
case dismiss case dismiss
} }
@@ -114,6 +115,7 @@ enum CreateRoomScreenViewAction {
case createRoom case createRoom
case displayCameraPicker case displayCameraPicker
case displayMediaPicker case displayMediaPicker
case displayFilePicker
case removeImage case removeImage
case updateRoomName(String) case updateRoomName(String)
case updateAliasLocalPart(String) case updateAliasLocalPart(String)
@@ -90,6 +90,8 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
actionsSubject.send(.displayCameraPicker) actionsSubject.send(.displayCameraPicker)
case .displayMediaPicker: case .displayMediaPicker:
actionsSubject.send(.displayMediaPicker) actionsSubject.send(.displayMediaPicker)
case .displayFilePicker:
actionsSubject.send(.displayFilePicker)
case .removeImage: case .removeImage:
state.avatarMediaInfo = nil state.avatarMediaInfo = nil
case .updateAliasLocalPart(let aliasLocalPart): case .updateAliasLocalPart(let aliasLocalPart):
@@ -139,11 +139,17 @@ struct CreateRoomScreen: View {
Button(L10n.actionTakePhoto) { Button(L10n.actionTakePhoto) {
context.send(viewAction: .displayCameraPicker) context.send(viewAction: .displayCameraPicker)
} }
Button(L10n.actionChoosePhoto) { Button(L10n.actionChoosePhoto) {
context.send(viewAction: .displayMediaPicker) context.send(viewAction: .displayMediaPicker)
} }
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.mediaPicker) .accessibilityIdentifier(A11yIdentifiers.createRoomScreen.mediaPicker)
Button(L10n.actionChooseFile) {
context.send(viewAction: .displayFilePicker)
}
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.filePicker)
if context.viewState.avatarImage != nil { if context.viewState.avatarImage != nil {
Button(L10n.actionRemove, role: .destructive) { Button(L10n.actionRemove, role: .destructive) {
context.send(viewAction: .removeImage) context.send(viewAction: .removeImage)
@@ -7,6 +7,7 @@
// //
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
enum DocumentPickerAction { enum DocumentPickerAction {
case selectedMediaAtURLs([URL]) case selectedMediaAtURLs([URL])
@@ -16,19 +17,22 @@ enum DocumentPickerAction {
struct DocumentPicker: UIViewControllerRepresentable { struct DocumentPicker: UIViewControllerRepresentable {
private let selectionType: MediaPickerScreenSelectionType private let selectionType: MediaPickerScreenSelectionType
private let contentTypes: [UTType]
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let callback: (DocumentPickerAction) -> Void private let callback: (DocumentPickerAction) -> Void
init(selectionType: MediaPickerScreenSelectionType, init(selectionType: MediaPickerScreenSelectionType,
contentTypes: [UTType],
userIndicatorController: UserIndicatorControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol,
callback: @escaping (DocumentPickerAction) -> Void) { callback: @escaping (DocumentPickerAction) -> Void) {
self.selectionType = selectionType self.selectionType = selectionType
self.contentTypes = contentTypes
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: contentTypes)
documentPicker.delegate = context.coordinator documentPicker.delegate = context.coordinator
documentPicker.allowsMultipleSelection = switch selectionType { documentPicker.allowsMultipleSelection = switch selectionType {
@@ -7,16 +7,17 @@
// //
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
struct MediaPickerScreenMode: Hashable { struct MediaPickerScreenMode: Hashable {
let source: MediaPickerScreenSource let source: MediaPickerScreenSource
let selectionType: MediaPickerScreenSelectionType let selectionType: MediaPickerScreenSelectionType
} }
enum MediaPickerScreenSource { enum MediaPickerScreenSource: Hashable {
case camera case camera
case photoLibrary case photoLibrary
case documents case documents(types: [UTType] = [.data])
} }
enum MediaPickerScreenSelectionType { enum MediaPickerScreenSelectionType {
@@ -79,10 +80,12 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol {
self?.callback(.selectedMediaAtURLs(urls)) self?.callback(.selectedMediaAtURLs(urls))
} }
} }
case .documents: case .documents(let types):
// 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(selectionType: mode.selectionType, userIndicatorController: userIndicatorController) { action in DocumentPicker(selectionType: mode.selectionType,
contentTypes: types,
userIndicatorController: userIndicatorController) { action in
switch action { switch action {
case .cancel: case .cancel:
self.callback(.cancel) self.callback(.cancel)
@@ -52,6 +52,8 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol {
self?.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)) self?.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single))
case .displayMediaPicker: case .displayMediaPicker:
self?.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)) self?.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single))
case .displayFilePicker:
self?.displayMediaPickerWithMode(.init(source: .documents(types: [.image]), selectionType: .single))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -13,6 +13,7 @@ enum RoomDetailsEditScreenViewModelAction {
case saveFinished case saveFinished
case displayCameraPicker case displayCameraPicker
case displayMediaPicker case displayMediaPicker
case displayFilePicker
} }
struct RoomDetailsEditScreenViewState: BindableState { struct RoomDetailsEditScreenViewState: BindableState {
@@ -81,5 +82,6 @@ enum RoomDetailsEditScreenViewAction {
case presentMediaSource case presentMediaSource
case displayCameraPicker case displayCameraPicker
case displayMediaPicker case displayMediaPicker
case displayFilePicker
case removeImage case removeImage
} }
@@ -69,6 +69,8 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
actionsSubject.send(.displayCameraPicker) actionsSubject.send(.displayCameraPicker)
case .displayMediaPicker: case .displayMediaPicker:
actionsSubject.send(.displayMediaPicker) actionsSubject.send(.displayMediaPicker)
case .displayFilePicker:
actionsSubject.send(.displayFilePicker)
case .removeImage: case .removeImage:
state.avatarURL = nil state.avatarURL = nil
state.localMedia = nil state.localMedia = nil
@@ -119,11 +119,19 @@ struct RoomDetailsEditScreen: View {
} label: { } label: {
Text(L10n.actionTakePhoto) Text(L10n.actionTakePhoto)
} }
Button { Button {
context.send(viewAction: .displayMediaPicker) context.send(viewAction: .displayMediaPicker)
} label: { } label: {
Text(L10n.actionChoosePhoto) Text(L10n.actionChoosePhoto)
} }
Button {
context.send(viewAction: .displayFilePicker)
} label: {
Text(L10n.actionChooseFile)
}
if context.viewState.showDeleteImageAction { if context.viewState.showDeleteImageAction {
Button(role: .destructive) { Button(role: .destructive) {
context.send(viewAction: .removeImage) context.send(viewAction: .removeImage)
@@ -126,7 +126,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
case .displayMediaPicker: case .displayMediaPicker:
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple))) actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple)))
case .displayDocumentPicker: case .displayDocumentPicker:
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents, selectionType: .multiple))) 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:
@@ -53,7 +53,7 @@ final class UserDetailsEditScreenCoordinator: CoordinatorProtocol {
case .displayMediaPicker: case .displayMediaPicker:
displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)) displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single))
case .displayFilePicker: case .displayFilePicker:
displayMediaPickerWithMode(.init(source: .documents, selectionType: .single)) displayMediaPickerWithMode(.init(source: .documents(types: [.image]), selectionType: .single))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -67,5 +67,6 @@ enum UserDetailsEditScreenViewAction {
case presentMediaSource case presentMediaSource
case displayCameraPicker case displayCameraPicker
case displayMediaPicker case displayMediaPicker
case displayFilePicker
case removeImage case removeImage
} }
@@ -77,6 +77,8 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
actionsSubject.send(.displayCameraPicker) actionsSubject.send(.displayCameraPicker)
case .displayMediaPicker: case .displayMediaPicker:
actionsSubject.send(.displayMediaPicker) actionsSubject.send(.displayMediaPicker)
case .displayFilePicker:
actionsSubject.send(.displayFilePicker)
case .removeImage: case .removeImage:
state.localMedia = nil state.localMedia = nil
state.selectedAvatarURL = nil state.selectedAvatarURL = nil
@@ -96,12 +96,19 @@ struct UserDetailsEditScreen: View {
} label: { } label: {
Text(L10n.actionTakePhoto) Text(L10n.actionTakePhoto)
} }
Button { Button {
context.send(viewAction: .displayMediaPicker) context.send(viewAction: .displayMediaPicker)
} label: { } label: {
Text(L10n.actionChoosePhoto) Text(L10n.actionChoosePhoto)
} }
Button {
context.send(viewAction: .displayFilePicker)
} label: {
Text(L10n.actionChooseFile)
}
if context.viewState.showDeleteImageAction { if context.viewState.showDeleteImageAction {
Button(role: .destructive) { Button(role: .destructive) {
context.send(viewAction: .removeImage) context.send(viewAction: .removeImage)
@@ -114,7 +114,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
case .displayMediaPicker: case .displayMediaPicker:
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple))) actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .photoLibrary, selectionType: .multiple)))
case .displayDocumentPicker: case .displayDocumentPicker:
actionsSubject.send(.presentMediaUploadPicker(mode: .init(source: .documents, selectionType: .multiple))) 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: