SDK Bump + Back pagination state listener (#1274)

* backPagination improved

* sdk bump

* removed unused line

* Update ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* cancelling the TaskHandle on deallocation

---------

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Mauro
2023-07-07 09:54:40 +02:00
committed by GitHub
parent 13b7357c66
commit d2fe663003
10 changed files with 111 additions and 95 deletions
@@ -1075,6 +1075,20 @@ class RoomProxyMock: RoomProxyProtocol {
}
}
}
class RoomTimelineProviderMock: RoomTimelineProviderProtocol {
var updatePublisher: AnyPublisher<TimelineProviderUpdate, Never> {
get { return underlyingUpdatePublisher }
set(value) { underlyingUpdatePublisher = value }
}
var underlyingUpdatePublisher: AnyPublisher<TimelineProviderUpdate, Never>!
var itemProxies: [TimelineItemProxy] = []
var backPaginationState: BackPaginationStatus {
get { return underlyingBackPaginationState }
set(value) { underlyingBackPaginationState = value }
}
var underlyingBackPaginationState: BackPaginationStatus!
}
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
get { return underlyingCallbacks }
@@ -651,31 +651,6 @@ class SDKClientMock: SDKClientProtocol {
setPusherIdentifiersKindAppDisplayNameDeviceDisplayNameProfileTagLangReceivedInvocations.append((identifiers: identifiers, kind: kind, appDisplayName: appDisplayName, deviceDisplayName: deviceDisplayName, profileTag: profileTag, lang: lang))
try setPusherIdentifiersKindAppDisplayNameDeviceDisplayNameProfileTagLangClosure?(`identifiers`, `kind`, `appDisplayName`, `deviceDisplayName`, `profileTag`, `lang`)
}
//MARK: - `slidingSync`
public var slidingSyncIdThrowableError: Error?
public var slidingSyncIdCallsCount = 0
public var slidingSyncIdCalled: Bool {
return slidingSyncIdCallsCount > 0
}
public var slidingSyncIdReceivedId: String?
public var slidingSyncIdReceivedInvocations: [String] = []
public var slidingSyncIdReturnValue: SlidingSyncBuilder!
public var slidingSyncIdClosure: ((String) throws -> SlidingSyncBuilder)?
public func `slidingSync`(`id`: String) throws -> SlidingSyncBuilder {
if let error = slidingSyncIdThrowableError {
throw error
}
slidingSyncIdCallsCount += 1
slidingSyncIdReceivedId = id
slidingSyncIdReceivedInvocations.append(`id`)
if let slidingSyncIdClosure = slidingSyncIdClosure {
return try slidingSyncIdClosure(`id`)
} else {
return slidingSyncIdReturnValue
}
}
//MARK: - `unignoreUser`
public var unignoreUserUserIdThrowableError: Error?
+35 -5
View File
@@ -34,7 +34,9 @@ class RoomProxy: RoomProxyProtocol {
private(set) var displayName: String?
private var roomTimelineObservationToken: TaskHandle?
private var backPaginationStateObservationToken: TaskHandle?
private let backPaginationStateSubject = PassthroughSubject<BackPaginationStatus, Never>()
private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> {
membersSubject.eraseToAnyPublisher()
@@ -50,9 +52,10 @@ class RoomProxy: RoomProxyProtocol {
var timelineProvider: RoomTimelineProviderProtocol {
innerTimelineProvider
}
deinit {
roomTimelineObservationToken?.cancel()
backPaginationStateObservationToken?.cancel()
roomListItem.unsubscribe()
}
@@ -79,9 +82,13 @@ class RoomProxy: RoomProxyProtocol {
let result = await room.addTimelineListener(listener: timelineListener)
roomTimelineObservationToken = result.itemsStream
subscribeToBackpagination()
innerTimelineProvider = RoomTimelineProvider(currentItems: result.items, updatePublisher: updatesPublisher)
innerTimelineProvider = RoomTimelineProvider(currentItems: result.items,
updatePublisher: updatesPublisher,
backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher())
Task {
await fetchMembers()
await updateMembers()
@@ -651,9 +658,20 @@ class RoomProxy: RoomProxyProtocol {
private func update(displayName: String) {
self.displayName = displayName
}
private func subscribeToBackpagination() {
let listener = RoomBackpaginationStatusListener { [weak self] status in
self?.backPaginationStateSubject.send(status)
}
do {
backPaginationStateObservationToken = try room.subscribeToBackPaginationStatus(listener: listener)
} catch {
MXLog.error("Failed to subscribe to back pagination state with error: \(error)")
}
}
}
private class RoomTimelineListener: TimelineListener {
private final class RoomTimelineListener: TimelineListener {
private let onUpdateClosure: (TimelineDiff) -> Void
init(_ onUpdateClosure: @escaping (TimelineDiff) -> Void) {
@@ -665,7 +683,7 @@ private class RoomTimelineListener: TimelineListener {
}
}
private class UploadProgressListener: ProgressWatcher {
private final class UploadProgressListener: ProgressWatcher {
private let onUpdateClosure: (Double) -> Void
init(_ onUpdateClosure: @escaping (Double) -> Void) {
@@ -678,3 +696,15 @@ private class UploadProgressListener: ProgressWatcher {
}
}
}
private final class RoomBackpaginationStatusListener: BackPaginationStatusListener {
private let onUpdateClosure: (BackPaginationStatus) -> Void
init(_ onUpdateClosure: @escaping (BackPaginationStatus) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func onUpdate(status: BackPaginationStatus) {
onUpdateClosure(status)
}
}
@@ -1,24 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import MatrixRustSDK
struct MockRoomTimelineProvider: RoomTimelineProviderProtocol {
var itemsPublisher = CurrentValuePublisher<[TimelineItemProxy], Never>([])
private var itemProxies = [TimelineItemProxy]()
}
@@ -21,31 +21,41 @@ import MatrixRustSDK
class RoomTimelineProvider: RoomTimelineProviderProtocol {
private var cancellables = Set<AnyCancellable>()
private let serialDispatchQueue: DispatchQueue
private let itemsSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
var itemsPublisher: CurrentValuePublisher<[TimelineItemProxy], Never> {
itemsSubject.asCurrentValuePublisher()
}
private var itemProxies: [TimelineItemProxy] {
didSet {
itemsSubject.send(itemProxies)
}
}
init(currentItems: [TimelineItem], updatePublisher: AnyPublisher<TimelineDiff, Never>) {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
itemProxies = currentItems.map(TimelineItemProxy.init)
private let backPaginationStateSubject = CurrentValueSubject<BackPaginationStatus, Never>(.idle)
var backPaginationState: BackPaginationStatus {
backPaginationStateSubject.value
}
private let itemProxiesSubject: CurrentValueSubject<[TimelineItemProxy], Never>
var itemProxies: [TimelineItemProxy] {
itemProxiesSubject.value
}
var updatePublisher: AnyPublisher<TimelineProviderUpdate, Never> {
itemProxiesSubject
.combineLatest(backPaginationStateSubject)
.map(TimelineProviderUpdate.init)
.eraseToAnyPublisher()
}
init(currentItems: [TimelineItem],
updatePublisher: AnyPublisher<TimelineDiff, Never>,
backPaginationStatePublisher: AnyPublisher<BackPaginationStatus, Never>) {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>(currentItems.map(TimelineItemProxy.init))
// Manually call it here as the didSet doesn't work from constructors
itemsSubject.send(itemProxies)
itemProxiesSubject.send(itemProxies)
updatePublisher
.collect(.byTime(serialDispatchQueue, 0.1))
.sink { [weak self] in self?.updateItemsWithDiffs($0) }
.store(in: &cancellables)
backPaginationStatePublisher
.sink { [weak self] in self?.backPaginationStateSubject.send($0) }
.store(in: &cancellables)
}
// MARK: - Private
@@ -59,7 +69,7 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
MXLog.verbose("Received timeline diff")
itemProxies = diffs
let items = diffs
.reduce(itemProxies) { currentItems, diff in
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
MXLog.error("Failed building CollectionDifference from \(diff)")
@@ -73,6 +83,8 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
return updatedItems
}
itemProxiesSubject.send(items)
MXLog.verbose("Finished applying diffs, current items (\(itemProxies.count)) : \(itemProxies.map(\.debugIdentifier))")
}
@@ -186,12 +198,8 @@ private extension VirtualTimelineItem {
switch self {
case .dayDivider(let timestamp):
return "DayDiviver(\(timestamp))"
case .loadingIndicator:
return "LoadingIndicator"
case .readMarker:
return "ReadMarker"
case .timelineStart:
return "TimelineStart"
}
}
}
@@ -17,7 +17,17 @@
import Combine
import Foundation
@MainActor
protocol RoomTimelineProviderProtocol {
var itemsPublisher: CurrentValuePublisher<[TimelineItemProxy], Never> { get }
import MatrixRustSDK
struct TimelineProviderUpdate {
let items: [TimelineItemProxy]
let backPaginationState: BackPaginationStatus
}
@MainActor
// sourcery: AutoMockable
protocol RoomTimelineProviderProtocol {
var updatePublisher: AnyPublisher<TimelineProviderUpdate, Never> { get }
var itemProxies: [TimelineItemProxy] { get }
var backPaginationState: BackPaginationStatus { get }
}
@@ -53,7 +53,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
timelineProvider
.itemsPublisher
.updatePublisher
.receive(on: serialDispatchQueue)
.sink { [weak self] _ in
guard let self else { return }
@@ -183,7 +183,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Handle this parallel to the timeline items so we're not forced
// to bundle the Rust side objects within them
func debugInfo(for itemID: String) -> TimelineItemDebugInfo {
for timelineItemProxy in timelineProvider.itemsPublisher.value {
for timelineItemProxy in timelineProvider.itemProxies {
switch timelineItemProxy {
case .event(let item):
if item.id == itemID {
@@ -245,7 +245,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
var isBackPaginating = false
var lastEncryptedHistoryItemIndex: Int?
let collapsibleChunks = timelineProvider.itemsPublisher.value.groupBy { isItemCollapsible($0) }
let collapsibleChunks = timelineProvider.itemProxies.groupBy { isItemCollapsible($0) }
for (index, collapsibleChunk) in collapsibleChunks.enumerated() {
let isLastItem = index == collapsibleChunks.indices.last
@@ -294,6 +294,17 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Remove everything up to the last encrypted history item.
// It only contains encrypted messages, state changes and date separators.
newTimelineItems.removeFirst(lastEncryptedHistoryItemIndex)
} else {
// Otherwise check if we need to add anything to the top of the timeline.
switch timelineProvider.backPaginationState {
case .timelineStartReached:
let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.displayName ?? roomProxy.name)
newTimelineItems.insert(timelineStart, at: 0)
case .paginating:
newTimelineItems.insert(PaginationIndicatorRoomTimelineItem(), at: 0)
case .idle:
break
}
}
timelineItems = newTimelineItems
@@ -330,10 +341,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return SeparatorRoomTimelineItem(id: identifier, text: dateString)
case .readMarker:
return ReadMarkerRoomTimelineItem()
case .loadingIndicator:
return PaginationIndicatorRoomTimelineItem()
case .timelineStart:
return TimelineStartRoomTimelineItem(name: roomProxy.displayName ?? roomProxy.name)
}
case .unknown:
return nil