* 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>
239 lines
8.1 KiB
Swift
239 lines
8.1 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Combine
|
|
import MatrixRustSDK
|
|
import SwiftUI
|
|
|
|
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
|
|
|
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
|
private let roomType: InviteUsersScreenRoomType
|
|
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
|
private let appSettings: AppSettings
|
|
|
|
private var suggestedUsers = [UserProfileProxy]()
|
|
|
|
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
|
|
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
|
|
actionsSubject.eraseToAnyPublisher()
|
|
}
|
|
|
|
private let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
|
|
|
|
init(userSession: UserSessionProtocol,
|
|
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?,
|
|
roomType: InviteUsersScreenRoomType,
|
|
userDiscoveryService: UserDiscoveryServiceProtocol,
|
|
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 ?? [],
|
|
isCreatingRoom: roomType.isCreatingRoom),
|
|
mediaProvider: userSession.mediaProvider)
|
|
|
|
setupSubscriptions()
|
|
fetchMembersIfNeeded()
|
|
|
|
Task {
|
|
suggestedUsers = await userSession.clientProxy.recentConversationCounterparts()
|
|
|
|
if state.usersSection.type == .suggestions {
|
|
state.usersSection = .init(type: .suggestions, users: suggestedUsers)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
override func process(viewAction: InviteUsersScreenViewAction) {
|
|
switch viewAction {
|
|
case .cancel:
|
|
actionsSubject.send(.dismiss)
|
|
case .proceed:
|
|
switch roomType {
|
|
case .draft:
|
|
actionsSubject.send(.proceed(selectedUsers: state.selectedUsers))
|
|
case .room(let roomProxy):
|
|
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
|
}
|
|
case .toggleUser(let 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()
|
|
|
|
Task.detached { [members] in
|
|
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
|
let membershipState = members
|
|
.reduce(into: [String: MembershipState]()) { partialResult, member in
|
|
partialResult[member.userID] = member.membership
|
|
}
|
|
|
|
Task { @MainActor in
|
|
self.state.membershipState = membershipState
|
|
self.hideLoader()
|
|
}
|
|
}
|
|
}
|
|
|
|
// periphery:ignore - automatically cancelled when set to nil
|
|
@CancellableTask
|
|
private var fetchUsersTask: Task<Void, Never>?
|
|
|
|
private func setupSubscriptions() {
|
|
context.$viewState
|
|
.map(\.bindings.searchQuery)
|
|
.debounceTextQueriesAndRemoveDuplicates()
|
|
.sink { [weak self] _ in
|
|
self?.fetchUsers()
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
if let selectedUsers {
|
|
selectedUsers
|
|
.sink { [weak self] users in
|
|
self?.state.selectedUsers = users
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
}
|
|
|
|
private func fetchMembersIfNeeded() {
|
|
guard case let .room(roomProxy) = roomType else {
|
|
return
|
|
}
|
|
|
|
Task {
|
|
showLoader()
|
|
await roomProxy.updateMembers()
|
|
hideLoader()
|
|
}
|
|
|
|
roomProxy.membersPublisher
|
|
.filter { !$0.isEmpty }
|
|
.first()
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] members in
|
|
self?.buildMembershipStateIfNeeded(members: members)
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
private func fetchUsers() {
|
|
guard searchQuery.count >= 3 else {
|
|
state.usersSection = .init(type: .suggestions, users: suggestedUsers)
|
|
return
|
|
}
|
|
|
|
state.isSearching = true
|
|
|
|
fetchUsersTask = Task {
|
|
let result = await userDiscoveryService.searchProfiles(with: searchQuery)
|
|
|
|
guard !Task.isCancelled else { return }
|
|
|
|
state.isSearching = false
|
|
|
|
switch result {
|
|
case .success(let users):
|
|
state.usersSection = .init(type: .searchResult, users: users)
|
|
case .failure:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private var searchQuery: String {
|
|
context.searchQuery
|
|
}
|
|
|
|
private let userIndicatorID = UUID().uuidString
|
|
|
|
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() {
|
|
userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
|
}
|
|
}
|
|
|
|
private extension InviteUsersScreenRoomType {
|
|
var isCreatingRoom: Bool {
|
|
switch self {
|
|
case .draft:
|
|
return true
|
|
case .room:
|
|
return false
|
|
}
|
|
}
|
|
}
|