diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 4b0cec78b..4a145dc34 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -173,8 +173,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { if case .thread(threadRootEventID: threadRootEventID, _) = stateMachine.state, let threadCoordinator = childThreads.last { threadCoordinator.focusOnEvent(eventID: eventID) } else { + // If we are showing the room timeline, we want to focus the thread root if childThreads.isEmpty { - roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false)) + roomScreenCoordinator?.focusOnEvent(.init(eventID: threadRootEventID, shouldSetPin: false)) } stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: eventID)) } diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 5c0e18966..217c3fb0f 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -96,11 +96,15 @@ extension ClientProxyMock { roomForIdentifierClosure = { [weak self] identifier in if let room = self?.roomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier }) { - await .joined(JoinedRoomProxyMock(.init(id: room.id, name: room.name))) + let roomProxy = await JoinedRoomProxyMock(.init(id: room.id, name: room.name)) + roomProxy.loadOrFetchEventForReturnValue = .success(TimelineEventSDKMock()) + return .joined(roomProxy) } else if let spaceRoomProxy = configuration.joinedSpaceRooms.first(where: { $0.id == identifier }) { - await .joined(JoinedRoomProxyMock(.init(id: spaceRoomProxy.id, name: spaceRoomProxy.name))) + let roomProxy = await JoinedRoomProxyMock(.init(id: spaceRoomProxy.id, name: spaceRoomProxy.name)) + roomProxy.loadOrFetchEventForReturnValue = .success(TimelineEventSDKMock()) + return .joined(roomProxy) } else { - nil + return nil } } diff --git a/ElementX/Sources/Mocks/TimelineControllerFactoryMock.swift b/ElementX/Sources/Mocks/TimelineControllerFactoryMock.swift index cb8b32367..cc507b1d8 100644 --- a/ElementX/Sources/Mocks/TimelineControllerFactoryMock.swift +++ b/ElementX/Sources/Mocks/TimelineControllerFactoryMock.swift @@ -10,6 +10,7 @@ import Foundation extension TimelineControllerFactoryMock { struct Configuration { var timelineController: TimelineControllerProtocol? + var threadTimelineController: TimelineControllerProtocol? } convenience init(_ configuration: Configuration) { @@ -20,5 +21,15 @@ extension TimelineControllerFactoryMock { timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk return timelineController }() + + buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderClosure = { threadRootEventID, _, _, _ in + if let threadTimelineController = configuration.threadTimelineController { + return .success(threadTimelineController) + } else { + let timelineController = MockTimelineController(timelineKind: .thread(rootEventID: threadRootEventID)) + timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk + return .success(timelineController) + } + } } } diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index ab5f8cde0..41859940a 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -18,6 +18,10 @@ class RoomFlowCoordinatorTests: XCTestCase { var navigationStackCoordinator: NavigationStackCoordinator! var cancellables = Set() + override func tearDown() { + AppSettings.resetAllSettings() + } + func testRoomPresentation() async throws { setupRoomFlowCoordinator() @@ -218,6 +222,87 @@ class RoomFlowCoordinatorTests: XCTestCase { XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator) } + func testThreadedEventRoutes() async throws { + ServiceLocator.shared.settings.threadsEnabled = true + setupRoomFlowCoordinator() + + // Navigate directly to the threaded event + var configuration = JoinedRoomProxyMockConfiguration(id: "1") + var roomProxy = JoinedRoomProxyMock(configuration) + + var roomInfoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) + roomProxy.infoPublisher = roomInfoSubject.asCurrentValuePublisher() + + var mockedEvent = TimelineEventSDKMock() + mockedEvent.threadRootEventIdReturnValue = "1" + roomProxy.loadOrFetchEventForReturnValue = .success(mockedEvent) + + clientProxy.roomForIdentifierClosure = { _ in + .joined(roomProxy) + } + + try await process(route: .event(eventID: "2", roomID: "1", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1) + XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator) + + // From the thread screen, navigate to another threaded event in the same room, and in the same thread. + let threadCoordinator = navigationStackCoordinator.stackCoordinators[0] as? ThreadTimelineScreenCoordinator + try await process(route: .childEvent(eventID: "3", roomID: "1", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1) + XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator) + XCTAssertIdentical(navigationStackCoordinator.stackCoordinators[0], threadCoordinator) + // Would be nice to test if the focusEvent function has been called but there is no way to mock that. + + // From the thread screen, navigate to another threaded event in the same room, but in a different thread. + mockedEvent = TimelineEventSDKMock() + mockedEvent.threadRootEventIdReturnValue = "4" + roomProxy.loadOrFetchEventForReturnValue = .success(mockedEvent) + try await process(route: .childEvent(eventID: "5", roomID: "1", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2) + XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator) + + // From the thread screen, navigate to another threaded event in a different room. + configuration = JoinedRoomProxyMockConfiguration(id: "2") + roomProxy = JoinedRoomProxyMock(configuration) + + roomInfoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) + roomProxy.infoPublisher = roomInfoSubject.asCurrentValuePublisher() + + mockedEvent = TimelineEventSDKMock() + mockedEvent.threadRootEventIdReturnValue = "1" + roomProxy.loadOrFetchEventForReturnValue = .success(mockedEvent) + + clientProxy.roomForIdentifierClosure = { _ in + .joined(roomProxy) + } + + try await process(route: .childEvent(eventID: "2", roomID: "2", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 4) + XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[2] is RoomScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[3] is ThreadTimelineScreenCoordinator) + + // From the thread screen, navigate to an event of the same room that is not threaded + mockedEvent = TimelineEventSDKMock() + mockedEvent.threadRootEventIdReturnValue = nil + roomProxy.loadOrFetchEventForReturnValue = .success(mockedEvent) + + try await process(route: .childEvent(eventID: "3", roomID: "2", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 5) + XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[2] is RoomScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[3] is ThreadTimelineScreenCoordinator) + XCTAssert(navigationStackCoordinator.stackCoordinators[4] is RoomScreenCoordinator) + } + func testShareMediaRoute() async throws { setupRoomFlowCoordinator() @@ -292,7 +377,7 @@ class RoomFlowCoordinatorTests: XCTestCase { try await fulfillment.fulfill() } - + // MARK: - Private private func process(route: AppRoute) async throws {