mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Flescio/add room image (#961)
* image media upload for room creation * fix remove duplicate logic * use Async Image, add Focusable Fields in create room * remove alert message which doesn't have a copy * add remove duplicates on crete room image * add button style for preventing undesired touches * add changelog, add error case for file too large * Fix iPad sheet presentation * add error media preprocess * dismissing focus on image picker
This commit is contained in:
@@ -28,6 +28,8 @@ 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 removeImage
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CreateRoomCoordinator: CoordinatorProtocol {
|
final class CreateRoomCoordinator: CoordinatorProtocol {
|
||||||
@@ -53,11 +55,17 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .deselectUser(let user):
|
case .deselectUser(let user):
|
||||||
self.actionsSubject.send(.deselectUser(user))
|
actionsSubject.send(.deselectUser(user))
|
||||||
case .openRoom(let identifier):
|
case .openRoom(let identifier):
|
||||||
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||||
case .updateDetails(let details):
|
case .updateDetails(let details):
|
||||||
self.actionsSubject.send(.updateDetails(details))
|
actionsSubject.send(.updateDetails(details))
|
||||||
|
case .displayCameraPicker:
|
||||||
|
actionsSubject.send(.displayMediaPickerWithSource(.camera))
|
||||||
|
case .displayMediaPicker:
|
||||||
|
actionsSubject.send(.displayMediaPickerWithSource(.photoLibrary))
|
||||||
|
case .removeImage:
|
||||||
|
actionsSubject.send(.removeImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import Foundation
|
|||||||
|
|
||||||
enum CreateRoomScreenErrorType: Error {
|
enum CreateRoomScreenErrorType: Error {
|
||||||
case failedCreatingRoom
|
case failedCreatingRoom
|
||||||
|
case failedUploadingMedia
|
||||||
|
case fileTooLarge
|
||||||
|
case mediaFileError
|
||||||
case unknown
|
case unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,12 +28,15 @@ enum CreateRoomViewModelAction {
|
|||||||
case openRoom(withIdentifier: String)
|
case openRoom(withIdentifier: String)
|
||||||
case deselectUser(UserProfileProxy)
|
case deselectUser(UserProfileProxy)
|
||||||
case updateDetails(CreateRoomFlowParameters)
|
case updateDetails(CreateRoomFlowParameters)
|
||||||
|
case displayMediaPicker
|
||||||
|
case displayCameraPicker
|
||||||
|
case removeImage
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateRoomViewState: BindableState {
|
struct CreateRoomViewState: BindableState {
|
||||||
var selectedUsers: [UserProfileProxy]
|
var selectedUsers: [UserProfileProxy]
|
||||||
var bindings: CreateRoomViewStateBindings
|
var bindings: CreateRoomViewStateBindings
|
||||||
|
var avatarURL: URL?
|
||||||
var canCreateRoom: Bool {
|
var canCreateRoom: Bool {
|
||||||
!bindings.roomName.isEmpty
|
!bindings.roomName.isEmpty
|
||||||
}
|
}
|
||||||
@@ -40,6 +46,7 @@ struct CreateRoomViewStateBindings {
|
|||||||
var roomName: String
|
var roomName: String
|
||||||
var roomTopic: String
|
var roomTopic: String
|
||||||
var isRoomPrivate: Bool
|
var isRoomPrivate: Bool
|
||||||
|
var showAttachmentConfirmationDialog = false
|
||||||
|
|
||||||
/// Information describing the currently displayed alert.
|
/// Information describing the currently displayed alert.
|
||||||
var alertInfo: AlertInfo<CreateRoomScreenErrorType>?
|
var alertInfo: AlertInfo<CreateRoomScreenErrorType>?
|
||||||
@@ -48,4 +55,7 @@ struct CreateRoomViewStateBindings {
|
|||||||
enum CreateRoomViewAction {
|
enum CreateRoomViewAction {
|
||||||
case createRoom
|
case createRoom
|
||||||
case deselectUser(UserProfileProxy)
|
case deselectUser(UserProfileProxy)
|
||||||
|
case displayCameraPicker
|
||||||
|
case displayMediaPicker
|
||||||
|
case removeImage
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,22 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
|
|
||||||
super.init(initialViewState: CreateRoomViewState(selectedUsers: selectedUsers.value, bindings: bindings), imageProvider: userSession.mediaProvider)
|
super.init(initialViewState: CreateRoomViewState(selectedUsers: selectedUsers.value, bindings: bindings), imageProvider: userSession.mediaProvider)
|
||||||
|
|
||||||
|
createRoomParameters
|
||||||
|
.map(\.avatarImageMedia)
|
||||||
|
.removeDuplicates { $0?.url == $1?.url }
|
||||||
|
.sink { [weak self] mediaInfo in
|
||||||
|
self?.createRoomParameters.avatarImageMedia = mediaInfo
|
||||||
|
switch mediaInfo {
|
||||||
|
case .image(_, let thumbnailURL, _):
|
||||||
|
self?.state.avatarURL = thumbnailURL
|
||||||
|
case nil:
|
||||||
|
self?.state.avatarURL = nil
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
selectedUsers
|
selectedUsers
|
||||||
.sink { [weak self] users in
|
.sink { [weak self] users in
|
||||||
self?.state.selectedUsers = users
|
self?.state.selectedUsers = users
|
||||||
@@ -60,6 +76,12 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
}
|
}
|
||||||
case .deselectUser(let user):
|
case .deselectUser(let user):
|
||||||
actionsSubject.send(.deselectUser(user))
|
actionsSubject.send(.deselectUser(user))
|
||||||
|
case .displayCameraPicker:
|
||||||
|
actionsSubject.send(.displayCameraPicker)
|
||||||
|
case .displayMediaPicker:
|
||||||
|
actionsSubject.send(.displayMediaPicker)
|
||||||
|
case .removeImage:
|
||||||
|
actionsSubject.send(.removeImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +91,9 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
context.$viewState
|
context.$viewState
|
||||||
.map(\.bindings)
|
.map(\.bindings)
|
||||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||||
|
.removeDuplicates { old, new in
|
||||||
|
old.roomName == new.roomName && old.roomTopic == new.roomTopic && old.isRoomPrivate == new.isRoomPrivate
|
||||||
|
}
|
||||||
.sink { [weak self] bindings in
|
.sink { [weak self] bindings in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
createRoomParameters.name = bindings.roomName
|
createRoomParameters.name = bindings.roomName
|
||||||
@@ -87,6 +112,16 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
message: L10n.screenStartChatErrorStartingChat)
|
message: L10n.screenStartChatErrorStartingChat)
|
||||||
case .failedSearchingUsers:
|
case .failedSearchingUsers:
|
||||||
state.bindings.alertInfo = AlertInfo(id: .unknown)
|
state.bindings.alertInfo = AlertInfo(id: .unknown)
|
||||||
|
case .failedUploadingMedia(let matrixError):
|
||||||
|
switch matrixError {
|
||||||
|
case .fileTooLarge:
|
||||||
|
// waiting for proper copy
|
||||||
|
state.bindings.alertInfo = AlertInfo(id: .fileTooLarge)
|
||||||
|
default:
|
||||||
|
state.bindings.alertInfo = AlertInfo(id: .failedUploadingMedia)
|
||||||
|
}
|
||||||
|
case .mediaFileError:
|
||||||
|
state.bindings.alertInfo = AlertInfo(id: .mediaFileError)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -101,7 +136,25 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
hideLoadingIndicator()
|
hideLoadingIndicator()
|
||||||
}
|
}
|
||||||
showLoadingIndicator()
|
showLoadingIndicator()
|
||||||
switch await clientProxy.createRoom(with: createRoomParameters, userIDs: state.selectedUsers.map(\.userID)) {
|
|
||||||
|
let avatarURL: URL?
|
||||||
|
if let media = createRoomParameters.avatarImageMedia {
|
||||||
|
switch await clientProxy.uploadMedia(media) {
|
||||||
|
case .success(let url):
|
||||||
|
avatarURL = URL(string: url)
|
||||||
|
case .failure(let error):
|
||||||
|
displayError(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatarURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch await clientProxy.createRoom(name: createRoomParameters.name,
|
||||||
|
topic: createRoomParameters.topic,
|
||||||
|
isRoomPrivate: createRoomParameters.isRoomPrivate,
|
||||||
|
userIDs: state.selectedUsers.map(\.userID),
|
||||||
|
avatarURL: avatarURL) {
|
||||||
case .success(let roomId):
|
case .success(let roomId):
|
||||||
actionsSubject.send(.openRoom(withIdentifier: roomId))
|
actionsSubject.send(.openRoom(withIdentifier: roomId))
|
||||||
case .failure(let failure):
|
case .failure(let failure):
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ import SwiftUI
|
|||||||
|
|
||||||
struct CreateRoomScreen: View {
|
struct CreateRoomScreen: View {
|
||||||
@ObservedObject var context: CreateRoomViewModel.Context
|
@ObservedObject var context: CreateRoomViewModel.Context
|
||||||
|
@FocusState private var focus: Focus?
|
||||||
|
|
||||||
|
private enum Focus {
|
||||||
|
case name
|
||||||
|
case topic
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
mainContent
|
mainContent
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
@@ -32,6 +38,7 @@ struct CreateRoomScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(ViewFrameReader(frame: $frame))
|
.background(ViewFrameReader(frame: $frame))
|
||||||
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main content of the view to be shown in a scroll view.
|
/// The main content of the view to be shown in a scroll view.
|
||||||
@@ -49,11 +56,38 @@ struct CreateRoomScreen: View {
|
|||||||
private var roomSection: some View {
|
private var roomSection: some View {
|
||||||
Section {
|
Section {
|
||||||
HStack(alignment: .center, spacing: 16) {
|
HStack(alignment: .center, spacing: 16) {
|
||||||
Image(systemName: "camera")
|
Button {
|
||||||
.foregroundColor(.element.secondaryContent)
|
focus = nil
|
||||||
.frame(width: roomIconSize, height: roomIconSize)
|
context.showAttachmentConfirmationDialog = true
|
||||||
.background(Color.element.quinaryContent)
|
} label: {
|
||||||
.clipShape(Circle())
|
if let url = context.viewState.avatarURL {
|
||||||
|
AsyncImage(url: url) { image in
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
} placeholder: {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
.frame(width: roomIconSize, height: roomIconSize)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else {
|
||||||
|
cameraImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.confirmationDialog("", isPresented: $context.showAttachmentConfirmationDialog) {
|
||||||
|
Button(L10n.actionTakePhoto) {
|
||||||
|
context.send(viewAction: .displayCameraPicker)
|
||||||
|
}
|
||||||
|
Button(L10n.actionChoosePhoto) {
|
||||||
|
context.send(viewAction: .displayMediaPicker)
|
||||||
|
}
|
||||||
|
if context.viewState.avatarURL != nil {
|
||||||
|
Button(L10n.actionRemove, role: .destructive) {
|
||||||
|
context.send(viewAction: .removeImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text(L10n.screenCreateRoomRoomNameLabel.uppercased())
|
Text(L10n.screenCreateRoomRoomNameLabel.uppercased())
|
||||||
.font(.compound.bodyXS)
|
.font(.compound.bodyXS)
|
||||||
@@ -63,6 +97,7 @@ struct CreateRoomScreen: View {
|
|||||||
text: $context.roomName,
|
text: $context.roomName,
|
||||||
prompt: Text(L10n.screenCreateRoomRoomNamePlaceholder),
|
prompt: Text(L10n.screenCreateRoomRoomNamePlaceholder),
|
||||||
axis: .horizontal)
|
axis: .horizontal)
|
||||||
|
.focused($focus, equals: .name)
|
||||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomName)
|
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomName)
|
||||||
.padding(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16))
|
.padding(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16))
|
||||||
.background(Color.element.formRowBackground)
|
.background(Color.element.formRowBackground)
|
||||||
@@ -75,12 +110,21 @@ struct CreateRoomScreen: View {
|
|||||||
.formSectionStyle()
|
.formSectionStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var cameraImage: some View {
|
||||||
|
Image(systemName: "camera")
|
||||||
|
.foregroundColor(.element.secondaryContent)
|
||||||
|
.frame(width: roomIconSize, height: roomIconSize)
|
||||||
|
.background(Color.element.quinaryContent)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
|
||||||
private var topicSection: some View {
|
private var topicSection: some View {
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.screenCreateRoomTopicLabel,
|
TextField(L10n.screenCreateRoomTopicLabel,
|
||||||
text: $context.roomTopic,
|
text: $context.roomTopic,
|
||||||
prompt: Text(L10n.screenCreateRoomTopicPlaceholder),
|
prompt: Text(L10n.screenCreateRoomTopicPlaceholder),
|
||||||
axis: .vertical)
|
axis: .vertical)
|
||||||
|
.focused($focus, equals: .topic)
|
||||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomTopic)
|
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomTopic)
|
||||||
.lineLimit(3, reservesSpace: false)
|
.lineLimit(3, reservesSpace: false)
|
||||||
} header: {
|
} header: {
|
||||||
@@ -150,7 +194,10 @@ struct CreateRoomScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var createButton: some View {
|
private var createButton: some View {
|
||||||
Button { context.send(viewAction: .createRoom) } label: {
|
Button {
|
||||||
|
focus = nil
|
||||||
|
context.send(viewAction: .createRoom)
|
||||||
|
} label: {
|
||||||
Text(L10n.actionCreate)
|
Text(L10n.actionCreate)
|
||||||
}
|
}
|
||||||
.disabled(!context.viewState.canCreateRoom)
|
.disabled(!context.viewState.canCreateRoom)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import SwiftUI
|
|||||||
struct StartChatScreenCoordinatorParameters {
|
struct StartChatScreenCoordinatorParameters {
|
||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
let navigationStackCoordinator: NavigationStackCoordinator?
|
let navigationStackCoordinator: NavigationStackCoordinator
|
||||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +45,14 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
selectedUsers.asCurrentValuePublisher()
|
selectedUsers.asCurrentValuePublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var navigationStackCoordinator: NavigationStackCoordinator {
|
||||||
|
parameters.navigationStackCoordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
private var userIndicatorController: UserIndicatorControllerProtocol? {
|
||||||
|
parameters.userIndicatorController
|
||||||
|
}
|
||||||
|
|
||||||
var actions: AnyPublisher<StartChatScreenCoordinatorAction, Never> {
|
var actions: AnyPublisher<StartChatScreenCoordinatorAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
@@ -60,12 +68,12 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .close:
|
case .close:
|
||||||
self.actionsSubject.send(.close)
|
actionsSubject.send(.close)
|
||||||
case .createRoom:
|
case .createRoom:
|
||||||
// before creating a room we select the users we would like to invite in that room
|
// before creating a room we select the users we would like to invite in that room
|
||||||
self.presentInviteUsersScreen()
|
presentInviteUsersScreen()
|
||||||
case .openRoom(let identifier):
|
case .openRoom(let identifier):
|
||||||
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -99,7 +107,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
parameters.navigationStackCoordinator?.push(coordinator) { [weak self] in
|
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||||
self?.createRoomParameters.send(.init())
|
self?.createRoomParameters.send(.init())
|
||||||
self?.selectedUsers.send([])
|
self?.selectedUsers.send([])
|
||||||
}
|
}
|
||||||
@@ -107,27 +115,71 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
private func openCreateRoomScreen() {
|
private func openCreateRoomScreen() {
|
||||||
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
|
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
|
||||||
userIndicatorController: parameters.userIndicatorController,
|
userIndicatorController: userIndicatorController,
|
||||||
createRoomParameters: createRoomParametersPublisher,
|
createRoomParameters: createRoomParametersPublisher,
|
||||||
selectedUsers: selectedUsersPublisher)
|
selectedUsers: selectedUsersPublisher)
|
||||||
let coordinator = CreateRoomCoordinator(parameters: createParameters)
|
let coordinator = CreateRoomCoordinator(parameters: createParameters)
|
||||||
coordinator.actions.sink { [weak self] result in
|
coordinator.actions.sink { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
switch result {
|
switch result {
|
||||||
case .deselectUser(let user):
|
case .deselectUser(let user):
|
||||||
self?.toggleUser(user)
|
self.toggleUser(user)
|
||||||
case .updateDetails(let details):
|
case .updateDetails(let details):
|
||||||
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):
|
||||||
|
self.displayMediaPickerWithSource(source)
|
||||||
|
case .removeImage:
|
||||||
|
var parameters = self.createRoomParameters.value
|
||||||
|
parameters.avatarImageMedia = nil
|
||||||
|
self.createRoomParameters.send(parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
parameters.navigationStackCoordinator?.push(coordinator)
|
navigationStackCoordinator.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
let mediaUploadingPreprocessor = MediaUploadingPreprocessor()
|
||||||
|
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
|
||||||
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
let userIndicatorController = UserIndicatorController(rootCoordinator: stackCoordinator)
|
||||||
|
|
||||||
|
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController, source: source) { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
switch action {
|
||||||
|
case .cancel:
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
case .selectMediaAtURL(let url):
|
||||||
|
processAvatar(from: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processAvatar(from url: URL) {
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
showLoadingIndicator()
|
||||||
|
Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
do {
|
||||||
|
let media = try await mediaUploadingPreprocessor.processMedia(at: url).get()
|
||||||
|
var parameters = createRoomParameters.value
|
||||||
|
parameters.avatarImageMedia = media
|
||||||
|
createRoomParameters.send(parameters)
|
||||||
|
} catch {
|
||||||
|
userIndicatorController?.alertInfo = AlertInfo(id: .init(), title: L10n.commonError, message: L10n.errorUnknown)
|
||||||
|
}
|
||||||
|
hideLoadingIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func toggleUser(_ user: UserProfileProxy) {
|
private func toggleUser(_ user: UserProfileProxy) {
|
||||||
var selectedUsers = selectedUsers.value
|
var selectedUsers = selectedUsers.value
|
||||||
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
||||||
@@ -137,4 +189,19 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
self.selectedUsers.send(selectedUsers)
|
self.selectedUsers.send(selectedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Loading indicator
|
||||||
|
|
||||||
|
private static let loadingIndicatorIdentifier = "StartChatCoordinatorLoading"
|
||||||
|
|
||||||
|
private func showLoadingIndicator() {
|
||||||
|
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
type: .modal,
|
||||||
|
title: L10n.commonLoading,
|
||||||
|
persistent: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideLoadingIndicator() {
|
||||||
|
userIndicatorController?.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ enum MatrixErrorCode: String, CaseIterable {
|
|||||||
case unknown = "M_UNKNOWN"
|
case unknown = "M_UNKNOWN"
|
||||||
case userDeactivated = "M_USER_DEACTIVATED"
|
case userDeactivated = "M_USER_DEACTIVATED"
|
||||||
case forbidden = "M_FORBIDDEN"
|
case forbidden = "M_FORBIDDEN"
|
||||||
|
case fileTooLarge = "M_TOO_LARGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ClientError {
|
||||||
|
var code: MatrixErrorCode {
|
||||||
|
guard case let .Generic(message) = self else { return .unknown }
|
||||||
|
|
||||||
|
guard let first = MatrixErrorCode.allCases.first(where: { message.contains($0.rawValue) }) else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
return first
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AuthenticationError {
|
extension AuthenticationError {
|
||||||
|
|||||||
@@ -197,17 +197,17 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
return await waitForRoomSummary(with: result, name: expectedRoomName)
|
return await waitForRoomSummary(with: result, name: expectedRoomName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRoom(with parameters: CreateRoomFlowParameters, userIDs: [String]) async -> Result<String, ClientProxyError> {
|
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
|
||||||
let result: Result<String, ClientProxyError> = await Task.dispatch(on: clientQueue) {
|
let result: Result<String, ClientProxyError> = await Task.dispatch(on: clientQueue) {
|
||||||
do {
|
do {
|
||||||
let parameters = CreateRoomParameters(name: parameters.name,
|
let parameters = CreateRoomParameters(name: name,
|
||||||
topic: parameters.topic,
|
topic: topic,
|
||||||
isEncrypted: parameters.isRoomPrivate,
|
isEncrypted: isRoomPrivate,
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
visibility: parameters.isRoomPrivate ? .private : .public,
|
visibility: isRoomPrivate ? .private : .public,
|
||||||
preset: parameters.isRoomPrivate ? .privateChat : .publicChat,
|
preset: isRoomPrivate ? .privateChat : .publicChat,
|
||||||
invite: userIDs,
|
invite: userIDs,
|
||||||
avatar: nil)
|
avatar: avatarURL?.absoluteString)
|
||||||
let roomId = try self.client.createRoom(request: parameters)
|
let roomId = try self.client.createRoom(request: parameters)
|
||||||
return .success(roomId)
|
return .success(roomId)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -215,7 +215,22 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await waitForRoomSummary(with: result, name: parameters.name)
|
return await waitForRoomSummary(with: result, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadMedia(_ media: MediaInfo) async -> Result<String, ClientProxyError> {
|
||||||
|
guard let mimeType = media.mimeType else { return .failure(ClientProxyError.mediaFileError) }
|
||||||
|
return await Task.dispatch(on: clientQueue) {
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: media.url)
|
||||||
|
let matrixUrl = try self.client.uploadMedia(mimeType: mimeType, data: [UInt8](data))
|
||||||
|
return .success(matrixUrl)
|
||||||
|
} catch let error as ClientError {
|
||||||
|
return .failure(ClientProxyError.failedUploadingMedia(error.code))
|
||||||
|
} catch {
|
||||||
|
return .failure(ClientProxyError.mediaFileError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Await the room to be available in the room summary list
|
/// Await the room to be available in the room summary list
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ enum ClientProxyError: Error {
|
|||||||
case failedSettingAccountData
|
case failedSettingAccountData
|
||||||
case failedRetrievingSessionVerificationController
|
case failedRetrievingSessionVerificationController
|
||||||
case failedLoadingMedia
|
case failedLoadingMedia
|
||||||
|
case mediaFileError
|
||||||
|
case failedUploadingMedia(MatrixErrorCode)
|
||||||
case failedSearchingUsers
|
case failedSearchingUsers
|
||||||
case failedGettingUserProfile
|
case failedGettingUserProfile
|
||||||
}
|
}
|
||||||
@@ -92,7 +94,9 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
|
|
||||||
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
||||||
|
|
||||||
func createRoom(with parameters: CreateRoomFlowParameters, userIDs: [String]) async -> Result<String, ClientProxyError>
|
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError>
|
||||||
|
|
||||||
|
func uploadMedia(_ media: MediaInfo) async -> Result<String, ClientProxyError>
|
||||||
|
|
||||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol?
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol?
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,14 @@ class MockClientProxy: ClientProxyProtocol {
|
|||||||
.failure(.failedCreatingRoom)
|
.failure(.failedCreatingRoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRoom(with parameters: CreateRoomFlowParameters, userIDs: [String]) async -> Result<String, ClientProxyError> {
|
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
|
||||||
.failure(.failedCreatingRoom)
|
.failure(.failedCreatingRoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func uploadMedia(_ media: MediaInfo) async -> Result<String, ClientProxyError> {
|
||||||
|
.failure(.failedUploadingMedia(.unknown))
|
||||||
|
}
|
||||||
|
|
||||||
var roomForIdentifierMocks: [String: RoomProxyMock] = .init()
|
var roomForIdentifierMocks: [String: RoomProxyMock] = .init()
|
||||||
@MainActor
|
@MainActor
|
||||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ struct CreateRoomFlowParameters {
|
|||||||
var name = ""
|
var name = ""
|
||||||
var topic = ""
|
var topic = ""
|
||||||
var isRoomPrivate = true
|
var isRoomPrivate = true
|
||||||
|
var avatarImageMedia: MediaInfo?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Add the image picker flow for the creation of a room
|
||||||
Reference in New Issue
Block a user