From 0bbdb05220c453ecdd4c2037e84c3a40f5f090c5 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 14 Aug 2024 18:03:46 +0200 Subject: [PATCH] RoomScreenViewModel refactor part 2 (#3169) --- .../RoomScreen/RoomScreenCoordinator.swift | 14 +++-- .../Screens/RoomScreen/RoomScreenModels.swift | 10 ++++ .../RoomScreen/RoomScreenViewModel.swift | 46 ++++++++++++--- .../Screens/RoomScreen/View/RoomScreen.swift | 25 ++++---- .../Timeline/TimelineInteractionHandler.swift | 4 -- .../Screens/Timeline/TimelineModels.swift | 57 ++++++++----------- .../Screens/Timeline/TimelineViewModel.swift | 32 +---------- .../Style/TimelineItemBubbledStylerView.swift | 2 +- .../HighlightedTimelineItemModifier.swift | 5 +- .../Screens/Timeline/View/TimelineView.swift | 7 ++- .../Sources/RoomScreenViewModelTests.swift | 41 ++++++++++++- 11 files changed, 144 insertions(+), 99 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index b3376b3387..97b37686b9 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -63,8 +63,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { init(parameters: RoomScreenCoordinatorParameters) { roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, + mediaProvider: parameters.mediaProvider, appMediator: parameters.appMediator, - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, focussedEventID: parameters.focussedEventID, @@ -103,8 +105,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .displayRoomDetails: - actionsSubject.send(.presentRoomDetails) case .displayEmojiPicker(let itemID, let selectedEmojis): actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) case .displayReportContent(let itemID, let senderID): @@ -121,7 +121,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentPollForm(mode: mode)) case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.presentMediaUploadPreviewScreen(url)) - case .displayRoomMemberDetails(userID: let userID): + case .tappedOnSenderDetails(userID: let userID): actionsSubject.send(.presentRoomMemberDetails(userID: userID)) case .displayMessageForwarding(let forwardingItem): actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem)) @@ -129,8 +129,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description)) case .composer(let action): composerViewModel.process(timelineAction: action) - case .displayCallScreen: - actionsSubject.send(.presentCallScreen) case .hasScrolled(direction: let direction): roomViewModel.timelineHasScrolled(direction: direction) } @@ -154,6 +152,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { focusOnEvent(eventID: eventID) case .displayPinnedEventsTimeline: actionsSubject.send(.presentPinnedEventsTimeline) + case .displayRoomDetails: + actionsSubject.send(.presentRoomDetails) + case .displayCall: + actionsSubject.send(.presentCallScreen) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index ef1480f06e..f4d4bd31ef 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -20,14 +20,21 @@ import OrderedCollections enum RoomScreenViewModelAction { case focusEvent(eventID: String) case displayPinnedEventsTimeline + case displayRoomDetails + case displayCall } enum RoomScreenViewAction { case tappedPinnedEventsBanner case viewAllPins + case displayRoomDetails + case displayCall } struct RoomScreenViewState: BindableState { + var roomTitle = "" + var roomAvatar: RoomAvatar + var lastScrollDirection: ScrollDirection? var isPinningEnabled = false // This is used to control the banner @@ -36,6 +43,9 @@ struct RoomScreenViewState: BindableState { isPinningEnabled && !pinnedEventsBannerState.isEmpty && lastScrollDirection != .top } + var canJoinCall = false + var hasOngoingCall: Bool + var bindings: RoomScreenViewStateBindings } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 78e23b1640..62d5b63a2f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -25,6 +25,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private let roomProxy: RoomProxyProtocol private let appMediator: AppMediatorProtocol private let appSettings: AppSettings + private let analyticsService: AnalyticsService private let pinnedEventStringBuilder: RoomEventStringBuilder private let actionsSubject: PassthroughSubject = .init() @@ -51,14 +52,21 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } init(roomProxy: RoomProxyProtocol, + mediaProvider: MediaProviderProtocol, appMediator: AppMediatorProtocol, - appSettings: AppSettings) { + appSettings: AppSettings, + analyticsService: AnalyticsService) { self.roomProxy = roomProxy self.appMediator = appMediator self.appSettings = appSettings + self.analyticsService = analyticsService pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) - super.init(initialViewState: .init(bindings: .init())) + super.init(initialViewState: .init(roomTitle: roomProxy.roomTitle, + roomAvatar: roomProxy.avatar, + hasOngoingCall: roomProxy.hasOngoingCall, + bindings: .init()), + imageProvider: mediaProvider) setupSubscriptions() } @@ -72,6 +80,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.pinnedEventsBannerState.previousPin() case .viewAllPins: actionsSubject.send(.displayPinnedEventsTimeline) + case .displayRoomDetails: + actionsSubject.send(.displayRoomDetails) + case .displayCall: + actionsSubject.send(.displayCall) + analyticsService.trackInteraction(name: .MobileRoomCallButton) } } @@ -84,15 +97,25 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .actionsPublisher .filter { $0 == .roomInfoUpdate } + roomInfoSubscription + .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] _ in + guard let self else { return } + state.roomTitle = roomProxy.roomTitle + state.roomAvatar = roomProxy.avatar + state.hasOngoingCall = roomProxy.hasOngoingCall + } + .store(in: &cancellables) + Task { [weak self] in // Don't guard let self here, otherwise the for await will strongify the self reference creating a strong reference cycle. // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. - await self?.updatePinnedEventIDs() + await self?.handleRoomInfoUpdate() for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { guard !Task.isCancelled else { return } - await self?.updatePinnedEventIDs() + await self?.handleRoomInfoUpdate() } } .store(in: &cancellables) @@ -128,12 +151,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.pinnedEventsBannerState.setPinnedEventContents(pinnedEventContents) } - private func updatePinnedEventIDs() async { + private func handleRoomInfoUpdate() async { let pinnedEventIDs = await roomProxy.pinnedEventIDs // Only update the loading state of the banner if state.pinnedEventsBannerState.isLoading { state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) } + + let userID = roomProxy.ownUserID + if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) { + state.canJoinCall = permission + } } private func setupPinnedEventsTimelineProviderIfNeeded() { @@ -154,10 +182,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } extension RoomScreenViewModel { - static func mock() -> RoomScreenViewModel { - RoomScreenViewModel(roomProxy: RoomProxyMock(.init()), + static func mock(roomProxyMock: RoomProxyMock) -> RoomScreenViewModel { + RoomScreenViewModel(roomProxy: roomProxyMock, + mediaProvider: MockMediaProvider(), appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 6ab1dcd7fc..3550e8023d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -163,29 +163,29 @@ struct RoomScreen: View { // .principal + .primaryAction works better than .navigation leading + trailing // as the latter disables interaction in the action button for rooms with long names ToolbarItem(placement: .principal) { - RoomHeaderView(roomName: timelineContext.viewState.roomTitle, - roomAvatar: timelineContext.viewState.roomAvatar, - imageProvider: timelineContext.imageProvider) + RoomHeaderView(roomName: roomContext.viewState.roomTitle, + roomAvatar: roomContext.viewState.roomAvatar, + imageProvider: roomContext.imageProvider) // Using a button stops it from getting truncated in the navigation bar .contentShape(.rect) .onTapGesture { - timelineContext.send(viewAction: .displayRoomDetails) + roomContext.send(viewAction: .displayRoomDetails) } } if !ProcessInfo.processInfo.isiOSAppOnMac { ToolbarItem(placement: .primaryAction) { callButton - .disabled(timelineContext.viewState.canJoinCall == false) + .disabled(roomContext.viewState.canJoinCall == false) } } } @ViewBuilder private var callButton: some View { - if timelineContext.viewState.hasOngoingCall { + if roomContext.viewState.hasOngoingCall { Button { - timelineContext.send(viewAction: .displayCall) + roomContext.send(viewAction: .displayCall) } label: { Label(L10n.actionJoin, icon: \.videoCallSolid) .labelStyle(.titleAndIcon) @@ -194,7 +194,7 @@ struct RoomScreen: View { .accessibilityIdentifier(A11yIdentifiers.roomScreen.joinCall) } else { Button { - timelineContext.send(viewAction: .displayCall) + roomContext.send(viewAction: .displayCall) } label: { CompoundIcon(\.videoCallSolid) } @@ -210,10 +210,11 @@ struct RoomScreen: View { // MARK: - Previews struct RoomScreen_Previews: PreviewProvider, TestablePreview { - static let roomViewModel = RoomScreenViewModel.mock() - static let timelineViewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", - name: "Preview room", - hasOngoingCall: true)), + static let roomProxyMock = RoomProxyMock(.init(id: "stable_id", + name: "Preview room", + hasOngoingCall: true)) + static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) + static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index ad694a0f09..4a01a1ab1a 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -503,10 +503,6 @@ class TimelineInteractionHandler { actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) } - func displayRoomMemberDetails(userID: String) async { - actionsSubject.send(.displayRoomMemberDetails(userID: userID)) - } - func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else { return .none diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index aef65a55dc..f19a508e3a 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -19,7 +19,6 @@ import OrderedCollections import SwiftUI enum TimelineViewModelAction { - case displayRoomDetails case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) case displayReportContent(itemID: TimelineItemIdentifier, senderID: String) case displayCameraPicker @@ -28,11 +27,10 @@ enum TimelineViewModelAction { case displayLocationPicker case displayPollForm(mode: PollFormMode) case displayMediaUploadPreviewScreen(url: URL) - case displayRoomMemberDetails(userID: String) + case tappedOnSenderDetails(userID: String) case displayMessageForwarding(forwardingItem: MessageForwardingItem) case displayLocation(body: String, geoURI: GeoURI, description: String?) case composer(action: TimelineComposerAction) - case displayCallScreen case hasScrolled(direction: ScrollDirection) } @@ -48,41 +46,39 @@ enum TimelineAudioPlayerAction { } enum TimelineViewAction { - case itemAppeared(itemID: TimelineItemIdentifier) // t - case itemDisappeared(itemID: TimelineItemIdentifier) // t + case itemAppeared(itemID: TimelineItemIdentifier) + case itemDisappeared(itemID: TimelineItemIdentifier) - case itemTapped(itemID: TimelineItemIdentifier) // t - case itemSendInfoTapped(itemID: TimelineItemIdentifier) // t - case toggleReaction(key: String, itemID: TimelineItemIdentifier) // t - case sendReadReceiptIfNeeded(TimelineItemIdentifier) // t - case paginateBackwards // t - case paginateForwards // t - case scrollToBottom // t + case itemTapped(itemID: TimelineItemIdentifier) + case itemSendInfoTapped(itemID: TimelineItemIdentifier) + case toggleReaction(key: String, itemID: TimelineItemIdentifier) + case sendReadReceiptIfNeeded(TimelineItemIdentifier) + case paginateBackwards + case paginateForwards + case scrollToBottom - case displayTimelineItemMenu(itemID: TimelineItemIdentifier) // t - case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) // not t + case displayTimelineItemMenu(itemID: TimelineItemIdentifier) + case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) - case displayRoomDetails // not t - case displayRoomMemberDetails(userID: String) // t -> change name - case displayReactionSummary(itemID: TimelineItemIdentifier, key: String) // t -> handle externally - case displayEmojiPicker(itemID: TimelineItemIdentifier) // t -> handle externally - case displayReadReceipts(itemID: TimelineItemIdentifier) // t -> handle externally - case displayCall // not t + case tappedOnSenderDetails(userID: String) + case displayReactionSummary(itemID: TimelineItemIdentifier, key: String) + case displayEmojiPicker(itemID: TimelineItemIdentifier) + case displayReadReceipts(itemID: TimelineItemIdentifier) - case handlePasteOrDrop(provider: NSItemProvider) // not t - case handlePollAction(TimelineViewPollAction) // t - case handleAudioPlayerAction(TimelineAudioPlayerAction) // t + case handlePasteOrDrop(provider: NSItemProvider) + case handlePollAction(TimelineViewPollAction) + case handleAudioPlayerAction(TimelineAudioPlayerAction) /// Focus the timeline onto the specified event ID (switching to a detached timeline if needed). - case focusOnEventID(String) // t + case focusOnEventID(String) /// Switch back to a live timeline (from a detached one). - case focusLive // t + case focusLive /// The timeline scrolled to reveal the focussed item. - case scrolledToFocussedItem // t + case scrolledToFocussedItem /// The table view has loaded the first items for a new timeline. - case hasSwitchedTimeline // t + case hasSwitchedTimeline - case hasScrolled(direction: ScrollDirection) // t + case hasScrolled(direction: ScrollDirection) } enum TimelineComposerAction { @@ -94,8 +90,6 @@ enum TimelineComposerAction { struct TimelineViewState: BindableState { var roomID: String - var roomTitle = "" - var roomAvatar: RoomAvatar var members: [String: RoomMemberState] = [:] var typingMembers: [String] = [] var showLoading = false @@ -113,9 +107,6 @@ struct TimelineViewState: BindableState { // It's updated from the room info, so it's faster than using the timeline var pinnedEventIDs: Set = [] - var canJoinCall = false - var hasOngoingCall = false - var bindings: TimelineViewStateBindings /// A closure providing the associated audio player state for an item in the timeline. diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 4634b2de2a..92f585a760 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -81,13 +81,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { analyticsService: analyticsService) super.init(initialViewState: TimelineViewState(roomID: roomProxy.id, - roomTitle: roomProxy.roomTitle, - roomAvatar: roomProxy.avatar, isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), ownUserID: roomProxy.ownUserID, isViewSourceEnabled: appSettings.viewSourceEnabled, - hasOngoingCall: roomProxy.hasOngoingCall, bindings: .init(reactionsCollapsed: [:])), imageProvider: mediaProvider) @@ -117,13 +114,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { // Note: beware if we get to e.g. restore a reply / edit, // maybe we are tracking a non-needed first initial state trackComposerMode(.default) - - Task { - let userID = roomProxy.ownUserID - if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) { - state.canJoinCall = permission - } - } } // MARK: - Public @@ -157,19 +147,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID) case .handleTimelineItemMenuAction(let itemID, let action): timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) - case .displayRoomDetails: - actionsSubject.send(.displayRoomDetails) - case .displayRoomMemberDetails(userID: let userID): - Task { await timelineInteractionHandler.displayRoomMemberDetails(userID: userID) } + case .tappedOnSenderDetails(userID: let userID): + actionsSubject.send(.tappedOnSenderDetails(userID: userID)) case .displayEmojiPicker(let itemID): timelineInteractionHandler.displayEmojiPicker(for: itemID) case .displayReactionSummary(let itemID, let key): displayReactionSummary(for: itemID, selectedKey: key) case .displayReadReceipts(itemID: let itemID): displayReadReceipts(for: itemID) - case .displayCall: - actionsSubject.send(.displayCallScreen) - analyticsService.trackInteraction(name: .MobileRoomCallButton) case .handlePasteOrDrop(let provider): timelineInteractionHandler.handlePasteOrDrop(provider) case .handlePollAction(let pollAction): @@ -389,17 +374,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { let roomInfoSubscription = roomProxy .actionsPublisher .filter { $0 == .roomInfoUpdate } - - roomInfoSubscription - .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] _ in - guard let self else { return } - state.roomTitle = roomProxy.roomTitle - state.roomAvatar = roomProxy.avatar - state.hasOngoingCall = roomProxy.hasOngoingCall - } - .store(in: &cancellables) - Task { [weak self] in // Don't guard let self here, otherwise the for await will strongify the self reference creating a strong reference cycle. // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. @@ -449,7 +423,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.displayMediaUploadPreviewScreen(url: url)) case .displayRoomMemberDetails(userID: let userID): - actionsSubject.send(.displayRoomMemberDetails(userID: userID)) + actionsSubject.send(.tappedOnSenderDetails(userID: userID)) case .showActionMenu(let actionMenuInfo): Task { await self.updatePermissions() diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 66b76abd42..5312394f4e 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -97,7 +97,7 @@ struct TimelineItemBubbledStylerView: View { // sender info are read inside the `TimelineAccessibilityModifier` .accessibilityHidden(true) .onTapGesture { - context.send(viewAction: .displayRoomMemberDetails(userID: timelineItem.sender.id)) + context.send(viewAction: .tappedOnSenderDetails(userID: timelineItem.sender.id)) } .padding(.top, 8) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift index 38aee5b641..dae13fbd26 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift @@ -93,9 +93,10 @@ struct HighlightedTimelineItemModifier_Previews: PreviewProvider, TestablePrevie /// A preview that allows quick testing of the highlight appearance across various timeline scenarios. struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { - static let roomViewModel = RoomScreenViewModel.mock() + static let roomProxyMock = RoomProxyMock(.init(name: "Preview room")) + static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) static let focussedEventID = "RoomTimelineItemFixtures.default.5" - static let timelineViewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), + static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, focussedEventID: focussedEventID, timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 010f0aaf7e..7308c17021 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -79,9 +79,10 @@ struct TimelineView: UIViewControllerRepresentable { // MARK: - Previews struct TimelineView_Previews: PreviewProvider, TestablePreview { - static let roomViewModel = RoomScreenViewModel.mock() - static let timelineViewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", - name: "Preview room")), + static let roomProxyMock = RoomProxyMock(.init(id: "stable_id", + name: "Preview room")) + static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) + static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index e3011801db..75890e0dac 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -43,8 +43,10 @@ class RoomScreenViewModelTests: XCTestCase { // setup the room proxy actions publisher roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + mediaProvider: MockMediaProvider(), appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) self.viewModel = viewModel // check if in the default state is not showing but is indeed loading @@ -101,4 +103,41 @@ class RoomScreenViewModelTests: XCTestCase { viewModel.timelineHasScrolled(direction: .bottom) XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner) } + + func testRoomInfoUpdate() async throws { + let updateSubject = PassthroughSubject() + let roomProxyMock = RoomProxyMock(.init(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)) + // setup the room proxy actions publisher + roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false) + roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() + let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + mediaProvider: MockMediaProvider(), + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) + self.viewModel = viewModel + + var deferred = deferFulfillment(viewModel.context.$viewState) { viewState in + viewState.roomTitle == "StartingName" && + viewState.roomAvatar == .room(id: "TestID", name: "StartingName", avatarURL: nil) && + !viewState.canJoinCall && + !viewState.hasOngoingCall + } + try await deferred.fulfill() + + roomProxyMock.name = "NewName" + roomProxyMock.avatar = .room(id: "TestID", name: "NewName", avatarURL: .documentsDirectory) + roomProxyMock.hasOngoingCall = true + roomProxyMock.canUserJoinCallUserIDReturnValue = .success(true) + + deferred = deferFulfillment(viewModel.context.$viewState) { viewState in + viewState.roomTitle == "NewName" && + viewState.roomAvatar == .room(id: "TestID", name: "NewName", avatarURL: .documentsDirectory) && + viewState.canJoinCall && + viewState.hasOngoingCall + } + + updateSubject.send(.roomInfoUpdate) + try await deferred.fulfill() + } }