Files
letro-ios/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift

223 lines
7.7 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 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 Combine
import Foundation
import MatrixRustSDK
class TimelineItemProvider: TimelineItemProviderProtocol {
private var cancellables = Set<AnyCancellable>()
private let serialDispatchQueue: DispatchQueue
private var roomTimelineObservationToken: TaskHandle?
private let paginationStateSubject = CurrentValueSubject<TimelinePaginationState, Never>(.initial)
var paginationState: TimelinePaginationState {
paginationStateSubject.value
}
private let itemProxiesSubject: CurrentValueSubject<[TimelineItemProxy], Never>
private(set) var itemProxies: [TimelineItemProxy] = [] {
didSet {
itemProxiesSubject.send(itemProxies)
}
}
var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> {
itemProxiesSubject
.combineLatest(paginationStateSubject)
.eraseToAnyPublisher()
}
let kind: TimelineKind
private let membershipChangeSubject = PassthroughSubject<Void, Never>()
var membershipChangePublisher: AnyPublisher<Void, Never> {
membershipChangeSubject
.eraseToAnyPublisher()
}
deinit {
roomTimelineObservationToken?.cancel()
}
init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher<TimelinePaginationState, Never>) {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timeline_item_provider", qos: .utility)
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
self.kind = kind
paginationStatePublisher
.sink { [weak self] in
self?.paginationStateSubject.send($0)
}
.store(in: &cancellables)
Task {
roomTimelineObservationToken = await timeline.addListener(listener: SDKListener { [weak self] timelineDiffs in
self?.serialDispatchQueue.sync {
self?.updateItemsWithDiffs(timelineDiffs)
}
})
}
}
// MARK: - Private
private func updateItemsWithDiffs(_ diffs: [TimelineDiff]) {
let span = MXLog.createSpan("process_timeline_list_diffs:\(kind)")
span.enter()
defer {
span.exit()
}
MXLog.verbose("Received diffs: \(diffs)")
itemProxies = diffs.reduce(itemProxies) { currentItems, diff in
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
MXLog.error("Failed building CollectionDifference from \(diff)")
return currentItems
}
guard let updatedItems = currentItems.applying(collectionDiff) else {
MXLog.error("Failed applying diff: \(collectionDiff)")
return currentItems
}
return updatedItems
}
}
private func buildDiff(from diff: TimelineDiff, on itemProxies: [TimelineItemProxy]) -> CollectionDifference<TimelineItemProxy>? {
var changes = [CollectionDifference<TimelineItemProxy>.Change]()
switch diff {
case .append(let items):
for (index, item) in items.enumerated() {
let itemProxy = TimelineItemProxy(item: item)
if itemProxy.isMembershipChange {
membershipChangeSubject.send(())
}
changes.append(.insert(offset: Int(itemProxies.count) + index, element: itemProxy, associatedWith: nil))
}
case .clear:
for (index, itemProxy) in itemProxies.enumerated() {
changes.append(.remove(offset: index, element: itemProxy, associatedWith: nil))
}
case .insert(let index, let item):
let itemProxy = TimelineItemProxy(item: item)
changes.append(.insert(offset: Int(index), element: itemProxy, associatedWith: nil))
case .popBack:
guard let itemProxy = itemProxies.last else { fatalError() }
changes.append(.remove(offset: itemProxies.count - 1, element: itemProxy, associatedWith: nil))
case .popFront:
guard let itemProxy = itemProxies.first else { fatalError() }
changes.append(.remove(offset: 0, element: itemProxy, associatedWith: nil))
case .pushBack(let item):
let itemProxy = TimelineItemProxy(item: item)
if itemProxy.isMembershipChange {
membershipChangeSubject.send(())
}
changes.append(.insert(offset: Int(itemProxies.count), element: itemProxy, associatedWith: nil))
case .pushFront(let item):
let itemProxy = TimelineItemProxy(item: item)
changes.append(.insert(offset: 0, element: itemProxy, associatedWith: nil))
case .remove(let index):
let itemProxy = itemProxies[Int(index)]
changes.append(.remove(offset: Int(index), element: itemProxy, associatedWith: nil))
case .reset(let items):
for (index, itemProxy) in itemProxies.enumerated() {
changes.append(.remove(offset: index, element: itemProxy, associatedWith: nil))
}
for (index, timelineItem) in items.enumerated() {
changes.append(.insert(offset: index, element: TimelineItemProxy(item: timelineItem), associatedWith: nil))
}
case .set(let index, let item):
let itemProxy = TimelineItemProxy(item: item)
changes.append(.remove(offset: Int(index), element: itemProxy, associatedWith: nil))
changes.append(.insert(offset: Int(index), element: itemProxy, associatedWith: nil))
case .truncate:
break
}
return CollectionDifference(changes)
}
}
private extension TimelineItemProxy {
var isMembershipChange: Bool {
switch self {
case .event(let eventTimelineItemProxy):
switch eventTimelineItemProxy.content {
case .roomMembership:
true
default:
false
}
case .virtual, .unknown:
false
}
}
}
private extension VirtualTimelineItem {
var description: String {
switch self {
case .dateDivider(let timestamp):
return "DayDiviver(\(timestamp))"
case .readMarker:
return "ReadMarker"
case .timelineStart:
return "TimelineStart"
}
}
}
private extension Array where Element == TimelineDiff {
var debugDescription: String {
"[" + map(\.debugDescription).joined(separator: ",") + "]"
}
}
extension TimelineDiff: @retroactive CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .append(let items):
return "Append(\(items.count))"
case .clear:
return "Clear"
case .insert:
return "Insert"
case .set(let index, _):
return "Set(\(index))"
case .remove(let index):
return "Remove(\(index))"
case .pushBack:
return "PushBack"
case .pushFront:
return "PushFront"
case .popBack:
return "PopBack"
case .popFront:
return "PopFront"
case .truncate(let length):
return "Truncate(\(length))"
case .reset(let items):
return "Reset(\(items.count)@\(items.startIndex)-\(items.endIndex))"
}
}
}