From d2fe6630033b647781ab61377420b05eee4e7dfd Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Fri, 7 Jul 2023 09:54:40 +0200 Subject: [PATCH] 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> --- ElementX.xcodeproj/project.pbxproj | 6 +-- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Mocks/Generated/GeneratedMocks.swift | 14 +++++ .../Mocks/Generated/SDKGeneratedMocks.swift | 25 --------- .../Sources/Services/Room/RoomProxy.swift | 40 ++++++++++++-- .../Timeline/MockRoomTimelineProvider.swift | 24 --------- .../Timeline/RoomTimelineProvider.swift | 54 +++++++++++-------- .../RoomTimelineProviderProtocol.swift | 16 ++++-- .../RoomTimelineController.swift | 21 +++++--- project.yml | 2 +- 10 files changed, 111 insertions(+), 95 deletions(-) delete mode 100644 ElementX/Sources/Services/Timeline/MockRoomTimelineProvider.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index f5fedd6e8..67871053d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -184,7 +184,6 @@ 43F35A7E5703D64DB0519C59 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */; }; 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; }; 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */; }; - 447E8580A0A2569E32529E17 /* MockRoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; }; 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; }; @@ -1142,7 +1141,6 @@ 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; - 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; @@ -3502,7 +3500,6 @@ isa = PBXGroup; children = ( 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */, - 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */, 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */, 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */, 00B62EE933FC3D5651AF4607 /* TimelineEventProxy.swift */, @@ -4300,7 +4297,6 @@ 2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */, 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */, 158A2D528CC78C4E7A8ED608 /* MockRoomTimelineControllerFactory.swift in Sources */, - 447E8580A0A2569E32529E17 /* MockRoomTimelineProvider.swift in Sources */, B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */, AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */, D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */, @@ -5207,7 +5203,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = "1.0.95-alpha"; + version = "1.0.96-alpha"; }; }; 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5f60712be..493fd82d2 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -111,8 +111,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "822c8b33dc6da7eb0951890af0197f633095188a", - "version" : "1.0.95-alpha" + "revision" : "8bc8015237083035cb5f4a00d1eedb9ebbbb83c6", + "version" : "1.0.96-alpha" } }, { diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 27f4fe2a2..42b00923d 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1075,6 +1075,20 @@ class RoomProxyMock: RoomProxyProtocol { } } } +class RoomTimelineProviderMock: RoomTimelineProviderProtocol { + var updatePublisher: AnyPublisher { + get { return underlyingUpdatePublisher } + set(value) { underlyingUpdatePublisher = value } + } + var underlyingUpdatePublisher: AnyPublisher! + var itemProxies: [TimelineItemProxy] = [] + var backPaginationState: BackPaginationStatus { + get { return underlyingBackPaginationState } + set(value) { underlyingBackPaginationState = value } + } + var underlyingBackPaginationState: BackPaginationStatus! + +} class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol { var callbacks: PassthroughSubject { get { return underlyingCallbacks } diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 8842e7f80..8c4d97280 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -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? diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 8dace4ced..4c31c72d3 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -34,7 +34,9 @@ class RoomProxy: RoomProxyProtocol { private(set) var displayName: String? private var roomTimelineObservationToken: TaskHandle? + private var backPaginationStateObservationToken: TaskHandle? + private let backPaginationStateSubject = PassthroughSubject() 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) + } +} diff --git a/ElementX/Sources/Services/Timeline/MockRoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/MockRoomTimelineProvider.swift deleted file mode 100644 index 2ddf025f3..000000000 --- a/ElementX/Sources/Services/Timeline/MockRoomTimelineProvider.swift +++ /dev/null @@ -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]() -} diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift index e04527ed0..941db7f1d 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift @@ -21,31 +21,41 @@ import MatrixRustSDK class RoomTimelineProvider: RoomTimelineProviderProtocol { private var cancellables = Set() 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) { - serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) - itemProxies = currentItems.map(TimelineItemProxy.init) + private let backPaginationStateSubject = CurrentValueSubject(.idle) + var backPaginationState: BackPaginationStatus { + backPaginationStateSubject.value + } + + private let itemProxiesSubject: CurrentValueSubject<[TimelineItemProxy], Never> + var itemProxies: [TimelineItemProxy] { + itemProxiesSubject.value + } + + var updatePublisher: AnyPublisher { + itemProxiesSubject + .combineLatest(backPaginationStateSubject) + .map(TimelineProviderUpdate.init) + .eraseToAnyPublisher() + } + + init(currentItems: [TimelineItem], + updatePublisher: AnyPublisher, + backPaginationStatePublisher: AnyPublisher) { + 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" } } } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift index 41ed0bf54..2e319ecb2 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift @@ -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 { get } + var itemProxies: [TimelineItemProxy] { get } + var backPaginationState: BackPaginationStatus { get } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 4bfa58a23..68e6466e4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -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 diff --git a/project.yml b/project.yml index bd27365ed..4a3f29e7f 100644 --- a/project.yml +++ b/project.yml @@ -44,7 +44,7 @@ include: packages: MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.0.95-alpha + exactVersion: 1.0.96-alpha # path: ../matrix-rust-sdk DesignKit: path: DesignKit