mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Introduce a StartChatFlowCoordinator instead of handing a navigation stack to the Screen Coordinator. (#4674)
* Introduce a basic StartChatFlowCoordinator. * Move the rest of the start chat flow from the screen coordinator into the flow coordinator. * Add a UI test for the entire start chat flow. * Refactor CreateRoom… to CreateRoomScreen…
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct CreateRoomCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>
|
||||
let selectedUsers: [UserProfileProxy]
|
||||
let appSettings: AppSettings
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
enum CreateRoomCoordinatorAction {
|
||||
case openRoom(withIdentifier: String)
|
||||
case updateSelectedUsers([UserProfileProxy])
|
||||
case updateDetails(CreateRoomFlowParameters)
|
||||
case displayMediaPickerWithMode(MediaPickerScreenMode)
|
||||
case removeImage
|
||||
}
|
||||
|
||||
final class CreateRoomCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: CreateRoomViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<CreateRoomCoordinatorAction, Never> = .init()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var actions: AnyPublisher<CreateRoomCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: CreateRoomCoordinatorParameters) {
|
||||
viewModel = CreateRoomViewModel(userSession: parameters.userSession,
|
||||
createRoomParameters: parameters.createRoomParameters,
|
||||
selectedUsers: parameters.selectedUsers,
|
||||
analytics: parameters.analytics,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .updateSelectedUsers(let users):
|
||||
actionsSubject.send(.updateSelectedUsers(users))
|
||||
case .openRoom(let identifier):
|
||||
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
case .updateDetails(let details):
|
||||
actionsSubject.send(.updateDetails(details))
|
||||
case .displayCameraPicker:
|
||||
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)))
|
||||
case .displayMediaPicker:
|
||||
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)))
|
||||
case .removeImage:
|
||||
actionsSubject.send(.removeImage)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(CreateRoomScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct CreateRoomScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let appSettings: AppSettings
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
enum CreateRoomScreenCoordinatorAction {
|
||||
case createdRoom(JoinedRoomProxyProtocol)
|
||||
case displayMediaPickerWithMode(MediaPickerScreenMode)
|
||||
}
|
||||
|
||||
final class CreateRoomScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: CreateRoomScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<CreateRoomScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var actions: AnyPublisher<CreateRoomScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: CreateRoomScreenCoordinatorParameters) {
|
||||
viewModel = CreateRoomScreenViewModel(userSession: parameters.userSession,
|
||||
analytics: parameters.analytics,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .createdRoom(let roomProxy):
|
||||
actionsSubject.send(.createdRoom(roomProxy))
|
||||
case .displayCameraPicker:
|
||||
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)))
|
||||
case .displayMediaPicker:
|
||||
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(CreateRoomScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
func updateAvatar(fileURL: URL) {
|
||||
viewModel.updateAvatar(fileURL: fileURL)
|
||||
}
|
||||
}
|
||||
+9
-14
@@ -16,28 +16,24 @@ enum CreateRoomScreenErrorType: Error {
|
||||
case unknown
|
||||
}
|
||||
|
||||
enum CreateRoomViewModelAction {
|
||||
case openRoom(withIdentifier: String)
|
||||
case updateSelectedUsers([UserProfileProxy])
|
||||
case updateDetails(CreateRoomFlowParameters)
|
||||
enum CreateRoomScreenViewModelAction {
|
||||
case createdRoom(JoinedRoomProxyProtocol)
|
||||
case displayMediaPicker
|
||||
case displayCameraPicker
|
||||
case removeImage
|
||||
}
|
||||
|
||||
struct CreateRoomViewState: BindableState {
|
||||
struct CreateRoomScreenViewState: BindableState {
|
||||
var roomName: String
|
||||
let serverName: String
|
||||
let isKnockingFeatureEnabled: Bool
|
||||
var selectedUsers: [UserProfileProxy]
|
||||
var aliasLocalPart: String
|
||||
var bindings: CreateRoomViewStateBindings
|
||||
var bindings: CreateRoomScreenViewStateBindings
|
||||
var avatarURL: URL?
|
||||
var canCreateRoom: Bool {
|
||||
!roomName.isEmpty && aliasErrors.isEmpty
|
||||
}
|
||||
|
||||
var aliasErrors: Set<CreateRoomAliasErrorState> = []
|
||||
var aliasErrors: Set<CreateRoomScreenAliasErrorState> = []
|
||||
var aliasErrorDescription: String? {
|
||||
if aliasErrors.contains(.alreadyExists) {
|
||||
L10n.errorRoomAddressAlreadyExists
|
||||
@@ -49,7 +45,7 @@ struct CreateRoomViewState: BindableState {
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateRoomViewStateBindings {
|
||||
struct CreateRoomScreenViewStateBindings {
|
||||
var roomTopic: String
|
||||
var isRoomPrivate: Bool
|
||||
var isKnockingOnly: Bool
|
||||
@@ -59,9 +55,8 @@ struct CreateRoomViewStateBindings {
|
||||
var alertInfo: AlertInfo<CreateRoomScreenErrorType>?
|
||||
}
|
||||
|
||||
enum CreateRoomViewAction {
|
||||
enum CreateRoomScreenViewAction {
|
||||
case createRoom
|
||||
case deselectUser(UserProfileProxy)
|
||||
case displayCameraPicker
|
||||
case displayMediaPicker
|
||||
case removeImage
|
||||
@@ -69,12 +64,12 @@ enum CreateRoomViewAction {
|
||||
case updateAliasLocalPart(String)
|
||||
}
|
||||
|
||||
enum CreateRoomAliasErrorState {
|
||||
enum CreateRoomScreenAliasErrorState {
|
||||
case alreadyExists
|
||||
case invalidSymbols
|
||||
}
|
||||
|
||||
extension Set<CreateRoomAliasErrorState> {
|
||||
extension Set<CreateRoomScreenAliasErrorState> {
|
||||
var errorDescription: String? {
|
||||
if contains(.alreadyExists) {
|
||||
return L10n.errorRoomAddressAlreadyExists
|
||||
+81
-63
@@ -10,83 +10,70 @@ import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
typealias CreateRoomViewModelType = StateStoreViewModel<CreateRoomViewState, CreateRoomViewAction>
|
||||
typealias CreateRoomScreenViewModelType = StateStoreViewModel<CreateRoomScreenViewState, CreateRoomScreenViewAction>
|
||||
|
||||
class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol {
|
||||
class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreenViewModelProtocol {
|
||||
struct Parameters {
|
||||
var name = ""
|
||||
var topic = ""
|
||||
var isRoomPrivate = true
|
||||
var isKnockingOnly = false
|
||||
var avatarImageMedia: MediaInfo?
|
||||
var aliasLocalPart: String?
|
||||
}
|
||||
|
||||
private let userSession: UserSessionProtocol
|
||||
private var createRoomParameters: CreateRoomFlowParameters
|
||||
private var parameters: Parameters
|
||||
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
||||
private let analytics: AnalyticsService
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private var syncNameAndAlias = true
|
||||
@CancellableTask private var checkAliasAvailabilityTask: Task<Void, Never>?
|
||||
|
||||
private var actionsSubject: PassthroughSubject<CreateRoomViewModelAction, Never> = .init()
|
||||
private var actionsSubject: PassthroughSubject<CreateRoomScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<CreateRoomViewModelAction, Never> {
|
||||
var actions: AnyPublisher<CreateRoomScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>,
|
||||
selectedUsers: [UserProfileProxy],
|
||||
initialParameters: Parameters = .init(),
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings) {
|
||||
let parameters = createRoomParameters.value
|
||||
|
||||
self.userSession = userSession
|
||||
self.createRoomParameters = parameters
|
||||
parameters = initialParameters
|
||||
mediaUploadingPreprocessor = MediaUploadingPreprocessor(appSettings: appSettings)
|
||||
self.analytics = analytics
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
let bindings = CreateRoomViewStateBindings(roomTopic: parameters.topic,
|
||||
isRoomPrivate: parameters.isRoomPrivate,
|
||||
isKnockingOnly: appSettings.knockingEnabled ? parameters.isKnockingOnly : false)
|
||||
let bindings = CreateRoomScreenViewStateBindings(roomTopic: parameters.topic,
|
||||
isRoomPrivate: parameters.isRoomPrivate,
|
||||
isKnockingOnly: appSettings.knockingEnabled ? parameters.isKnockingOnly : false)
|
||||
|
||||
super.init(initialViewState: CreateRoomViewState(roomName: parameters.name,
|
||||
serverName: userSession.clientProxy.userIDServerName ?? "",
|
||||
isKnockingFeatureEnabled: appSettings.knockingEnabled,
|
||||
selectedUsers: selectedUsers,
|
||||
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
|
||||
bindings: bindings),
|
||||
super.init(initialViewState: CreateRoomScreenViewState(roomName: parameters.name,
|
||||
serverName: userSession.clientProxy.userIDServerName ?? "",
|
||||
isKnockingFeatureEnabled: appSettings.knockingEnabled,
|
||||
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
|
||||
bindings: bindings),
|
||||
mediaProvider: 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)
|
||||
|
||||
setupBindings()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: CreateRoomViewAction) {
|
||||
override func process(viewAction: CreateRoomScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .createRoom:
|
||||
Task {
|
||||
await createRoom()
|
||||
}
|
||||
case .deselectUser(let user):
|
||||
state.selectedUsers.removeAll { $0.userID == user.userID }
|
||||
actionsSubject.send(.updateSelectedUsers(state.selectedUsers))
|
||||
Task { await createRoom() }
|
||||
case .displayCameraPicker:
|
||||
actionsSubject.send(.displayCameraPicker)
|
||||
case .displayMediaPicker:
|
||||
actionsSubject.send(.displayMediaPicker)
|
||||
case .removeImage:
|
||||
actionsSubject.send(.removeImage)
|
||||
parameters.avatarImageMedia = nil
|
||||
state.avatarURL = nil
|
||||
case .updateAliasLocalPart(let aliasLocalPart):
|
||||
state.aliasLocalPart = aliasLocalPart.lowercased()
|
||||
// If this has been called this means that the user wants a custom address not necessarily reflecting the name
|
||||
@@ -104,6 +91,32 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func updateAvatar(fileURL: URL) {
|
||||
showLoadingIndicator()
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
guard case let .success(maxUploadSize) = await userSession.clientProxy.maxMediaUploadSize else {
|
||||
MXLog.error("Failed to get max upload size")
|
||||
userIndicatorController.alertInfo = AlertInfo(id: .init())
|
||||
return
|
||||
}
|
||||
let mediaInfo = try await mediaUploadingPreprocessor.processMedia(at: fileURL, maxUploadSize: maxUploadSize).get()
|
||||
|
||||
switch mediaInfo {
|
||||
case .image(_, let thumbnailURL, _):
|
||||
parameters.avatarImageMedia = mediaInfo
|
||||
state.avatarURL = thumbnailURL
|
||||
default:
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
userIndicatorController.alertInfo = AlertInfo(id: .init())
|
||||
}
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupBindings() {
|
||||
@@ -135,7 +148,6 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
.sink { [weak self] state in
|
||||
guard let self else { return }
|
||||
updateParameters(state: state)
|
||||
actionsSubject.send(.updateDetails(createRoomParameters))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -183,15 +195,15 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func updateParameters(state: CreateRoomViewState) {
|
||||
createRoomParameters.name = state.roomName
|
||||
createRoomParameters.topic = state.bindings.roomTopic
|
||||
createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate
|
||||
createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly
|
||||
private func updateParameters(state: CreateRoomScreenViewState) {
|
||||
parameters.name = state.roomName
|
||||
parameters.topic = state.bindings.roomTopic
|
||||
parameters.isRoomPrivate = state.bindings.isRoomPrivate
|
||||
parameters.isKnockingOnly = state.bindings.isKnockingOnly
|
||||
if state.isKnockingFeatureEnabled, !state.aliasLocalPart.isEmpty {
|
||||
createRoomParameters.aliasLocalPart = state.aliasLocalPart
|
||||
parameters.aliasLocalPart = state.aliasLocalPart
|
||||
} else {
|
||||
createRoomParameters.aliasLocalPart = nil
|
||||
parameters.aliasLocalPart = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,8 +217,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
updateParameters(state: state)
|
||||
|
||||
// Better to double check the errors also when trying to create the room
|
||||
if state.isKnockingFeatureEnabled, !createRoomParameters.isRoomPrivate {
|
||||
guard let canonicalAlias = String.makeCanonicalAlias(aliasLocalPart: createRoomParameters.aliasLocalPart,
|
||||
if state.isKnockingFeatureEnabled, !parameters.isRoomPrivate {
|
||||
guard let canonicalAlias = String.makeCanonicalAlias(aliasLocalPart: parameters.aliasLocalPart,
|
||||
serverName: state.serverName),
|
||||
isRoomAliasFormatValid(alias: canonicalAlias) else {
|
||||
state.aliasErrors = [.invalidSymbols]
|
||||
@@ -226,7 +238,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
}
|
||||
|
||||
let avatarURL: URL?
|
||||
if let media = createRoomParameters.avatarImageMedia {
|
||||
if let media = parameters.avatarImageMedia {
|
||||
switch await userSession.clientProxy.uploadMedia(media) {
|
||||
case .success(let url):
|
||||
avatarURL = URL(string: url)
|
||||
@@ -251,17 +263,23 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
avatarURL = nil
|
||||
}
|
||||
|
||||
switch await userSession.clientProxy.createRoom(name: createRoomParameters.name,
|
||||
topic: createRoomParameters.topic.isBlank ? nil : createRoomParameters.topic,
|
||||
isRoomPrivate: createRoomParameters.isRoomPrivate,
|
||||
switch await userSession.clientProxy.createRoom(name: parameters.name,
|
||||
topic: parameters.topic.isBlank ? nil : parameters.topic,
|
||||
isRoomPrivate: parameters.isRoomPrivate,
|
||||
// As of right now we don't want to make private rooms with the knock rule
|
||||
isKnockingOnly: createRoomParameters.isRoomPrivate ? false : createRoomParameters.isKnockingOnly,
|
||||
userIDs: state.selectedUsers.map(\.userID),
|
||||
isKnockingOnly: parameters.isRoomPrivate ? false : parameters.isKnockingOnly,
|
||||
userIDs: [], // The invite users screen is shown next so we don't need to invite anyone right now.
|
||||
avatarURL: avatarURL,
|
||||
aliasLocalPart: createRoomParameters.isRoomPrivate ? nil : createRoomParameters.aliasLocalPart) {
|
||||
case .success(let roomId):
|
||||
aliasLocalPart: parameters.isRoomPrivate ? nil : parameters.aliasLocalPart) {
|
||||
case .success(let roomID):
|
||||
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||
state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom,
|
||||
title: L10n.commonError,
|
||||
message: L10n.screenStartChatErrorStartingChat)
|
||||
return
|
||||
}
|
||||
analytics.trackCreatedRoom(isDM: false)
|
||||
actionsSubject.send(.openRoom(withIdentifier: roomId))
|
||||
actionsSubject.send(.createdRoom(roomProxy))
|
||||
case .failure:
|
||||
state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom,
|
||||
title: L10n.commonError,
|
||||
@@ -271,7 +289,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
|
||||
// MARK: Loading indicator
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(CreateRoomViewModel.self)-Loading"
|
||||
private static let loadingIndicatorIdentifier = "\(CreateRoomScreenViewModel.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
+6
-3
@@ -7,9 +7,12 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol CreateRoomViewModelProtocol {
|
||||
var actions: AnyPublisher<CreateRoomViewModelAction, Never> { get }
|
||||
var context: CreateRoomViewModelType.Context { get }
|
||||
protocol CreateRoomScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<CreateRoomScreenViewModelAction, Never> { get }
|
||||
var context: CreateRoomScreenViewModelType.Context { get }
|
||||
|
||||
func updateAvatar(fileURL: URL)
|
||||
}
|
||||
+30
-77
@@ -10,7 +10,7 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct CreateRoomScreen: View {
|
||||
@ObservedObject var context: CreateRoomViewModel.Context
|
||||
@ObservedObject var context: CreateRoomScreenViewModel.Context
|
||||
@FocusState private var focus: Focus?
|
||||
|
||||
private enum Focus {
|
||||
@@ -52,7 +52,6 @@ struct CreateRoomScreen: View {
|
||||
.navigationTitle(L10n.screenCreateRoomTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.readFrame($frame)
|
||||
.alert(item: $context.alertInfo)
|
||||
.shouldScrollOnKeyboardDidShow(focus == .alias, to: Focus.alias)
|
||||
}
|
||||
@@ -109,6 +108,7 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomAvatar)
|
||||
.confirmationDialog("", isPresented: $context.showAttachmentConfirmationDialog) {
|
||||
Button(L10n.actionTakePhoto) {
|
||||
context.send(viewAction: .displayCameraPicker)
|
||||
@@ -116,6 +116,8 @@ struct CreateRoomScreen: View {
|
||||
Button(L10n.actionChoosePhoto) {
|
||||
context.send(viewAction: .displayMediaPicker)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.mediaPicker)
|
||||
|
||||
if context.viewState.avatarURL != nil {
|
||||
Button(L10n.actionRemove, role: .destructive) {
|
||||
context.send(viewAction: .removeImage)
|
||||
@@ -134,32 +136,9 @@ struct CreateRoomScreen: View {
|
||||
} header: {
|
||||
Text(L10n.screenCreateRoomTopicLabel)
|
||||
.compoundListSectionHeader()
|
||||
} footer: {
|
||||
if !context.viewState.selectedUsers.isEmpty {
|
||||
selectedUsersSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var frame: CGRect = .zero
|
||||
@ScaledMetric private var invitedUserCellWidth: CGFloat = 72
|
||||
|
||||
private var selectedUsersSection: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
|
||||
InviteUsersScreenSelectedItem(user: user, mediaProvider: context.mediaProvider) {
|
||||
context.send(viewAction: .deselectUser(user))
|
||||
}
|
||||
.frame(width: invitedUserCellWidth)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, ListRowPadding.horizontal)
|
||||
.padding(.vertical, 22)
|
||||
}
|
||||
.frame(width: frame.width)
|
||||
}
|
||||
|
||||
private var securitySection: some View {
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.screenCreateRoomPrivateOptionTitle,
|
||||
@@ -223,6 +202,7 @@ struct CreateRoomScreen: View {
|
||||
context.send(viewAction: .createRoom)
|
||||
}
|
||||
.disabled(!context.viewState.canCreateRoom)
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.create)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,65 +212,43 @@ struct CreateRoomScreen: View {
|
||||
struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
|
||||
let parameters = CreateRoomFlowParameters()
|
||||
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
selectedUsers: .init(selectedUsers),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static let emtpyViewModel = {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
|
||||
let parameters = CreateRoomFlowParameters()
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
selectedUsers: .init([]),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
return CreateRoomScreenViewModel(userSession: userSession,
|
||||
initialParameters: .init(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static let publicRoomViewModel = {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
|
||||
let parameters = CreateRoomFlowParameters(isRoomPrivate: false)
|
||||
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
selectedUsers: .init([]),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
return CreateRoomScreenViewModel(userSession: userSession,
|
||||
initialParameters: .init(isRoomPrivate: false),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static let publicRoomInvalidAliasViewModel = {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
|
||||
let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "#:")
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
selectedUsers: .init([]),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
return CreateRoomScreenViewModel(userSession: userSession,
|
||||
initialParameters: .init(isRoomPrivate: false, aliasLocalPart: "#:"),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static let publicRoomExistingAliasViewModel = {
|
||||
let clientProxy = ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))
|
||||
clientProxy.isAliasAvailableReturnValue = .success(false)
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "existing")
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
selectedUsers: .init([]),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
return CreateRoomScreenViewModel(userSession: userSession,
|
||||
initialParameters: .init(isRoomPrivate: false, aliasLocalPart: "existing"),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
@@ -298,27 +256,22 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
CreateRoomScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Create Room")
|
||||
NavigationStack {
|
||||
CreateRoomScreen(context: emtpyViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Create Room without users")
|
||||
|
||||
NavigationStack {
|
||||
CreateRoomScreen(context: publicRoomViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Create Public Room")
|
||||
|
||||
NavigationStack {
|
||||
CreateRoomScreen(context: publicRoomInvalidAliasViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { state in
|
||||
!state.aliasErrors.isEmpty
|
||||
})
|
||||
.snapshotPreferences(expect: publicRoomInvalidAliasViewModel.context.$viewState.map { !$0.aliasErrors.isEmpty })
|
||||
.previewDisplayName("Create Public Room, invalid alias")
|
||||
|
||||
NavigationStack {
|
||||
CreateRoomScreen(context: publicRoomExistingAliasViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { state in
|
||||
!state.aliasErrors.isEmpty
|
||||
})
|
||||
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { !$0.aliasErrors.isEmpty })
|
||||
.previewDisplayName("Create Public Room, existing alias")
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import SwiftUI
|
||||
|
||||
struct InviteUsersScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
|
||||
let roomType: InviteUsersScreenRoomType
|
||||
let roomProxy: JoinedRoomProxyProtocol
|
||||
let isSkippable: Bool
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let appSettings: AppSettings
|
||||
@@ -20,7 +20,6 @@ struct InviteUsersScreenCoordinatorParameters {
|
||||
|
||||
enum InviteUsersScreenCoordinatorAction {
|
||||
case dismiss
|
||||
case proceed(selectedUsers: [UserProfileProxy])
|
||||
}
|
||||
|
||||
final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -34,8 +33,8 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: InviteUsersScreenCoordinatorParameters) {
|
||||
viewModel = InviteUsersScreenViewModel(userSession: parameters.userSession,
|
||||
selectedUsers: parameters.selectedUsers,
|
||||
roomType: parameters.roomType,
|
||||
roomProxy: parameters.roomProxy,
|
||||
isSkippable: parameters.isSkippable,
|
||||
userDiscoveryService: parameters.userDiscoveryService,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings)
|
||||
@@ -47,8 +46,6 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .proceed(let selectedUsers):
|
||||
actionsSubject.send(.proceed(selectedUsers: selectedUsers))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -16,7 +16,6 @@ enum InviteUsersScreenErrorType: Error {
|
||||
|
||||
enum InviteUsersScreenViewModelAction {
|
||||
case dismiss
|
||||
case proceed(selectedUsers: [UserProfileProxy])
|
||||
}
|
||||
|
||||
enum InviteUsersScreenRoomType {
|
||||
@@ -53,18 +52,18 @@ struct InviteUsersScreenViewState: BindableState {
|
||||
membershipState[user.userID]
|
||||
}
|
||||
|
||||
let isCreatingRoom: Bool
|
||||
let isSkippable: Bool
|
||||
|
||||
var actionText: String {
|
||||
if isCreatingRoom {
|
||||
return selectedUsers.isEmpty ? L10n.actionSkip : L10n.actionNext
|
||||
if isSkippable, selectedUsers.isEmpty {
|
||||
L10n.actionSkip
|
||||
} else {
|
||||
return L10n.actionInvite
|
||||
L10n.actionInvite
|
||||
}
|
||||
}
|
||||
|
||||
var isActionDisabled: Bool {
|
||||
isCreatingRoom ? false : selectedUsers.isEmpty
|
||||
isSkippable ? false : selectedUsers.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import SwiftUI
|
||||
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
||||
|
||||
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
||||
private let roomType: InviteUsersScreenRoomType
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appSettings: AppSettings
|
||||
@@ -25,22 +25,19 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?,
|
||||
roomType: InviteUsersScreenRoomType,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
isSkippable: Bool,
|
||||
userDiscoveryService: UserDiscoveryServiceProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings) {
|
||||
self.roomType = roomType
|
||||
self.roomProxy = roomProxy
|
||||
self.userDiscoveryService = userDiscoveryService
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appSettings = appSettings
|
||||
self.selectedUsers = selectedUsers
|
||||
|
||||
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers?.value ?? [],
|
||||
isCreatingRoom: roomType.isCreatingRoom),
|
||||
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: [],
|
||||
isSkippable: isSkippable),
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
|
||||
setupSubscriptions()
|
||||
@@ -62,12 +59,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
case .cancel:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .proceed:
|
||||
switch roomType {
|
||||
case .draft:
|
||||
actionsSubject.send(.proceed(selectedUsers: state.selectedUsers))
|
||||
case .room(let roomProxy):
|
||||
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
||||
}
|
||||
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
||||
case .toggleUser(let user):
|
||||
toggleUser(user)
|
||||
}
|
||||
@@ -78,7 +70,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
private func toggleUser(_ user: UserProfileProxy) {
|
||||
if state.selectedUsers.contains(user) {
|
||||
state.scrollToLastID = nil
|
||||
state.selectedUsers.removeAll(where: { $0.userID == user.userID })
|
||||
state.selectedUsers.removeAll { $0.userID == user.userID }
|
||||
} else {
|
||||
state.scrollToLastID = user.userID
|
||||
state.selectedUsers.append(user)
|
||||
@@ -87,15 +79,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
|
||||
private func inviteUsers(_ users: [String], roomProxy: JoinedRoomProxyProtocol) {
|
||||
if appSettings.enableKeyShareOnInvite {
|
||||
showLoader(title: L10n.screenRoomDetailsInvitePeoplePreparing,
|
||||
message: L10n.screenRoomDetailsInvitePeopleDontClose)
|
||||
showLoadingIndicator(title: L10n.screenRoomDetailsInvitePeoplePreparing, message: L10n.screenRoomDetailsInvitePeopleDontClose)
|
||||
} else {
|
||||
showLoader()
|
||||
showLoadingIndicator()
|
||||
}
|
||||
|
||||
Task {
|
||||
defer {
|
||||
hideLoader()
|
||||
hideLoadingIndicator()
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
|
||||
@@ -122,7 +113,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
}
|
||||
|
||||
private func buildMembershipStateIfNeeded(members: [RoomMemberProxyProtocol]) {
|
||||
showLoader()
|
||||
showLoadingIndicator()
|
||||
|
||||
Task.detached { [members] in
|
||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
||||
@@ -133,7 +124,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
|
||||
Task { @MainActor in
|
||||
self.state.membershipState = membershipState
|
||||
self.hideLoader()
|
||||
self.hideLoadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,25 +141,13 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
self?.fetchUsers()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if let selectedUsers {
|
||||
selectedUsers
|
||||
.sink { [weak self] users in
|
||||
self?.state.selectedUsers = users
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchMembersIfNeeded() {
|
||||
guard case let .room(roomProxy) = roomType else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
showLoader()
|
||||
showLoadingIndicator()
|
||||
await roomProxy.updateMembers()
|
||||
hideLoader()
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
roomProxy.membersPublisher
|
||||
@@ -211,8 +190,8 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
|
||||
private let userIndicatorID = UUID().uuidString
|
||||
|
||||
private func showLoader(title: String = L10n.commonLoading,
|
||||
message: String? = nil) {
|
||||
private func showLoadingIndicator(title: String = L10n.commonLoading,
|
||||
message: String? = nil) {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID,
|
||||
type: .modal,
|
||||
title: title,
|
||||
@@ -221,18 +200,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
delay: .milliseconds(200))
|
||||
}
|
||||
|
||||
private func hideLoader() {
|
||||
private func hideLoadingIndicator() {
|
||||
userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
||||
}
|
||||
}
|
||||
|
||||
private extension InviteUsersScreenRoomType {
|
||||
var isCreatingRoom: Bool {
|
||||
switch self {
|
||||
case .draft:
|
||||
return true
|
||||
case .room:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ struct InviteUsersScreen: View {
|
||||
accessibilityFocusOnStart: true)
|
||||
.compoundSearchField()
|
||||
.alert(item: $context.alertInfo)
|
||||
.navigationBarBackButtonHidden(context.viewState.isSkippable)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -126,7 +127,7 @@ struct InviteUsersScreen: View {
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
if !context.viewState.isCreatingRoom {
|
||||
if !context.viewState.isSkippable {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
@@ -155,8 +156,8 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
||||
let userDiscoveryService = UserDiscoveryServiceMock()
|
||||
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
|
||||
return InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
|
||||
selectedUsers: .init([]),
|
||||
roomType: .draft,
|
||||
roomProxy: JoinedRoomProxyMock(.init()),
|
||||
isSkippable: true,
|
||||
userDiscoveryService: userDiscoveryService,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
@@ -10,38 +10,26 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct StartChatScreenCoordinatorParameters {
|
||||
let orientationManager: OrientationManagerProtocol
|
||||
let userSession: UserSessionProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let appSettings: AppSettings
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
enum StartChatScreenCoordinatorAction {
|
||||
case close
|
||||
case openRoom(withIdentifier: String)
|
||||
case createRoom
|
||||
case openRoom(roomID: String)
|
||||
case openRoomDirectorySearch
|
||||
}
|
||||
|
||||
final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: StartChatScreenCoordinatorParameters
|
||||
private var viewModel: StartChatScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<StartChatScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var createRoomParameters = CurrentValueSubject<CreateRoomFlowParameters, Never>(.init())
|
||||
private var createRoomParametersPublisher: CurrentValuePublisher<CreateRoomFlowParameters, Never> {
|
||||
createRoomParameters.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private let selectedUsers = CurrentValueSubject<[UserProfileProxy], Never>([])
|
||||
private var selectedUsersPublisher: CurrentValuePublisher<[UserProfileProxy], Never> {
|
||||
selectedUsers.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private let actionsSubject: PassthroughSubject<StartChatScreenCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<StartChatScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
@@ -63,10 +51,9 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
case .close:
|
||||
actionsSubject.send(.close)
|
||||
case .createRoom:
|
||||
// before creating a room we select the users we would like to invite in that room
|
||||
presentInviteUsersScreen()
|
||||
case .showRoom(let identifier):
|
||||
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
actionsSubject.send(.createRoom)
|
||||
case .showRoom(let roomID):
|
||||
actionsSubject.send(.openRoom(roomID: roomID))
|
||||
case .openRoomDirectorySearch:
|
||||
actionsSubject.send(.openRoomDirectorySearch)
|
||||
}
|
||||
@@ -79,126 +66,4 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(StartChatScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func presentInviteUsersScreen() {
|
||||
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: parameters.userSession,
|
||||
selectedUsers: selectedUsersPublisher,
|
||||
roomType: .draft,
|
||||
userDiscoveryService: parameters.userDiscoveryService,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings)
|
||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
fatalError("Not shown in this flow.")
|
||||
case .proceed(let selectedUsers):
|
||||
self.selectedUsers.send(selectedUsers)
|
||||
openCreateRoomScreen()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
parameters.navigationStackCoordinator?.push(coordinator) { [weak self] in
|
||||
self?.createRoomParameters.send(.init())
|
||||
self?.selectedUsers.send([])
|
||||
}
|
||||
}
|
||||
|
||||
private func openCreateRoomScreen() {
|
||||
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
createRoomParameters: createRoomParametersPublisher,
|
||||
selectedUsers: selectedUsers.value,
|
||||
appSettings: parameters.appSettings,
|
||||
analytics: parameters.analytics)
|
||||
let coordinator = CreateRoomCoordinator(parameters: createParameters)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .updateSelectedUsers(let users):
|
||||
self.selectedUsers.send(users)
|
||||
case .updateDetails(let details):
|
||||
self.createRoomParameters.send(details)
|
||||
case .openRoom(let identifier):
|
||||
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
case .displayMediaPickerWithMode(let mode):
|
||||
self.displayMediaPickerWithMode(mode)
|
||||
case .removeImage:
|
||||
var parameters = self.createRoomParameters.value
|
||||
parameters.avatarImageMedia = nil
|
||||
self.createRoomParameters.send(parameters)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
parameters.navigationStackCoordinator?.push(coordinator)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func displayMediaPickerWithMode(_ mode: MediaPickerScreenMode) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
orientationManager: parameters.orientationManager) { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
case .selectedMediaAtURLs(let urls):
|
||||
guard urls.count == 1,
|
||||
let url = urls.first else {
|
||||
fatalError("Received an invalid number of URLs")
|
||||
}
|
||||
|
||||
processAvatar(from: url)
|
||||
}
|
||||
}
|
||||
|
||||
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
||||
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(stackCoordinator)
|
||||
}
|
||||
|
||||
private func processAvatar(from url: URL) {
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
showLoadingIndicator()
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
guard case let .success(maxUploadSize) = await parameters.userSession.clientProxy.maxMediaUploadSize else {
|
||||
MXLog.error("Failed to get max upload size")
|
||||
parameters.userIndicatorController.alertInfo = AlertInfo(id: .init())
|
||||
return
|
||||
}
|
||||
let media = try await parameters.mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize).get()
|
||||
var parameters = createRoomParameters.value
|
||||
parameters.avatarImageMedia = media
|
||||
createRoomParameters.send(parameters)
|
||||
} catch {
|
||||
parameters.userIndicatorController.alertInfo = AlertInfo(id: .init())
|
||||
}
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Loading indicator
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(StartChatScreenCoordinator.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func hideLoadingIndicator() {
|
||||
parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ enum StartChatScreenErrorType: Error {
|
||||
enum StartChatScreenViewModelAction: Equatable {
|
||||
case close
|
||||
case createRoom
|
||||
case showRoom(withIdentifier: String)
|
||||
case showRoom(roomID: String)
|
||||
case openRoomDirectorySearch
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
|
||||
switch currentDirectRoom {
|
||||
case .success(.some(let roomId)):
|
||||
hideLoadingIndicator()
|
||||
actionsSubject.send(.showRoom(withIdentifier: roomId))
|
||||
actionsSubject.send(.showRoom(roomID: roomId))
|
||||
case .success:
|
||||
hideLoadingIndicator()
|
||||
state.bindings.selectedUserToInvite = user
|
||||
@@ -190,7 +190,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
|
||||
switch await userSession.clientProxy.createDirectRoom(with: user.userID, expectedRoomName: user.displayName) {
|
||||
case .success(let roomId):
|
||||
analytics.trackCreatedRoom(isDM: true)
|
||||
actionsSubject.send(.showRoom(withIdentifier: roomId))
|
||||
actionsSubject.send(.showRoom(roomID: roomId))
|
||||
case .failure:
|
||||
displayError()
|
||||
}
|
||||
@@ -205,7 +205,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
|
||||
private func joinRoomByAddress() {
|
||||
if case let .addressFound(lastTestedAddress, roomID) = internalRoomAddressState,
|
||||
lastTestedAddress == state.bindings.roomAddress {
|
||||
actionsSubject.send(.showRoom(withIdentifier: roomID))
|
||||
actionsSubject.send(.showRoom(roomID: roomID))
|
||||
} else if let resolveAliasTask {
|
||||
// If the task is still running we wait for it to complete and we check the state again
|
||||
showLoadingIndicator(delay: .milliseconds(250))
|
||||
|
||||
Reference in New Issue
Block a user