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
@@ -11,7 +11,7 @@ import Combine
import Foundation
import MatrixRustSDK
class MockRoomTimelineController: RoomTimelineControllerProtocol {
class MockTimelineController: TimelineControllerProtocol {
/// An array of timeline item arrays that will be inserted in order for each back pagination request.
var backPaginationResponses: [[RoomTimelineItemProtocol]] = []
/// An array of timeline items that will be appended in order when ``simulateIncomingItems()`` is called.
@@ -21,7 +21,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
var roomID: String { roomProxy?.id ?? "MockRoomIdentifier" }
var timelineKind: TimelineKind
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
let callbacks = PassthroughSubject<TimelineControllerCallback, Never>()
var paginationState: PaginationState = .initial {
didSet {
@@ -34,14 +34,14 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
private var client: UITestsSignalling.Client?
static var mediaGallery: MockRoomTimelineController {
MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen), timelineItems: (0..<5).reduce([]) { partialResult, _ in
static var mediaGallery: MockTimelineController {
MockTimelineController(timelineKind: .media(.mediaFilesScreen), timelineItems: (0..<5).reduce([]) { partialResult, _ in
partialResult + [RoomTimelineItemFixtures.separator] + RoomTimelineItemFixtures.mediaChunk
})
}
static var emptyMediaGallery: MockRoomTimelineController {
let mock = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
static var emptyMediaGallery: MockTimelineController {
let mock = MockTimelineController(timelineKind: .media(.mediaFilesScreen))
mock.paginationState = PaginationState(backward: .timelineEndReached, forward: .timelineEndReached)
return mock
}
@@ -63,7 +63,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
}
private(set) var focusOnEventCallCount = 0
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, TimelineControllerError> {
focusOnEventCallCount += 1
callbacks.send(.isLive(false))
return .success(())
@@ -75,7 +75,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
callbacks.send(.isLive(true))
}
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
paginationState = PaginationState(backward: .paginating, forward: .timelineEndReached)
if client == nil {
@@ -85,7 +85,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
return .success(())
}
func paginateForwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
// try? await simulateForwardPagination()
.success(())
}
@@ -1,55 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol {
RoomTimelineController(roomProxy: roomProxy,
timelineProxy: roomProxy.timeline,
initialFocussedEventID: initialFocussedEventID,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings)
}
func buildPinnedEventsRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? {
guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else {
return nil
}
return RoomTimelineController(roomProxy: roomProxy,
timelineProxy: pinnedEventsTimeline,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings)
}
func buildMessageFilteredRoomTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError> {
switch await roomProxy.messageFilteredTimeline(allowedMessageTypes: allowedMessageTypes) {
case .success(let timelineProxy):
return .success(RoomTimelineController(roomProxy: roomProxy,
timelineProxy: timelineProxy,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings))
case .failure(let error):
return .failure(.roomProxyError(error))
}
}
}
@@ -1,33 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
enum RoomTimelineFactoryControllerError: Error {
case roomProxyError(RoomProxyError)
}
@MainActor
protocol RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol
func buildPinnedEventsRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol?
func buildMessageFilteredRoomTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError>
}
// sourcery: AutoMockable
extension RoomTimelineControllerFactoryProtocol { }
@@ -11,19 +11,19 @@ import IntentsUI
import MatrixRustSDK
import UIKit
class RoomTimelineController: RoomTimelineControllerProtocol {
class TimelineController: TimelineControllerProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private let liveTimelineProvider: RoomTimelineProviderProtocol
private let liveTimelineProvider: TimelineProviderProtocol
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
private let mediaProvider: MediaProviderProtocol
private let appSettings: AppSettings
private let serialDispatchQueue: DispatchQueue
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
let callbacks = PassthroughSubject<TimelineControllerCallback, Never>()
private var activeTimeline: TimelineProxyProtocol
private var activeTimelineProvider: RoomTimelineProviderProtocol {
private var activeTimelineProvider: TimelineProviderProtocol {
didSet {
configureActiveTimelineProvider()
}
@@ -57,7 +57,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
self.mediaProvider = mediaProvider
self.appSettings = appSettings
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timelineprovider", qos: .utility)
activeTimeline = timelineProxy
activeTimelineProvider = liveTimelineProvider
@@ -82,7 +82,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, TimelineControllerError> {
switch await roomProxy.timelineFocusedOnEvent(eventID: eventID, numberOfEvents: timelineSize) {
case .success(let timeline):
await timeline.subscribeForUpdates()
@@ -103,7 +103,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
activeTimelineProvider = liveTimelineProvider
}
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
MXLog.info("Started back pagination request")
switch await activeTimeline.paginateBackwards(requestSize: requestSize) {
case .success:
@@ -115,7 +115,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func paginateForwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError> {
MXLog.info("Started forward pagination request")
switch await activeTimeline.paginateForwards(requestSize: requestSize) {
case .success:
@@ -0,0 +1,56 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
struct TimelineControllerFactory: TimelineControllerFactoryProtocol {
func buildTimelineController(roomProxy: JoinedRoomProxyProtocol,
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> TimelineControllerProtocol {
TimelineController(roomProxy: roomProxy,
timelineProxy: roomProxy.timeline,
initialFocussedEventID: initialFocussedEventID,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings)
}
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? {
guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else {
return nil
}
return TimelineController(roomProxy: roomProxy,
timelineProxy: pinnedEventsTimeline,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings)
}
func buildMessageFilteredTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
presentation: TimelineKind.MediaPresentation,
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
switch await roomProxy.messageFilteredTimeline(allowedMessageTypes: allowedMessageTypes, presentation: presentation) {
case .success(let timelineProxy):
return .success(TimelineController(roomProxy: roomProxy,
timelineProxy: timelineProxy,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings))
case .failure(let error):
return .failure(.roomProxyError(error))
}
}
}
@@ -0,0 +1,34 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
enum TimelineFactoryControllerError: Error {
case roomProxyError(RoomProxyError)
}
@MainActor
protocol TimelineControllerFactoryProtocol {
func buildTimelineController(roomProxy: JoinedRoomProxyProtocol,
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> TimelineControllerProtocol
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol?
func buildMessageFilteredTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
presentation: TimelineKind.MediaPresentation,
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError>
}
// sourcery: AutoMockable
extension TimelineControllerFactoryProtocol { }
@@ -9,25 +9,25 @@ import Combine
import MatrixRustSDK
import SwiftUI
enum RoomTimelineControllerCallback {
enum TimelineControllerCallback {
case updatedTimelineItems(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool)
case paginationState(PaginationState)
case isLive(Bool)
}
enum RoomTimelineControllerAction {
enum TimelineControllerAction {
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
case displayLocation(body: String, geoURI: GeoURI, description: String?)
case none
}
enum RoomTimelineControllerError: Error {
enum TimelineControllerError: Error {
case generic
case eventNotFound
}
@MainActor
protocol RoomTimelineControllerProtocol {
protocol TimelineControllerProtocol {
var roomID: String { get }
var timelineKind: TimelineKind { get }
@@ -37,17 +37,17 @@ protocol RoomTimelineControllerProtocol {
/// The current pagination state, use only for setting up the intial state
var paginationState: PaginationState { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
var callbacks: PassthroughSubject<TimelineControllerCallback, Never> { get }
func processItemAppearance(_ itemID: TimelineItemIdentifier) async
func processItemDisappearance(_ itemID: TimelineItemIdentifier) async
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError>
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, TimelineControllerError>
func focusLive()
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError>
func paginateForwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError>
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError>
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineControllerError>
func sendReadReceipt(for itemID: TimelineItemIdentifier) async
@@ -9,7 +9,7 @@ import Combine
import Foundation
import MatrixRustSDK
class RoomTimelineProvider: RoomTimelineProviderProtocol {
class TimelineProvider: TimelineProviderProtocol {
private var cancellables = Set<AnyCancellable>()
private let serialDispatchQueue: DispatchQueue
@@ -46,7 +46,7 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
}
init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher<PaginationState, Never>) {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timelineprovider", qos: .utility)
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
self.kind = kind
@@ -29,7 +29,7 @@ struct PaginationState: Equatable {
@MainActor
// sourcery: AutoMockable
protocol RoomTimelineProviderProtocol {
protocol TimelineProviderProtocol {
/// A publisher that signals when ``itemProxies`` or ``paginationState`` are changed.
var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { get }
/// The current set of items in the timeline.
@@ -20,8 +20,8 @@ final class TimelineProxy: TimelineProxyProtocol {
private let kind: TimelineKind
private var innerTimelineProvider: RoomTimelineProviderProtocol!
var timelineProvider: RoomTimelineProviderProtocol {
private var innerTimelineProvider: TimelineProviderProtocol!
var timelineProvider: TimelineProviderProtocol {
innerTimelineProvider
}
@@ -47,7 +47,7 @@ final class TimelineProxy: TimelineProxyProtocol {
await subscribeToPagination()
let provider = await RoomTimelineProvider(timeline: timeline, kind: kind, paginationStatePublisher: paginationStatePublisher)
let provider = await TimelineProvider(timeline: timeline, kind: kind, paginationStatePublisher: paginationStatePublisher)
// Make sure the existing items are built so that we have content in the timeline before
// determining whether or not the timeline should paginate to load more items.
await provider.waitForInitialItems()
@@ -27,7 +27,7 @@ enum TimelineProxyError: Error {
// sourcery: AutoMockable
protocol TimelineProxyProtocol {
var timelineProvider: RoomTimelineProviderProtocol { get }
var timelineProvider: TimelineProviderProtocol { get }
func subscribeForUpdates() async