diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 61462fbc3..f812a6af6 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -317,7 +317,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return } - let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, + let params = RoomDetailsScreenCoordinatorParameters(accountUserID: userSession.userID, + navigationStackCoordinator: navigationStackCoordinator, roomProxy: roomProxy, mediaProvider: userSession.mediaProvider, userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy)) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 1ac16d9ee..99724f8d9 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -437,21 +437,21 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingMembersPublisher = value } } var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>! - var invitedMembersCount: UInt { + var invitedMembersCount: Int { get { return underlyingInvitedMembersCount } set(value) { underlyingInvitedMembersCount = value } } - var underlyingInvitedMembersCount: UInt! - var joinedMembersCount: UInt { + var underlyingInvitedMembersCount: Int! + var joinedMembersCount: Int { get { return underlyingJoinedMembersCount } set(value) { underlyingJoinedMembersCount = value } } - var underlyingJoinedMembersCount: UInt! - var activeMembersCount: UInt { + var underlyingJoinedMembersCount: Int! + var activeMembersCount: Int { get { return underlyingActiveMembersCount } set(value) { underlyingActiveMembersCount = value } } - var underlyingActiveMembersCount: UInt! + var underlyingActiveMembersCount: Int! var updatesPublisher: AnyPublisher { get { return underlyingUpdatesPublisher } set(value) { underlyingUpdatesPublisher = value } diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index 735203fde..10324fa9b 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -100,11 +100,12 @@ extension RoomMemberProxyMock { isIgnored: true)) } - static func mockOwner(allowedStateEvents: [StateEventType]) -> RoomMemberProxyMock { + static func mockOwner(allowedStateEvents: [StateEventType], canInviteUsers: Bool = true) -> RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@foo:some.org", displayName: "User owner", membership: .join, isAccountOwner: true, + canInviteUsers: canInviteUsers, canSendStateEvent: { allowedStateEvents.contains($0) })) } } diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index 335994793..78e173851 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -33,10 +33,11 @@ struct RoomProxyMockConfiguration { var hasUnreadNotifications = Bool.random() var members: [RoomMemberProxyProtocol]? var inviter: RoomMemberProxyMock? + var memberForID: RoomMemberProxyMock = .mockMe - var invitedMembersCount: UInt = 100 - var joinedMembersCount: UInt = 50 - var activeMembersCount: UInt = 25 + var invitedMembersCount = 100 + var joinedMembersCount = 50 + var activeMembersCount = 25 } extension RoomProxyMock { @@ -77,5 +78,6 @@ extension RoomProxyMock { underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher() setNameClosure = { _ in .success(()) } setTopicClosure = { _ in .success(()) } + getMemberUserIDReturnValue = .success(configuration.memberForID) } } diff --git a/ElementX/Sources/Other/UserIndicator/UserIndicatorControllerProtocol.swift b/ElementX/Sources/Other/UserIndicator/UserIndicatorControllerProtocol.swift index f5f25e673..520794768 100644 --- a/ElementX/Sources/Other/UserIndicator/UserIndicatorControllerProtocol.swift +++ b/ElementX/Sources/Other/UserIndicator/UserIndicatorControllerProtocol.swift @@ -27,7 +27,7 @@ protocol UserIndicatorControllerProtocol: CoordinatorProtocol { extension UserIndicatorControllerProtocol { /// Allows to submit a delayed indicator, this returns a Task so that it's also possible to cancel the action func submitIndicator(_ indicator: UserIndicator, delay: Duration) -> Task { - Task { + Task { @MainActor in try? await Task.sleep(for: delay) guard !Task.isCancelled else { return } submitIndicator(indicator) diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift index 9f3c74e35..c3d3a413f 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift @@ -22,6 +22,7 @@ struct InviteUsersScreenCoordinatorParameters { let roomType: InviteUsersScreenRoomType let mediaProvider: MediaProviderProtocol let userDiscoveryService: UserDiscoveryServiceProtocol + weak var userIndicatorController: UserIndicatorControllerProtocol? } enum InviteUsersScreenCoordinatorAction { @@ -46,7 +47,8 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol { viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers, roomType: parameters.roomType, mediaProvider: parameters.mediaProvider, - userDiscoveryService: parameters.userDiscoveryService) + userDiscoveryService: parameters.userDiscoveryService, + userIndicatorController: parameters.userIndicatorController) } func start() { diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift index 144f671ae..bb63d332b 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift @@ -29,7 +29,7 @@ enum InviteUsersScreenViewModelAction { enum InviteUsersScreenRoomType { case draft - case room(members: [RoomMemberProxyProtocol], userIndicatorController: UserIndicatorControllerProtocol) + case room(roomProxy: RoomProxyProtocol) } struct InviteUsersScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift index 8d1faa720..0eef0ea6e 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift @@ -24,7 +24,9 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr private let mediaProvider: MediaProviderProtocol private let userDiscoveryService: UserDiscoveryServiceProtocol private let roomType: InviteUsersScreenRoomType + private weak var userIndicatorController: UserIndicatorControllerProtocol? private let actionsSubject: PassthroughSubject = .init() + @CancellableTask private var showLoaderTask: Task? var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -33,14 +35,16 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr init(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>, roomType: InviteUsersScreenRoomType, mediaProvider: MediaProviderProtocol, - userDiscoveryService: UserDiscoveryServiceProtocol) { + userDiscoveryService: UserDiscoveryServiceProtocol, + userIndicatorController: UserIndicatorControllerProtocol?) { self.roomType = roomType self.mediaProvider = mediaProvider self.userDiscoveryService = userDiscoveryService + self.userIndicatorController = userIndicatorController super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider) - buildMembershipStateIfNeeded() setupSubscriptions(selectedUsers: selectedUsers) + fetchMembersIfNeeded() } // MARK: - Public @@ -60,13 +64,11 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr actionsSubject.send(.toggleUser(user)) } } + + // MARK: - Private - private func buildMembershipStateIfNeeded() { - guard case let .room(members, userIndicatorController) = roomType else { - return - } - let indicatorID = UUID().uuidString - userIndicatorController.submitIndicator(UserIndicator(id: indicatorID, type: .modal, title: L10n.commonLoading, persistent: true)) + 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. @@ -77,12 +79,10 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr Task { @MainActor in self.state.membershipState = membershipState - userIndicatorController.retractIndicatorWithId(indicatorID) + self.hideLoader() } } } - - // MARK: - Private @CancellableTask private var fetchUsersTask: Task? @@ -103,6 +103,26 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr .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() + .sink { [weak self] members in + self?.buildMembershipStateIfNeeded(members: members) + } + .store(in: &cancellables) + } + private func fetchUsers() { guard searchQuery.count >= 3 else { fetchSuggestions() @@ -139,6 +159,17 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr private var searchQuery: String { context.searchQuery } + + private let userIndicatorID = UUID().uuidString + + private func showLoader() { + showLoaderTask = userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200)) + } + + private func hideLoader() { + showLoaderTask = nil + userIndicatorController?.retractIndicatorWithId(userIndicatorID) + } } private extension InviteUsersScreenRoomType { diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift index 9bdb4ff44..87cd3b344 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift @@ -140,7 +140,7 @@ struct InviteUsersScreen_Previews: PreviewProvider { let userDiscoveryService = UserDiscoveryServiceMock() userDiscoveryService.fetchSuggestionsReturnValue = .success([.mockAlice]) userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice]) - return InviteUsersScreenViewModel(selectedUsers: .init([]), roomType: .draft, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService) + return InviteUsersScreenViewModel(selectedUsers: .init([]), roomType: .draft, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService, userIndicatorController: UserIndicatorControllerMock()) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift index bd16552ee..f94f9edcf 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift @@ -20,9 +20,9 @@ import SwiftUI struct RoomDetailsEditScreenCoordinatorParameters { let accountOwner: RoomMemberProxyProtocol let mediaProvider: MediaProviderProtocol - let navigationStackCoordinator: NavigationStackCoordinator + weak var navigationStackCoordinator: NavigationStackCoordinator? let roomProxy: RoomProxyProtocol - let userIndicatorController: UserIndicatorControllerProtocol + weak var userIndicatorController: UserIndicatorControllerProtocol? } enum RoomDetailsEditScreenCoordinatorAction { @@ -77,14 +77,14 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { case .cancel: - parameters.navigationStackCoordinator.setSheetCoordinator(nil) + parameters.navigationStackCoordinator?.setSheetCoordinator(nil) case .selectMediaAtURL(let url): - parameters.navigationStackCoordinator.setSheetCoordinator(nil) + parameters.navigationStackCoordinator?.setSheetCoordinator(nil) viewModel.didSelectMediaUrl(url: url) } } stackCoordinator.setRootCoordinator(mediaPickerCoordinator) - parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController) + parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) } } diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift index 4e71a17e4..8570e9b42 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift @@ -22,17 +22,17 @@ typealias RoomDetailsEditScreenViewModelType = StateStoreViewModel = .init() private let roomProxy: RoomProxyProtocol - private let userIndicatorController: UserIndicatorControllerProtocol + private weak var userIndicatorController: UserIndicatorControllerProtocol? private let mediaPreprocessor: MediaUploadingPreprocessor = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(accountOwner: RoomMemberProxyProtocol, mediaProvider: MediaProviderProtocol, roomProxy: RoomProxyProtocol, - userIndicatorController: UserIndicatorControllerProtocol) { + userIndicatorController: UserIndicatorControllerProtocol?) { self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController @@ -75,12 +75,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe Task { let userIndicatorID = UUID().uuidString defer { - userIndicatorController.retractIndicatorWithId(userIndicatorID) + userIndicatorController?.retractIndicatorWithId(userIndicatorID) } - userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, - type: .modal(interactiveDismissDisabled: true), - title: L10n.commonLoading, - persistent: true)) + userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID, + type: .modal(interactiveDismissDisabled: true), + title: L10n.commonLoading, + persistent: true)) let mediaResult = await mediaPreprocessor.processMedia(at: url) @@ -88,7 +88,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe case .success(.image): state.localMedia = try? mediaResult.get() case .failure, .success: - userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown) + userIndicatorController?.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown) } } } @@ -99,12 +99,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe Task { let userIndicatorID = UUID().uuidString defer { - userIndicatorController.retractIndicatorWithId(userIndicatorID) + userIndicatorController?.retractIndicatorWithId(userIndicatorID) } - userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, - type: .modal(interactiveDismissDisabled: true), - title: L10n.screenRoomDetailsUpdatingRoom, - persistent: true)) + userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID, + type: .modal(interactiveDismissDisabled: true), + title: L10n.screenRoomDetailsUpdatingRoom, + persistent: true)) do { try await withThrowingTaskGroup(of: Void.self) { group in @@ -135,9 +135,9 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe actionsSubject.send(.saveFinished) } catch { - userIndicatorController.alertInfo = .init(id: .init(), - title: L10n.screenRoomDetailsEditionErrorTitle, - message: L10n.screenRoomDetailsEditionError) + userIndicatorController?.alertInfo = .init(id: .init(), + title: L10n.screenRoomDetailsEditionErrorTitle, + message: L10n.screenRoomDetailsEditionError) } } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index e5e04f00a..8fad0ae57 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -18,7 +18,8 @@ import Combine import SwiftUI struct RoomDetailsScreenCoordinatorParameters { - let navigationStackCoordinator: NavigationStackCoordinator + let accountUserID: String + weak var navigationStackCoordinator: NavigationStackCoordinator? let roomProxy: RoomProxyProtocol let mediaProvider: MediaProviderProtocol let userDiscoveryService: UserDiscoveryServiceProtocol @@ -33,7 +34,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { private var viewModel: RoomDetailsScreenViewModelProtocol private var cancellables: Set = .init() private let selectedUsers: CurrentValueSubject<[UserProfileProxy], Never> = .init([]) - private var navigationStackCoordinator: NavigationStackCoordinator { + private var navigationStackCoordinator: NavigationStackCoordinator? { parameters.navigationStackCoordinator } @@ -42,7 +43,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { init(parameters: RoomDetailsScreenCoordinatorParameters) { self.parameters = parameters - viewModel = RoomDetailsScreenViewModel(roomProxy: parameters.roomProxy, + viewModel = RoomDetailsScreenViewModel(accountUserID: parameters.accountUserID, + roomProxy: parameters.roomProxy, mediaProvider: parameters.mediaProvider) } @@ -53,10 +55,10 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .requestMemberDetailsPresentation(let members): - self.presentRoomMembersList(members) - case .requestInvitePeoplePresentation(let members): - self.presentInviteUsersScreen(members: members) + case .requestMemberDetailsPresentation: + self.presentRoomMembersList() + case .requestInvitePeoplePresentation: + self.presentInviteUsersScreen() case .leftRoom: self.callback?(.leftRoom) case .requestEditDetailsPresentation(let accountOwner): @@ -71,32 +73,32 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { // MARK: - Private - private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) { + private func presentRoomMembersList() { let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, mediaProvider: parameters.mediaProvider, - members: members) + roomProxy: parameters.roomProxy) let coordinator = RoomMembersListScreenCoordinator(parameters: params) coordinator.callback = { [weak self] action in switch action { case .invite: - self?.presentInviteUsersScreen(members: members) + self?.presentInviteUsersScreen() } } - navigationStackCoordinator.push(coordinator) + navigationStackCoordinator?.push(coordinator) } - private func presentInviteUsersScreen(members: [RoomMemberProxyProtocol]) { - let navigationStackCoordinator = NavigationStackCoordinator() - let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator) + private func presentInviteUsersScreen() { + let inviteUsersStackCoordinator = NavigationStackCoordinator() + let userIndicatorController = UserIndicatorController(rootCoordinator: inviteUsersStackCoordinator) let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsers), - roomType: .room(members: members, userIndicatorController: userIndicatorController), + roomType: .room(roomProxy: parameters.roomProxy), mediaProvider: parameters.mediaProvider, - userDiscoveryService: parameters.userDiscoveryService) + userDiscoveryService: parameters.userDiscoveryService, userIndicatorController: userIndicatorController) let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters) - navigationStackCoordinator.setRootCoordinator(coordinator) + inviteUsersStackCoordinator.setRootCoordinator(coordinator) coordinator.actions.sink { [weak self] result in guard let self else { return } @@ -112,7 +114,9 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) - parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController) + parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) { [weak self] in + self?.selectedUsers.value = [] + } } private func presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) { @@ -129,14 +133,14 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { roomDetailsEditCoordinator.actions.sink { [weak self] action in switch action { case .dismiss: - self?.parameters.navigationStackCoordinator.setSheetCoordinator(nil) + self?.navigationStackCoordinator?.setSheetCoordinator(nil) } } .store(in: &cancellables) navigationStackCoordinator.setRootCoordinator(roomDetailsEditCoordinator) - parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController) + self.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) } private func toggleUser(_ user: UserProfileProxy) { @@ -150,7 +154,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { } private func inviteUsers(_ users: [String], in room: RoomProxyProtocol) { - navigationStackCoordinator.setSheetCoordinator(nil) + navigationStackCoordinator?.setSheetCoordinator(nil) Task { let result: Result = await withTaskGroup(of: Result.self) { group in diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index d1734646e..34e1b5813 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -22,8 +22,8 @@ import UIKit // MARK: View model enum RoomDetailsScreenViewModelAction { - case requestMemberDetailsPresentation([RoomMemberProxyProtocol]) - case requestInvitePeoplePresentation([RoomMemberProxyProtocol]) + case requestMemberDetailsPresentation + case requestInvitePeoplePresentation case leftRoom case requestEditDetailsPresentation(RoomMemberProxyProtocol) } @@ -35,22 +35,18 @@ struct RoomDetailsScreenViewState: BindableState { let canonicalAlias: String? let isEncrypted: Bool let isDirect: Bool + let permalink: URL? + var title = "" var topic: String? var avatarURL: URL? - let permalink: URL? - var members: [RoomMemberDetails] = [] - var joinedMembersCount = 0 + var joinedMembersCount: Int var isProcessingIgnoreRequest = false var canInviteUsers = false var canEditRoomName = false var canEditRoomTopic = false var canEditRoomAvatar = false - var isLoadingMembers: Bool { - members.isEmpty - } - var canEdit: Bool { !isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar) } @@ -62,10 +58,6 @@ struct RoomDetailsScreenViewState: BindableState { var bindings: RoomDetailsScreenViewStateBindings var dmRecipient: RoomMemberDetails? - - private var isDMRoom: Bool { - isEncrypted && isDirect && members.count == 2 - } } struct RoomDetailsScreenViewStateBindings { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 678f84d66..0b5301684 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -20,35 +20,36 @@ import SwiftUI typealias RoomDetailsScreenViewModelType = StateStoreViewModel class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol { + private let accountUserID: String private let roomProxy: RoomProxyProtocol - private var members: [RoomMemberProxyProtocol] = [] + + private var accountOwner: RoomMemberProxyProtocol? { + didSet { updatePowerLevelPermissions() } + } + private var dmRecipient: RoomMemberProxyProtocol? - private var accountOwner: RoomMemberProxyProtocol? - - @CancellableTask - private var buildMembersDetailsTask: Task? var callback: ((RoomDetailsScreenViewModelAction) -> Void)? - - init(roomProxy: RoomProxyProtocol, + + init(accountUserID: String, + roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol) { + self.accountUserID = accountUserID self.roomProxy = roomProxy super.init(initialViewState: .init(roomId: roomProxy.id, canonicalAlias: roomProxy.canonicalAlias, isEncrypted: roomProxy.isEncrypted, isDirect: roomProxy.isDirect, + permalink: roomProxy.permalink, title: roomProxy.roomTitle, topic: roomProxy.topic, avatarURL: roomProxy.avatarURL, - permalink: roomProxy.permalink, + joinedMembersCount: roomProxy.joinedMembersCount, bindings: .init()), imageProvider: mediaProvider) - setupSubscriptions() - - Task { - await roomProxy.updateMembers() - } + setupRoomSubscription() + fetchMembers() } // MARK: - Public @@ -57,12 +58,11 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr override func process(viewAction: RoomDetailsScreenViewAction) { switch viewAction { case .processTapPeople: - callback?(.requestMemberDetailsPresentation(members)) + callback?(.requestMemberDetailsPresentation) case .processTapInvite: - callback?(.requestInvitePeoplePresentation(members)) + callback?(.requestInvitePeoplePresentation) case .processTapLeave: - let joinedMembers = members.filter { $0.membership == .join } - guard joinedMembers.count > 1 else { + guard state.joinedMembersCount > 1 else { state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomId: roomProxy.id, state: .empty) return } @@ -88,7 +88,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr // MARK: - Private - private func setupSubscriptions() { + private func setupRoomSubscription() { switch roomProxy.registerTimelineListenerIfNeeded() { case .success, .failure(.roomListenerAlreadyRegistered): break @@ -96,32 +96,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr MXLog.error("Failed to register a room listener in room's details for the room \(roomProxy.id)") } - roomProxy.membersPublisher - .sink { [weak self] members in - guard let self else { return } - - buildMembersDetailsTask = Task { - let roomMembersDetails = await self.buildMembersDetails(members: members) - - guard !Task.isCancelled else { return } - - if self.roomProxy.isDirect, self.roomProxy.isEncrypted, members.count == 2 { - self.dmRecipient = members.first(where: { !$0.isAccountOwner }) - } - - self.state.members = roomMembersDetails.members - self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount - self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:)) - self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false - self.state.canEditRoomName = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomName) ?? false - self.state.canEditRoomTopic = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomTopic) ?? false - self.state.canEditRoomAvatar = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false - self.members = members - self.accountOwner = roomMembersDetails.accountOwner - } - } - .store(in: &cancellables) - roomProxy.updatesPublisher .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in @@ -129,33 +103,50 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr self.state.title = self.roomProxy.roomTitle self.state.topic = self.roomProxy.topic self.state.avatarURL = self.roomProxy.avatarURL + self.state.joinedMembersCount = self.roomProxy.joinedMembersCount } .store(in: &cancellables) } - private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails { - await Task.detached { - // accessing RoomMember's properties is very slow. We need to do it in a background thread. - var roomMembersDetails: [RoomMemberDetails] = [] - var accountOwner: RoomMemberProxyProtocol? - var joinedMembersCount = 0 - roomMembersDetails.reserveCapacity(members.count) - - for member in members { - roomMembersDetails.append(RoomMemberDetails(withProxy: member)) - - if member.membership == .join { - joinedMembersCount += 1 - } - - if accountOwner == nil, member.isAccountOwner { - accountOwner = member - } - } - - return .init(members: roomMembersDetails, joinedMembersCount: joinedMembersCount, accountOwner: accountOwner) + private func fetchMembers() { + Task { + await fetchMembersIfNeeded() + await fetchAccountOwner() } - .value + } + + private func fetchMembersIfNeeded() async { + // We need to fetch members just in 1-to-1 chat to get the member object for the other person + guard roomProxy.isEncryptedOneToOneRoom else { + return + } + + roomProxy.membersPublisher + .sink { [weak self] members in + guard let self else { return } + let dmRecipient = members.first(where: { !$0.isAccountOwner }) + self.dmRecipient = dmRecipient + self.state.dmRecipient = dmRecipient.map(RoomMemberDetails.init(withProxy:)) + } + .store(in: &cancellables) + + await roomProxy.updateMembers() + } + + private func fetchAccountOwner() async { + switch await roomProxy.getMember(userID: accountUserID) { + case .success(let member): + accountOwner = member + case .failure(let error): + MXLog.error("Failed (error: \(error) to get account owner member with id: \(accountUserID), in room: \(roomProxy.id)") + } + } + + private func updatePowerLevelPermissions() { + state.canInviteUsers = accountOwner?.canInviteUsers ?? false + state.canEditRoomName = accountOwner?.canSendStateEvent(type: .roomName) ?? false + state.canEditRoomTopic = accountOwner?.canSendStateEvent(type: .roomTopic) ?? false + state.canEditRoomAvatar = accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false } private static let leaveRoomLoadingID = "LeaveRoomLoading" @@ -196,9 +187,3 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } } } - -private struct RoomMembersDetails { - let members: [RoomMemberDetails] - let joinedMembersCount: Int - let accountOwner: RoomMemberProxyProtocol? -} diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 3b792a720..fa1b5dee0 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -137,13 +137,9 @@ struct RoomDetailsScreen: View { context.send(viewAction: .processTapPeople) } label: { LabeledContent { - if context.viewState.isLoadingMembers { - ProgressView() - } else { - Text(String(context.viewState.joinedMembersCount)) - .foregroundColor(.element.tertiaryContent) - .font(.compound.bodyLG) - } + Text(String(context.viewState.joinedMembersCount)) + .foregroundColor(.element.tertiaryContent) + .font(.compound.bodyLG) } label: { Label(L10n.commonPeople, systemImage: "person") } @@ -160,9 +156,8 @@ struct RoomDetailsScreen: View { } } .listRowSeparatorTint(.element.quinaryContent) - .buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink)) + .buttonStyle(FormButtonStyle(accessory: .navigationLink)) .foregroundColor(.element.primaryContent) - .disabled(context.viewState.isLoadingMembers) } @ViewBuilder @@ -257,7 +252,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider { canonicalAlias: "#alias:domain.com", members: members)) - return RoomDetailsScreenViewModel(roomProxy: roomProxy, + return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", + roomProxy: roomProxy, mediaProvider: MockMediaProvider()) }() @@ -274,7 +270,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider { canonicalAlias: "#alias:domain.com", members: members)) - return RoomDetailsScreenViewModel(roomProxy: roomProxy, + return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", + roomProxy: roomProxy, mediaProvider: MockMediaProvider()) }() diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift index 4d7c7f78d..d3a39b81c 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift @@ -17,9 +17,9 @@ import SwiftUI struct RoomMembersListScreenCoordinatorParameters { - let navigationStackCoordinator: NavigationStackCoordinator + weak var navigationStackCoordinator: NavigationStackCoordinator? let mediaProvider: MediaProviderProtocol - let members: [RoomMemberProxyProtocol] + let roomProxy: RoomProxyProtocol } enum RoomMembersListScreenCoordinatorAction { @@ -29,15 +29,17 @@ enum RoomMembersListScreenCoordinatorAction { final class RoomMembersListScreenCoordinator: CoordinatorProtocol { private let parameters: RoomMembersListScreenCoordinatorParameters private var viewModel: RoomMembersListScreenViewModelProtocol - private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator } + private var navigationStackCoordinator: NavigationStackCoordinator? { + parameters.navigationStackCoordinator + } var callback: ((RoomMembersListScreenCoordinatorAction) -> Void)? init(parameters: RoomMembersListScreenCoordinatorParameters) { self.parameters = parameters - viewModel = RoomMembersListScreenViewModel(mediaProvider: parameters.mediaProvider, - members: parameters.members) + viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy, + mediaProvider: parameters.mediaProvider) } func start() { @@ -63,6 +65,6 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol { let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomMemberProxy: member, mediaProvider: parameters.mediaProvider) let coordinator = RoomMemberDetailsScreenCoordinator(parameters: parameters) - navigationStackCoordinator.push(coordinator) + navigationStackCoordinator?.push(coordinator) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift index c475fdac7..7845255dc 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift @@ -24,10 +24,16 @@ enum RoomMembersListScreenViewModelAction { struct RoomMembersListScreenViewState: BindableState { private var joinedMembers: [RoomMemberDetails] private var invitedMembers: [RoomMemberDetails] + + let joinedMembersCount: Int var canInviteUsers = false var bindings: RoomMembersListScreenViewStateBindings - init(joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], bindings: RoomMembersListScreenViewStateBindings = .init()) { + init(joinedMembersCount: Int, + joinedMembers: [RoomMemberDetails] = [], + invitedMembers: [RoomMemberDetails] = [], + bindings: RoomMembersListScreenViewStateBindings = .init()) { + self.joinedMembersCount = joinedMembersCount self.joinedMembers = joinedMembers self.invitedMembers = invitedMembers self.bindings = bindings @@ -46,10 +52,6 @@ struct RoomMembersListScreenViewState: BindableState { member.matches(searchQuery: bindings.searchQuery) } } - - var joinedMembersCount: Int { - joinedMembers.count - } } struct RoomMembersListScreenViewStateBindings { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index ca55a0129..18a325513 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -19,17 +19,18 @@ import SwiftUI typealias RoomMembersListScreenViewModelType = StateStoreViewModel class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol { - private let mediaProvider: MediaProviderProtocol - private let members: [RoomMemberProxyProtocol] + private let roomProxy: RoomProxyProtocol + private var members: [RoomMemberProxyProtocol] = [] + @CancellableTask private var showLoaderTask: Task? var callback: ((RoomMembersListScreenViewModelAction) -> Void)? - init(mediaProvider: MediaProviderProtocol, members: [RoomMemberProxyProtocol]) { - self.mediaProvider = mediaProvider - self.members = members - super.init(initialViewState: .init(), imageProvider: mediaProvider) + init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol) { + self.roomProxy = roomProxy + super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount), + imageProvider: mediaProvider) - setupState(members: members) + setupMembers() } // MARK: - Public @@ -49,17 +50,32 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe // MARK: - Private - private func setupState(members: [RoomMemberProxyProtocol]) { + private func setupMembers() { Task { - let indicatorId = UUID().uuidString - defer { - ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(indicatorId) + showLoader() + await roomProxy.updateMembers() + hideLoader() + } + + roomProxy.membersPublisher + .filter { !$0.isEmpty } + .first() + .sink { [weak self] members in + self?.updateState(members: members) } - ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: indicatorId, type: .modal, title: L10n.commonLoading, persistent: true)) - + .store(in: &cancellables) + } + + private func updateState(members: [RoomMemberProxyProtocol]) { + Task { + showLoader() let roomMembersDetails = await buildMembersDetails(members: members) - self.state = .init(joinedMembers: roomMembersDetails.joinedMembers, invitedMembers: roomMembersDetails.invitedMembers) + self.members = members + self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount, + joinedMembers: roomMembersDetails.joinedMembers, + invitedMembers: roomMembersDetails.invitedMembers) self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false + hideLoader() } } @@ -89,6 +105,17 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe } .value } + + private let userIndicatorID = UUID().uuidString + + private func showLoader() { + showLoaderTask = ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200)) + } + + private func hideLoader() { + showLoaderTask = nil + ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(userIndicatorID) + } } private struct RoomMembersDetails { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift index 8e986c34d..a4cdd47e2 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift @@ -26,7 +26,7 @@ struct RoomMembersListScreen: View { ScrollView { LazyVStack(alignment: .leading) { membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle) - membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(context.viewState.joinedMembersCount)) + membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount))) } } .searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always)) @@ -80,8 +80,8 @@ struct RoomMembersListScreen_Previews: PreviewProvider { .mockBob, .mockCharlie ] - return RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(), - members: members) + return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)), + mediaProvider: MockMediaProvider()) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift index 293383044..efbe68f3f 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift @@ -53,8 +53,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider { .mockBob, .mockCharlie ] - let viewModel = RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(), - members: members) + let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)), + mediaProvider: MockMediaProvider()) return VStack { ForEach(members, id: \.userID) { member in diff --git a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift index 61e248815..10c9d061d 100644 --- a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift +++ b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift @@ -20,7 +20,7 @@ import SwiftUI struct StartChatScreenCoordinatorParameters { let userSession: UserSessionProtocol weak var userIndicatorController: UserIndicatorControllerProtocol? - let navigationStackCoordinator: NavigationStackCoordinator + weak var navigationStackCoordinator: NavigationStackCoordinator? let userDiscoveryService: UserDiscoveryServiceProtocol } @@ -45,7 +45,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { selectedUsers.asCurrentValuePublisher() } - private var navigationStackCoordinator: NavigationStackCoordinator { + private var navigationStackCoordinator: NavigationStackCoordinator? { parameters.navigationStackCoordinator } @@ -107,7 +107,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) - navigationStackCoordinator.push(coordinator) { [weak self] in + navigationStackCoordinator?.push(coordinator) { [weak self] in self?.createRoomParameters.send(.init()) self?.selectedUsers.send([]) } @@ -138,7 +138,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) - navigationStackCoordinator.push(coordinator) + navigationStackCoordinator?.push(coordinator) } // MARK: - Private @@ -152,7 +152,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { case .cancel: - navigationStackCoordinator.setSheetCoordinator(nil) + navigationStackCoordinator?.setSheetCoordinator(nil) case .selectMediaAtURL(let url): processAvatar(from: url) } @@ -160,11 +160,11 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { stackCoordinator.setRootCoordinator(mediaPickerCoordinator) - navigationStackCoordinator.setSheetCoordinator(userIndicatorController) + navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) } private func processAvatar(from url: URL) { - navigationStackCoordinator.setSheetCoordinator(nil) + navigationStackCoordinator?.setSheetCoordinator(nil) showLoadingIndicator() Task { [weak self] in guard let self else { return } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index dc3a41e44..f0b10d4c6 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -120,16 +120,16 @@ class RoomProxy: RoomProxyProtocol { return Asset.Images.encryptionTrusted.image } - var invitedMembersCount: UInt { - UInt(room.invitedMembersCount()) + var invitedMembersCount: Int { + Int(room.invitedMembersCount()) } - var joinedMembersCount: UInt { - UInt(room.joinedMembersCount()) + var joinedMembersCount: Int { + Int(room.joinedMembersCount()) } - var activeMembersCount: UInt { - UInt(room.activeMembersCount()) + var activeMembersCount: Int { + Int(room.activeMembersCount()) } func loadAvatarURLForUserId(_ userId: String) async -> Result { @@ -380,11 +380,13 @@ class RoomProxy: RoomProxyProtocol { func updateMembers() async { do { - let roomMembers = try await Task.dispatch(on: .global()) { - try self.room.members() + let roomMembersProxies = try await Task.dispatch(on: .global()) { + try self.room.members().map { + RoomMemberProxy(member: $0, backgroundTaskService: self.backgroundTaskService) + } } - membersSubject.value = buildRoomMemberProxies(members: roomMembers) + membersSubject.value = roomMembersProxies } catch { return } @@ -422,11 +424,6 @@ class RoomProxy: RoomProxyProtocol { } } - @MainActor - private func buildRoomMemberProxies(members: [RoomMember]) -> [RoomMemberProxy] { - members.map { RoomMemberProxy(member: $0, backgroundTaskService: backgroundTaskService) } - } - func retryDecryption(for sessionID: String) async { await Task.dispatch(on: .global()) { [weak self] in self?.room.retryDecryption(sessionIds: [sessionID]) diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index c5d8339b4..6c124bb52 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -66,11 +66,11 @@ protocol RoomProxyProtocol { var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get } - var invitedMembersCount: UInt { get } + var invitedMembersCount: Int { get } - var joinedMembersCount: UInt { get } + var joinedMembersCount: Int { get } - var activeMembersCount: UInt { get } + var activeMembersCount: Int { get } /// Publishes the room's updates. /// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()` @@ -157,4 +157,8 @@ extension RoomProxyProtocol { var roomTitle: String { displayName ?? name ?? "Unknown room 💥" } + + var isEncryptedOneToOneRoom: Bool { + isDirect && isEncrypted && joinedMembersCount == 2 + } } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index ba2d219e9..2c7c6a3db 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -306,8 +306,11 @@ class MockScreen: Identifiable { let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", displayName: "Room", isEncrypted: true, - members: members)) - let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, + members: members, + memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false), + joinedMembersCount: members.count)) + let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", + navigationStackCoordinator: navigationStackCoordinator, roomProxy: roomProxy, mediaProvider: MockMediaProvider(), userDiscoveryService: UserDiscoveryServiceMock())) @@ -322,8 +325,11 @@ class MockScreen: Identifiable { avatarURL: URL.picturesDirectory, isEncrypted: true, canonicalAlias: "#mock:room.org", - members: members)) - let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, + members: members, + memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false), + joinedMembersCount: members.count)) + let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", + navigationStackCoordinator: navigationStackCoordinator, roomProxy: roomProxy, mediaProvider: MockMediaProvider(), userDiscoveryService: UserDiscoveryServiceMock())) @@ -331,7 +337,8 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .roomDetailsScreenWithEmptyTopic: let navigationStackCoordinator = NavigationStackCoordinator() - let members: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockCharlie] + let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomTopic], canInviteUsers: false) + let members: [RoomMemberProxyMock] = [owner, .mockBob, .mockCharlie] let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", displayName: "Room", topic: nil, @@ -339,8 +346,11 @@ class MockScreen: Identifiable { isDirect: false, isEncrypted: true, canonicalAlias: "#mock:room.org", - members: members)) - let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, + members: members, + memberForID: owner, + joinedMembersCount: members.count)) + let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", + navigationStackCoordinator: navigationStackCoordinator, roomProxy: roomProxy, mediaProvider: MockMediaProvider(), userDiscoveryService: UserDiscoveryServiceMock())) @@ -348,12 +358,34 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .roomDetailsScreenWithInvite: let navigationStackCoordinator = NavigationStackCoordinator() - let members: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockCharlie] + let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [], canInviteUsers: true) + let members: [RoomMemberProxyMock] = [owner, .mockBob, .mockCharlie] let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", displayName: "Room", isEncrypted: true, - members: members)) - let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, + members: members, + memberForID: owner, + joinedMembersCount: members.count)) + let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", + navigationStackCoordinator: navigationStackCoordinator, + roomProxy: roomProxy, + mediaProvider: MockMediaProvider(), + userDiscoveryService: UserDiscoveryServiceMock())) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator + case .roomDetailsScreenDmDetails: + let navigationStackCoordinator = NavigationStackCoordinator() + let members: [RoomMemberProxyMock] = [.mockMe, .mockDan] + let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", + displayName: "Room", + topic: "test", + isDirect: true, + isEncrypted: true, + members: members, + memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false), + joinedMembersCount: members.count)) + let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", + navigationStackCoordinator: navigationStackCoordinator, roomProxy: roomProxy, mediaProvider: MockMediaProvider(), userDiscoveryService: UserDiscoveryServiceMock())) @@ -379,7 +411,7 @@ class MockScreen: Identifiable { let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie] let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, mediaProvider: MockMediaProvider(), - members: members)) + roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMembersListScreenPendingInvites: @@ -387,7 +419,7 @@ class MockScreen: Identifiable { let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, mediaProvider: MockMediaProvider(), - members: members)) + roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .reportContent: @@ -433,21 +465,6 @@ class MockScreen: Identifiable { let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider())) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator - case .roomDetailsScreenDmDetails: - let navigationStackCoordinator = NavigationStackCoordinator() - let members: [RoomMemberProxyMock] = [.mockMe, .mockDan] - let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", - displayName: "Room", - topic: "test", - isDirect: true, - isEncrypted: true, - members: members)) - let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, - roomProxy: roomProxy, - mediaProvider: MockMediaProvider(), - userDiscoveryService: UserDiscoveryServiceMock())) - navigationStackCoordinator.setRootCoordinator(coordinator) - return navigationStackCoordinator case .invitesWithBadges: ServiceLocator.shared.settings.seenInvites = Set([RoomSummary].mockInvites.dropFirst(1).compactMap(\.id)) let navigationStackCoordinator = NavigationStackCoordinator() @@ -487,7 +504,8 @@ class MockScreen: Identifiable { let mediaProvider = MockMediaProvider() let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([]) let members: [RoomMemberProxyMock] = id == .inviteUsersInRoomExistingMembers ? [.mockInvitedAlice, .mockBob] : [] - let roomType: InviteUsersScreenRoomType = id == .inviteUsers ? .draft : .room(members: members, userIndicatorController: UserIndicatorControllerMock.default) + let roomProxy = RoomProxyMock(with: .init(displayName: "test", members: members)) + let roomType: InviteUsersScreenRoomType = id == .inviteUsers ? .draft : .room(roomProxy: roomProxy) let coordinator = InviteUsersScreenCoordinator(parameters: .init(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: mediaProvider, userDiscoveryService: userDiscoveryMock)) coordinator.actions.sink { action in switch action { diff --git a/UnitTests/Sources/InviteUsersViewModelTests.swift b/UnitTests/Sources/InviteUsersViewModelTests.swift index be99b9ab9..8b30b6d4b 100644 --- a/UnitTests/Sources/InviteUsersViewModelTests.swift +++ b/UnitTests/Sources/InviteUsersViewModelTests.swift @@ -61,7 +61,7 @@ class InviteUsersScreenViewModelTests: XCTestCase { func testInviteButton() async { let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob] - setupWithRoomType(roomType: .room(members: mockedMembers, userIndicatorController: UserIndicatorControllerMock.default)) + setupWithRoomType(roomType: .room(roomProxy: RoomProxyMock(with: .init(displayName: "test", members: mockedMembers)))) _ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false }) context.send(viewAction: .toggleUser(.mockAlice)) @@ -86,7 +86,10 @@ class InviteUsersScreenViewModelTests: XCTestCase { userDiscoveryService.fetchSuggestionsReturnValue = .success([]) userDiscoveryService.searchProfilesWithReturnValue = .success([]) usersSubject.send([]) - let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService) + let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(), + roomType: roomType, mediaProvider: MockMediaProvider(), + userDiscoveryService: userDiscoveryService, + userIndicatorController: UserIndicatorControllerMock()) viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie]) self.viewModel = viewModel diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index 5db61ca75..b29f46b95 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -21,13 +21,13 @@ import XCTest @MainActor class RoomDetailsScreenViewModelTests: XCTestCase { - var viewModel: RoomDetailsScreenViewModelProtocol! + var viewModel: RoomDetailsScreenViewModel! var roomProxyMock: RoomProxyMock! var context: RoomDetailsScreenViewModelType.Context { viewModel.context } override func setUp() { - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test")) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) AppSettings.reset() } @@ -35,7 +35,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testLeaveRoomTappedWhenPublic() async { let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice] roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) await context.nextViewState() context.send(viewAction: .processTapLeave) XCTAssertEqual(context.leaveRoomAlertItem?.state, .public) @@ -45,7 +45,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testLeaveRoomTappedWhenRoomNotPublic() async { let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice] roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: false, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) await context.nextViewState() context.send(viewAction: .processTapLeave) XCTAssertEqual(context.leaveRoomAlertItem?.state, .private) @@ -88,8 +88,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testInitialDMDetailsState() async { let recipient = RoomMemberProxyMock.mockDan let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) await context.nextViewState() XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) } @@ -101,8 +101,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase { return .success(()) } let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) await context.nextViewState() XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) @@ -123,8 +123,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase { return .failure(.ignoreUserFailed) } let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) await context.nextViewState() XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) @@ -146,8 +146,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase { return .success(()) } let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) await context.nextViewState() XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) @@ -168,8 +168,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase { return .failure(.unignoreUserFailed) } let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) await context.nextViewState() XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) @@ -186,28 +186,32 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testCannotInvitePeople() async { let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", + isPublic: true, + members: mockedMembers, + memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false), + joinedMembersCount: mockedMembers.count)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) - await context.nextViewState() + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertFalse(context.viewState.canInviteUsers) } func testInvitePeople() async { let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers, joinedMembersCount: mockedMembers.count)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) - await context.nextViewState() + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertTrue(context.viewState.canInviteUsers) var callbackCorrectlyCalled = false viewModel.callback = { action in switch action { - case .requestInvitePeoplePresentation(let members): - callbackCorrectlyCalled = members.map(\.userID) == mockedMembers.map(\.userID) + case .requestInvitePeoplePresentation: + callbackCorrectlyCalled = true default: callbackCorrectlyCalled = false } @@ -219,16 +223,16 @@ class RoomDetailsScreenViewModelTests: XCTestCase { } func testRoomSubscription() async { - await context.nextViewState() XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1) } func testCanEditAvatar() async { - let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar]), .mockBob, .mockAlice] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomAvatar]) + let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice] + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) - _ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertTrue(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomName) @@ -237,11 +241,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase { } func testCanEditName() async { - let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomName]), .mockBob, .mockAlice] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomName]) + let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice] + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) - _ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertTrue(context.viewState.canEditRoomName) @@ -250,11 +255,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase { } func testCanEditTopic() async { - let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockAlice] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomTopic]) + let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice] + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) - _ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomName) @@ -263,11 +269,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase { } func testCannotEditRoom() async { - let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: []), .mockBob, .mockAlice] - roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: []) + let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice] + roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner)) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) - _ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomName) @@ -278,9 +285,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testCannotEditDirectRoom() async { let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice] roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isPublic: false, members: mockedMembers)) - viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) + viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) - _ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() XCTAssertFalse(context.viewState.canEdit) } diff --git a/UnitTests/Sources/RoomMembersListViewModelTests.swift b/UnitTests/Sources/RoomMembersListViewModelTests.swift index 12edc1e20..461f94cd6 100644 --- a/UnitTests/Sources/RoomMembersListViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListViewModelTests.swift @@ -75,6 +75,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase { } private func setup(with members: [RoomMemberProxyMock]) { - viewModel = .init(mediaProvider: MockMediaProvider(), members: members) + viewModel = .init(roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members, joinedMembersCount: members.filter { $0.membership == .join }.count)), + mediaProvider: MockMediaProvider()) } }