RoomTimeline… refactor (drop the Room). (#3728)

* Add the timeline controller factory to the timeline view model.

In preparation for building a timeline to swipe through media in QuickLook.

* Refactor RoomTimelineControllerFactory.

* Refactor RoomTimelineController.

* Refactor RoomTimelineProvider.
This commit is contained in:
Doug
2025-02-03 14:14:01 +00:00
committed by GitHub
parent 9df5a8fddb
commit d195e603c9
53 changed files with 688 additions and 631 deletions
+9 -6
View File
@@ -18,7 +18,7 @@ class PillContextTests: XCTestCase {
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
proxyMock.membersPublisher = subject.asCurrentValuePublisher()
let mock = TimelineViewModel(roomProxy: proxyMock,
timelineController: MockRoomTimelineController(),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -26,7 +26,8 @@ class PillContextTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention)
@@ -47,7 +48,7 @@ class PillContextTests: XCTestCase {
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
proxyMock.membersPublisher = subject.asCurrentValuePublisher()
let mock = TimelineViewModel(roomProxy: proxyMock,
timelineController: MockRoomTimelineController(),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -55,7 +56,8 @@ class PillContextTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
XCTAssertTrue(context.viewState.isOwnMention)
@@ -66,7 +68,7 @@ class PillContextTests: XCTestCase {
let id = "test_room"
let displayName = "Test"
let proxyMock = JoinedRoomProxyMock(.init(id: id, name: displayName, avatarURL: avatarURL))
let mockController = MockRoomTimelineController()
let mockController = MockTimelineController()
mockController.roomProxy = proxyMock
let mock = TimelineViewModel(roomProxy: proxyMock,
timelineController: mockController,
@@ -77,7 +79,8 @@ class PillContextTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
XCTAssertTrue(context.viewState.isOwnMention)
@@ -13,7 +13,7 @@ import Combine
@MainActor
class RoomFlowCoordinatorTests: XCTestCase {
var clientProxy: ClientProxyMock!
var timelineControllerFactory: RoomTimelineControllerFactoryMock!
var timelineControllerFactory: TimelineControllerFactoryMock!
var roomFlowCoordinator: RoomFlowCoordinator!
var navigationStackCoordinator: NavigationStackCoordinator!
var cancellables = Set<AnyCancellable>()
@@ -311,7 +311,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
private func setupRoomFlowCoordinator(asChildFlow: Bool = false, roomType: RoomType? = nil) async {
cancellables.removeAll()
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
timelineControllerFactory = RoomTimelineControllerFactoryMock(configuration: .init())
timelineControllerFactory = TimelineControllerFactoryMock(.init())
clientProxy.roomPreviewForIdentifierViaClosure = { [roomType] roomID, _ in
switch roomType {
@@ -336,7 +336,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
roomFlowCoordinator = await RoomFlowCoordinator(roomID: roomID,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
isChildFlow: asChildFlow,
roomTimelineControllerFactory: timelineControllerFactory,
timelineControllerFactory: timelineControllerFactory,
navigationStackCoordinator: navigationStackCoordinator,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
ongoingCallRoomIDPublisher: .init(.init(nil)),
@@ -13,13 +13,13 @@ import XCTest
class RoomPollsHistoryScreenViewModelTests: XCTestCase {
var viewModel: RoomPollsHistoryScreenViewModelProtocol!
var interactionHandler: PollInteractionHandlerMock!
var timelineController: MockRoomTimelineController!
var timelineController: MockTimelineController!
override func setUpWithError() throws {
interactionHandler = PollInteractionHandlerMock()
timelineController = MockRoomTimelineController()
timelineController = MockTimelineController()
viewModel = RoomPollsHistoryScreenViewModel(pollInteractionHandler: interactionHandler,
roomTimelineController: timelineController,
timelineController: timelineController,
userIndicatorController: UserIndicatorControllerMock())
}
@@ -66,7 +66,7 @@ class RoomScreenViewModelTests: XCTestCase {
// setup the loaded pinned events injection in the timeline
let pinnedTimelineMock = TimelineProxyMock()
let pinnedTimelineProviderMock = RoomTimelineProviderMock()
let pinnedTimelineProviderMock = TimelineProviderMock()
let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>()
pinnedTimelineProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher()
pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock
@@ -107,7 +107,7 @@ class RoomScreenViewModelTests: XCTestCase {
let roomProxyMock = JoinedRoomProxyMock(.init())
// setup a way to inject the mock of the pinned events timeline
let pinnedTimelineMock = TimelineProxyMock()
let pinnedTimelineProviderMock = RoomTimelineProviderMock()
let pinnedTimelineProviderMock = TimelineProviderMock()
pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock
pinnedTimelineProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher()
pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init(id: "1"))),
@@ -19,7 +19,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
var context: TimelineMediaPreviewViewModel.Context { viewModel.context }
var mediaProvider: MediaProviderMock!
var photoLibraryManager: PhotoLibraryManagerMock!
var timelineController: MockRoomTimelineController!
var timelineController: MockTimelineController!
func testLoadingItem() async throws {
// Given a fresh view model.
@@ -274,7 +274,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
private func setupViewModel(initialItemIndex: Int = 0, photoLibraryAuthorizationDenied: Bool = false) {
let initialItems = makeItems()
timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
timelineController = MockTimelineController(timelineKind: .media(.mediaFilesScreen))
timelineController.timelineItems = initialItems
mediaProvider = MediaProviderMock(configuration: .init())
+25 -20
View File
@@ -40,7 +40,7 @@ class TimelineViewModelTests: XCTestCase {
]
// When showing them in a timeline.
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -68,7 +68,7 @@ class TimelineViewModelTests: XCTestCase {
]
// When showing them in a timeline.
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -94,7 +94,7 @@ class TimelineViewModelTests: XCTestCase {
]
// When showing them in a timeline.
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -117,7 +117,7 @@ class TimelineViewModelTests: XCTestCase {
]
// When showing them in a timeline.
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -140,7 +140,7 @@ class TimelineViewModelTests: XCTestCase {
]
// When showing them in a timeline.
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -157,7 +157,7 @@ class TimelineViewModelTests: XCTestCase {
let items = [TextRoomTimelineItem(eventID: "t1"),
TextRoomTimelineItem(eventID: "t2"),
TextRoomTimelineItem(eventID: "t3")]
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -181,7 +181,7 @@ class TimelineViewModelTests: XCTestCase {
let items = [TextRoomTimelineItem(eventID: "t1"),
TextRoomTimelineItem(eventID: "t2"),
TextRoomTimelineItem(eventID: "t3")]
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -205,7 +205,7 @@ class TimelineViewModelTests: XCTestCase {
let items = [TextRoomTimelineItem(eventID: "t1"),
TextRoomTimelineItem(eventID: "t2"),
TextRoomTimelineItem(eventID: "t3")]
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = items
let viewModel = makeViewModel(timelineController: timelineController)
@@ -230,7 +230,7 @@ class TimelineViewModelTests: XCTestCase {
}
func testInitialFocusViewState() async throws {
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
let viewModel = makeViewModel(focussedEventID: "t10", timelineController: timelineController)
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t10", appearance: .immediate))
@@ -289,13 +289,13 @@ class TimelineViewModelTests: XCTestCase {
private func readReceiptsConfiguration(with items: [RoomTimelineItemProtocol]) -> (TimelineViewModel,
JoinedRoomProxyMock,
TimelineProxyMock,
MockRoomTimelineController) {
MockTimelineController) {
let roomProxy = JoinedRoomProxyMock(.init(name: ""))
let timelineProxy = TimelineProxyMock()
roomProxy.timeline = timelineProxy
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineProxy.sendReadReceiptForTypeReturnValue = .success(())
@@ -311,7 +311,8 @@ class TimelineViewModelTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
return (viewModel, roomProxy, timelineProxy, timelineController)
}
@@ -325,7 +326,7 @@ class TimelineViewModelTests: XCTestCase {
let id = message.id
// When showing them in a timeline.
let timelineController = MockRoomTimelineController()
let timelineController = MockTimelineController()
timelineController.timelineItems = [message]
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])),
timelineController: timelineController,
@@ -336,7 +337,8 @@ class TimelineViewModelTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts
@@ -356,7 +358,7 @@ class TimelineViewModelTests: XCTestCase {
roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -364,7 +366,8 @@ class TimelineViewModelTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs)
configuration.pinnedEventIDs = ["test1", "test2"]
@@ -382,7 +385,7 @@ class TimelineViewModelTests: XCTestCase {
roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
@@ -390,7 +393,8 @@ class TimelineViewModelTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
var deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserPin
@@ -409,7 +413,7 @@ class TimelineViewModelTests: XCTestCase {
private func makeViewModel(roomProxy: JoinedRoomProxyProtocol? = nil,
focussedEventID: String? = nil,
timelineController: RoomTimelineControllerProtocol) -> TimelineViewModel {
timelineController: TimelineControllerProtocol) -> TimelineViewModel {
TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")),
focussedEventID: focussedEventID,
timelineController: timelineController,
@@ -420,7 +424,8 @@ class TimelineViewModelTests: XCTestCase {
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
}
}
@@ -13,7 +13,7 @@ import Combine
@MainActor
class UserSessionFlowCoordinatorTests: XCTestCase {
var clientProxy: ClientProxyMock!
var timelineControllerFactory: RoomTimelineControllerFactoryMock!
var timelineControllerFactory: TimelineControllerFactoryMock!
var userSessionFlowCoordinator: UserSessionFlowCoordinator!
var navigationRootCoordinator: NavigationRootCoordinator!
var notificationManager: NotificationManagerMock!
@@ -27,7 +27,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
override func setUp() async throws {
cancellables.removeAll()
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
timelineControllerFactory = RoomTimelineControllerFactoryMock(configuration: .init())
timelineControllerFactory = TimelineControllerFactoryMock(.init())
navigationRootCoordinator = NavigationRootCoordinator()
@@ -38,7 +38,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
appLockService: AppLockServiceMock(),
bugReportService: BugReportServiceMock(),
elementCallService: ElementCallServiceMock(.init()),
roomTimelineControllerFactory: timelineControllerFactory,
timelineControllerFactory: timelineControllerFactory,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(),
@@ -211,8 +211,8 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 1)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "1")
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 1)
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "1")
// A child event route should push a new room screen onto the stack and focus on the event.
userSessionFlowCoordinator.handleAppRoute(.childEvent(eventID: "2", roomID: "2", via: []), animated: true)
@@ -221,24 +221,24 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1)
XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 2)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "2")
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 2)
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "2")
// A subsequent regular event route should clear the stack and set the new room as the root of the stack.
try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 3)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "3")
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 3)
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "3")
// A regular event route for the same room should set a new instance of the room as the root of the stack.
try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 4)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "4",
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 4)
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "4",
"A new timeline should be created for the same room ID, so that the screen isn't stale while loading.")
}