diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index dd1f0bcea..dfbfb5a0a 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -60,6 +60,13 @@ struct FocusEvent: Hashable { let shouldSetPin: Bool } +enum InviteUsersFlow: Hashable { + /// Invite people into the current room. + case existingRoom + /// Start a new room with the given invitee pre-selected and mandatory. + case newRoom(mandatoryInvitee: UserProfileProxy) +} + // swiftlint:disable:next type_body_length class RoomFlowCoordinator: FlowCoordinatorProtocol { private let roomID: String @@ -537,8 +544,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentMediaUploadPreviewScreen(for: mediaURLs, timelineController: timelineController, animated: animated) - case (_, .presentInviteUsersScreen, .inviteUsersScreen): - presentInviteUsersScreen() + case (_, .presentInviteUsersScreen(let flow), .inviteUsersScreen): + presentInviteUsersScreen(flow: flow) case (_, .presentTransferOwnershipScreen, .transferOwnershipScreen): presentTransferOwnershipScreen() @@ -955,7 +962,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case .presentNotificationSettingsScreen: stateMachine.tryEvent(.presentNotificationSettingsScreen) case .presentInviteUsersScreen: - stateMachine.tryEvent(.presentInviteUsersScreen) + stateMachine.tryEvent(.presentInviteUsersScreen(flow: .existingRoom)) + case .presentInviteToNewRoom(let mandatoryInvitee): + stateMachine.tryEvent(.presentInviteUsersScreen(flow: .newRoom(mandatoryInvitee: mandatoryInvitee))) case .presentPollsHistory: stateMachine.tryEvent(.presentPollsHistory) case .presentRolesAndPermissionsScreen: @@ -1326,10 +1335,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentInviteUsersScreen() { + private func presentInviteUsersScreen(flow: InviteUsersFlow = .existingRoom) { let stackCoordinator = NavigationStackCoordinator() + + let roomType: InviteUsersScreenRoomType = switch flow { + case .existingRoom: .existingRoom(roomProxy: roomProxy) + case .newRoom(let mandatoryInvitee): .draft(mandatoryInvitees: [mandatoryInvitee]) + } + let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: userSession, - roomProxy: roomProxy, + roomType: roomType, isSkippable: false, userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy), userIndicatorController: flowParameters.userIndicatorController, @@ -1344,6 +1359,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { switch action { case .dismiss: navigationStackCoordinator.setSheetCoordinator(nil) + case .openRoom(let roomID): + guard case .newRoom = flow else { return } + navigationStackCoordinator.setSheetCoordinator(nil) + stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift index 016ae724c..272fbbf68 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift @@ -65,7 +65,7 @@ extension RoomFlowCoordinator { case roomDetailsEditScreen case notificationSettings case globalNotificationSettings - case inviteUsersScreen(previousState: State) + case inviteUsersScreen(flow: InviteUsersFlow, previousState: State) case mediaUploadPicker(mode: MediaPickerScreenMode, previousState: State) case mediaUploadPreview(mediaURLs: [URL], previousState: State) case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set, previousState: State) @@ -136,7 +136,7 @@ extension RoomFlowCoordinator { case presentGlobalNotificationSettingsScreen case dismissGlobalNotificationSettingsScreen - case presentInviteUsersScreen + case presentInviteUsersScreen(flow: InviteUsersFlow) case dismissInviteUsersScreen case presentMediaUploadPicker(mode: MediaPickerScreenMode) @@ -404,9 +404,9 @@ extension RoomFlowCoordinator { case (.mediaUploadPicker(_, let previousMediaUploadPickerState), .presentMediaUploadPreview(let mediaURLs)): return .mediaUploadPreview(mediaURLs: mediaURLs, previousState: previousMediaUploadPickerState) - case (_, .presentInviteUsersScreen): - return .inviteUsersScreen(previousState: fromState) - case (.inviteUsersScreen(let previousState), .dismissInviteUsersScreen): + case (_, .presentInviteUsersScreen(let flow)): + return .inviteUsersScreen(flow: flow, previousState: fromState) + case (.inviteUsersScreen(_, let previousState), .dismissInviteUsersScreen): return previousState case (_, .presentTransferOwnershipScreen): diff --git a/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift index ede424562..8c38630c9 100644 --- a/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift @@ -261,7 +261,7 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol { private func presentInviteUsersScreen() { let stackCoordinator = NavigationStackCoordinator() let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: flowParameters.userSession, - roomProxy: roomProxy, + roomType: .existingRoom(roomProxy: roomProxy), isSkippable: false, userDiscoveryService: UserDiscoveryService(clientProxy: flowParameters.userSession.clientProxy), userIndicatorController: flowParameters.userIndicatorController, @@ -276,6 +276,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol { switch action { case .dismiss: navigationStackCoordinator.setSheetCoordinator(nil) + case .openRoom: + break } } .store(in: &cancellables) diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift index d872fcff4..29cd5888a 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -240,7 +240,7 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case .transferOwnership: stateMachine.tryEvent(.presentTransferOwnership) case .presentRecipientDetails, .presentNotificationSettingsScreen, .presentReportRoomScreen, - .presentInviteUsersScreen, .presentPollsHistory, .presentCall, + .presentInviteUsersScreen, .presentInviteToNewRoom, .presentPollsHistory, .presentCall, .presentPinnedEventsTimeline, .presentMediaEventsTimeline, .presentKnockingRequestsListScreen: fatalError("Not handled in the space context") } diff --git a/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift index 246d8339f..bcfd7d77c 100644 --- a/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/StartChatFlowCoordinator.swift @@ -270,7 +270,7 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol { private func presentInviteUsersScreen(roomProxy: JoinedRoomProxyProtocol, spaceRoomListProxy: SpaceRoomListProxyProtocol?) { let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: flowParameters.userSession, - roomProxy: roomProxy, + roomType: .existingRoom(roomProxy: roomProxy), isSkippable: true, userDiscoveryService: userDiscoveryService, userIndicatorController: flowParameters.userIndicatorController, @@ -285,6 +285,8 @@ class StartChatFlowCoordinator: FlowCoordinatorProtocol { } else { actionsSubject.send(.finished(.room(id: roomProxy.id))) } + case .openRoom: + break } } .store(in: &cancellables) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index c68ed7ecf..c211cca0a 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -3367,8 +3367,8 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled: Bool { return createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCallsCount > 0 } - var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments: (name: String, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)? - var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedInvocations: [(name: String, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)] = [] + var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments: (name: String?, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)? + var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedInvocations: [(name: String?, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)] = [] var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue: Result! var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue: Result! { @@ -3394,9 +3394,9 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { } } } - var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartClosure: ((String, String?, CreateRoomAccessType, Bool, [String], URL?, String?) async -> Result)? + var createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartClosure: ((String?, String?, CreateRoomAccessType, Bool, [String], URL?, String?) async -> Result)? - func createRoom(name: String, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?) async -> Result { + func createRoom(name: String?, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?) async -> Result { createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCallsCount += 1 createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments = (name: name, topic: topic, accessType: accessType, isSpace: isSpace, userIDs: userIDs, avatarURL: avatarURL, aliasLocalPart: aliasLocalPart) DispatchQueue.main.async { diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift index 6eb0733a2..95e043132 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift @@ -11,7 +11,7 @@ import SwiftUI struct InviteUsersScreenCoordinatorParameters { let userSession: UserSessionProtocol - let roomProxy: JoinedRoomProxyProtocol + let roomType: InviteUsersScreenRoomType let isSkippable: Bool let userDiscoveryService: UserDiscoveryServiceProtocol let userIndicatorController: UserIndicatorControllerProtocol @@ -20,6 +20,7 @@ struct InviteUsersScreenCoordinatorParameters { enum InviteUsersScreenCoordinatorAction { case dismiss + case openRoom(roomID: String) } final class InviteUsersScreenCoordinator: CoordinatorProtocol { @@ -33,7 +34,7 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol { init(parameters: InviteUsersScreenCoordinatorParameters) { viewModel = InviteUsersScreenViewModel(userSession: parameters.userSession, - roomProxy: parameters.roomProxy, + roomType: parameters.roomType, isSkippable: parameters.isSkippable, userDiscoveryService: parameters.userDiscoveryService, userIndicatorController: parameters.userIndicatorController, @@ -46,6 +47,8 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol { switch action { case .dismiss: actionsSubject.send(.dismiss) + case .openRoom(let roomID): + actionsSubject.send(.openRoom(roomID: roomID)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift index a9cc51ab1..0a9ba7f36 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift @@ -16,11 +16,12 @@ enum InviteUsersScreenErrorType: Error { enum InviteUsersScreenViewModelAction { case dismiss + case openRoom(roomID: String) } enum InviteUsersScreenRoomType { - case draft - case room(roomProxy: JoinedRoomProxyProtocol) + case draft(mandatoryInvitees: [UserProfileProxy]) + case existingRoom(roomProxy: JoinedRoomProxyProtocol) } struct InviteUsersScreenViewState: BindableState { @@ -29,6 +30,7 @@ struct InviteUsersScreenViewState: BindableState { var usersSection: UserDiscoverySection = .init(type: .suggestions, users: []) var selectedUsers: [UserProfileProxy] = [] + var mandatoryInvitees: [UserProfileProxy] = [] var membershipState: [String: MembershipState] = .init() var usersToConfirm: [UserProfileProxy] = [] @@ -38,32 +40,29 @@ struct InviteUsersScreenViewState: BindableState { !isSearching && usersSection.type == .searchResult && usersSection.users.isEmpty } + var hasInvitableSelectedUsers: Bool { + selectedUsers.contains { !isInviteeMandatory($0) } + } + func isUserSelected(_ user: UserProfileProxy) -> Bool { isUserDisabled(user) || selectedUsers.contains { $0.userID == user.userID } } func isUserDisabled(_ user: UserProfileProxy) -> Bool { + if isInviteeMandatory(user) { return true } let membershipState = membershipState(user) return membershipState == .invite || membershipState == .join } + func isInviteeMandatory(_ user: UserProfileProxy) -> Bool { + mandatoryInvitees.contains { $0.userID == user.userID } + } + func membershipState(_ user: UserProfileProxy) -> MembershipState? { membershipState[user.userID] } let isSkippable: Bool - - var actionText: String { - if isSkippable, selectedUsers.isEmpty { - L10n.actionSkip - } else { - L10n.actionInvite - } - } - - var isActionDisabled: Bool { - isSkippable ? false : selectedUsers.isEmpty - } } struct InviteUsersScreenViewStateBindings { diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift index f5f20290b..4222de8dd 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift @@ -14,7 +14,7 @@ typealias InviteUsersScreenViewModelType = StateStoreViewModel InviteUsersScreenViewModel { let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init()))), - roomProxy: JoinedRoomProxyMock(.init(members: [])), + roomType: .existingRoom(roomProxy: JoinedRoomProxyMock(.init(members: []))), isSkippable: true, userDiscoveryService: UserDiscoveryServiceMock(), userIndicatorController: UserIndicatorControllerMock(), diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift index 989e62786..0db16105d 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift @@ -110,7 +110,9 @@ struct InviteUsersScreen: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(context.viewState.selectedUsers, id: \.userID) { user in - InviteUsersScreenSelectedItem(user: user, mediaProvider: context.mediaProvider) { + InviteUsersScreenSelectedItem(user: user, + mediaProvider: context.mediaProvider, + isLocked: context.viewState.isInviteeMandatory(user)) { deselect(user) } .frame(width: selectedUserCellWidth) @@ -133,11 +135,18 @@ struct InviteUsersScreen: View { } ToolbarItem(placement: .confirmationAction) { - Button(context.viewState.actionText) { - context.send(viewAction: .proceed) + if context.viewState.isSkippable, context.viewState.selectedUsers.isEmpty { + Button(L10n.actionSkip) { + context.send(viewAction: .proceed) + } + .accessibilityIdentifier(A11yIdentifiers.inviteUsersScreen.proceed) + } else { + ToolbarButton(role: .confirm(title: L10n.actionInvite)) { + context.send(viewAction: .proceed) + } + .accessibilityIdentifier(A11yIdentifiers.inviteUsersScreen.proceed) + .disabled(!context.viewState.hasInvitableSelectedUsers) } - .accessibilityIdentifier(A11yIdentifiers.inviteUsersScreen.proceed) - .disabled(context.viewState.isActionDisabled) } } @@ -153,6 +162,7 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview { static let searchingViewModel = makeViewModel(searchQuery: "Alice") static let selectedViewModel = makeViewModel(hasSelection: true) static let confirmSelectedViewModel = makeViewModel(shouldConfirm: true) + static let draftViewModel = makeViewModel(roomType: .draft(mandatoryInvitees: [.mockAlice]), isSkippable: false) static var previews: some View { ElementNavigationStack { @@ -179,18 +189,28 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview { InviteUsersScreen(context: confirmSelectedViewModel.context) } .previewDisplayName("Confirm Selected") + + ElementNavigationStack { + InviteUsersScreen(context: draftViewModel.context) + } + .previewDisplayName("Draft (locked invitee)") + .snapshotPreferences(expect: draftViewModel.context.$viewState.map { !$0.mandatoryInvitees.isEmpty }) } - static func makeViewModel(searchQuery: String? = nil, hasSelection: Bool = false, shouldConfirm: Bool = false) -> InviteUsersScreenViewModel { + static func makeViewModel(searchQuery: String? = nil, + hasSelection: Bool = false, + shouldConfirm: Bool = false, + roomType: InviteUsersScreenRoomType? = nil, + isSkippable: Bool = true) -> InviteUsersScreenViewModel { let clientProxy = ClientProxyMock(.init()) clientProxy.recentConversationCounterpartsReturnValue = [.mockAlice, .mockBob, .mockCharlie, .mockDan, .mockVerbose] - + let userDiscoveryService = UserDiscoveryServiceMock() userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice]) - + let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), - roomProxy: JoinedRoomProxyMock(.init(members: [])), - isSkippable: true, + roomType: roomType ?? .existingRoom(roomProxy: JoinedRoomProxyMock(.init(members: []))), + isSkippable: isSkippable, userDiscoveryService: userDiscoveryService, userIndicatorController: UserIndicatorControllerMock(), appSettings: AppSettings()) diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift index a42a4886b..4d205a78a 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift @@ -12,6 +12,7 @@ import SwiftUI struct InviteUsersScreenSelectedItem: View { let user: UserProfileProxy let mediaProvider: MediaProviderProtocol? + var isLocked = false let dismissAction: () -> Void var body: some View { @@ -25,18 +26,27 @@ struct InviteUsersScreenSelectedItem: View { .lineLimit(1) } .accessibilityElement(children: .combine) - .accessibilityAction(named: L10n.actionRemove, dismissAction) + .accessibilityActions { + if !isLocked { + Button(L10n.actionRemove, action: dismissAction) + } + } } // MARK: - Private + @ViewBuilder var avatar: some View { - LoadableAvatarImage(url: user.avatarURL, - name: user.displayName, - contentID: user.userID, - avatarSize: .user(on: .inviteUsers), - mediaProvider: mediaProvider) - .overlayRemoveItemButton(action: dismissAction) + let avatarImage = LoadableAvatarImage(url: user.avatarURL, + name: user.displayName, + contentID: user.userID, + avatarSize: .user(on: .inviteUsers), + mediaProvider: mediaProvider) + if isLocked { + avatarImage + } else { + avatarImage.overlayRemoveItemButton(action: dismissAction) + } } var closeButtonLabel: some View { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index dd8d69c64..908b9c714 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -26,6 +26,7 @@ enum RoomDetailsScreenCoordinatorAction { case presentRoomDetailsEditScreen case presentNotificationSettingsScreen case presentInviteUsersScreen + case presentInviteToNewRoom(invitee: UserProfileProxy) case presentPollsHistory case presentRolesAndPermissionsScreen case presentCall(isVoiceCall: Bool) @@ -71,6 +72,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentRoomMembersList) case .requestInvitePeoplePresentation: actionsSubject.send(.presentInviteUsersScreen) + case .requestInviteToNewRoomPresentation(let invitee): + actionsSubject.send(.presentInviteToNewRoom(invitee: invitee)) case .leftRoom: actionsSubject.send(.leftRoom) case .requestEditDetailsPresentation: diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 071315acd..520b66e39 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -18,6 +18,7 @@ enum RoomDetailsScreenViewModelAction: Equatable { case requestMemberDetailsPresentation case requestRecipientDetailsPresentation(userID: String) case requestInvitePeoplePresentation + case requestInviteToNewRoomPresentation(selectedInvitee: UserProfileProxy) case leftRoom case requestEditDetailsPresentation case requestPollsHistoryPresentation @@ -98,6 +99,7 @@ struct RoomDetailsScreenViewState: BindableState { } shortcuts.append(.videoCall) } + // The invite flow is different for DMs if dmRecipientInfo == nil, canInviteUsers { shortcuts.append(.invite) } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 9c8a8b6a3..60bfb3a37 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -110,7 +110,11 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr case .processTapPeople: actionsSubject.send(.requestMemberDetailsPresentation) case .processTapInvite: - actionsSubject.send(.requestInvitePeoplePresentation) + if let dmRecipient = state.dmRecipientInfo { + actionsSubject.send(.requestInviteToNewRoomPresentation(selectedInvitee: .init(member: dmRecipient.member))) + } else { + actionsSubject.send(.requestInvitePeoplePresentation) + } case .processTapLeave: processTapToLeave() case .confirmLeave: diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index c553e0c8c..74ab1ef47 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -22,6 +22,11 @@ struct RoomDetailsScreen: View { aboutSection + // The invitation flow is different for DMs + if context.viewState.dmRecipientInfo != nil { + inviteToNewRoomSection + } + configurationSection if context.viewState.dmRecipientInfo == nil { @@ -165,6 +170,13 @@ struct RoomDetailsScreen: View { } } + private var inviteToNewRoomSection: some View { + ListRow(label: .default(title: L10n.actionInvite, icon: \.userAdd), + kind: .navigationLink { + context.send(viewAction: .processTapInvite) + }) + } + private var configurationSection: some View { Section { ListRow(label: .default(title: L10n.screenRoomDetailsNotificationTitle, icon: \.notifications), diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index dbcb037ce..d978b5441 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -516,7 +516,7 @@ class ClientProxy: ClientProxyProtocol { } } - func createRoom(name: String, + func createRoom(name: String?, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index d7fd837c2..9cec00177 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -183,7 +183,7 @@ protocol ClientProxyProtocol: AnyObject { func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result - func createRoom(name: String, + func createRoom(name: String?, topic: String?, accessType: CreateRoomAccessType, isSpace: Bool, diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPad-en-GB.png new file mode 100644 index 000000000..d2898ec55 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9413c3be57bf3882bff847d80d76616413d2c16a63ba316a760548fff2b6386 +size 151997 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPad-pseudo.png new file mode 100644 index 000000000..871a52984 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e70eaef9bc01eccd462be75ac69e1d556784596a0287da9a8f3f7e10e4e58b0 +size 156413 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPhone-en-GB.png new file mode 100644 index 000000000..a0f099c5a --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5338f0606064bc4c3c098c2458f1923db59f206c059cfa11bdbc387d42b3af3a +size 99061 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPhone-pseudo.png new file mode 100644 index 000000000..10808572b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Draft-locked-invitee-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c9456d7cf947a55063eae44ea955c34fd3af41663ffffbe1f72ba47f791cab0 +size 102079 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-en-GB.png index 73a28e909..0d79e5e3b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2efff4b459ba93bc6c8290a3ff46a26a684b2c6abe2812c8a4667a86b060711 -size 151210 +oid sha256:5a87b4c36df926a3cde0d0a15022e5b5c0c0e545c97974f916b506b4862fc5ab +size 149551 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-pseudo.png index d37db25eb..b2a9c7448 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a6fa5cea94e92da8768cb87d2d157b9552ec3624fbc4f1241c0c212e93f2d98 -size 155938 +oid sha256:d450d451e9ca7779f4f186f2eabf8a23cef9b2dd861e581ed1c61d0dbbb06499 +size 153569 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-en-GB.png index 60f55e134..758300dfe 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34bbaf989ac4635edb87a1e2855c45af93eba45aae198a56173186014b79ddb3 -size 98426 +oid sha256:c793431f951a5ad023ab9b358658a5d75244e292cf56c0fc4a808b84df654e7a +size 97084 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-pseudo.png index ba5fd1993..2164be812 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cebab4fae25f0ea54e0583563719429d06d52949d7847458a98dec6c0ce746d -size 101873 +oid sha256:68c222b7a50badb09b9f2cbdcc6010edac0a7a2727ed9f2f98713e2874ffa214 +size 100304 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-en-GB.png index b3641154d..17529a6b1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c993321bdec5ce94cdd241664bce503fb1c65dc5341355c593f4eeae7f6c222f -size 244787 +oid sha256:6bcc46e5933065a1b02dff5c2bf8abc09c626cf6adb6b91acca6c0d27bf8d967 +size 236706 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-pseudo.png index abea42b39..a8cc0479d 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a0c1fd96fe4299e6075b6ebadc330a5943fe5393e3589a93288dc5d87f73ac8 -size 253574 +oid sha256:f892f94486579fcebfdb3bd5723c233130b10fdabbadac17f687fcedab365085 +size 244369 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-en-GB.png index 1b0c83b2e..4be9ac87e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68b3531545e4cecc6b6be0bb009e448cae7bc7ed2cee4686b48a20a780c82bb3 -size 181065 +oid sha256:ebb51488b9ce71449d0f8ee04bba5f260f924c61186b41c0d6fe8903b06de6b9 +size 174363 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-pseudo.png index 378d86261..fa437f546 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verification-Violation-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:040b3df78b47409a450c6ffec62fb3ff72cbfe74e15f3e85dfbc046a5f2695a7 -size 185625 +oid sha256:f87dbe6bdd53d79ef618cb86707360df944aeb0ad5916f64a9826eaae400e2b0 +size 185369 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-en-GB.png index b3641154d..17529a6b1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c993321bdec5ce94cdd241664bce503fb1c65dc5341355c593f4eeae7f6c222f -size 244787 +oid sha256:6bcc46e5933065a1b02dff5c2bf8abc09c626cf6adb6b91acca6c0d27bf8d967 +size 236706 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-pseudo.png index abea42b39..a8cc0479d 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a0c1fd96fe4299e6075b6ebadc330a5943fe5393e3589a93288dc5d87f73ac8 -size 253574 +oid sha256:f892f94486579fcebfdb3bd5723c233130b10fdabbadac17f687fcedab365085 +size 244369 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-en-GB.png index 1b0c83b2e..4be9ac87e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68b3531545e4cecc6b6be0bb009e448cae7bc7ed2cee4686b48a20a780c82bb3 -size 181065 +oid sha256:ebb51488b9ce71449d0f8ee04bba5f260f924c61186b41c0d6fe8903b06de6b9 +size 174363 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-pseudo.png index 378d86261..fa437f546 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-Verified-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:040b3df78b47409a450c6ffec62fb3ff72cbfe74e15f3e85dfbc046a5f2695a7 -size 185625 +oid sha256:f87dbe6bdd53d79ef618cb86707360df944aeb0ad5916f64a9826eaae400e2b0 +size 185369 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-en-GB.png index b3641154d..17529a6b1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c993321bdec5ce94cdd241664bce503fb1c65dc5341355c593f4eeae7f6c222f -size 244787 +oid sha256:6bcc46e5933065a1b02dff5c2bf8abc09c626cf6adb6b91acca6c0d27bf8d967 +size 236706 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-pseudo.png index abea42b39..a8cc0479d 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a0c1fd96fe4299e6075b6ebadc330a5943fe5393e3589a93288dc5d87f73ac8 -size 253574 +oid sha256:f892f94486579fcebfdb3bd5723c233130b10fdabbadac17f687fcedab365085 +size 244369 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-en-GB.png index 1b0c83b2e..4be9ac87e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68b3531545e4cecc6b6be0bb009e448cae7bc7ed2cee4686b48a20a780c82bb3 -size 181065 +oid sha256:ebb51488b9ce71449d0f8ee04bba5f260f924c61186b41c0d6fe8903b06de6b9 +size 174363 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-pseudo.png index 378d86261..fa437f546 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomDetailsScreen.DM-Room-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:040b3df78b47409a450c6ffec62fb3ff72cbfe74e15f3e85dfbc046a5f2695a7 -size 185625 +oid sha256:f87dbe6bdd53d79ef618cb86707360df944aeb0ad5916f64a9826eaae400e2b0 +size 185369 diff --git a/UnitTests/Sources/InviteUsersViewModelTests.swift b/UnitTests/Sources/InviteUsersViewModelTests.swift index 80de7f375..7eae02263 100644 --- a/UnitTests/Sources/InviteUsersViewModelTests.swift +++ b/UnitTests/Sources/InviteUsersViewModelTests.swift @@ -87,6 +87,8 @@ final class InviteUsersScreenViewModelTests { switch action { case .dismiss: return true + case .openRoom: + return false } } @@ -122,6 +124,8 @@ final class InviteUsersScreenViewModelTests { switch action { case .dismiss: return true + case .openRoom: + return false } } @@ -159,6 +163,74 @@ final class InviteUsersScreenViewModelTests { try await deferredState.fulfill() } + // MARK: - Draft (new room) + + @Test + func createsNewRoomInDraftMode() async throws { + userDiscoveryService = UserDiscoveryServiceMock() + userDiscoveryService.searchProfilesWithReturnValue = .success([]) + + clientProxy = ClientProxyMock(.init(userID: "@mock:client.com")) + let newRoomID = "!newroom:example.com" + clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success(newRoomID) + + viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), + roomType: .draft(mandatoryInvitees: [.mockAlice]), + isSkippable: false, + userDiscoveryService: userDiscoveryService, + userIndicatorController: UserIndicatorControllerMock(), + appSettings: AppSettings()) + + // The locked invitee starts pre-selected and locked. + #expect(context.viewState.selectedUsers.map(\.userID) == [UserProfileProxy.mockAlice.userID]) + #expect(context.viewState.isInviteeMandatory(.mockAlice)) + // The proceed button is disabled while the only selected user is the locked invitee. + #expect(!context.viewState.hasInvitableSelectedUsers) + + // Selecting a non-locked user enables the proceed button. + var deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.isUserSelected(.mockBob) + } + context.send(viewAction: .toggleUser(.mockBob)) + try await deferredState.fulfill() + #expect(context.viewState.hasInvitableSelectedUsers) + + // Deselecting the non-locked user disables the proceed button again. + deferredState = deferFulfillment(viewModel.context.$viewState) { state in + !state.isUserSelected(.mockBob) + } + context.send(viewAction: .toggleUser(.mockBob)) + try await deferredState.fulfill() + #expect(!context.viewState.hasInvitableSelectedUsers) + + // Re-select the non-locked user before proceeding. + deferredState = deferFulfillment(viewModel.context.$viewState) { state in + state.isUserSelected(.mockBob) + } + context.send(viewAction: .toggleUser(.mockBob)) + try await deferredState.fulfill() + + let deferredAction = deferFulfillment(viewModel.actions) { action in + if case .openRoom(let roomID) = action, roomID == newRoomID { + return true + } + return false + } + + context.send(viewAction: .proceed) + + try await deferredAction.fulfill() + + let args = try #require(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments) + #expect(args.name == nil) + #expect(args.topic == nil) + #expect(args.accessType == .private) + #expect(args.isSpace == false) + #expect(args.userIDs == [UserProfileProxy.mockAlice.userID, UserProfileProxy.mockBob.userID]) + #expect(args.avatarURL == nil) + #expect(args.aliasLocalPart == nil) + } + // MARK: - Helpers private func setupViewModel(roomProxy: JoinedRoomProxyProtocol, isSkippable: Bool) { @@ -168,7 +240,7 @@ final class InviteUsersScreenViewModelTests { clientProxy = ClientProxyMock(.init(userID: "@mock:client.com")) let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), - roomProxy: roomProxy, + roomType: .existingRoom(roomProxy: roomProxy), isSkippable: isSkippable, userDiscoveryService: userDiscoveryService, userIndicatorController: UserIndicatorControllerMock(),