mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
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:
committed by
Stefan Ceriu
parent
823e2afa97
commit
e97ed6ec5c
@@ -6930,6 +6930,70 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
return threadTimelineEventIDReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - threadListService
|
||||
|
||||
var threadListServiceUnderlyingCallsCount = 0
|
||||
var threadListServiceCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return threadListServiceUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = threadListServiceUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
threadListServiceUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
threadListServiceUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var threadListServiceCalled: Bool {
|
||||
return threadListServiceCallsCount > 0
|
||||
}
|
||||
|
||||
var threadListServiceUnderlyingReturnValue: RoomThreadListServiceProxyProtocol!
|
||||
var threadListServiceReturnValue: RoomThreadListServiceProxyProtocol! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return threadListServiceUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: RoomThreadListServiceProxyProtocol? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = threadListServiceUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
threadListServiceUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
threadListServiceUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var threadListServiceClosure: (() async -> RoomThreadListServiceProxyProtocol)?
|
||||
|
||||
func threadListService() async -> RoomThreadListServiceProxyProtocol {
|
||||
threadListServiceCallsCount += 1
|
||||
if let threadListServiceClosure = threadListServiceClosure {
|
||||
return await threadListServiceClosure()
|
||||
} else {
|
||||
return threadListServiceReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - loadOrFetchEventDetails
|
||||
|
||||
var loadOrFetchEventDetailsForUnderlyingCallsCount = 0
|
||||
@@ -15957,6 +16021,83 @@ class RoomSummaryProviderMock: RoomSummaryProviderProtocol, @unchecked Sendable
|
||||
setRoomListClosure?(roomList)
|
||||
}
|
||||
}
|
||||
class RoomThreadListServiceProxyMock: RoomThreadListServiceProxyProtocol, @unchecked Sendable {
|
||||
var itemsPublisher: CurrentValuePublisher<[RoomThreadListItem], Never> {
|
||||
get { return underlyingItemsPublisher }
|
||||
set(value) { underlyingItemsPublisher = value }
|
||||
}
|
||||
var underlyingItemsPublisher: CurrentValuePublisher<[RoomThreadListItem], Never>!
|
||||
var paginationStatePublisher: CurrentValuePublisher<RoomThreadListPaginationState, Never> {
|
||||
get { return underlyingPaginationStatePublisher }
|
||||
set(value) { underlyingPaginationStatePublisher = value }
|
||||
}
|
||||
var underlyingPaginationStatePublisher: CurrentValuePublisher<RoomThreadListPaginationState, Never>!
|
||||
|
||||
//MARK: - paginate
|
||||
|
||||
var paginateUnderlyingCallsCount = 0
|
||||
var paginateCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return paginateUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = paginateUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
paginateUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
paginateUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var paginateCalled: Bool {
|
||||
return paginateCallsCount > 0
|
||||
}
|
||||
|
||||
var paginateUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var paginateReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return paginateUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = paginateUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
paginateUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
paginateUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var paginateClosure: (() async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func paginate() async -> Result<Void, RoomProxyError> {
|
||||
paginateCallsCount += 1
|
||||
if let paginateClosure = paginateClosure {
|
||||
return await paginateClosure()
|
||||
} else {
|
||||
return paginateReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class SecureBackupControllerMock: SecureBackupControllerProtocol, @unchecked Sendable {
|
||||
var recoveryState: CurrentValuePublisher<SecureBackupRecoveryState, Never> {
|
||||
get { return underlyingRecoveryState }
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
struct RoomThreadListProxyMockConfiguration {
|
||||
var items: [RoomThreadListItem] = .mocks
|
||||
}
|
||||
|
||||
extension RoomThreadListServiceProxyMock {
|
||||
convenience init(_ configuration: RoomThreadListProxyMockConfiguration) {
|
||||
self.init()
|
||||
|
||||
itemsPublisher = CurrentValueSubject(configuration.items).asCurrentValuePublisher()
|
||||
|
||||
paginateReturnValue = .success(())
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == RoomThreadListItem {
|
||||
static let mocks: [RoomThreadListItem] = [
|
||||
.init(id: "1",
|
||||
rootMessageDetails: .init(sender: .init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: .mockMXCUserAvatar),
|
||||
timestamp: .distantPast,
|
||||
message: .init("Ping")),
|
||||
latestMessageDetails: .init(sender: .init(id: "@bob:matrix.org"),
|
||||
timestamp: .distantFuture,
|
||||
message: .init("Pong")),
|
||||
numberOfReplies: 5),
|
||||
.init(id: "2",
|
||||
rootMessageDetails: .init(sender: .init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: .mockMXCUserAvatar),
|
||||
timestamp: .distantPast,
|
||||
message: .init("Can we schedule a meeting for next week?")),
|
||||
latestMessageDetails: .init(sender: .init(id: "@bob:matrix.org"),
|
||||
timestamp: .distantFuture,
|
||||
message: .init("Looking forward to our next steps!")),
|
||||
numberOfReplies: 10)
|
||||
]
|
||||
}
|
||||
@@ -197,6 +197,18 @@ extension SDKListener: KnockRequestsListener where T == [KnockRequest] {
|
||||
}
|
||||
}
|
||||
|
||||
extension SDKListener: ThreadListEntriesListener where T == [ThreadListUpdate] {
|
||||
func onUpdate(diff: [ThreadListUpdate]) {
|
||||
onUpdateClosure(diff)
|
||||
}
|
||||
}
|
||||
|
||||
extension SDKListener: ThreadListPaginationStateListener where T == ThreadListPaginationState {
|
||||
func onUpdate(state: ThreadListPaginationState) {
|
||||
onUpdateClosure(state)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TimelineProxy
|
||||
|
||||
extension SDKListener: PaginationStatusListener where T == PaginationStatus {
|
||||
|
||||
@@ -64,6 +64,8 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
let spaceService: SpaceServiceProxyProtocol
|
||||
|
||||
let eventStringBuilder: RoomEventStringBuilder
|
||||
|
||||
private static var roomCreationPowerLevelOverrides: PowerLevels {
|
||||
.init(usersDefault: nil,
|
||||
eventsDefault: nil,
|
||||
@@ -216,6 +218,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
roomSummaryProvider = configuredAppService.roomSummaryProvider
|
||||
alternateRoomSummaryProvider = configuredAppService.alternateRoomSummaryProvider
|
||||
staticRoomSummaryProvider = configuredAppService.staticRoomSummaryProvider
|
||||
eventStringBuilder = configuredAppService.eventStringBuilder
|
||||
|
||||
syncServiceStateUpdateTaskHandle = createSyncServiceStateObserver(syncService)
|
||||
roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService)
|
||||
@@ -1169,7 +1172,8 @@ class ClientProxy: ClientProxyProtocol {
|
||||
let roomProxy = try await JoinedRoomProxy(roomListService: roomListService,
|
||||
room: room,
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService)
|
||||
analyticsService: analyticsService,
|
||||
eventStringBuilder: eventStringBuilder)
|
||||
|
||||
return .joined(roomProxy)
|
||||
case .left:
|
||||
@@ -1318,6 +1322,7 @@ private struct ClientProxyServices {
|
||||
let roomSummaryProvider: RoomSummaryProviderProtocol
|
||||
let alternateRoomSummaryProvider: RoomSummaryProviderProtocol
|
||||
let staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol
|
||||
let eventStringBuilder: RoomEventStringBuilder
|
||||
|
||||
init(client: ClientProtocol,
|
||||
actionsSubject: PassthroughSubject<ClientProxyAction, Never>,
|
||||
@@ -1334,10 +1339,11 @@ private struct ClientProxyServices {
|
||||
let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList",
|
||||
mentionBuilder: PlainMentionBuilder()),
|
||||
style: .senderPrefixed)
|
||||
let eventStringBuilder = try RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: client.userId(), shouldDisambiguateDisplayNames: false),
|
||||
messageEventStringBuilder: roomMessageEventStringBuilder,
|
||||
shouldDisambiguateDisplayNames: false,
|
||||
shouldPrefixSenderName: true)
|
||||
|
||||
eventStringBuilder = try RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: client.userId(), shouldDisambiguateDisplayNames: false),
|
||||
messageEventStringBuilder: roomMessageEventStringBuilder,
|
||||
shouldDisambiguateDisplayNames: false,
|
||||
shouldPrefixSenderName: true)
|
||||
|
||||
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
||||
eventStringBuilder: eventStringBuilder,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user