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
+14 -2
View File
@@ -127,6 +127,7 @@
12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */; };
12E6D052D055531A6783E21B /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */; };
12EC6BC99F373FE5C6EB9B64 /* TimelineMediaPreviewDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */; };
12F7172C93873638B23CD6AD /* RoomThreadListServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EB6035C1F33F25F1EBFB7D /* RoomThreadListServiceProxyProtocol.swift */; };
1307268DC41730E5BCF7D9A0 /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638790D3F915F0909315C47A /* PollView.swift */; };
1318721F4E5F307586D98112 /* VoiceMessageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */; };
13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; };
@@ -337,6 +338,7 @@
388D39ED9FE1122EA6D76BF2 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BC84BA0AF11C2128D58ABD /* Common.swift */; };
3895969759E68FAB90C63EF7 /* ElementCallServiceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */; };
38E0F9A4CB5237F039A2EEE6 /* RoomHistorySharingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722013E20EEED73553EC6F8D /* RoomHistorySharingState.swift */; };
38E8F5CC90DC0D716B799DE1 /* RoomThreadListServiceProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7396A6491DEC64FCB66F60A1 /* RoomThreadListServiceProxyMock.swift */; };
3982C505960006B341CFD0C6 /* UserDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */; };
3982E60F9C126437D5E488A3 /* PillContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */; };
39A987B3E41B976D1DF944C6 /* CallScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */; };
@@ -654,6 +656,7 @@
70558528EF68CAAEF09972D5 /* RoomTimelineItemFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */; };
706289B086B0A6B0C211763F /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; };
707E49BE07E8EB8A13C0EB1E /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */; };
708FC3184CCED825F0A36273 /* RoomThreadListServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE06F4A71FD46C9D8CD432E /* RoomThreadListServiceProxy.swift */; };
709A9B52FC26B4CB86B8B020 /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */; };
70B83D44043293B4B77440B9 /* PollFormScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */; };
71643093F87153F633A1B025 /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */; };
@@ -1602,6 +1605,7 @@
048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = "<group>"; };
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
04EB6035C1F33F25F1EBFB7D /* RoomThreadListServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListServiceProxyProtocol.swift; sourceTree = "<group>"; };
0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = "<group>"; };
052A8C0B00FE8AD7C972F2A4 /* LocationSharingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenViewModel.swift; sourceTree = "<group>"; };
052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = "<group>"; };
@@ -2003,7 +2007,7 @@
4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = "<group>"; };
4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = "<group>"; };
4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = "<group>"; };
4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper.icon; path = AppIcon.icon; sourceTree = "<group>"; };
4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; path = AppIcon.icon; sourceTree = "<group>"; };
4B2B564CA6570E1487A7C7CC /* SpaceRoomListProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxy.swift; sourceTree = "<group>"; };
4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = "<group>"; };
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
@@ -2203,6 +2207,7 @@
7310D8DFE01AF45F0689C3AA /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = "<group>"; };
7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportFlowCoordinator.swift; sourceTree = "<group>"; };
739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPreviewProxyProtocol.swift; sourceTree = "<group>"; };
7396A6491DEC64FCB66F60A1 /* RoomThreadListServiceProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListServiceProxyMock.swift; sourceTree = "<group>"; };
73A5C3F7C9C1DA10CAEC6A98 /* VoiceMessageRecordingComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingComposer.swift; sourceTree = "<group>"; };
73F3153AB9D628110878E24F /* libMatrixRustSDKMocks.a */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = archive.ar; path = libMatrixRustSDKMocks.a; sourceTree = BUILT_PRODUCTS_DIR; };
73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -2708,6 +2713,7 @@
CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHeaderView.swift; sourceTree = "<group>"; };
CC8E9ECC1118A92FAD5191CD /* SpaceAddRoomsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreen.swift; sourceTree = "<group>"; };
CCAA1B97A17A750AC706B112 /* SpaceServiceRoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceRoom.swift; sourceTree = "<group>"; };
CCE06F4A71FD46C9D8CD432E /* RoomThreadListServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListServiceProxy.swift; sourceTree = "<group>"; };
CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenViewModel.swift; sourceTree = "<group>"; };
CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = "<group>"; };
CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItemContent.swift; sourceTree = "<group>"; };
@@ -3714,6 +3720,7 @@
E944F717FC10A428D027074D /* RoomPowerLevelsProxyMock.swift */,
6695C64F066628411EAD21E9 /* RoomPreviewProxyMock.swift */,
FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */,
7396A6491DEC64FCB66F60A1 /* RoomThreadListServiceProxyMock.swift */,
4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */,
248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */,
7DB2A5FF1E68BA580A20D405 /* SpaceRoomListProxyMock.swift */,
@@ -4087,6 +4094,8 @@
CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */,
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */,
578AF9CE60816069536C0953 /* RoomRole.swift */,
CCE06F4A71FD46C9D8CD432E /* RoomThreadListServiceProxy.swift */,
04EB6035C1F33F25F1EBFB7D /* RoomThreadListServiceProxyProtocol.swift */,
2C0F49BD446849654C0D24E0 /* RoomMember */,
4DC0344D2EBD0AE5D71754A9 /* RoomMembershipDetails */,
7FC3F8FA5EA765AC3B000F55 /* RoomPreview */,
@@ -7250,6 +7259,7 @@
};
};
buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@@ -7325,7 +7335,6 @@
C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 681566846AF307E9BA4C72C6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
@@ -8649,6 +8658,9 @@
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */,
B5899F18AD6C56CE08FE532B /* RoomSummaryProviderMock.swift in Sources */,
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */,
708FC3184CCED825F0A36273 /* RoomThreadListServiceProxy.swift in Sources */,
38E8F5CC90DC0D716B799DE1 /* RoomThreadListServiceProxyMock.swift in Sources */,
12F7172C93873638B23CD6AD /* RoomThreadListServiceProxyProtocol.swift in Sources */,
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */,
13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */,
70558528EF68CAAEF09972D5 /* RoomTimelineItemFixtures.swift in Sources */,
@@ -4,8 +4,7 @@
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -30,12 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -45,10 +38,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
@@ -58,6 +47,12 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -79,8 +74,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "HTTPS_PROXY"
@@ -115,8 +108,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
@@ -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)
]
}
+12
View File
@@ -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
}