mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Display members of a space (#4629)
* Present members of a space * present the members modally from the space * Implemented a room members flow coordinator to make such flow more modular and reusable this is required since we will need to reuse this module also in the space settings, and later we could also replace it in the RoomFlowCoordinator. * the implementation to support at least the SpaceFlowCoordinator is done a follow UP should do the refactor. * remove modal usage from the flow, we want to always be a navigation flow * Improved and implemented the room navigation in the members flow coordinator * pr suggestions and refactored the start chat flow and the invite screen * updated copies for managing room members * Update ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
@@ -636,6 +636,7 @@
|
|||||||
71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */; };
|
71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */; };
|
||||||
71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; };
|
71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; };
|
||||||
7254FB2EFDD43BC8BB7A1213 /* SecurityAndPrivacyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AE42C19EDE64B7CB7BE4D0 /* SecurityAndPrivacyScreen.swift */; };
|
7254FB2EFDD43BC8BB7A1213 /* SecurityAndPrivacyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AE42C19EDE64B7CB7BE4D0 /* SecurityAndPrivacyScreen.swift */; };
|
||||||
|
726AA74DF4E5EFCEBD78CE3F /* RoomMembersFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A70B03471F6027C90EE868C /* RoomMembersFlowCoordinator.swift */; };
|
||||||
72D2298DE695A6797CDA1A2A /* SpaceScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */; };
|
72D2298DE695A6797CDA1A2A /* SpaceScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */; };
|
||||||
733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; };
|
733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; };
|
||||||
7366E5783D1871D42CF99D34 /* OIDCConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D354D4232DED9649FD0FF4 /* OIDCConfiguration.swift */; };
|
7366E5783D1871D42CF99D34 /* OIDCConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D354D4232DED9649FD0FF4 /* OIDCConfiguration.swift */; };
|
||||||
@@ -1991,6 +1992,7 @@
|
|||||||
5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
|
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
5A4EF5724C0F894911AF7811 /* SpaceExplorerFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceExplorerFlowCoordinator.swift; sourceTree = "<group>"; };
|
5A4EF5724C0F894911AF7811 /* SpaceExplorerFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceExplorerFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
5A70B03471F6027C90EE868C /* RoomMembersFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
|
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewViewModelTests.swift; sourceTree = "<group>"; };
|
5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -4288,6 +4290,7 @@
|
|||||||
A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */,
|
A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */,
|
||||||
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */,
|
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */,
|
||||||
407C8DD85179D2DB896FC0FA /* RoomFlowCoordinatorStateMachine.swift */,
|
407C8DD85179D2DB896FC0FA /* RoomFlowCoordinatorStateMachine.swift */,
|
||||||
|
5A70B03471F6027C90EE868C /* RoomMembersFlowCoordinator.swift */,
|
||||||
0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */,
|
0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */,
|
||||||
D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */,
|
D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */,
|
||||||
5A4EF5724C0F894911AF7811 /* SpaceExplorerFlowCoordinator.swift */,
|
5A4EF5724C0F894911AF7811 /* SpaceExplorerFlowCoordinator.swift */,
|
||||||
@@ -8155,6 +8158,7 @@
|
|||||||
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */,
|
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */,
|
||||||
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */,
|
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */,
|
||||||
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */,
|
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */,
|
||||||
|
726AA74DF4E5EFCEBD78CE3F /* RoomMembersFlowCoordinator.swift in Sources */,
|
||||||
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */,
|
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */,
|
||||||
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */,
|
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */,
|
||||||
A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */,
|
A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */,
|
||||||
|
|||||||
@@ -495,19 +495,19 @@
|
|||||||
"screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite";
|
"screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite";
|
||||||
"screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?";
|
"screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?";
|
||||||
"screen_bottom_sheet_create_dm_title" = "Send invite?";
|
"screen_bottom_sheet_create_dm_title" = "Send invite?";
|
||||||
"screen_bottom_sheet_manage_room_member_ban" = "Ban from room";
|
"screen_bottom_sheet_manage_room_member_ban" = "Ban user";
|
||||||
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban";
|
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban";
|
||||||
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
|
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join again if invited.";
|
||||||
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
|
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
|
||||||
"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@";
|
"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@";
|
||||||
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove";
|
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove";
|
||||||
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
|
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
|
||||||
"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile";
|
"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile";
|
||||||
"screen_bottom_sheet_manage_room_member_remove" = "Remove from room";
|
"screen_bottom_sheet_manage_room_member_remove" = "Remove user";
|
||||||
"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
|
"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
|
||||||
"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…";
|
"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…";
|
||||||
"screen_bottom_sheet_manage_room_member_unban" = "Unban from room";
|
"screen_bottom_sheet_manage_room_member_unban" = "Unban user";
|
||||||
"screen_bottom_sheet_manage_room_member_unban_member_confirmation_description" = "They would be able to join the room again if invited";
|
"screen_bottom_sheet_manage_room_member_unban_member_confirmation_description" = "They would be able to join again if invited";
|
||||||
"screen_bottom_sheet_manage_room_member_unban_member_confirmation_title" = "Are you sure you want to unban this member?";
|
"screen_bottom_sheet_manage_room_member_unban_member_confirmation_title" = "Are you sure you want to unban this member?";
|
||||||
"screen_bug_report_a11y_screenshot" = "Screenshot";
|
"screen_bug_report_a11y_screenshot" = "Screenshot";
|
||||||
"screen_create_poll_option_accessibility_label" = "%1$@: %2$@";
|
"screen_create_poll_option_accessibility_label" = "%1$@: %2$@";
|
||||||
@@ -1416,7 +1416,7 @@
|
|||||||
"screen_room_details_security_title" = "Security";
|
"screen_room_details_security_title" = "Security";
|
||||||
"screen_room_details_topic_title" = "Topic";
|
"screen_room_details_topic_title" = "Topic";
|
||||||
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
|
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
|
||||||
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban from room";
|
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban user";
|
||||||
"screen_room_notification_settings_mode_all_messages" = "All messages";
|
"screen_room_notification_settings_mode_all_messages" = "All messages";
|
||||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
|
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
|
||||||
"screen_room_timeline_reactions_show_less" = "Show less";
|
"screen_room_timeline_reactions_show_less" = "Show less";
|
||||||
|
|||||||
@@ -1354,7 +1354,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
selectedUsers: .init(selectedUsersSubject),
|
selectedUsers: .init(selectedUsersSubject),
|
||||||
roomType: .room(roomProxy: roomProxy),
|
roomType: .room(roomProxy: roomProxy),
|
||||||
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy),
|
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy),
|
||||||
userIndicatorController: flowParameters.userIndicatorController)
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
|
appSettings: flowParameters.appSettings)
|
||||||
|
|
||||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||||
stackCoordinator.setRootCoordinator(coordinator)
|
stackCoordinator.setRootCoordinator(coordinator)
|
||||||
@@ -1363,22 +1364,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .dismiss:
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
case .proceed:
|
case .proceed:
|
||||||
break
|
fatalError("Not handled in this flow.")
|
||||||
case .invite(let users):
|
|
||||||
self.inviteUsers(users, in: roomProxy)
|
|
||||||
case .toggleUser(let user):
|
|
||||||
var selectedUsers = selectedUsersSubject.value
|
|
||||||
|
|
||||||
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
|
||||||
selectedUsers.remove(at: index)
|
|
||||||
} else {
|
|
||||||
selectedUsers.append(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedUsersSubject.send(selectedUsers)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -1388,42 +1377,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func inviteUsers(_ users: [String], in room: JoinedRoomProxyProtocol) {
|
|
||||||
if flowParameters.appSettings.enableKeyShareOnInvite {
|
|
||||||
showLoadingIndicator(title: L10n.screenRoomDetailsInvitePeoplePreparing,
|
|
||||||
message: L10n.screenRoomDetailsInvitePeopleDontClose)
|
|
||||||
} else {
|
|
||||||
showLoadingIndicator()
|
|
||||||
}
|
|
||||||
|
|
||||||
Task {
|
|
||||||
defer {
|
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
|
||||||
hideLoadingIndicator()
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in
|
|
||||||
for user in users {
|
|
||||||
group.addTask {
|
|
||||||
await room.invite(userID: user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await group.first { inviteResult in
|
|
||||||
inviteResult.isFailure
|
|
||||||
} ?? .success(())
|
|
||||||
}
|
|
||||||
|
|
||||||
guard case .failure = result else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
flowParameters.userIndicatorController.alertInfo = .init(id: .init(),
|
|
||||||
title: L10n.commonUnableToInviteTitle,
|
|
||||||
message: L10n.commonUnableToInviteMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func presentRolesAndPermissionsScreen() {
|
private func presentRolesAndPermissionsScreen() {
|
||||||
let parameters = RoomRolesAndPermissionsFlowCoordinatorParameters(roomProxy: roomProxy,
|
let parameters = RoomRolesAndPermissionsFlowCoordinatorParameters(roomProxy: roomProxy,
|
||||||
mediaProvider: userSession.mediaProvider,
|
mediaProvider: userSession.mediaProvider,
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 Element Creations 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 SwiftState
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum RoomMembersFlowCoordinatorAction {
|
||||||
|
case finished
|
||||||
|
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||||
|
case verifyUser(userID: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RoomMembersFlowCoordinatorEntryPoint: Equatable {
|
||||||
|
/// To be used in a room when a member name is tapped
|
||||||
|
case roomMember(userID: String)
|
||||||
|
/// To be used in the context of room details, space details etc.
|
||||||
|
case roomMembersList
|
||||||
|
}
|
||||||
|
|
||||||
|
final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
||||||
|
indirect enum State: StateType {
|
||||||
|
/// The state machine hasn't started.
|
||||||
|
case initial
|
||||||
|
/// The room members list
|
||||||
|
case roomMembersList
|
||||||
|
/// The details for a member of the room
|
||||||
|
case roomMemberDetails(userID: String, previousState: State)
|
||||||
|
/// In case the details won't load because the user has left the room we load the profile
|
||||||
|
case userProfile(userID: String, previousState: State)
|
||||||
|
/// The invite users screen
|
||||||
|
case inviteUsersScreen
|
||||||
|
/// A room flow has been started
|
||||||
|
case roomFlow(roomID: String, previousState: State)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Event: EventType {
|
||||||
|
case start
|
||||||
|
|
||||||
|
case presentRoomMembersList
|
||||||
|
|
||||||
|
case presentRoomMemberDetails(userID: String)
|
||||||
|
case dismissedRoomMemberDetails
|
||||||
|
|
||||||
|
case presentInviteUsersScreen
|
||||||
|
case dismissedInviteUsersScreen
|
||||||
|
|
||||||
|
case presentUserProfile(userID: String)
|
||||||
|
case dismissedUserProfile
|
||||||
|
|
||||||
|
case startRoomFlow(roomID: String)
|
||||||
|
case stopRoomFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
private let entryPoint: RoomMembersFlowCoordinatorEntryPoint
|
||||||
|
private let roomProxy: JoinedRoomProxyProtocol
|
||||||
|
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||||
|
private let flowParameters: CommonFlowParameters
|
||||||
|
|
||||||
|
private let stateMachine: StateMachine<State, Event>
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private let actionsSubject: PassthroughSubject<RoomMembersFlowCoordinatorAction, Never> = .init()
|
||||||
|
var actions: AnyPublisher<RoomMembersFlowCoordinatorAction, Never> {
|
||||||
|
actionsSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var roomFlowCoordinator: RoomFlowCoordinator?
|
||||||
|
|
||||||
|
init(entryPoint: RoomMembersFlowCoordinatorEntryPoint,
|
||||||
|
roomProxy: JoinedRoomProxyProtocol,
|
||||||
|
navigationStackCoordinator: NavigationStackCoordinator,
|
||||||
|
flowParameters: CommonFlowParameters) {
|
||||||
|
self.entryPoint = entryPoint
|
||||||
|
self.roomProxy = roomProxy
|
||||||
|
self.flowParameters = flowParameters
|
||||||
|
self.navigationStackCoordinator = navigationStackCoordinator
|
||||||
|
|
||||||
|
stateMachine = .init(state: .initial)
|
||||||
|
configureStateMachine()
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
switch entryPoint {
|
||||||
|
case .roomMember(let userID):
|
||||||
|
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
|
||||||
|
case .roomMembersList:
|
||||||
|
stateMachine.tryEvent(.presentRoomMembersList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||||
|
fatalError("Unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearRoute(animated: Bool) {
|
||||||
|
if stateMachine.state == .inviteUsersScreen {
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil, animated: animated)
|
||||||
|
} else if let roomFlowCoordinator {
|
||||||
|
roomFlowCoordinator.clearRoute(animated: animated)
|
||||||
|
}
|
||||||
|
// We don't support dismissing a sub flow by itself, only the entire chain.
|
||||||
|
// The presenter flow will take care of dismissing it
|
||||||
|
actionsSubject.send(.finished)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureStateMachine() {
|
||||||
|
stateMachine.addRouteMapping { event, fromState, _ in
|
||||||
|
switch (fromState, event) {
|
||||||
|
case (.initial, .presentRoomMembersList):
|
||||||
|
return .roomMembersList
|
||||||
|
case (.initial, .presentRoomMemberDetails(let userID)):
|
||||||
|
// previous state doesn't matter in this csase
|
||||||
|
return .roomMemberDetails(userID: userID, previousState: fromState)
|
||||||
|
|
||||||
|
case (.roomMembersList, .presentRoomMemberDetails(let userID)):
|
||||||
|
return .roomMemberDetails(userID: userID, previousState: fromState)
|
||||||
|
case (.roomMemberDetails, .dismissedRoomMemberDetails):
|
||||||
|
return .roomMembersList
|
||||||
|
|
||||||
|
case (.roomMembersList, .presentInviteUsersScreen):
|
||||||
|
return .inviteUsersScreen
|
||||||
|
case (.inviteUsersScreen, .dismissedInviteUsersScreen):
|
||||||
|
return .roomMembersList
|
||||||
|
|
||||||
|
case (.roomMemberDetails(_, let previousState), .presentUserProfile(let userID)):
|
||||||
|
return .userProfile(userID: userID, previousState: previousState)
|
||||||
|
case (.userProfile(_, let previousState), .dismissedUserProfile):
|
||||||
|
return previousState
|
||||||
|
|
||||||
|
case (_, .startRoomFlow(let roomID)):
|
||||||
|
return .roomFlow(roomID: roomID, previousState: fromState)
|
||||||
|
case (.roomFlow(_, let previousState), .stopRoomFlow):
|
||||||
|
return previousState
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.addAnyHandler(.any => .any) { [weak self] context in
|
||||||
|
guard let self else { return }
|
||||||
|
switch (context.fromState, context.event, context.toState) {
|
||||||
|
case (.initial, .presentRoomMembersList, .roomMembersList):
|
||||||
|
presentRoomMembersList()
|
||||||
|
case (.initial, .presentRoomMemberDetails, .roomMemberDetails(let userID, _)):
|
||||||
|
presentRoomMemberDetails(userID: userID)
|
||||||
|
|
||||||
|
case (.roomMembersList, .presentRoomMemberDetails, .roomMemberDetails(let userID, _)):
|
||||||
|
presentRoomMemberDetails(userID: userID)
|
||||||
|
case (.roomMemberDetails, .dismissedRoomMemberDetails, .roomMembersList):
|
||||||
|
break
|
||||||
|
|
||||||
|
case (.roomMembersList, .presentInviteUsersScreen, .inviteUsersScreen):
|
||||||
|
presentInviteUsersScreen()
|
||||||
|
case (.inviteUsersScreen, .dismissedInviteUsersScreen, .roomMembersList):
|
||||||
|
break
|
||||||
|
|
||||||
|
case (.roomMemberDetails, .presentUserProfile, .userProfile(let userID, _)):
|
||||||
|
replaceRoomMemberDetailsWithUserProfile(userID: userID)
|
||||||
|
case (.userProfile, .dismissedUserProfile, _):
|
||||||
|
break
|
||||||
|
|
||||||
|
case (_, .startRoomFlow(let roomID), .roomFlow):
|
||||||
|
startRoomFlow(roomID: roomID)
|
||||||
|
case (.roomFlow, .stopRoomFlow, _):
|
||||||
|
roomFlowCoordinator = nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
fatalError("Unhandled transition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentRoomMembersList() {
|
||||||
|
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(userSession: flowParameters.userSession,
|
||||||
|
roomProxy: roomProxy,
|
||||||
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
|
analytics: flowParameters.analytics))
|
||||||
|
coordinator.actions.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
switch action {
|
||||||
|
case .invite:
|
||||||
|
stateMachine.tryEvent(.presentInviteUsersScreen)
|
||||||
|
case .selectedMember(let member):
|
||||||
|
stateMachine.tryEvent(.presentRoomMemberDetails(userID: member.userID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||||
|
self?.actionsSubject.send(.finished)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentRoomMemberDetails(userID: String) {
|
||||||
|
let params = RoomMemberDetailsScreenCoordinatorParameters(userID: userID,
|
||||||
|
roomProxy: roomProxy,
|
||||||
|
userSession: flowParameters.userSession,
|
||||||
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
|
analytics: flowParameters.analytics)
|
||||||
|
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
|
coordinator.actions.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
switch action {
|
||||||
|
case .openUserProfile:
|
||||||
|
stateMachine.tryEvent(.presentUserProfile(userID: userID))
|
||||||
|
case .openDirectChat(let roomID):
|
||||||
|
stateMachine.tryEvent(.startRoomFlow(roomID: roomID))
|
||||||
|
case .startCall(let roomProxy):
|
||||||
|
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||||
|
case .verifyUser(let userID):
|
||||||
|
actionsSubject.send(.verifyUser(userID: userID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
if entryPoint == .roomMember(userID: userID) {
|
||||||
|
actionsSubject.send(.finished)
|
||||||
|
} else {
|
||||||
|
stateMachine.tryEvent(.dismissedRoomMemberDetails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentInviteUsersScreen() {
|
||||||
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: flowParameters.userSession,
|
||||||
|
selectedUsers: nil,
|
||||||
|
roomType: .room(roomProxy: roomProxy),
|
||||||
|
userDiscoveryService: UserDiscoveryService(clientProxy: flowParameters.userSession.clientProxy),
|
||||||
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
|
appSettings: flowParameters.appSettings)
|
||||||
|
|
||||||
|
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||||
|
stackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
|
||||||
|
coordinator.actions.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .dismiss:
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
case .proceed:
|
||||||
|
fatalError("Not handled in this flow.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||||
|
self?.stateMachine.tryEvent(.dismissedInviteUsersScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func replaceRoomMemberDetailsWithUserProfile(userID: String) {
|
||||||
|
let parameters = UserProfileScreenCoordinatorParameters(userID: userID,
|
||||||
|
isPresentedModally: false,
|
||||||
|
userSession: flowParameters.userSession,
|
||||||
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
|
analytics: flowParameters.analytics)
|
||||||
|
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
||||||
|
coordinator.actionsPublisher.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .openDirectChat(let roomID):
|
||||||
|
stateMachine.tryEvent(.startRoomFlow(roomID: roomID))
|
||||||
|
case .startCall(let roomProxy):
|
||||||
|
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||||
|
case .dismiss:
|
||||||
|
break // Not supported when pushed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Replace the RoomMemberDetailsScreen without any animation.
|
||||||
|
// If this pop and push happens before the previous navigation is completed it might break screen presentation logic
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
|
||||||
|
self.navigationStackCoordinator.pop(animated: false)
|
||||||
|
self.navigationStackCoordinator.push(coordinator, animated: false) { [weak self] in
|
||||||
|
self?.stateMachine.tryEvent(.dismissedUserProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startRoomFlow(roomID: String) {
|
||||||
|
let coordinator = RoomFlowCoordinator(roomID: roomID,
|
||||||
|
isChildFlow: true,
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
|
flowParameters: flowParameters)
|
||||||
|
coordinator.actions
|
||||||
|
.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .presentCallScreen(let roomProxy):
|
||||||
|
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||||
|
case .verifyUser(let userID):
|
||||||
|
actionsSubject.send(.verifyUser(userID: userID))
|
||||||
|
case .continueWithSpaceFlow:
|
||||||
|
fatalError("Will never trigger because only direct chats can be displayed in this flow")
|
||||||
|
case .finished:
|
||||||
|
stateMachine.tryEvent(.stopRoomFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
roomFlowCoordinator = coordinator
|
||||||
|
coordinator.handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
private var childSpaceFlowCoordinator: SpaceFlowCoordinator?
|
private var childSpaceFlowCoordinator: SpaceFlowCoordinator?
|
||||||
private var roomFlowCoordinator: RoomFlowCoordinator?
|
private var roomFlowCoordinator: RoomFlowCoordinator?
|
||||||
|
private var membersFlowCoordinator: RoomMembersFlowCoordinator?
|
||||||
|
|
||||||
indirect enum State: StateType {
|
indirect enum State: StateType {
|
||||||
/// The state machine hasn't started.
|
/// The state machine hasn't started.
|
||||||
@@ -53,6 +54,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case presentingChild(childSpaceID: String, previousState: State)
|
case presentingChild(childSpaceID: String, previousState: State)
|
||||||
/// A room flow is in progress
|
/// A room flow is in progress
|
||||||
case roomFlow(previousState: State)
|
case roomFlow(previousState: State)
|
||||||
|
/// A members flow is in progress
|
||||||
|
case membersFlow
|
||||||
|
|
||||||
case leftSpace
|
case leftSpace
|
||||||
}
|
}
|
||||||
@@ -77,6 +80,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
case startRoomFlow(roomID: String)
|
case startRoomFlow(roomID: String)
|
||||||
case stopRoomFlow
|
case stopRoomFlow
|
||||||
|
|
||||||
|
case startMembersFlow
|
||||||
|
case stopMembersFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
private let stateMachine: StateMachine<State, Event>
|
private let stateMachine: StateMachine<State, Event>
|
||||||
@@ -133,6 +139,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .roomFlow:
|
case .roomFlow:
|
||||||
roomFlowCoordinator?.clearRoute(animated: animated)
|
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||||
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
|
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
|
||||||
|
case .membersFlow:
|
||||||
|
membersFlowCoordinator?.clearRoute(animated: animated)
|
||||||
|
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +202,26 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
selectedSpaceRoomSubject.send(nil)
|
selectedSpaceRoomSubject.send(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stateMachine.addRouteMapping { event, fromState, _ in
|
||||||
|
guard case .startMembersFlow = event, case .space = fromState else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return .membersFlow
|
||||||
|
} handler: { [weak self] context in
|
||||||
|
guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else {
|
||||||
|
fatalError("The room proxy must always be provided")
|
||||||
|
}
|
||||||
|
Task { await self.startMembersFlow(roomProxy: roomProxy) }
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.addRouteMapping { event, fromState, _ in
|
||||||
|
guard event == .stopMembersFlow, case .membersFlow = fromState else { return nil }
|
||||||
|
return .space
|
||||||
|
} handler: { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
membersFlowCoordinator = nil
|
||||||
|
}
|
||||||
|
|
||||||
stateMachine.addErrorHandler { context in
|
stateMachine.addErrorHandler { context in
|
||||||
fatalError("Unexpected transition: \(context)")
|
fatalError("Unexpected transition: \(context)")
|
||||||
}
|
}
|
||||||
@@ -219,6 +248,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
stateMachine.tryEvent(.startRoomFlow(roomID: roomID))
|
stateMachine.tryEvent(.startRoomFlow(roomID: roomID))
|
||||||
case .leftSpace:
|
case .leftSpace:
|
||||||
stateMachine.tryEvent(.leftSpace)
|
stateMachine.tryEvent(.leftSpace)
|
||||||
|
case .displayMembers(let roomProxy):
|
||||||
|
stateMachine.tryEvent(.startMembersFlow, userInfo: roomProxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -339,4 +370,26 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
coordinator.handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
coordinator.handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
||||||
selectedSpaceRoomSubject.send(roomID)
|
selectedSpaceRoomSubject.send(roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startMembersFlow(roomProxy: JoinedRoomProxyProtocol) async {
|
||||||
|
let flowCoordinator = RoomMembersFlowCoordinator(entryPoint: .roomMembersList,
|
||||||
|
roomProxy: roomProxy,
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
|
flowParameters: flowParameters)
|
||||||
|
|
||||||
|
flowCoordinator.actions.sink { [weak self] actions in
|
||||||
|
guard let self else { return }
|
||||||
|
switch actions {
|
||||||
|
case .finished:
|
||||||
|
stateMachine.tryEvent(.stopMembersFlow)
|
||||||
|
case .presentCallScreen(let roomProxy):
|
||||||
|
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||||
|
case .verifyUser(let userID):
|
||||||
|
actionsSubject.send(.verifyUser(userID: userID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
membersFlowCoordinator = flowCoordinator
|
||||||
|
flowCoordinator.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1312,11 +1312,11 @@ internal enum L10n {
|
|||||||
}
|
}
|
||||||
/// Send invite?
|
/// Send invite?
|
||||||
internal static var screenBottomSheetCreateDmTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_title") }
|
internal static var screenBottomSheetCreateDmTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_title") }
|
||||||
/// Ban from room
|
/// Ban user
|
||||||
internal static var screenBottomSheetManageRoomMemberBan: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban") }
|
internal static var screenBottomSheetManageRoomMemberBan: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban") }
|
||||||
/// Ban
|
/// Ban
|
||||||
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_action") }
|
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_action") }
|
||||||
/// They won’t be able to join this room again if invited.
|
/// They won’t be able to join again if invited.
|
||||||
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_description") }
|
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_description") }
|
||||||
/// Are you sure you want to ban this member?
|
/// Are you sure you want to ban this member?
|
||||||
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_title") }
|
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_title") }
|
||||||
@@ -1332,7 +1332,7 @@ internal enum L10n {
|
|||||||
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_title") }
|
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_title") }
|
||||||
/// View profile
|
/// View profile
|
||||||
internal static var screenBottomSheetManageRoomMemberMemberUserInfo: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_member_user_info") }
|
internal static var screenBottomSheetManageRoomMemberMemberUserInfo: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_member_user_info") }
|
||||||
/// Remove from room
|
/// Remove user
|
||||||
internal static var screenBottomSheetManageRoomMemberRemove: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_remove") }
|
internal static var screenBottomSheetManageRoomMemberRemove: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_remove") }
|
||||||
/// Remove member and ban from joining in the future?
|
/// Remove member and ban from joining in the future?
|
||||||
internal static var screenBottomSheetManageRoomMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_remove_confirmation_title") }
|
internal static var screenBottomSheetManageRoomMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_remove_confirmation_title") }
|
||||||
@@ -1340,11 +1340,11 @@ internal enum L10n {
|
|||||||
internal static func screenBottomSheetManageRoomMemberRemovingUser(_ p1: Any) -> String {
|
internal static func screenBottomSheetManageRoomMemberRemovingUser(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_removing_user", String(describing: p1))
|
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_removing_user", String(describing: p1))
|
||||||
}
|
}
|
||||||
/// Unban from room
|
/// Unban user
|
||||||
internal static var screenBottomSheetManageRoomMemberUnban: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban") }
|
internal static var screenBottomSheetManageRoomMemberUnban: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban") }
|
||||||
/// Unban
|
/// Unban
|
||||||
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_action") }
|
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_action") }
|
||||||
/// They would be able to join the room again if invited
|
/// They would be able to join again if invited
|
||||||
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_description") }
|
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_description") }
|
||||||
/// Are you sure you want to unban this member?
|
/// Are you sure you want to unban this member?
|
||||||
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_title") }
|
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_title") }
|
||||||
@@ -2468,7 +2468,7 @@ internal enum L10n {
|
|||||||
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
|
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
|
||||||
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
|
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
|
||||||
}
|
}
|
||||||
/// Ban from room
|
/// Ban user
|
||||||
internal static var screenRoomMemberListManageMemberRemoveConfirmationBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_ban") }
|
internal static var screenRoomMemberListManageMemberRemoveConfirmationBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_ban") }
|
||||||
/// Only remove member
|
/// Only remove member
|
||||||
internal static var screenRoomMemberListManageMemberRemoveConfirmationKick: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_kick") }
|
internal static var screenRoomMemberListManageMemberRemoveConfirmationKick: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_kick") }
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ struct CreateRoomCoordinatorParameters {
|
|||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
let createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>
|
let createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>
|
||||||
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>
|
let selectedUsers: [UserProfileProxy]
|
||||||
let appSettings: AppSettings
|
let appSettings: AppSettings
|
||||||
let analytics: AnalyticsService
|
let analytics: AnalyticsService
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CreateRoomCoordinatorAction {
|
enum CreateRoomCoordinatorAction {
|
||||||
case openRoom(withIdentifier: String)
|
case openRoom(withIdentifier: String)
|
||||||
case deselectUser(UserProfileProxy)
|
case updateSelectedUsers([UserProfileProxy])
|
||||||
case updateDetails(CreateRoomFlowParameters)
|
case updateDetails(CreateRoomFlowParameters)
|
||||||
case displayMediaPickerWithMode(MediaPickerScreenMode)
|
case displayMediaPickerWithMode(MediaPickerScreenMode)
|
||||||
case removeImage
|
case removeImage
|
||||||
@@ -48,8 +48,8 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
|
|||||||
viewModel.actions.sink { [weak self] action in
|
viewModel.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .deselectUser(let user):
|
case .updateSelectedUsers(let users):
|
||||||
actionsSubject.send(.deselectUser(user))
|
actionsSubject.send(.updateSelectedUsers(users))
|
||||||
case .openRoom(let identifier):
|
case .openRoom(let identifier):
|
||||||
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||||
case .updateDetails(let details):
|
case .updateDetails(let details):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ enum CreateRoomScreenErrorType: Error {
|
|||||||
|
|
||||||
enum CreateRoomViewModelAction {
|
enum CreateRoomViewModelAction {
|
||||||
case openRoom(withIdentifier: String)
|
case openRoom(withIdentifier: String)
|
||||||
case deselectUser(UserProfileProxy)
|
case updateSelectedUsers([UserProfileProxy])
|
||||||
case updateDetails(CreateRoomFlowParameters)
|
case updateDetails(CreateRoomFlowParameters)
|
||||||
case displayMediaPicker
|
case displayMediaPicker
|
||||||
case displayCameraPicker
|
case displayCameraPicker
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
|
|
||||||
init(userSession: UserSessionProtocol,
|
init(userSession: UserSessionProtocol,
|
||||||
createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>,
|
createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>,
|
||||||
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
|
selectedUsers: [UserProfileProxy],
|
||||||
analytics: AnalyticsService,
|
analytics: AnalyticsService,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
appSettings: AppSettings) {
|
appSettings: AppSettings) {
|
||||||
@@ -46,7 +46,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
super.init(initialViewState: CreateRoomViewState(roomName: parameters.name,
|
super.init(initialViewState: CreateRoomViewState(roomName: parameters.name,
|
||||||
serverName: userSession.clientProxy.userIDServerName ?? "",
|
serverName: userSession.clientProxy.userIDServerName ?? "",
|
||||||
isKnockingFeatureEnabled: appSettings.knockingEnabled,
|
isKnockingFeatureEnabled: appSettings.knockingEnabled,
|
||||||
selectedUsers: selectedUsers.value,
|
selectedUsers: selectedUsers,
|
||||||
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
|
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
|
||||||
bindings: bindings),
|
bindings: bindings),
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider)
|
||||||
@@ -67,12 +67,6 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
selectedUsers
|
|
||||||
.sink { [weak self] users in
|
|
||||||
self?.state.selectedUsers = users
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
setupBindings()
|
setupBindings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +79,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
|||||||
await createRoom()
|
await createRoom()
|
||||||
}
|
}
|
||||||
case .deselectUser(let user):
|
case .deselectUser(let user):
|
||||||
actionsSubject.send(.deselectUser(user))
|
state.selectedUsers.removeAll { $0.userID == user.userID }
|
||||||
|
actionsSubject.send(.updateSelectedUsers(state.selectedUsers))
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
actionsSubject.send(.displayCameraPicker)
|
actionsSubject.send(.displayCameraPicker)
|
||||||
case .displayMediaPicker:
|
case .displayMediaPicker:
|
||||||
|
|||||||
@@ -11,17 +11,16 @@ import SwiftUI
|
|||||||
|
|
||||||
struct InviteUsersScreenCoordinatorParameters {
|
struct InviteUsersScreenCoordinatorParameters {
|
||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>
|
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
|
||||||
let roomType: InviteUsersScreenRoomType
|
let roomType: InviteUsersScreenRoomType
|
||||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
let appSettings: AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenCoordinatorAction {
|
enum InviteUsersScreenCoordinatorAction {
|
||||||
case cancel
|
case dismiss
|
||||||
case proceed
|
case proceed(selectedUsers: [UserProfileProxy])
|
||||||
case invite(users: [String])
|
|
||||||
case toggleUser(UserProfileProxy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
||||||
@@ -38,21 +37,18 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
|||||||
selectedUsers: parameters.selectedUsers,
|
selectedUsers: parameters.selectedUsers,
|
||||||
roomType: parameters.roomType,
|
roomType: parameters.roomType,
|
||||||
userDiscoveryService: parameters.userDiscoveryService,
|
userDiscoveryService: parameters.userDiscoveryService,
|
||||||
userIndicatorController: parameters.userIndicatorController)
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
|
appSettings: parameters.appSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
viewModel.actions.sink { [weak self] action in
|
viewModel.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .dismiss:
|
||||||
actionsSubject.send(.cancel)
|
actionsSubject.send(.dismiss)
|
||||||
case .proceed:
|
case .proceed(let selectedUsers):
|
||||||
actionsSubject.send(.proceed)
|
actionsSubject.send(.proceed(selectedUsers: selectedUsers))
|
||||||
case .invite(let users):
|
|
||||||
actionsSubject.send(.invite(users: users))
|
|
||||||
case .toggleUser(let user):
|
|
||||||
actionsSubject.send(.toggleUser(user))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ enum InviteUsersScreenErrorType: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenViewModelAction {
|
enum InviteUsersScreenViewModelAction {
|
||||||
case cancel
|
case dismiss
|
||||||
case proceed
|
case proceed(selectedUsers: [UserProfileProxy])
|
||||||
case invite(users: [String])
|
|
||||||
case toggleUser(UserProfileProxy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenRoomType {
|
enum InviteUsersScreenRoomType {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
private let roomType: InviteUsersScreenRoomType
|
private let roomType: InviteUsersScreenRoomType
|
||||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
private let appSettings: AppSettings
|
||||||
|
|
||||||
private var suggestedUsers = [UserProfileProxy]()
|
private var suggestedUsers = [UserProfileProxy]()
|
||||||
|
|
||||||
@@ -24,20 +25,25 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
|
||||||
|
|
||||||
init(userSession: UserSessionProtocol,
|
init(userSession: UserSessionProtocol,
|
||||||
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
|
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?,
|
||||||
roomType: InviteUsersScreenRoomType,
|
roomType: InviteUsersScreenRoomType,
|
||||||
userDiscoveryService: UserDiscoveryServiceProtocol,
|
userDiscoveryService: UserDiscoveryServiceProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
|
appSettings: AppSettings) {
|
||||||
self.roomType = roomType
|
self.roomType = roomType
|
||||||
self.userDiscoveryService = userDiscoveryService
|
self.userDiscoveryService = userDiscoveryService
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
self.appSettings = appSettings
|
||||||
|
self.selectedUsers = selectedUsers
|
||||||
|
|
||||||
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value,
|
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers?.value ?? [],
|
||||||
isCreatingRoom: roomType.isCreatingRoom),
|
isCreatingRoom: roomType.isCreatingRoom),
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider)
|
||||||
|
|
||||||
setupSubscriptions(selectedUsers: selectedUsers)
|
setupSubscriptions()
|
||||||
fetchMembersIfNeeded()
|
fetchMembersIfNeeded()
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
@@ -54,23 +60,67 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
override func process(viewAction: InviteUsersScreenViewAction) {
|
override func process(viewAction: InviteUsersScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
actionsSubject.send(.cancel)
|
actionsSubject.send(.dismiss)
|
||||||
case .proceed:
|
case .proceed:
|
||||||
switch roomType {
|
switch roomType {
|
||||||
case .draft:
|
case .draft:
|
||||||
actionsSubject.send(.proceed)
|
actionsSubject.send(.proceed(selectedUsers: state.selectedUsers))
|
||||||
case .room:
|
case .room(let roomProxy):
|
||||||
actionsSubject.send(.invite(users: state.selectedUsers.map(\.userID)))
|
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
||||||
}
|
}
|
||||||
case .toggleUser(let user):
|
case .toggleUser(let user):
|
||||||
let willSelectUser = !state.selectedUsers.contains(user)
|
toggleUser(user)
|
||||||
state.scrollToLastID = willSelectUser ? user.userID : nil
|
|
||||||
actionsSubject.send(.toggleUser(user))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func toggleUser(_ user: UserProfileProxy) {
|
||||||
|
if state.selectedUsers.contains(user) {
|
||||||
|
state.scrollToLastID = nil
|
||||||
|
state.selectedUsers.removeAll(where: { $0.userID == user.userID })
|
||||||
|
} else {
|
||||||
|
state.scrollToLastID = user.userID
|
||||||
|
state.selectedUsers.append(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func inviteUsers(_ users: [String], roomProxy: JoinedRoomProxyProtocol) {
|
||||||
|
if appSettings.enableKeyShareOnInvite {
|
||||||
|
showLoader(title: L10n.screenRoomDetailsInvitePeoplePreparing,
|
||||||
|
message: L10n.screenRoomDetailsInvitePeopleDontClose)
|
||||||
|
} else {
|
||||||
|
showLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
defer {
|
||||||
|
hideLoader()
|
||||||
|
actionsSubject.send(.dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in
|
||||||
|
for user in users {
|
||||||
|
group.addTask {
|
||||||
|
await roomProxy.invite(userID: user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await group.first { inviteResult in
|
||||||
|
inviteResult.isFailure
|
||||||
|
} ?? .success(())
|
||||||
|
}
|
||||||
|
|
||||||
|
guard case .failure = result else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIndicatorController.alertInfo = .init(id: .init(),
|
||||||
|
title: L10n.commonUnableToInviteTitle,
|
||||||
|
message: L10n.commonUnableToInviteMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func buildMembershipStateIfNeeded(members: [RoomMemberProxyProtocol]) {
|
private func buildMembershipStateIfNeeded(members: [RoomMemberProxyProtocol]) {
|
||||||
showLoader()
|
showLoader()
|
||||||
|
|
||||||
@@ -92,7 +142,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
@CancellableTask
|
@CancellableTask
|
||||||
private var fetchUsersTask: Task<Void, Never>?
|
private var fetchUsersTask: Task<Void, Never>?
|
||||||
|
|
||||||
private func setupSubscriptions(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>) {
|
private func setupSubscriptions() {
|
||||||
context.$viewState
|
context.$viewState
|
||||||
.map(\.bindings.searchQuery)
|
.map(\.bindings.searchQuery)
|
||||||
.debounceTextQueriesAndRemoveDuplicates()
|
.debounceTextQueriesAndRemoveDuplicates()
|
||||||
@@ -101,11 +151,13 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
selectedUsers
|
if let selectedUsers {
|
||||||
.sink { [weak self] users in
|
selectedUsers
|
||||||
self?.state.selectedUsers = users
|
.sink { [weak self] users in
|
||||||
}
|
self?.state.selectedUsers = users
|
||||||
.store(in: &cancellables)
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchMembersIfNeeded() {
|
private func fetchMembersIfNeeded() {
|
||||||
@@ -159,8 +211,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
|
|
||||||
private let userIndicatorID = UUID().uuidString
|
private let userIndicatorID = UUID().uuidString
|
||||||
|
|
||||||
private func showLoader() {
|
private func showLoader(title: String = L10n.commonLoading,
|
||||||
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200))
|
message: String? = nil) {
|
||||||
|
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID,
|
||||||
|
type: .modal,
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
persistent: true),
|
||||||
|
delay: .milliseconds(200))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hideLoader() {
|
private func hideLoader() {
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
selectedUsers: .init([]),
|
selectedUsers: .init([]),
|
||||||
roomType: .draft,
|
roomType: .draft,
|
||||||
userDiscoveryService: userDiscoveryService,
|
userDiscoveryService: userDiscoveryService,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ struct RoomMembersListScreen: View {
|
|||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||||
.navigationTitle(L10n.commonPeople)
|
.navigationTitle(L10n.commonPeople)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.sheet(item: $context.manageMemeberViewModel) {
|
.sheet(item: $context.manageMemeberViewModel) {
|
||||||
ManageRoomMemberSheetView(context: $0.context)
|
ManageRoomMemberSheetView(context: $0.context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum SpaceScreenCoordinatorAction {
|
|||||||
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
||||||
case selectRoom(roomID: String)
|
case selectRoom(roomID: String)
|
||||||
case leftSpace
|
case leftSpace
|
||||||
|
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SpaceScreenCoordinator: CoordinatorProtocol {
|
final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||||
@@ -61,6 +62,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
|
|||||||
actionsSubject.send(.selectRoom(roomID: roomID))
|
actionsSubject.send(.selectRoom(roomID: roomID))
|
||||||
case .leftSpace:
|
case .leftSpace:
|
||||||
actionsSubject.send(.leftSpace)
|
actionsSubject.send(.leftSpace)
|
||||||
|
case .displayMembers(let roomProxy):
|
||||||
|
actionsSubject.send(.displayMembers(roomProxy: roomProxy))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ enum SpaceScreenViewModelAction {
|
|||||||
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
||||||
case selectRoom(roomID: String)
|
case selectRoom(roomID: String)
|
||||||
case leftSpace
|
case leftSpace
|
||||||
|
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SpaceScreenViewState: BindableState {
|
struct SpaceScreenViewState: BindableState {
|
||||||
var space: SpaceRoomProxyProtocol
|
var space: SpaceRoomProxyProtocol
|
||||||
|
|
||||||
var permalink: URL?
|
var permalink: URL?
|
||||||
|
var roomProxy: JoinedRoomProxyProtocol?
|
||||||
|
|
||||||
var isPaginating = false
|
var isPaginating = false
|
||||||
var rooms: [SpaceRoomProxyProtocol]
|
var rooms: [SpaceRoomProxyProtocol]
|
||||||
@@ -42,4 +44,5 @@ enum SpaceScreenViewAction {
|
|||||||
case toggleLeaveSpaceRoomDetails(id: String)
|
case toggleLeaveSpaceRoomDetails(id: String)
|
||||||
case confirmLeaveSpace
|
case confirmLeaveSpace
|
||||||
case spaceSettings
|
case spaceSettings
|
||||||
|
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,13 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id),
|
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id) {
|
||||||
case let .success(permalinkURL) = await roomProxy.matrixToPermalink() {
|
// Required to listen for membership updates in the members flow
|
||||||
state.permalink = permalinkURL
|
await roomProxy.subscribeForUpdates()
|
||||||
|
state.roomProxy = roomProxy
|
||||||
|
if case let .success(permalinkURL) = await roomProxy.matrixToPermalink() {
|
||||||
|
state.permalink = permalinkURL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,6 +119,8 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
|||||||
}
|
}
|
||||||
case .confirmLeaveSpace:
|
case .confirmLeaveSpace:
|
||||||
Task { await confirmLeaveSpace() }
|
Task { await confirmLeaveSpace() }
|
||||||
|
case .displayMembers(let roomProxy):
|
||||||
|
actionsSubject.send(.displayMembers(roomProxy: roomProxy))
|
||||||
case .spaceSettings:
|
case .spaceSettings:
|
||||||
break // Not implemented.
|
break // Not implemented.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,13 @@ struct SpaceScreen: View {
|
|||||||
// controller attempts to anchor itself to the button that is no longer visible.
|
// controller attempts to anchor itself to the button that is no longer visible.
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Menu {
|
Menu {
|
||||||
if let permalink = context.viewState.permalink {
|
Section {
|
||||||
Section {
|
if let roomProxy = context.viewState.roomProxy {
|
||||||
|
Button { context.send(viewAction: .displayMembers(roomProxy: roomProxy)) } label: {
|
||||||
|
Label(L10n.screenSpaceMenuActionMembers, icon: \.user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let permalink = context.viewState.permalink {
|
||||||
ShareLink(item: permalink) {
|
ShareLink(item: permalink) {
|
||||||
Label(L10n.actionShare, icon: \.shareIos)
|
Label(L10n.actionShare, icon: \.shareIos)
|
||||||
}
|
}
|
||||||
@@ -106,10 +111,16 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy,
|
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy,
|
||||||
initialSpaceRooms: .mockSpaceList))
|
initialSpaceRooms: .mockSpaceList))
|
||||||
|
|
||||||
|
let clientProxy = ClientProxyMock(.init())
|
||||||
|
clientProxy.roomForIdentifierClosure = { _ in
|
||||||
|
.joined(JoinedRoomProxyMock(.init()))
|
||||||
|
}
|
||||||
|
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||||
|
|
||||||
let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
|
let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
|
||||||
spaceServiceProxy: SpaceServiceProxyMock(.init()),
|
spaceServiceProxy: SpaceServiceProxyMock(.init()),
|
||||||
selectedSpaceRoomPublisher: .init(nil),
|
selectedSpaceRoomPublisher: .init(nil),
|
||||||
userSession: UserSessionMock(.init()),
|
userSession: userSession,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,20 +87,17 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
selectedUsers: selectedUsersPublisher,
|
selectedUsers: selectedUsersPublisher,
|
||||||
roomType: .draft,
|
roomType: .draft,
|
||||||
userDiscoveryService: parameters.userDiscoveryService,
|
userDiscoveryService: parameters.userDiscoveryService,
|
||||||
userIndicatorController: parameters.userIndicatorController)
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
|
appSettings: parameters.appSettings)
|
||||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .dismiss:
|
||||||
break // Not shown in this flow.
|
fatalError("Not shown in this flow.")
|
||||||
case .proceed:
|
case .proceed(let selectedUsers):
|
||||||
|
self.selectedUsers.send(selectedUsers)
|
||||||
openCreateRoomScreen()
|
openCreateRoomScreen()
|
||||||
case .invite:
|
|
||||||
break
|
|
||||||
case .toggleUser(let user):
|
|
||||||
toggleUser(user)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -115,15 +112,15 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
|
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
|
||||||
userIndicatorController: parameters.userIndicatorController,
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
createRoomParameters: createRoomParametersPublisher,
|
createRoomParameters: createRoomParametersPublisher,
|
||||||
selectedUsers: selectedUsersPublisher,
|
selectedUsers: selectedUsers.value,
|
||||||
appSettings: parameters.appSettings,
|
appSettings: parameters.appSettings,
|
||||||
analytics: parameters.analytics)
|
analytics: parameters.analytics)
|
||||||
let coordinator = CreateRoomCoordinator(parameters: createParameters)
|
let coordinator = CreateRoomCoordinator(parameters: createParameters)
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .deselectUser(let user):
|
case .updateSelectedUsers(let users):
|
||||||
self.toggleUser(user)
|
self.selectedUsers.send(users)
|
||||||
case .updateDetails(let details):
|
case .updateDetails(let details):
|
||||||
self.createRoomParameters.send(details)
|
self.createRoomParameters.send(details)
|
||||||
case .openRoom(let identifier):
|
case .openRoom(let identifier):
|
||||||
@@ -190,16 +187,6 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleUser(_ user: UserProfileProxy) {
|
|
||||||
var selectedUsers = selectedUsers.value
|
|
||||||
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
|
||||||
selectedUsers.remove(at: index)
|
|
||||||
} else {
|
|
||||||
selectedUsers.append(user)
|
|
||||||
}
|
|
||||||
self.selectedUsers.send(selectedUsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Loading indicator
|
// MARK: Loading indicator
|
||||||
|
|
||||||
private static let loadingIndicatorIdentifier = "\(StartChatScreenCoordinator.self)-Loading"
|
private static let loadingIndicatorIdentifier = "\(StartChatScreenCoordinator.self)-Loading"
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8d2b28b530b24af558f8fae27de0d170e3b1e0578120e33e83970d9d49723e34
|
oid sha256:09ed4bd158ac78c3bc2c79b002123c8acd01fa2c916117c4b2f25c546c2877dd
|
||||||
size 196380
|
size 196319
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d6f04d43984ace12233b6b3362af3bb77d4942ea1297e0050b8b5955c25130ef
|
oid sha256:c93361807cc6a52d22c29e9795a0d2b9ca39b6538f82e99d47d5ec205ff604c9
|
||||||
size 137585
|
size 136562
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a95d9aade23b43dc3941fd270aa56b40b02b9e9ab08f6944ebed09870f13b120
|
oid sha256:65ed07a3fede8980ff18ad8948b54a9d45f2cae7ea3889f070e0bcb5212aff6c
|
||||||
size 196824
|
size 196864
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:68236a129b6528f9a9a688ce70205cf15afbcfe5c923b604b7ec1678c4a846a5
|
oid sha256:63763276f73d6f8a0e632853f0a746b65e0bca5438d62d34da5bcee8f22de111
|
||||||
size 138159
|
size 137171
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d9d357a82eea7676227d54dc28e6b9ec84845c07c69ce9e0e24810e8a8a46335
|
oid sha256:8691d45d5629c189e6a97421aad1ff73f89bbe5373271cae18f2c0321a4fd3cc
|
||||||
size 190488
|
size 190379
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1f67d0bc3d1407fe230c418d4d3fe492af4e8a4a7057d07124f8d259801328ae
|
oid sha256:3398bc4647612b235ce3bfeb18941044fd985cefc239379b51f3003a57bc6ba4
|
||||||
size 131411
|
size 130984
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:552d0fddd4944914a7aeb2678cdb991d5f55599f8f09938ca10cc23f37de8967
|
oid sha256:0661718686f07bd5a4c36f7211064030f7d0f76193ef489f9927dd83fd9703d1
|
||||||
size 189773
|
size 189924
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:684526b282a46532d45dc048c806b1c5b81ed06fa3ec12496d5f0c2036c53192
|
oid sha256:86e26201a4c716cb1b2b229b3f89805297828f52feb29fa8cff22a9c29c8eda8
|
||||||
size 131331
|
size 130816
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bba02f0b1a660e3a7b813fb36bf66afe00d3adee4c287651531ac5cf95863b9a
|
oid sha256:072af060db2d6ab8464cd15e064dc792bd781d45b08b7ef6cc70115e5f2ecfa0
|
||||||
size 105278
|
size 104992
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7bd4668b039105fc66b51759be8f12581dbfba3d8012dc595e6a342dbc366b60
|
oid sha256:a88d8981907ce176f1af65d81804b27366b04feec2c9ea185d335aa6f5c5858f
|
||||||
size 56240
|
size 55886
|
||||||
|
|||||||
@@ -18,41 +18,23 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
|||||||
var userSession: UserSessionMock!
|
var userSession: UserSessionMock!
|
||||||
|
|
||||||
private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
|
private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
var context: CreateRoomViewModel.Context {
|
var context: CreateRoomViewModel.Context {
|
||||||
viewModel.context
|
viewModel.context
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
cancellables.removeAll()
|
|
||||||
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", userID: "@a:b.com"))
|
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", userID: "@a:b.com"))
|
||||||
userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||||
let parameters = CreateRoomFlowParameters()
|
let parameters = CreateRoomFlowParameters()
|
||||||
usersSubject.send([.mockAlice, .mockBob, .mockCharlie])
|
|
||||||
ServiceLocator.shared.settings.knockingEnabled = true
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
let viewModel = CreateRoomViewModel(userSession: userSession,
|
let viewModel = CreateRoomViewModel(userSession: userSession,
|
||||||
createRoomParameters: .init(parameters),
|
createRoomParameters: .init(parameters),
|
||||||
selectedUsers: usersSubject.asCurrentValuePublisher(),
|
selectedUsers: [.mockAlice, .mockBob, .mockCharlie],
|
||||||
analytics: ServiceLocator.shared.analytics,
|
analytics: ServiceLocator.shared.analytics,
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
appSettings: ServiceLocator.shared.settings)
|
appSettings: ServiceLocator.shared.settings)
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.actions.sink { [weak self] action in
|
|
||||||
guard let self else { return }
|
|
||||||
switch action {
|
|
||||||
case .deselectUser(let user):
|
|
||||||
var selectedUsers = usersSubject.value
|
|
||||||
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
|
||||||
selectedUsers.remove(at: index)
|
|
||||||
}
|
|
||||||
usersSubject.send(selectedUsers)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDeselectUser() {
|
func testDeselectUser() {
|
||||||
|
|||||||
@@ -15,17 +15,11 @@ import XCTest
|
|||||||
class InviteUsersScreenViewModelTests: XCTestCase {
|
class InviteUsersScreenViewModelTests: XCTestCase {
|
||||||
var viewModel: InviteUsersScreenViewModelProtocol!
|
var viewModel: InviteUsersScreenViewModelProtocol!
|
||||||
var userDiscoveryService: UserDiscoveryServiceMock!
|
var userDiscoveryService: UserDiscoveryServiceMock!
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
var context: InviteUsersScreenViewModel.Context {
|
var context: InviteUsersScreenViewModel.Context {
|
||||||
viewModel.context
|
viewModel.context
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setUp() {
|
|
||||||
cancellables.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSelectUser() {
|
func testSelectUser() {
|
||||||
setupWithRoomType(roomType: .draft)
|
setupWithRoomType(roomType: .draft)
|
||||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||||
@@ -56,7 +50,9 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
func testInviteButton() async throws {
|
func testInviteButton() async throws {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
||||||
setupWithRoomType(roomType: .room(roomProxy: JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))))
|
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
|
||||||
|
roomProxy.inviteUserIDReturnValue = .success(())
|
||||||
|
setupWithRoomType(roomType: .room(roomProxy: roomProxy))
|
||||||
|
|
||||||
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
state.isUserSelected(.mockAlice)
|
state.isUserSelected(.mockAlice)
|
||||||
@@ -68,7 +64,7 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .invite:
|
case .dismiss:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -77,41 +73,20 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
context.send(viewAction: .proceed)
|
context.send(viewAction: .proceed)
|
||||||
|
|
||||||
guard case let .invite(members) = try await deferredAction.fulfill() else {
|
try await deferredAction.fulfill()
|
||||||
XCTFail("Sent action should be 'invite'")
|
XCTAssertEqual(roomProxy.inviteUserIDReceivedInvocations, [RoomMemberProxyMock.mockAlice.userID])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
XCTAssertEqual(members, [RoomMemberProxyMock.mockAlice.userID])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupWithRoomType(roomType: InviteUsersScreenRoomType) {
|
private func setupWithRoomType(roomType: InviteUsersScreenRoomType) {
|
||||||
let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
|
|
||||||
userDiscoveryService = UserDiscoveryServiceMock()
|
userDiscoveryService = UserDiscoveryServiceMock()
|
||||||
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
||||||
usersSubject.send([])
|
|
||||||
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
|
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
|
||||||
selectedUsers: usersSubject.asCurrentValuePublisher(),
|
selectedUsers: nil,
|
||||||
roomType: roomType,
|
roomType: roomType,
|
||||||
userDiscoveryService: userDiscoveryService,
|
userDiscoveryService: userDiscoveryService,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie])
|
viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie])
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.actions.sink { action in
|
|
||||||
switch action {
|
|
||||||
case .toggleUser(let user):
|
|
||||||
var selectedUsers = usersSubject.value
|
|
||||||
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
|
||||||
selectedUsers.remove(at: index)
|
|
||||||
} else {
|
|
||||||
selectedUsers.append(user)
|
|
||||||
}
|
|
||||||
usersSubject.send(selectedUsers)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user