From 0941a1f056c153fcf1799ed758f6edb33b20ca85 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 6 Feb 2024 17:00:37 +0200 Subject: [PATCH] Fixes #2414 - Move member loading to the room member detail screen, avoid blocking the whole application --- .../Sources/Application/Application.swift | 1 + .../RoomFlowCoordinator.swift | 47 +++---- .../RoomMemberDetailsScreenCoordinator.swift | 14 +- .../RoomMemberDetailsScreenModels.swift | 5 +- .../RoomMemberDetailsScreenViewModel.swift | 67 ++++++++-- ...MemberDetailsScreenViewModelProtocol.swift | 1 + .../View/RoomMemberDetailsScreen.swift | 120 ++++++++++------- .../RoomMembersListScreenCoordinator.swift | 4 + .../RoomMembersListScreenViewModel.swift | 10 +- ...omMembersListScreenViewModelProtocol.swift | 2 + .../RoomScreen/RoomScreenCoordinator.swift | 6 +- .../RoomScreenInteractionHandler.swift | 32 +---- .../Screens/RoomScreen/RoomScreenModels.swift | 2 +- .../RoomScreen/RoomScreenViewModel.swift | 4 +- .../Sources/Services/Room/RoomProxy.swift | 4 + .../UITests/UITestsAppCoordinator.swift | 24 +++- ...-iPad-9th-generation.roomMemberDetails.png | 4 +- ...neration.roomMemberDetailsAccountOwner.png | 4 +- ...eneration.roomMemberDetailsIgnoredUser.png | 4 +- .../en-GB-iPhone-14.roomMemberDetails.png | 4 +- ...Phone-14.roomMemberDetailsAccountOwner.png | 4 +- ...iPhone-14.roomMemberDetailsIgnoredUser.png | 4 +- ...-iPad-9th-generation.roomMemberDetails.png | 4 +- ...neration.roomMemberDetailsAccountOwner.png | 4 +- ...eneration.roomMemberDetailsIgnoredUser.png | 4 +- .../pseudo-iPhone-14.roomMemberDetails.png | 4 +- ...Phone-14.roomMemberDetailsAccountOwner.png | 4 +- ...iPhone-14.roomMemberDetailsIgnoredUser.png | 4 +- .../RoomMemberDetailsViewModelTests.swift | 66 ++++++--- .../Sources/RoomScreenViewModelTests.swift | 125 ------------------ changelog.d/2414.change | 1 + 31 files changed, 278 insertions(+), 305 deletions(-) create mode 100644 changelog.d/2414.change diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift index 5267a8776f..b7db31a3c9 100644 --- a/ElementX/Sources/Application/Application.swift +++ b/ElementX/Sources/Application/Application.swift @@ -31,6 +31,7 @@ struct Application: App { } else { appCoordinator = AppCoordinator(appDelegate: appDelegate) } + SceneDelegate.windowManager = appCoordinator.windowManager } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 16b7f64c86..449bc6e8cf 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -109,16 +109,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case .roomList: stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated)) case .roomMemberDetails(let userID): - Task { - switch await roomProxy?.getMember(userID: userID) { - case .success(let member): - stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member))) - case .failure(let error): - MXLog.error("[RoomFlowCoordinator] Failed to get member: \(error)") - case .none: - MXLog.error("[RoomFlowCoordinator] Failed to get member: RoomProxy is nil") - } - } + stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID)) case .genericCallLink, .oidcCallback, .settings, .chatBackupSettings: break } @@ -173,10 +164,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.roomMembersList(let roomID), .dismissRoomMembersList): return .roomDetails(roomID: roomID, isRoot: false) - case (.room(let roomID), .presentRoomMemberDetails(let member)): - return .roomMemberDetails(roomID: roomID, member: member, fromRoomMembersList: false) - case (.roomMembersList(let roomID), .presentRoomMemberDetails(let member)): - return .roomMemberDetails(roomID: roomID, member: member, fromRoomMembersList: true) + case (.room(let roomID), .presentRoomMemberDetails(userID: let userID)): + return .roomMemberDetails(roomID: roomID, userID: userID, fromRoomMembersList: false) + case (.roomMembersList(let roomID), .presentRoomMemberDetails(userID: let userID)): + return .roomMemberDetails(roomID: roomID, userID: userID, fromRoomMembersList: true) case (.roomMemberDetails(let roomID, _, let fromRoomMembersList), .dismissRoomMemberDetails): return fromRoomMembersList ? .roomMembersList(roomID: roomID) : .room(roomID: roomID) @@ -285,13 +276,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.roomMembersList, .dismissRoomMembersList, .roomDetails): break - case (.room, .presentRoomMemberDetails, .roomMemberDetails(_, let member, _)): - presentRoomMemberDetails(member: member.value) + case (.room, .presentRoomMemberDetails, .roomMemberDetails(_, let userID, _)): + presentRoomMemberDetails(userID: userID) case (.roomMemberDetails, .dismissRoomMemberDetails, .room): break - case (.roomMembersList, .presentRoomMemberDetails, .roomMemberDetails(_, let member, _)): - presentRoomMemberDetails(member: member.value) + case (.roomMembersList, .presentRoomMemberDetails, .roomMemberDetails(_, let userID, _)): + presentRoomMemberDetails(userID: userID) case (.roomMemberDetails, .dismissRoomMemberDetails, .roomMembersList): break @@ -469,8 +460,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentPollForm(mode: mode)) case .presentLocationViewer(_, let geoURI, let description): stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI, description: description))) - case .presentRoomMemberDetails(member: let member): - stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member))) + case .presentRoomMemberDetails(userID: let userID): + stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID)) case .presentMessageForwarding(let itemID): stateMachine.tryEvent(.presentMessageForwarding(itemID: itemID)) case .presentCallScreen: @@ -598,7 +589,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case .invite: stateMachine.tryEvent(.presentInviteUsersScreen) case .selectedMember(let member): - stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member))) + stateMachine.tryEvent(.presentRoomMemberDetails(userID: member.userID)) } } .store(in: &cancellables) @@ -931,13 +922,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentRoomMemberDetails(member: RoomMemberProxyProtocol) { + private func presentRoomMemberDetails(userID: String) { guard let roomProxy else { fatalError() } let params = RoomMemberDetailsScreenCoordinatorParameters(roomProxy: roomProxy, - roomMemberProxy: member, + userID: userID, mediaProvider: userSession.mediaProvider, userIndicatorController: userIndicatorController) let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params) @@ -945,7 +936,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { coordinator.actions.sink { [weak self] action in guard let self else { return } switch action { - case .openDirectChat: + case .openDirectChat(let displayName): let loadingIndicatorIdentifier = "OpenDirectChatLoadingIndicator" userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, @@ -956,12 +947,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { Task { [weak self] in guard let self else { return } - let currentDirectRoom = await userSession.clientProxy.directRoomForUserID(member.userID) + let currentDirectRoom = await userSession.clientProxy.directRoomForUserID(userID) switch currentDirectRoom { case .success(.some(let roomID)): stateMachine.tryEvent(.presentRoom(roomID: roomID)) case .success(nil): - switch await userSession.clientProxy.createDirectRoom(with: member.userID, expectedRoomName: member.displayName) { + switch await userSession.clientProxy.createDirectRoom(with: userID, expectedRoomName: displayName) { case .success(let roomID): analytics.trackCreatedRoom(isDM: true) stateMachine.tryEvent(.presentRoom(roomID: roomID)) @@ -1185,7 +1176,7 @@ private extension RoomFlowCoordinator { case notificationSettings(roomID: String) case globalNotificationSettings(roomID: String) case roomMembersList(roomID: String) - case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper, fromRoomMembersList: Bool) + case roomMemberDetails(roomID: String, userID: String, fromRoomMembersList: Bool) case inviteUsersScreen(roomID: String, fromRoomMembersList: Bool) case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource) case mediaUploadPreview(roomID: String, fileURL: URL) @@ -1225,7 +1216,7 @@ private extension RoomFlowCoordinator { case presentRoomMembersList case dismissRoomMembersList - case presentRoomMemberDetails(member: HashableRoomMemberWrapper) + case presentRoomMemberDetails(userID: String) case dismissRoomMemberDetails case presentInviteUsersScreen diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift index 53daccae4a..d71a4f8b23 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift @@ -19,13 +19,13 @@ import SwiftUI struct RoomMemberDetailsScreenCoordinatorParameters { let roomProxy: RoomProxyProtocol - let roomMemberProxy: RoomMemberProxyProtocol + let userID: String let mediaProvider: MediaProviderProtocol let userIndicatorController: UserIndicatorControllerProtocol } enum RoomMemberDetailsScreenCoordinatorAction { - case openDirectChat + case openDirectChat(displayName: String?) } final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol { @@ -40,7 +40,7 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol { init(parameters: RoomMemberDetailsScreenCoordinatorParameters) { viewModel = RoomMemberDetailsScreenViewModel(roomProxy: parameters.roomProxy, - roomMemberProxy: parameters.roomMemberProxy, + userID: parameters.userID, mediaProvider: parameters.mediaProvider, userIndicatorController: parameters.userIndicatorController) } @@ -50,14 +50,16 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .openDirectChat: - actionsSubject.send(.openDirectChat) + case .openDirectChat(let displayName): + actionsSubject.send(.openDirectChat(displayName: displayName)) } } .store(in: &cancellables) } - func stop() { viewModel.stop() } + func stop() { + viewModel.stop() + } func toPresentable() -> AnyView { AnyView(RoomMemberDetailsScreen(context: viewModel.context)) diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift index fa24dcca1e..7ac63fd716 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift @@ -17,11 +17,12 @@ import Foundation enum RoomMemberDetailsScreenViewModelAction { - case openDirectChat + case openDirectChat(displayName: String?) } struct RoomMemberDetailsScreenViewState: BindableState { - var details: RoomMemberDetails + let userID: String + var memberDetails: RoomMemberDetails? var isProcessingIgnoreRequest = false var bindings: RoomMemberDetailsScreenViewStateBindings diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 7043c729f4..971340c2d8 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -21,29 +21,46 @@ typealias RoomMemberDetailsScreenViewModelType = StateStoreViewModel = .init() + private var roomMemberProxy: RoomMemberProxyProtocol? + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } init(roomProxy: RoomProxyProtocol, - roomMemberProxy: RoomMemberProxyProtocol, + userID: String, mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.roomProxy = roomProxy - self.roomMemberProxy = roomMemberProxy + self.userID = userID self.mediaProvider = mediaProvider self.userIndicatorController = userIndicatorController - let initialViewState = RoomMemberDetailsScreenViewState(details: RoomMemberDetails(withProxy: roomMemberProxy), - bindings: .init()) + let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, bindings: .init()) super.init(initialViewState: initialViewState, imageProvider: mediaProvider) + + showMemberLoadingIndicator() + Task { + defer { + hideMemberLoadingIndicator() + } + + switch await roomProxy.getMember(userID: userID) { + case .success(let member): + roomMemberProxy = member + state.memberDetails = RoomMemberDetails(withProxy: member) + case .failure(let error): + state.bindings.alertInfo = .init(id: .unknown) + MXLog.error("[RoomFlowCoordinator] Failed to get member: \(error)") + } + } } // MARK: - Public @@ -51,6 +68,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro func stop() { // Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier. state.bindings.mediaPreviewItem = nil + + hideMemberLoadingIndicator() } override func process(viewAction: RoomMemberDetailsScreenViewAction) { @@ -66,7 +85,11 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro case .displayAvatar: displayFullScreenAvatar() case .openDirectChat: - actionsSubject.send(.openDirectChat) + guard let roomMemberProxy else { + fatalError() + } + + actionsSubject.send(.openDirectChat(displayName: roomMemberProxy.displayName)) } } @@ -74,12 +97,16 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro @MainActor private func ignoreUser() async { + guard let roomMemberProxy else { + fatalError() + } + state.isProcessingIgnoreRequest = true let result = await roomMemberProxy.ignoreUser() state.isProcessingIgnoreRequest = false switch result { case .success: - state.details.isIgnored = true + state.memberDetails?.isIgnored = true updateMembers() case .failure: state.bindings.alertInfo = .init(id: .unknown) @@ -88,12 +115,16 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro @MainActor private func unignoreUser() async { + guard let roomMemberProxy else { + fatalError() + } + state.isProcessingIgnoreRequest = true let result = await roomMemberProxy.unignoreUser() state.isProcessingIgnoreRequest = false switch result { case .success: - state.details.isIgnored = false + state.memberDetails?.isIgnored = false updateMembers() case .failure: state.bindings.alertInfo = .init(id: .unknown) @@ -107,6 +138,10 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro } private func displayFullScreenAvatar() { + guard let roomMemberProxy else { + fatalError() + } + guard let avatarURL = roomMemberProxy.avatarURL else { return } @@ -125,4 +160,20 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro } } } + + // MARK: Loading indicator + + private static let loadingIndicatorIdentifier = "RoomMemberLoading" + + private func showMemberLoadingIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true), + title: L10n.commonLoading, + persistent: true), + delay: .milliseconds(100)) + } + + private func hideMemberLoadingIndicator() { + userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift index 56a58c9006..e7ee561a61 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift @@ -20,5 +20,6 @@ import Combine protocol RoomMemberDetailsScreenViewModelProtocol { var actions: AnyPublisher { get } var context: RoomMemberDetailsScreenViewModelType.Context { get } + func stop() } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift index a18baef0ad..3231365167 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift @@ -21,42 +21,58 @@ struct RoomMemberDetailsScreen: View { @ObservedObject var context: RoomMemberDetailsScreenViewModel.Context var body: some View { + content + .compoundList() + .alert(item: $context.ignoreUserAlert, actions: blockUserAlertActions, message: blockUserAlertMessage) + .alert(item: $context.alertInfo) + .track(screen: .User) + .interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true) + } + + // MARK: - Private + + @ViewBuilder + private var content: some View { Form { headerSection - - if !context.viewState.details.isAccountOwner { + + if let memberDetails = context.viewState.memberDetails, + !memberDetails.isAccountOwner { directChatSection blockUserSection } } - .compoundList() - .alert(item: $context.ignoreUserAlert, actions: blockUserAlertActions, message: blockUserAlertMessage) - .alert(item: $context.alertInfo) - .track(screen: .User) - .interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true) } - // MARK: - Private - @ViewBuilder private var headerSection: some View { - AvatarHeaderView(avatarUrl: context.viewState.details.avatarURL, - name: context.viewState.details.name, - id: context.viewState.details.id, - avatarSize: .user(on: .memberDetails), - imageProvider: context.imageProvider, - subtitle: context.viewState.details.id) { - context.send(viewAction: .displayAvatar) - } footer: { - if let permalink = context.viewState.details.permalink { - HStack(spacing: 32) { - ShareLink(item: permalink) { - CompoundIcon(\.shareIos) + if let memberDetails = context.viewState.memberDetails { + AvatarHeaderView(avatarUrl: memberDetails.avatarURL, + name: memberDetails.name, + id: memberDetails.id, + avatarSize: .user(on: .memberDetails), + imageProvider: context.imageProvider, + subtitle: memberDetails.id) { + context.send(viewAction: .displayAvatar) + } footer: { + if let permalink = memberDetails.permalink { + HStack(spacing: 32) { + ShareLink(item: permalink) { + CompoundIcon(\.shareIos) + } + .buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) } - .buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) + .padding(.top, 32) } - .padding(.top, 32) } + } else { + AvatarHeaderView(avatarUrl: nil, + name: nil, + id: context.viewState.userID, + avatarSize: .user(on: .memberDetails), + imageProvider: context.imageProvider, + subtitle: nil, + footer: { }) } } @@ -71,32 +87,27 @@ struct RoomMemberDetailsScreen: View { } } + @ViewBuilder private var blockUserSection: some View { - Section { - ListRow(label: .default(title: blockUserButtonTitle, - icon: \.block, - role: context.viewState.details.isIgnored ? nil : .destructive), - details: .isWaiting(context.viewState.isProcessingIgnoreRequest), - kind: .button { - context.send(viewAction: blockUserButtonAction) - }) - .accessibilityIdentifier(blockUserButtonAccessibilityIdentifier) - .disabled(context.viewState.isProcessingIgnoreRequest) + if let memberDetails = context.viewState.memberDetails { + let title = memberDetails.isIgnored ? L10n.screenRoomMemberDetailsUnblockUser : L10n.screenRoomMemberDetailsBlockUser + let action: RoomMemberDetailsScreenViewAction = memberDetails.isIgnored ? .showUnignoreAlert : .showIgnoreAlert + let accessibilityIdentifier = memberDetails.isIgnored ? A11yIdentifiers.roomMemberDetailsScreen.unignore : A11yIdentifiers.roomMemberDetailsScreen.ignore + + Section { + ListRow(label: .default(title: title, + icon: \.block, + role: memberDetails.isIgnored ? nil : .destructive), + details: .isWaiting(context.viewState.isProcessingIgnoreRequest), + kind: .button { + context.send(viewAction: action) + }) + .accessibilityIdentifier(accessibilityIdentifier) + .disabled(context.viewState.isProcessingIgnoreRequest) + } } } - private var blockUserButtonAction: RoomMemberDetailsScreenViewAction { - context.viewState.details.isIgnored ? .showUnignoreAlert : .showIgnoreAlert - } - - private var blockUserButtonTitle: String { - context.viewState.details.isIgnored ? L10n.screenRoomMemberDetailsUnblockUser : L10n.screenRoomMemberDetailsBlockUser - } - - private var blockUserButtonAccessibilityIdentifier: String { - context.viewState.details.isIgnored ? A11yIdentifiers.roomMemberDetailsScreen.unignore : A11yIdentifiers.roomMemberDetailsScreen.ignore - } - @ViewBuilder private func blockUserAlertActions(_ item: RoomMemberDetailsScreenViewStateBindings.IgnoreUserAlertItem) -> some View { Button(item.cancelTitle, role: .cancel) { } @@ -114,27 +125,35 @@ struct RoomMemberDetailsScreen: View { // MARK: - Previews struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { - static let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) static let otherUserViewModel = { let member = RoomMemberProxyMock.mockDan + let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + roomProxyMock.getMemberUserIDReturnValue = .success(member) + return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: member, + userID: member.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) }() static let accountOwnerViewModel = { let member = RoomMemberProxyMock.mockMe + let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + roomProxyMock.getMemberUserIDReturnValue = .success(member) + return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: member, + userID: member.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) }() static let ignoredUserViewModel = { let member = RoomMemberProxyMock.mockIgnored + let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + roomProxyMock.getMemberUserIDReturnValue = .success(member) + return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: member, + userID: member.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) }() @@ -142,9 +161,12 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { static var previews: some View { RoomMemberDetailsScreen(context: otherUserViewModel.context) .previewDisplayName("Other User") + .snapshot(delay: 0.25) RoomMemberDetailsScreen(context: accountOwnerViewModel.context) .previewDisplayName("Account Owner") + .snapshot(delay: 0.25) RoomMemberDetailsScreen(context: ignoredUserViewModel.context) .previewDisplayName("Ignored User") + .snapshot(delay: 0.25) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift index 9ef48688e9..0d00b024eb 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift @@ -57,6 +57,10 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } + + func stop() { + viewModel.stop() + } func toPresentable() -> AnyView { AnyView(RoomMembersListScreen(context: viewModel.context)) diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index 76ca09f403..f42a3f53d3 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -58,6 +58,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe } } + func stop() { + hideLoader() + } + // MARK: - Private private func setupMembers() { @@ -120,7 +124,11 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe private let userIndicatorID = UUID().uuidString private func showLoader() { - userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200)) + userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true), + title: L10n.commonLoading, + persistent: true), + delay: .milliseconds(200)) } private func hideLoader() { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift index 27eb47a768..76107e37e4 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift @@ -20,4 +20,6 @@ import Combine protocol RoomMembersListScreenViewModelProtocol { var actions: AnyPublisher { get } var context: RoomMembersListScreenViewModelType.Context { get } + + func stop() } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 476af9a216..9eb380b9bf 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -39,7 +39,7 @@ enum RoomScreenCoordinatorAction { case presentPollForm(mode: PollFormMode) case presentLocationViewer(body: String, geoURI: GeoURI, description: String?) case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) - case presentRoomMemberDetails(member: RoomMemberProxyProtocol) + case presentRoomMemberDetails(userID: String) case presentMessageForwarding(itemID: TimelineItemIdentifier) case presentCallScreen } @@ -105,8 +105,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentPollForm(mode: mode)) case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.presentMediaUploadPreviewScreen(url)) - case .displayRoomMemberDetails(let member): - actionsSubject.send(.presentRoomMemberDetails(member: member)) + case .displayRoomMemberDetails(userID: let userID): + actionsSubject.send(.presentRoomMemberDetails(userID: userID)) case .displayMessageForwarding(let itemID): actionsSubject.send(.presentMessageForwarding(itemID: itemID)) case .displayLocation(let body, let geoURI, let description): diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift index e3a43a6523..6e7153f095 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift @@ -25,7 +25,7 @@ enum RoomScreenInteractionHandlerAction { case displayMessageForwarding(itemID: TimelineItemIdentifier) case displayMediaUploadPreviewScreen(url: URL) case displayPollForm(mode: PollFormMode) - case displayRoomMemberDetails(member: RoomMemberProxyProtocol) + case displayRoomMemberDetails(userID: String) case showActionMenu(TimelineItemActionMenuInfo) case showDebugInfo(TimelineItemDebugInfo) case showConfirmationAlert(AlertInfo) @@ -579,19 +579,7 @@ class RoomScreenInteractionHandler { } func handleTappedUser(userID: String) async { - // This is generally fast but it could take some time for rooms with thousands of users on first load - // Show a loader only if it takes more than 0.1 seconds - showLoadingIndicator(with: .milliseconds(100)) - let result = await roomProxy.getMember(userID: userID) - hideLoadingIndicator() - - switch result { - case .success(let member): - actionsSubject.send(.displayRoomMemberDetails(member: member)) - case .failure(let error): - actionsSubject.send(.displayError(.alert(L10n.screenRoomErrorFailedRetrievingUserDetails))) - MXLog.error("Failed retrieving the user given the following id \(userID) with error: \(error)") - } + actionsSubject.send(.displayRoomMemberDetails(userID: userID)) } func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction { @@ -660,22 +648,6 @@ class RoomScreenInteractionHandler { return .none } } - - // MARK: User indicators - - private static let loadingIndicatorIdentifier = "RoomScreenLoadingIndicator" - - private func showLoadingIndicator(with delay: Duration) { - userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, - type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), - title: L10n.commonLoading, - persistent: true), - delay: delay) - } - - private func hideLoadingIndicator() { - userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) - } } private struct ReplyInfo { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index a0d3cd4ad7..be343d1332 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -30,7 +30,7 @@ enum RoomScreenViewModelAction { case displayLocationPicker case displayPollForm(mode: PollFormMode) case displayMediaUploadPreviewScreen(url: URL) - case displayRoomMemberDetails(member: RoomMemberProxyProtocol) + case displayRoomMemberDetails(userID: String) case displayMessageForwarding(itemID: TimelineItemIdentifier) case displayLocation(body: String, geoURI: GeoURI, description: String?) case composer(action: RoomScreenComposerAction) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 503309e987..3d05fcd2b5 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -337,8 +337,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.displayReportContent(itemID: itemID, senderID: senderID)) case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.displayMediaUploadPreviewScreen(url: url)) - case .displayRoomMemberDetails(let member): - actionsSubject.send(.displayRoomMemberDetails(member: member)) + case .displayRoomMemberDetails(userID: let userID): + actionsSubject.send(.displayRoomMemberDetails(userID: userID)) case .showActionMenu(let actionMenuInfo): state.bindings.actionMenuInfo = actionMenuInfo case .showDebugInfo(let debugInfo): diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 7f20be67dc..21c5957202 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -203,6 +203,10 @@ class RoomProxy: RoomProxyProtocol { } func getMember(userID: String) async -> Result { + if let member = members.value.filter({ $0.userID == userID }).first { + return .success(member) + } + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) defer { sendMessageBackgroundTask?.stop() diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index f1055437e5..7cbeaacb27 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -765,25 +765,37 @@ class MockScreen: Identifiable { navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMemberDetailsAccountOwner: + let member = RoomMemberProxyMock.mockMe + let roomProxy = RoomProxyMock(with: .init(displayName: "")) + roomProxy.getMemberUserIDReturnValue = .success(member) + let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), - roomMemberProxy: RoomMemberProxyMock.mockMe, + let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: roomProxy, + userID: member.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMemberDetails: + let member = RoomMemberProxyMock.mockAlice + let roomProxy = RoomProxyMock(with: .init(displayName: "")) + roomProxy.getMemberUserIDReturnValue = .success(member) + let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), - roomMemberProxy: RoomMemberProxyMock.mockAlice, + let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: roomProxy, + userID: member.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMemberDetailsIgnoredUser: + let member = RoomMemberProxyMock.mockIgnored + let roomProxy = RoomProxyMock(with: .init(displayName: "")) + roomProxy.getMemberUserIDReturnValue = .success(member) + let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), - roomMemberProxy: RoomMemberProxyMock.mockIgnored, + let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: roomProxy, + userID: member.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetails.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetails.png index 2a303682d1..b8281c762e 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetails.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7796428efb06e0bdc519e8ccd75ed2bae0d26eb2ed1a75fee143b620aa05ac2f -size 80998 +oid sha256:fc1d94cd94c0ffa482707042e08970b9ba280804d0a7062c5674df3767cd6fbf +size 81052 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsAccountOwner.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsAccountOwner.png index 2435587674..12e519c766 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsAccountOwner.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsAccountOwner.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fe0377786a0837bfce257531b9a25eb6c9249aaa244a0ce46ef8e0a51c156a1 -size 86009 +oid sha256:5a853c9add8c1ca60541024ff68797c8eb0bb8ced14aa4f30a91206c1af0e745 +size 85853 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsIgnoredUser.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsIgnoredUser.png index 0422c86601..99a16551aa 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsIgnoredUser.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsIgnoredUser.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8493f5a8c052742c71503b4961fa79d6dab9f15204fff5e031fac20b29d4c8d2 -size 82132 +oid sha256:e376247a6481f076c86a4d3f479fcf47fb9e4431a12a7979270dfdd285ef64f6 +size 82304 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetails.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetails.png index d27d8d93c4..79e8b9b64f 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetails.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ad2483488656f627d796a513fdfc73e6e95234532a9e3991071398b0a6d877c -size 93983 +oid sha256:4240cb1086b7b5eec729f998bebee2a538dbb66b0cabfe106de35282846c2f06 +size 94162 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsAccountOwner.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsAccountOwner.png index c2a133f119..867b305328 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsAccountOwner.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsAccountOwner.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b8463d12166442fa4b38588efff9fe3104b3315a9b8d6491d74ee4a1282903b -size 109456 +oid sha256:4a8e373a160e53b4a1b47acb4c0f7e6f392256eb1ed60b3a39801e7e3ed0e070 +size 109543 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsIgnoredUser.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsIgnoredUser.png index 40c29c8146..5b36e827ef 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsIgnoredUser.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsIgnoredUser.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd3aa1cf99160b3dc6444cbda7880fd82a72e8f1fb37c9924be6b332386dc5cb -size 95708 +oid sha256:b40b1dee358f4beb14919d10e914603de5a2c9ed3155c4915130ffa8fbd3b0e3 +size 95956 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetails.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetails.png index 4bcece20fb..bf85fdfb03 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetails.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b576b859c7623d04d690e07db1497a3ef96bdb991f99b3d7491b92416e509914 -size 82531 +oid sha256:36a8cc8330733bf7759f9c59f409a483c65b4baac0e42e25cfe817a1d8f53de5 +size 82610 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsAccountOwner.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsAccountOwner.png index 53c1272f02..bcca39d009 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsAccountOwner.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsAccountOwner.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4f66540153d511f73751417230261ed0ab3045125793ca62f70f7f771020fe7 -size 86618 +oid sha256:a0b91a069211430da0d3c23668dd41fc3d152b167d2c9d7dc2423b018046defd +size 86466 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsIgnoredUser.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsIgnoredUser.png index 1983bae8fe..4c77a6a35b 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsIgnoredUser.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomMemberDetailsIgnoredUser.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a766ae738267e9fb0b82e452abd9ee75a8932ff9e804ce51e5cbcd4ff296929 -size 83794 +oid sha256:9247769c091d629b96306e1b3df84065334c79bea019a5a309b984c6967e57b6 +size 83971 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetails.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetails.png index 00df94b263..2075b1c8d8 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetails.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8eddfe670a05acc38693d421d56c7e4ca2648b6688b2abbb12fe6b67c5b89a58 -size 96045 +oid sha256:6e34ac88160a5e648f9be27ed3b6f86fdd57cd5f586d15cdf5851b67faad94d3 +size 96222 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsAccountOwner.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsAccountOwner.png index bdb83527b5..a9ada13a73 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsAccountOwner.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsAccountOwner.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6393286dae6a498c765162b129af1b9e6e65fe67475ab4c3a93473f40ab6ca2f -size 109818 +oid sha256:0c1408a1910c6857201570c86bfbc40358ac9dc153eeee8ceeec313392f9c8e2 +size 109902 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsIgnoredUser.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsIgnoredUser.png index 38baa92dbe..ace3939850 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsIgnoredUser.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomMemberDetailsIgnoredUser.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e976d63cbc1b01d5262859c81d3c267bbdc10a434a27547a17ac3e282f859044 -size 97559 +oid sha256:47cfee933d2249d93f759bf5fee05439d0ed08e08c7c968c90225ac712f9b28c +size 97819 diff --git a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift index 0e2a04122a..f10a6e57d4 100644 --- a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift @@ -27,16 +27,23 @@ class RoomMemberDetailsViewModelTests: XCTestCase { override func setUp() async throws { roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + + roomProxyMock.getMemberUserIDClosure = { _ in + .success(self.roomMemberProxyMock) + } } - func testInitialState() async { + func testInitialState() async throws { roomMemberProxyMock = RoomMemberProxyMock.mockAlice viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: roomMemberProxyMock, + userID: roomMemberProxyMock.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } + try await waitForMemberToLoad.fulfill() - XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock)) + XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock)) XCTAssertNil(context.ignoreUserAlert) XCTAssertNil(context.alertInfo) } @@ -48,9 +55,12 @@ class RoomMemberDetailsViewModelTests: XCTestCase { return .success(()) } viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: roomMemberProxyMock, + userID: roomMemberProxyMock.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } + try await waitForMemberToLoad.fulfill() context.send(viewAction: .showIgnoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) @@ -58,13 +68,13 @@ class RoomMemberDetailsViewModelTests: XCTestCase { context.send(viewAction: .ignoreConfirmed) let deferred = deferFulfillment(context.$viewState) { state in - state.details.isIgnored + state.memberDetails?.isIgnored == true } try await deferred.fulfill() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) - XCTAssertTrue(context.viewState.details.isIgnored) + XCTAssertTrue(context.viewState.memberDetails?.isIgnored ?? false) try await Task.sleep(for: .milliseconds(100)) XCTAssertTrue(roomProxyMock.updateMembersCalled) } @@ -76,9 +86,13 @@ class RoomMemberDetailsViewModelTests: XCTestCase { return .failure(.ignoreUserFailed) } viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: roomMemberProxyMock, + userID: roomMemberProxyMock.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } + try await waitForMemberToLoad.fulfill() + context.send(viewAction: .showIgnoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) @@ -91,7 +105,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { try await deferred.fulfill() XCTAssertNotNil(context.alertInfo) - XCTAssertFalse(context.viewState.details.isIgnored) + XCTAssertFalse(context.viewState.memberDetails?.isIgnored ?? false) try await Task.sleep(for: .milliseconds(100)) XCTAssertFalse(roomProxyMock.updateMembersCalled) } @@ -102,10 +116,14 @@ class RoomMemberDetailsViewModelTests: XCTestCase { try? await Task.sleep(for: .milliseconds(100)) return .success(()) } + viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: roomMemberProxyMock, + userID: roomMemberProxyMock.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } + try await waitForMemberToLoad.fulfill() context.send(viewAction: .showUnignoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) @@ -113,27 +131,29 @@ class RoomMemberDetailsViewModelTests: XCTestCase { context.send(viewAction: .unignoreConfirmed) let deferred = deferFulfillment(context.$viewState) { state in - state.details.isIgnored == false + state.memberDetails?.isIgnored == false } try await deferred.fulfill() - XCTAssertFalse(context.viewState.details.isIgnored) + XCTAssertFalse(context.viewState.memberDetails?.isIgnored ?? false) try await Task.sleep(for: .milliseconds(100)) XCTAssertTrue(roomProxyMock.updateMembersCalled) } func testUnignoreFailure() async throws { - roomProxyMock = RoomProxyMock(with: .init(displayName: "")) roomMemberProxyMock = RoomMemberProxyMock.mockIgnored roomMemberProxyMock.unignoreUserClosure = { try? await Task.sleep(for: .milliseconds(100)) return .failure(.unignoreUserFailed) } viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: roomMemberProxyMock, + userID: roomMemberProxyMock.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } + try await waitForMemberToLoad.fulfill() context.send(viewAction: .showUnignoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) @@ -146,32 +166,38 @@ class RoomMemberDetailsViewModelTests: XCTestCase { try await deferred.fulfill() - XCTAssertTrue(context.viewState.details.isIgnored) + XCTAssertTrue(context.viewState.memberDetails?.isIgnored ?? false) XCTAssertNotNil(context.alertInfo) try await Task.sleep(for: .milliseconds(100)) XCTAssertFalse(roomProxyMock.updateMembersCalled) } - func testInitialStateAccountOwner() async { + func testInitialStateAccountOwner() async throws { roomMemberProxyMock = RoomMemberProxyMock.mockMe viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: roomMemberProxyMock, + userID: roomMemberProxyMock.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } + try await waitForMemberToLoad.fulfill() - XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock)) + XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock)) XCTAssertNil(context.ignoreUserAlert) XCTAssertNil(context.alertInfo) } - func testInitialStateIgnoredUser() async { + func testInitialStateIgnoredUser() async throws { roomMemberProxyMock = RoomMemberProxyMock.mockIgnored viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, - roomMemberProxy: roomMemberProxyMock, + userID: roomMemberProxyMock.userID, mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil } + try await waitForMemberToLoad.fulfill() - XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock)) + XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock)) XCTAssertNil(context.ignoreUserAlert) XCTAssertNil(context.alertInfo) } diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index f6e94e7792..158e2ae787 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -203,131 +203,6 @@ class RoomScreenViewModelTests: XCTestCase { XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.") } - // MARK: - User Details - - func testGoToUserDetailsSuccessNoDelay() async { - // Setup - let expectation = expectation(description: #function) - let timelineController = MockRoomTimelineController() - let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) - let roomMemberMock = RoomMemberProxyMock() - roomMemberMock.userID = "bob" - roomProxyMock.getMemberUserIDReturnValue = .success(roomMemberMock) - - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, - timelineController: timelineController, - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: userIndicatorControllerMock, - application: ApplicationMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics, - notificationCenter: NotificationCenterMock()) - viewModel.actions - .sink { action in - switch action { - case .displayRoomMemberDetails(let member): - XCTAssert(member === roomMemberMock) - default: - XCTFail("Did not received the expected action") - } - expectation.fulfill() - } - .store(in: &cancellables) - - // Test - viewModel.context.send(viewAction: .tappedOnUser(userID: "bob")) - await fulfillment(of: [expectation]) - XCTAssert(userIndicatorControllerMock.submitIndicatorDelayCallsCount == 1) - XCTAssert(roomProxyMock.getMemberUserIDCallsCount == 1) - XCTAssertEqual(roomProxyMock.getMemberUserIDReceivedUserID, "bob") - } - - func testGoToUserDetailsSuccessWithDelay() async { - // Setup - let timelineController = MockRoomTimelineController() - let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) - let roomMemberMock = RoomMemberProxyMock() - roomMemberMock.userID = "bob" - let expectation = XCTestExpectation(description: "Go to user details") - - roomProxyMock.getMemberUserIDClosure = { _ in - .success(roomMemberMock) - } - - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, - timelineController: timelineController, - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: userIndicatorControllerMock, - application: ApplicationMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics, - notificationCenter: NotificationCenterMock()) - - viewModel.actions - .sink { action in - switch action { - case .displayRoomMemberDetails(let member): - XCTAssert(member === roomMemberMock) - expectation.fulfill() - default: - XCTFail("Did not received the expected action") - } - } - .store(in: &cancellables) - - // Test - viewModel.context.send(viewAction: .tappedOnUser(userID: "bob")) - await fulfillment(of: [expectation]) - - XCTAssert(userIndicatorControllerMock.submitIndicatorDelayCallsCount == 1) - XCTAssert(userIndicatorControllerMock.retractIndicatorWithIdCallsCount == 1) - XCTAssert(roomProxyMock.getMemberUserIDCallsCount == 1) - XCTAssertEqual(roomProxyMock.getMemberUserIDReceivedUserID, "bob") - } - - func testGoToUserDetailsFailure() async throws { - // Setup - let timelineController = MockRoomTimelineController() - let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) - let roomMemberMock = RoomMemberProxyMock() - roomMemberMock.userID = "bob" - roomProxyMock.getMemberUserIDClosure = { _ in - .failure(.failedRetrievingMember) - } - - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, - timelineController: timelineController, - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: userIndicatorControllerMock, - application: ApplicationMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics, - notificationCenter: NotificationCenterMock()) - viewModel.actions - .sink { _ in - XCTFail("Should not receive any action") - } - .store(in: &cancellables) - - let deferred = deferFulfillment(viewModel.context.$viewState) { value in - value.bindings.alertInfo != nil - } - - viewModel.context.send(viewAction: .tappedOnUser(userID: "bob")) - - try await deferred.fulfill() - - XCTAssertFalse(viewModel.state.bindings.alertInfo.isNil) - XCTAssert(roomProxyMock.getMemberUserIDCallsCount == 1) - XCTAssertEqual(roomProxyMock.getMemberUserIDReceivedUserID, "bob") - } - // MARK: - Sending func testRetrySend() async throws { diff --git a/changelog.d/2414.change b/changelog.d/2414.change new file mode 100644 index 0000000000..cce57c0c58 --- /dev/null +++ b/changelog.d/2414.change @@ -0,0 +1 @@ +Move member loading to the room member detail screen, avoid blocking the whole application \ No newline at end of file