Introduce the RoomThreadListServiceProxy and expose it from the RoomProxy

Also update the sdk and app mocks and add a simple `RoomThreadListServiceProxyMock`

# Conflicts:
#	ElementX.xcodeproj/project.pbxproj
#	ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
#	SDKMocks/Sources/Generated/SDKGeneratedMocks.swift
#	project.yml
This commit is contained in:
Stefan Ceriu
2026-03-23 17:43:38 +02:00
committed by Stefan Ceriu
parent 823e2afa97
commit e97ed6ec5c
10 changed files with 377 additions and 24 deletions
@@ -16,6 +16,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private let room: RoomProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
private let eventStringBuilder: RoomEventStringBuilder
// periphery:ignore - required for instance retention in the rust codebase
private var roomInfoObservationToken: TaskHandle?
@@ -78,11 +79,13 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
init(roomListService: RoomListServiceProtocol,
room: RoomProtocol,
appSettings: AppSettings,
analyticsService: AnalyticsService) async throws {
analyticsService: AnalyticsService,
eventStringBuilder: RoomEventStringBuilder) async throws {
self.roomListService = roomListService
self.room = room
self.appSettings = appSettings
self.analyticsService = analyticsService
self.eventStringBuilder = eventStringBuilder
infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo()))
@@ -200,6 +203,10 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
}
}
func threadListService() async -> RoomThreadListServiceProxyProtocol {
await RoomThreadListProxy(threadListService: room.threadListService(), eventStringBuilder: eventStringBuilder)
}
func loadOrFetchEventDetails(for eventID: String) async -> Result<TimelineEvent, RoomProxyError> {
do {
let event = try await room.loadOrFetchEvent(eventId: eventID)
@@ -98,6 +98,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
func threadTimeline(eventID: String) async -> Result<TimelineProxyProtocol, RoomProxyError>
func threadListService() async -> RoomThreadListServiceProxyProtocol
func loadOrFetchEventDetails(for eventID: String) async -> Result<TimelineEvent, RoomProxyError>
func messageFilteredTimeline(focus: TimelineFocus,
@@ -0,0 +1,101 @@
//
// Copyright 2026 Element Creations 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 RoomThreadListProxy: RoomThreadListServiceProxyProtocol {
private let threadListService: ThreadListServiceProtocol
private let eventStringBuilder: RoomEventStringBuilder
private var itemUpdatesHandle: TaskHandle?
private let itemsSubject = CurrentValueSubject<[RoomThreadListItem], Never>([])
var itemsPublisher: CurrentValuePublisher<[RoomThreadListItem], Never> {
itemsSubject.asCurrentValuePublisher()
}
private var paginationStateHandle: TaskHandle?
let paginationStatePublisher: CurrentValuePublisher<RoomThreadListPaginationState, Never>
init(threadListService: ThreadListServiceProtocol, eventStringBuilder: RoomEventStringBuilder, paginationToken: String? = nil) {
self.threadListService = threadListService
self.eventStringBuilder = eventStringBuilder
let paginationStateSubject = CurrentValueSubject<RoomThreadListPaginationState, Never>(.init(sdkState: threadListService.paginationState()))
paginationStatePublisher = paginationStateSubject.asCurrentValuePublisher()
paginationStateHandle = threadListService.subscribeToPaginationStateUpdates(listener: SDKListener { state in
paginationStateSubject.send(.init(sdkState: state))
})
updateItems()
itemUpdatesHandle = self.threadListService.subscribeToItemsUpdates(listener: SDKListener { [weak self] _ in
self?.updateItems()
})
}
func paginate() async -> Result<Void, RoomProxyError> {
do {
try await threadListService.paginate()
return .success(())
} catch {
MXLog.error("Failed paginating with error: \(error)")
return .failure(.sdkError(error))
}
}
// MARK: - Private
private func updateItems() {
let items = threadListService.items().map { item in
guard let rootMessageDetails = buildMessageDetails(item.rootEvent) else {
fatalError("This should never fail™")
}
let latestMessageDetails = buildMessageDetails(item.latestEvent)
return RoomThreadListItem(id: item.rootEvent.eventId,
rootMessageDetails: rootMessageDetails,
latestMessageDetails: latestMessageDetails,
numberOfReplies: UInt(item.numReplies))
}
itemsSubject.send(items)
}
private func buildMessageDetails(_ threadListItemEvent: ThreadListItemEvent?) -> RoomThreadListItem.MessageDetails? {
guard let threadListItemEvent else {
return nil
}
let sender = TimelineItemSender(senderID: threadListItemEvent.sender, senderProfile: threadListItemEvent.senderProfile)
let timestamp = Date(timeIntervalSince1970: TimeInterval(threadListItemEvent.timestamp / 1000))
let message: AttributedString? = if let content = threadListItemEvent.content {
eventStringBuilder.buildAttributedString(for: content,
sender: sender,
isOutgoing: threadListItemEvent.isOwn)
} else {
nil
}
return .init(sender: sender, timestamp: timestamp, message: message)
}
}
extension RoomThreadListPaginationState {
init(sdkState: MatrixRustSDK.ThreadListPaginationState) {
switch sdkState {
case .loading:
self = .loading
case .idle(let endReached):
self = .idle(endReached: endReached)
}
}
}
@@ -0,0 +1,38 @@
//
// Copyright 2026 Element Creations 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
// sourcery: AutoMockable
protocol RoomThreadListServiceProxyProtocol {
var itemsPublisher: CurrentValuePublisher<[RoomThreadListItem], Never> { get }
var paginationStatePublisher: CurrentValuePublisher<RoomThreadListPaginationState, Never> { get }
func paginate() async -> Result<Void, RoomProxyError>
}
struct RoomThreadListItem: Identifiable {
struct MessageDetails {
let sender: TimelineItemSender
let timestamp: Date
let message: AttributedString?
}
let id: String
let rootMessageDetails: MessageDetails
let latestMessageDetails: MessageDetails?
let numberOfReplies: UInt
}
enum RoomThreadListPaginationState: Equatable {
case idle(endReached: Bool)
case loading
}