diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index c024238cc7..7f8ffab523 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -58,6 +58,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol private let navigationStackCoordinator: NavigationStackCoordinator private let emojiProvider: EmojiProviderProtocol + private let ongoingCallRoomIDPublisher: CurrentValuePublisher private let appMediator: AppMediatorProtocol private let appSettings: AppSettings private let analytics: AnalyticsService @@ -90,6 +91,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol, navigationStackCoordinator: NavigationStackCoordinator, emojiProvider: EmojiProviderProtocol, + ongoingCallRoomIDPublisher: CurrentValuePublisher, appMediator: AppMediatorProtocol, appSettings: AppSettings, analytics: AnalyticsService, @@ -100,6 +102,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { self.roomTimelineControllerFactory = roomTimelineControllerFactory self.navigationStackCoordinator = navigationStackCoordinator self.emojiProvider = emojiProvider + self.ongoingCallRoomIDPublisher = ongoingCallRoomIDPublisher self.appMediator = appMediator self.appSettings = appSettings self.analytics = analytics @@ -580,6 +583,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { voiceMessageMediaManager: userSession.voiceMessageMediaManager, emojiProvider: emojiProvider, completionSuggestionService: completionSuggestionService, + ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher, appMediator: appMediator, appSettings: appSettings, composerDraftService: composerDraftService) @@ -1350,6 +1354,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { roomTimelineControllerFactory: roomTimelineControllerFactory, navigationStackCoordinator: navigationStackCoordinator, emojiProvider: emojiProvider, + ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher, appMediator: appMediator, appSettings: appSettings, analytics: analytics, diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 96a2c83c47..f459b5e175 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -477,6 +477,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { roomTimelineControllerFactory: roomTimelineControllerFactory, navigationStackCoordinator: detailNavigationStackCoordinator, emojiProvider: EmojiProvider(), + ongoingCallRoomIDPublisher: elementCallService.ongoingCallRoomIDPublisher, appMediator: appMediator, appSettings: appSettings, analytics: analytics, @@ -580,7 +581,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private var callScreenPictureInPictureController: AVPictureInPictureController? private func presentCallScreen(configuration: ElementCallConfiguration) { - guard elementCallService.ongoingCallRoomID != configuration.callRoomID else { + guard elementCallService.ongoingCallRoomIDPublisher.value != configuration.callRoomID else { MXLog.info("Returning to existing call.") callScreenPictureInPictureController?.stopPictureInPicture() return diff --git a/ElementX/Sources/Mocks/ElementCallServiceMock.swift b/ElementX/Sources/Mocks/ElementCallServiceMock.swift index 27b2e81255..bfbf302636 100644 --- a/ElementX/Sources/Mocks/ElementCallServiceMock.swift +++ b/ElementX/Sources/Mocks/ElementCallServiceMock.swift @@ -17,12 +17,15 @@ import Combine import Foundation -struct ElementCallServiceMockConfiguration { } +struct ElementCallServiceMockConfiguration { + var ongoingCallRoomID: String? +} extension ElementCallServiceMock { convenience init(_ configuration: ElementCallServiceMockConfiguration) { self.init() underlyingActions = PassthroughSubject().eraseToAnyPublisher() + underlyingOngoingCallRoomIDPublisher = .init(.init(configuration.ongoingCallRoomID)) } } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index e1f88901a3..e9bf04f3d0 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -4894,7 +4894,11 @@ class ElementCallServiceMock: ElementCallServiceProtocol { set(value) { underlyingActions = value } } var underlyingActions: AnyPublisher! - var ongoingCallRoomID: String? + var ongoingCallRoomIDPublisher: CurrentValuePublisher { + get { return underlyingOngoingCallRoomIDPublisher } + set(value) { underlyingOngoingCallRoomIDPublisher = value } + } + var underlyingOngoingCallRoomIDPublisher: CurrentValuePublisher! //MARK: - setClientProxy diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index cea9b0c85e..54a562fb0c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -28,6 +28,7 @@ struct RoomScreenCoordinatorParameters { let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol let emojiProvider: EmojiProviderProtocol let completionSuggestionService: CompletionSuggestionServiceProtocol + let ongoingCallRoomIDPublisher: CurrentValuePublisher let appMediator: AppMediatorProtocol let appSettings: AppSettings let composerDraftService: ComposerDraftServiceProtocol @@ -64,6 +65,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { init(parameters: RoomScreenCoordinatorParameters) { roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, mediaProvider: parameters.mediaProvider, + ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher, appMediator: parameters.appMediator, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 866d1252e2..95a41b30c1 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -46,6 +46,7 @@ struct RoomScreenViewState: BindableState { var canJoinCall = false var hasOngoingCall: Bool + var shouldShowCallButton = true var bindings: RoomScreenViewStateBindings } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index b950056558..9236dd8617 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -53,6 +53,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol init(roomProxy: JoinedRoomProxyProtocol, mediaProvider: MediaProviderProtocol, + ongoingCallRoomIDPublisher: CurrentValuePublisher, appMediator: AppMediatorProtocol, appSettings: AppSettings, analyticsService: AnalyticsService) { @@ -68,7 +69,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol bindings: .init()), mediaProvider: mediaProvider) - setupSubscriptions() + setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher) } override func process(viewAction: RoomScreenViewAction) { @@ -93,7 +94,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.lastScrollDirection = direction } - private func setupSubscriptions() { + private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher) { let roomInfoSubscription = roomProxy .actionsPublisher .filter { $0 == .roomInfoUpdate } @@ -135,6 +136,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol self?.setupPinnedEventsTimelineProviderIfNeeded() } .store(in: &cancellables) + + ongoingCallRoomIDPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] ongoingCallRoomID in + guard let self else { return } + state.shouldShowCallButton = ongoingCallRoomID != roomProxy.id + } + .store(in: &cancellables) } private func buildPinnedEventContents(timelineItems: [TimelineItemProxy]) { @@ -186,6 +195,7 @@ extension RoomScreenViewModel { static func mock(roomProxyMock: JoinedRoomProxyMock) -> RoomScreenViewModel { RoomScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider(), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, 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 9f2dae5151..024d71f830 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -175,8 +175,10 @@ struct RoomScreen: View { if !ProcessInfo.processInfo.isiOSAppOnMac { ToolbarItem(placement: .primaryAction) { - callButton - .disabled(roomContext.viewState.canJoinCall == false) + if roomContext.viewState.shouldShowCallButton { + callButton + .disabled(roomContext.viewState.canJoinCall == false) + } } } } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index db0316152f..1f8b5b145e 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -57,9 +57,14 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe private var endUnansweredCallTask: Task? - private var ongoingCallID: CallID? + private var ongoingCallID: CallID? { + didSet { ongoingCallRoomIDSubject.send(ongoingCallID?.roomID) } + } - var ongoingCallRoomID: String? { ongoingCallID?.roomID } + let ongoingCallRoomIDSubject = CurrentValueSubject(nil) + var ongoingCallRoomIDPublisher: CurrentValuePublisher { + ongoingCallRoomIDSubject.asCurrentValuePublisher() + } private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { diff --git a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift index c58a683945..4cc57aa8b3 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift @@ -26,7 +26,7 @@ enum ElementCallServiceAction { protocol ElementCallServiceProtocol { var actions: AnyPublisher { get } - var ongoingCallRoomID: String? { get } + var ongoingCallRoomIDPublisher: CurrentValuePublisher { get } func setClientProxy(_ clientProxy: ClientProxyProtocol) diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 9234c0bf36..428bdcbf33 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -255,6 +255,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -272,6 +273,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -289,6 +291,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -306,6 +309,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -326,6 +330,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -346,6 +351,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -366,6 +372,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -387,6 +394,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -407,6 +415,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -426,6 +435,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -459,6 +469,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -479,6 +490,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) @@ -499,6 +511,7 @@ class MockScreen: Identifiable { voiceMessageMediaManager: VoiceMessageMediaManagerMock(), emojiProvider: EmojiProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, composerDraftService: ComposerDraftServiceMock(.init())) diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index 0bbbfeffff..5c94307d2b 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -308,6 +308,7 @@ class RoomFlowCoordinatorTests: XCTestCase { roomTimelineControllerFactory: timelineControllerFactory, navigationStackCoordinator: navigationStackCoordinator, emojiProvider: EmojiProvider(), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index aaa944f7ce..07570b025d 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -44,6 +44,7 @@ class RoomScreenViewModelTests: XCTestCase { roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider(), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics) @@ -112,6 +113,7 @@ class RoomScreenViewModelTests: XCTestCase { roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider(), + ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics) @@ -140,4 +142,42 @@ class RoomScreenViewModelTests: XCTestCase { updateSubject.send(.roomInfoUpdate) try await deferred.fulfill() } + + func testCallButtonVisibility() async throws { + // Given a room screen with no ongoing call. + let ongoingCallRoomIDSubject = CurrentValueSubject(nil) + let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID")) + let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + mediaProvider: MockMediaProvider(), + ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(), + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) + self.viewModel = viewModel + XCTAssertTrue(viewModel.state.shouldShowCallButton) + + // When a call starts in this room. + var deferred = deferFulfillment(viewModel.context.$viewState) { !$0.shouldShowCallButton } + ongoingCallRoomIDSubject.send("MyRoomID") + try await deferred.fulfill() + + // Then the call button should be hidden. + XCTAssertFalse(viewModel.state.shouldShowCallButton) + + // When a call starts in a different room. + deferred = deferFulfillment(viewModel.context.$viewState) { $0.shouldShowCallButton } + ongoingCallRoomIDSubject.send("OtherRoomID") + try await deferred.fulfill() + + // Then the call button should be shown again. + XCTAssertTrue(viewModel.state.shouldShowCallButton) + + // When the call from the other room finishes. + let deferredFailure = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.shouldShowCallButton } + ongoingCallRoomIDSubject.send(nil) + try await deferredFailure.fulfill() + + // Then the call button should remain visible shown. + XCTAssertTrue(viewModel.state.shouldShowCallButton) + } }