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:
@@ -13,14 +13,14 @@ struct CreateRoomCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>
|
||||
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>
|
||||
let selectedUsers: [UserProfileProxy]
|
||||
let appSettings: AppSettings
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
enum CreateRoomCoordinatorAction {
|
||||
case openRoom(withIdentifier: String)
|
||||
case deselectUser(UserProfileProxy)
|
||||
case updateSelectedUsers([UserProfileProxy])
|
||||
case updateDetails(CreateRoomFlowParameters)
|
||||
case displayMediaPickerWithMode(MediaPickerScreenMode)
|
||||
case removeImage
|
||||
@@ -48,8 +48,8 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .deselectUser(let user):
|
||||
actionsSubject.send(.deselectUser(user))
|
||||
case .updateSelectedUsers(let users):
|
||||
actionsSubject.send(.updateSelectedUsers(users))
|
||||
case .openRoom(let identifier):
|
||||
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
case .updateDetails(let details):
|
||||
|
||||
@@ -18,7 +18,7 @@ enum CreateRoomScreenErrorType: Error {
|
||||
|
||||
enum CreateRoomViewModelAction {
|
||||
case openRoom(withIdentifier: String)
|
||||
case deselectUser(UserProfileProxy)
|
||||
case updateSelectedUsers([UserProfileProxy])
|
||||
case updateDetails(CreateRoomFlowParameters)
|
||||
case displayMediaPicker
|
||||
case displayCameraPicker
|
||||
|
||||
@@ -28,7 +28,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>,
|
||||
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
|
||||
selectedUsers: [UserProfileProxy],
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings) {
|
||||
@@ -46,7 +46,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
super.init(initialViewState: CreateRoomViewState(roomName: parameters.name,
|
||||
serverName: userSession.clientProxy.userIDServerName ?? "",
|
||||
isKnockingFeatureEnabled: appSettings.knockingEnabled,
|
||||
selectedUsers: selectedUsers.value,
|
||||
selectedUsers: selectedUsers,
|
||||
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
|
||||
bindings: bindings),
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
@@ -67,12 +67,6 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
selectedUsers
|
||||
.sink { [weak self] users in
|
||||
self?.state.selectedUsers = users
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
setupBindings()
|
||||
}
|
||||
|
||||
@@ -85,7 +79,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
await createRoom()
|
||||
}
|
||||
case .deselectUser(let user):
|
||||
actionsSubject.send(.deselectUser(user))
|
||||
state.selectedUsers.removeAll { $0.userID == user.userID }
|
||||
actionsSubject.send(.updateSelectedUsers(state.selectedUsers))
|
||||
case .displayCameraPicker:
|
||||
actionsSubject.send(.displayCameraPicker)
|
||||
case .displayMediaPicker:
|
||||
|
||||
@@ -11,17 +11,16 @@ import SwiftUI
|
||||
|
||||
struct InviteUsersScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>
|
||||
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
|
||||
let roomType: InviteUsersScreenRoomType
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let appSettings: AppSettings
|
||||
}
|
||||
|
||||
enum InviteUsersScreenCoordinatorAction {
|
||||
case cancel
|
||||
case proceed
|
||||
case invite(users: [String])
|
||||
case toggleUser(UserProfileProxy)
|
||||
case dismiss
|
||||
case proceed(selectedUsers: [UserProfileProxy])
|
||||
}
|
||||
|
||||
final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -38,21 +37,18 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
||||
selectedUsers: parameters.selectedUsers,
|
||||
roomType: parameters.roomType,
|
||||
userDiscoveryService: parameters.userDiscoveryService,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
case .proceed:
|
||||
actionsSubject.send(.proceed)
|
||||
case .invite(let users):
|
||||
actionsSubject.send(.invite(users: users))
|
||||
case .toggleUser(let user):
|
||||
actionsSubject.send(.toggleUser(user))
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .proceed(let selectedUsers):
|
||||
actionsSubject.send(.proceed(selectedUsers: selectedUsers))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -15,10 +15,8 @@ enum InviteUsersScreenErrorType: Error {
|
||||
}
|
||||
|
||||
enum InviteUsersScreenViewModelAction {
|
||||
case cancel
|
||||
case proceed
|
||||
case invite(users: [String])
|
||||
case toggleUser(UserProfileProxy)
|
||||
case dismiss
|
||||
case proceed(selectedUsers: [UserProfileProxy])
|
||||
}
|
||||
|
||||
enum InviteUsersScreenRoomType {
|
||||
|
||||
@@ -16,6 +16,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
private let roomType: InviteUsersScreenRoomType
|
||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private var suggestedUsers = [UserProfileProxy]()
|
||||
|
||||
@@ -24,20 +25,25 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
|
||||
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?,
|
||||
roomType: InviteUsersScreenRoomType,
|
||||
userDiscoveryService: UserDiscoveryServiceProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings) {
|
||||
self.roomType = roomType
|
||||
self.userDiscoveryService = userDiscoveryService
|
||||
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),
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
|
||||
setupSubscriptions(selectedUsers: selectedUsers)
|
||||
setupSubscriptions()
|
||||
fetchMembersIfNeeded()
|
||||
|
||||
Task {
|
||||
@@ -54,23 +60,67 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
override func process(viewAction: InviteUsersScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
actionsSubject.send(.dismiss)
|
||||
case .proceed:
|
||||
switch roomType {
|
||||
case .draft:
|
||||
actionsSubject.send(.proceed)
|
||||
case .room:
|
||||
actionsSubject.send(.invite(users: state.selectedUsers.map(\.userID)))
|
||||
actionsSubject.send(.proceed(selectedUsers: state.selectedUsers))
|
||||
case .room(let roomProxy):
|
||||
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
||||
}
|
||||
case .toggleUser(let user):
|
||||
let willSelectUser = !state.selectedUsers.contains(user)
|
||||
state.scrollToLastID = willSelectUser ? user.userID : nil
|
||||
actionsSubject.send(.toggleUser(user))
|
||||
toggleUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
// 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]) {
|
||||
showLoader()
|
||||
|
||||
@@ -92,7 +142,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
@CancellableTask
|
||||
private var fetchUsersTask: Task<Void, Never>?
|
||||
|
||||
private func setupSubscriptions(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>) {
|
||||
private func setupSubscriptions() {
|
||||
context.$viewState
|
||||
.map(\.bindings.searchQuery)
|
||||
.debounceTextQueriesAndRemoveDuplicates()
|
||||
@@ -101,11 +151,13 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
selectedUsers
|
||||
.sink { [weak self] users in
|
||||
self?.state.selectedUsers = users
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
if let selectedUsers {
|
||||
selectedUsers
|
||||
.sink { [weak self] users in
|
||||
self?.state.selectedUsers = users
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchMembersIfNeeded() {
|
||||
@@ -159,8 +211,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
|
||||
private let userIndicatorID = UUID().uuidString
|
||||
|
||||
private func showLoader() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200))
|
||||
private func showLoader(title: String = L10n.commonLoading,
|
||||
message: String? = nil) {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID,
|
||||
type: .modal,
|
||||
title: title,
|
||||
message: message,
|
||||
persistent: true),
|
||||
delay: .milliseconds(200))
|
||||
}
|
||||
|
||||
private func hideLoader() {
|
||||
|
||||
@@ -158,7 +158,8 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
||||
selectedUsers: .init([]),
|
||||
roomType: .draft,
|
||||
userDiscoveryService: userDiscoveryService,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -48,6 +48,7 @@ struct RoomMembersListScreen: View {
|
||||
.autocorrectionDisabled()
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.navigationTitle(L10n.commonPeople)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.sheet(item: $context.manageMemeberViewModel) {
|
||||
ManageRoomMemberSheetView(context: $0.context)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ enum SpaceScreenCoordinatorAction {
|
||||
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
||||
case selectRoom(roomID: String)
|
||||
case leftSpace
|
||||
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||
}
|
||||
|
||||
final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -61,6 +62,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.selectRoom(roomID: roomID))
|
||||
case .leftSpace:
|
||||
actionsSubject.send(.leftSpace)
|
||||
case .displayMembers(let roomProxy):
|
||||
actionsSubject.send(.displayMembers(roomProxy: roomProxy))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -13,12 +13,14 @@ enum SpaceScreenViewModelAction {
|
||||
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
||||
case selectRoom(roomID: String)
|
||||
case leftSpace
|
||||
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||
}
|
||||
|
||||
struct SpaceScreenViewState: BindableState {
|
||||
var space: SpaceRoomProxyProtocol
|
||||
|
||||
var permalink: URL?
|
||||
var roomProxy: JoinedRoomProxyProtocol?
|
||||
|
||||
var isPaginating = false
|
||||
var rooms: [SpaceRoomProxyProtocol]
|
||||
@@ -42,4 +44,5 @@ enum SpaceScreenViewAction {
|
||||
case toggleLeaveSpaceRoomDetails(id: String)
|
||||
case confirmLeaveSpace
|
||||
case spaceSettings
|
||||
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||
}
|
||||
|
||||
@@ -68,9 +68,13 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id),
|
||||
case let .success(permalinkURL) = await roomProxy.matrixToPermalink() {
|
||||
state.permalink = permalinkURL
|
||||
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id) {
|
||||
// Required to listen for membership updates in the members flow
|
||||
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:
|
||||
Task { await confirmLeaveSpace() }
|
||||
case .displayMembers(let roomProxy):
|
||||
actionsSubject.send(.displayMembers(roomProxy: roomProxy))
|
||||
case .spaceSettings:
|
||||
break // Not implemented.
|
||||
}
|
||||
|
||||
@@ -62,8 +62,13 @@ struct SpaceScreen: View {
|
||||
// controller attempts to anchor itself to the button that is no longer visible.
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
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) {
|
||||
Label(L10n.actionShare, icon: \.shareIos)
|
||||
}
|
||||
@@ -106,10 +111,16 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
|
||||
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy,
|
||||
initialSpaceRooms: .mockSpaceList))
|
||||
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
.joined(JoinedRoomProxyMock(.init()))
|
||||
}
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
|
||||
spaceServiceProxy: SpaceServiceProxyMock(.init()),
|
||||
selectedSpaceRoomPublisher: .init(nil),
|
||||
userSession: UserSessionMock(.init()),
|
||||
userSession: userSession,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@@ -87,20 +87,17 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
selectedUsers: selectedUsersPublisher,
|
||||
roomType: .draft,
|
||||
userDiscoveryService: parameters.userDiscoveryService,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings)
|
||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
break // Not shown in this flow.
|
||||
case .proceed:
|
||||
case .dismiss:
|
||||
fatalError("Not shown in this flow.")
|
||||
case .proceed(let selectedUsers):
|
||||
self.selectedUsers.send(selectedUsers)
|
||||
openCreateRoomScreen()
|
||||
case .invite:
|
||||
break
|
||||
case .toggleUser(let user):
|
||||
toggleUser(user)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -115,15 +112,15 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
createRoomParameters: createRoomParametersPublisher,
|
||||
selectedUsers: selectedUsersPublisher,
|
||||
selectedUsers: selectedUsers.value,
|
||||
appSettings: parameters.appSettings,
|
||||
analytics: parameters.analytics)
|
||||
let coordinator = CreateRoomCoordinator(parameters: createParameters)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .deselectUser(let user):
|
||||
self.toggleUser(user)
|
||||
case .updateSelectedUsers(let users):
|
||||
self.selectedUsers.send(users)
|
||||
case .updateDetails(let details):
|
||||
self.createRoomParameters.send(details)
|
||||
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
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(StartChatScreenCoordinator.self)-Loading"
|
||||
|
||||
Reference in New Issue
Block a user