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:
Doug
2025-10-30 16:12:56 +00:00
committed by GitHub
parent 401ff49882
commit 933891614c
68 changed files with 764 additions and 789 deletions
@@ -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)