Add a new RoomThreadListScreen and hook it up to the RoomThreadListService

It will automatically paginate to fill the screen and update the list as updates come in.
This commit is contained in:
Stefan Ceriu
2026-03-23 17:55:50 +02:00
committed by Stefan Ceriu
parent d27b6697a9
commit ba810116a0
16 changed files with 413 additions and 8 deletions

View File

@@ -591,6 +591,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "RoomSelectionScreen_Previews")
}
func testRoomThreadListScreen() async throws {
try await performAccessibilityAudit(named: "RoomThreadListScreen_Previews")
}
func testSFNumberedListView() async throws {
try await performAccessibilityAudit(named: "SFNumberedListView_Previews")
}

View File

@@ -515,6 +515,7 @@
572474C7CA4B03FF0B5DF548 /* ChatsTabFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C782FCBBCC9A0CD30453C50 /* ChatsTabFlowCoordinator.swift */; };
5732395A4F71F51F9C754C5A /* ElementCallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33AE897D86784CCA5E4E9227 /* ElementCallService.swift */; };
5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */; };
5787613A79C6D553DEA28C5F /* RoomThreadListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F91AC45CDDF85E946391CA5C /* RoomThreadListScreenViewModel.swift */; };
583A41A4BE76E2E9E0B97881 /* ResolveVerifiedUserSendFailureScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */; };
585DCA0487A0A6F4E59EF5CA /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353024006CB726E9F9187B3A /* AnalyticsConsentState.swift */; };
5877255D6E6ED898D402ED8D /* LocationPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408ACC0D28656F82A5EB6A7E /* LocationPickerSheet.swift */; };
@@ -683,6 +684,7 @@
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; };
756EA0D663261889EF64E6D4 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */; };
7573D682F089205F7F1D96CF /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; };
757862045774A0F458357E19 /* RoomThreadListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544B9262E1BC6F489C03FFFA /* RoomThreadListScreen.swift */; };
75AD7C09BD604A68E2FAA1D9 /* OIDCConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D354D4232DED9649FD0FF4 /* OIDCConfiguration.swift */; };
75ED4B73983228BB6922CE3C /* KnockRequestsListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5C217DD0749EC709EED028 /* KnockRequestsListScreenViewModelProtocol.swift */; };
761EA50B2619307AB30891B8 /* PhishingDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB07F03461023BC39C730922 /* PhishingDetector.swift */; };
@@ -692,6 +694,7 @@
763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; };
7640A4B412CACF15D143CCD4 /* Strings+SAS.swift in Sources */ = {isa = PBXBuildFile; fileRef = B172057567E049007A5C4D92 /* Strings+SAS.swift */; };
767D366C40F1311CFA333763 /* PillContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86376BEE425704AEE197CA54 /* PillContext.swift */; };
76B6046788017DEF088FBF87 /* RoomThreadListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3534D37BFC074DD26F2FFE /* RoomThreadListScreenModels.swift */; };
76C874243A8C440D6CF7B344 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
77574A519A4E484880053EAD /* IdentityConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */; };
@@ -752,6 +755,7 @@
7FF6E1FBE6E9517FD29A1D8E /* RoomChangeRolesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48A5C34C4E4268EF65D171EF /* RoomChangeRolesScreenModels.swift */; };
8015842CB4DE1BE414D2CDED /* AppLockSetupBiometricsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C62E07C1164F5120727A2A8 /* AppLockSetupBiometricsScreenCoordinator.swift */; };
804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; };
805D16A15BDF97B4EA8D3EC6 /* RoomThreadListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14FD296A75F5F5637EDC365 /* RoomThreadListScreenCoordinator.swift */; };
80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */; };
80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */; };
81CFE6FE42DF26BBCEDC7FF2 /* JoinCallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABC939BC8F08CA3E967D6C /* JoinCallButton.swift */; };
@@ -1198,6 +1202,7 @@
CDAE3A37D4DF136F9D07DB61 /* RoomChangeRolesScreenSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF710CB1C31F8938EAA3A7D /* RoomChangeRolesScreenSection.swift */; };
CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; };
CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; };
CE23FE7F463AD5C7D80353AA /* RoomThreadListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008D864B3F51B41DF483B860 /* RoomThreadListScreenViewModelProtocol.swift */; };
CE3B7FC34FB2C279AAA5EA01 /* AVMetadataMachineReadableCodeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */; };
CE4B342F9DD747CF4BEDB5AB /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43F773904F87FF5ADFE4DD1 /* TestablePreview.swift */; };
CE6F237360875D3D573FD0B2 /* RoomNotificationSettingsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */; };
@@ -1578,6 +1583,7 @@
002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreen.swift; sourceTree = "<group>"; };
00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = "<group>"; };
007C16779FDCF10DA4F1A510 /* LinkNewDeviceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceService.swift; sourceTree = "<group>"; };
008D864B3F51B41DF483B860 /* RoomThreadListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreenViewModelProtocol.swift; sourceTree = "<group>"; };
00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreen.swift; sourceTree = "<group>"; };
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
011AFA4990C585D157829679 /* DeclineAndBlockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModel.swift; sourceTree = "<group>"; };
@@ -1633,6 +1639,7 @@
094F6B21835890B470DF540C /* SpaceScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenCoordinator.swift; sourceTree = "<group>"; };
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
0A2074C0449B83D5858BD2D7 /* FrequentlyUsedEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrequentlyUsedEmoji.swift; sourceTree = "<group>"; };
0A3534D37BFC074DD26F2FFE /* RoomThreadListScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreenModels.swift; sourceTree = "<group>"; };
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = "<group>"; };
0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = "<group>"; };
@@ -2054,6 +2061,7 @@
53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewViewModel.swift; sourceTree = "<group>"; };
53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = test_animated_image.gif; sourceTree = "<group>"; };
5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
544B9262E1BC6F489C03FFFA /* RoomThreadListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreen.swift; sourceTree = "<group>"; };
5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreen.swift; sourceTree = "<group>"; };
54A5E6F398C269AD52C9AE21 /* EncryptionResetPasswordScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenModels.swift; sourceTree = "<group>"; };
54A7923F0115CF17ABC8047F /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/SAS.strings; sourceTree = "<group>"; };
@@ -2468,6 +2476,7 @@
A1087DCC491CD4C027173DDA /* EmojiPickerScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModelTests.swift; sourceTree = "<group>"; };
A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = "<group>"; };
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A14FD296A75F5F5637EDC365 /* RoomThreadListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreenCoordinator.swift; sourceTree = "<group>"; };
A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = "<group>"; };
A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenCoordinator.swift; sourceTree = "<group>"; };
A175D0FDEDBFA44C47FE13AE /* MediaEventsTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2950,6 +2959,7 @@
F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinatorTests.swift; sourceTree = "<group>"; };
F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionScreenTests.swift; sourceTree = "<group>"; };
F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreen.swift; sourceTree = "<group>"; };
F91AC45CDDF85E946391CA5C /* RoomThreadListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreenViewModel.swift; sourceTree = "<group>"; };
F9E543072DE58E751F028998 /* TimelineProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxy.swift; sourceTree = "<group>"; };
FA2397174D0DC3918A7A8A7B /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = "<group>"; };
FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkTests.swift; sourceTree = "<group>"; };
@@ -5060,6 +5070,14 @@
path = Media;
sourceTree = "<group>";
};
7AC9A0C3B3506D052CAC7743 /* View */ = {
isa = PBXGroup;
children = (
544B9262E1BC6F489C03FFFA /* RoomThreadListScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
7AE042B6E4318E352DD3991A /* View */ = {
isa = PBXGroup;
children = (
@@ -5942,6 +5960,18 @@
path = Sources;
sourceTree = "<group>";
};
B329D7B5D250717039B2814A /* RoomThreadListScreen */ = {
isa = PBXGroup;
children = (
A14FD296A75F5F5637EDC365 /* RoomThreadListScreenCoordinator.swift */,
0A3534D37BFC074DD26F2FFE /* RoomThreadListScreenModels.swift */,
F91AC45CDDF85E946391CA5C /* RoomThreadListScreenViewModel.swift */,
008D864B3F51B41DF483B860 /* RoomThreadListScreenViewModelProtocol.swift */,
7AC9A0C3B3506D052CAC7743 /* View */,
);
path = RoomThreadListScreen;
sourceTree = "<group>";
};
B364E08924AD15820350CDD9 /* SettingsScreen */ = {
isa = PBXGroup;
children = (
@@ -6599,6 +6629,7 @@
7B890CCD20B037760BFDF957 /* RoomRolesAndPermissionsScreen */,
679E9837ECA8D6776079D16E /* RoomScreen */,
2E42D43DB6835A58D88B2F91 /* RoomSelectionScreen */,
B329D7B5D250717039B2814A /* RoomThreadListScreen */,
2565414373E6F68005966B8E /* SecureBackup */,
C59BA103987B953BA374509F /* SecurityAndPrivacyScreen */,
70B74A432C241E56A7ACE610 /* Settings */,
@@ -8658,6 +8689,11 @@
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */,
B5899F18AD6C56CE08FE532B /* RoomSummaryProviderMock.swift in Sources */,
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */,
757862045774A0F458357E19 /* RoomThreadListScreen.swift in Sources */,
805D16A15BDF97B4EA8D3EC6 /* RoomThreadListScreenCoordinator.swift in Sources */,
76B6046788017DEF088FBF87 /* RoomThreadListScreenModels.swift in Sources */,
5787613A79C6D553DEA28C5F /* RoomThreadListScreenViewModel.swift in Sources */,
CE23FE7F463AD5C7D80353AA /* RoomThreadListScreenViewModelProtocol.swift in Sources */,
708FC3184CCED825F0A36273 /* RoomThreadListServiceProxy.swift in Sources */,
38E8F5CC90DC0D716B799DE1 /* RoomThreadListServiceProxyMock.swift in Sources */,
12F7172C93873638B23CD6AD /* RoomThreadListServiceProxyProtocol.swift in Sources */,

View File

@@ -4,7 +4,8 @@
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -29,6 +30,12 @@
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,6 +45,10 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
@@ -47,12 +58,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -74,6 +79,8 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "HTTPS_PROXY"
@@ -108,6 +115,8 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">

View File

@@ -720,6 +720,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
sendHandle: sendHandle))
case .presentKnockRequestsList:
stateMachine.tryEvent(.presentKnockRequestsListScreen)
case .presentThreadList:
Task {
await self.presentThreadList(animated: true)
}
case .presentThread(let threadRootEventID, let focussedEventID):
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: focussedEventID))
case .presentRoom(let roomID, let via):
@@ -733,6 +737,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return coordinator
}
private func presentThreadList(animated: Bool) async {
let coordinator = await RoomThreadListScreenCoordinator(parameters: .init(threadListServiceProxy: roomProxy.threadListService(),
mediaProvider: userSession.mediaProvider))
coordinator.actionsPublisher.sink { [weak self] _ in }.store(in: &cancellables)
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in }
}
private func presentThread(threadRootEventID: String, focusEventID: String?, animated: Bool) async {
showLoadingIndicator()
defer { hideLoadingIndicator() }

View File

@@ -83,6 +83,7 @@ enum UserAvatarSizeOnScreen {
case mediaPreviewDetails
case sendInviteConfirmation
case sessionVerification
case threadList
case threadSummary
case map
@@ -106,7 +107,7 @@ enum UserAvatarSizeOnScreen {
case .roomDetails:
44
case .inviteUsers, .knockingUserList, .sessionVerification,
.settings:
.settings, .threadList:
52
case .roomChangeRoles:
56

View File

@@ -155,6 +155,7 @@ enum TestablePreviewsDictionary {
"RoomScreenFooterView_Previews" : RoomScreenFooterView_Previews.self,
"RoomScreen_Previews" : RoomScreen_Previews.self,
"RoomSelectionScreen_Previews" : RoomSelectionScreen_Previews.self,
"RoomThreadListScreen_Previews" : RoomThreadListScreen_Previews.self,
"SFNumberedListView_Previews" : SFNumberedListView_Previews.self,
"SecureBackupKeyBackupScreen_Previews" : SecureBackupKeyBackupScreen_Previews.self,
"SecureBackupLogoutConfirmationScreen_Previews" : SecureBackupLogoutConfirmationScreen_Previews.self,

View File

@@ -47,6 +47,7 @@ enum RoomScreenCoordinatorAction {
case presentPinnedEventsTimeline
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case presentKnockRequestsList
case presentThreadList
case presentThread(threadRootEventID: String, focussedEventID: String?)
case presentRoom(roomID: String, via: [String])
}
@@ -187,6 +188,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentRoom(roomID: roomID, via: via))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
case .displayThreadList:
actionsSubject.send(.presentThreadList)
case .displayThread(let threadRootEventID, let focussedEventID):
actionsSubject.send(.presentThread(threadRootEventID: threadRootEventID, focussedEventID: focussedEventID))
}

View File

@@ -12,6 +12,7 @@ import OrderedCollections
enum RoomScreenViewModelAction: Equatable {
case focusEvent(eventID: String)
case displayThreadList
case displayThread(threadRootEventID: String, focussedEventID: String)
case displayPinnedEventsTimeline
case displayRoomDetails
@@ -32,6 +33,7 @@ enum RoomScreenViewAction {
case dismissKnockRequests
case viewKnockRequests
case displaySuccessorRoom
case displayThreadList
}
struct RoomScreenViewState: BindableState {

View File

@@ -119,6 +119,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
guard let successorID = roomProxy.infoPublisher.value.successor?.roomId else { return }
let serverNames = roomProxy.knownServerNames(maxCount: 50) // Limit to the same number used by ClientProxy.resolveRoomAlias(_:)
actionsSubject.send(.displayRoom(roomID: successorID, via: Array(serverNames)))
case .displayThreadList:
actionsSubject.send(.displayThreadList)
}
}

View File

@@ -179,6 +179,17 @@ struct RoomScreen: View {
}
}
}
if #available(iOS 26, *) {
ToolbarSpacer(.fixed, placement: .primaryAction)
}
ToolbarItem(placement: .primaryAction) {
Button {
context.send(viewAction: .displayThreadList)
} label: {
CompoundIcon(\.threads)
}
}
}
@ViewBuilder

View File

@@ -0,0 +1,48 @@
//
// Copyright 2025 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.
//
// periphery:ignore:all - this is just a roomThreadList remove this comment once generating the final file
import Combine
import SwiftUI
struct RoomThreadListScreenCoordinatorParameters {
let threadListServiceProxy: RoomThreadListServiceProxyProtocol
let mediaProvider: MediaProviderProtocol
}
enum RoomThreadListScreenCoordinatorAction { }
final class RoomThreadListScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomThreadListScreenCoordinatorParameters
private let viewModel: RoomThreadListScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<RoomThreadListScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<RoomThreadListScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: RoomThreadListScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = RoomThreadListScreenViewModel(threadListServiceProxy: parameters.threadListServiceProxy,
mediaProvider: parameters.mediaProvider)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(RoomThreadListScreen(context: viewModel.context))
}
}

View File

@@ -0,0 +1,25 @@
//
// Copyright 2025 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 Foundation
enum RoomThreadListScreenViewModelAction { }
struct RoomThreadListScreenViewState: BindableState {
var items = [RoomThreadListItem]()
var isPaginating = false
var bindings: RoomThreadListScreenViewStateBindings
}
struct RoomThreadListScreenViewStateBindings { }
enum RoomThreadListScreenViewAction {
case oldestItemDidAppear
case oldestItemDidDisappear
}

View File

@@ -0,0 +1,75 @@
//
// Copyright 2025 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 SwiftUI
typealias RoomThreadListScreenViewModelType = StateStoreViewModelV2<RoomThreadListScreenViewState, RoomThreadListScreenViewAction>
class RoomThreadListScreenViewModel: RoomThreadListScreenViewModelType, RoomThreadListScreenViewModelProtocol {
private let threadListServiceProxy: RoomThreadListServiceProxyProtocol
private var isOldestItemVisible = false
private let actionsSubject: PassthroughSubject<RoomThreadListScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<RoomThreadListScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(threadListServiceProxy: RoomThreadListServiceProxyProtocol, mediaProvider: MediaProviderProtocol) {
self.threadListServiceProxy = threadListServiceProxy
super.init(initialViewState: .init(bindings: .init()), mediaProvider: mediaProvider)
updateItems(self.threadListServiceProxy.itemsPublisher.value)
threadListServiceProxy.itemsPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] items in
self?.updateItems(items)
}
.store(in: &cancellables)
threadListServiceProxy.paginationStatePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] paginationState in
guard let self else { return }
state.isPaginating = paginationState == .loading
Task { await self.paginateIfNecessary(paginationState: paginationState) }
}
.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: RoomThreadListScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .oldestItemDidAppear:
isOldestItemVisible = true
Task {
await paginateIfNecessary(paginationState: threadListServiceProxy.paginationStatePublisher.value)
}
case .oldestItemDidDisappear:
isOldestItemVisible = false
}
}
// MARK: - Private
private func paginateIfNecessary(paginationState: RoomThreadListPaginationState) async {
if isOldestItemVisible, case .idle(endReached: false) = paginationState {
_ = await threadListServiceProxy.paginate()
}
}
private func updateItems(_ items: [RoomThreadListItem]) {
state.items = items
}
}

View File

@@ -0,0 +1,14 @@
//
// Copyright 2025 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
@MainActor
protocol RoomThreadListScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<RoomThreadListScreenViewModelAction, Never> { get }
var context: RoomThreadListScreenViewModelType.Context { get }
}

View File

@@ -0,0 +1,153 @@
//
// Copyright 2025 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 Compound
import SwiftUI
struct RoomThreadListScreen: View {
@Bindable var context: RoomThreadListScreenViewModel.Context
var body: some View {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(context.viewState.items) { item in
RoomThreadListCell(item: item, mediaProvider: context.mediaProvider)
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
footer
}
}
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationTitle(L10n.commonThreads)
.navigationBarTitleDisplayMode(.inline)
}
private var footer: some View {
// Needs to be wrapped in a LazyStack otherwise appearance calls don't trigger
LazyVStack(spacing: 0) {
ProgressView()
.padding()
.opacity(context.viewState.isPaginating ? 1 : 0)
Rectangle()
.frame(height: 1)
.foregroundStyle(.compound.bgCanvasDefault)
.onAppear {
context.send(viewAction: .oldestItemDidAppear)
}
.onDisappear {
context.send(viewAction: .oldestItemDidDisappear)
}
}
}
}
private struct RoomThreadListCell: View {
let item: RoomThreadListItem
let mediaProvider: MediaProviderProtocol?
var body: some View {
HStack(alignment: .center, spacing: 16) {
LoadableAvatarImage(url: item.rootMessageDetails.sender.avatarURL,
name: item.rootMessageDetails.sender.displayName,
contentID: item.rootMessageDetails.sender.id,
avatarSize: .user(on: .threadList),
mediaProvider: mediaProvider)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .center, spacing: 16) {
creatorDetails
Spacer()
timestamp
}
rootMessageDetails
latestMessageDetails
}
}
}
private var creatorDetails: some View {
Text(item.rootMessageDetails.sender.disambiguatedDisplayName ?? item.rootMessageDetails.sender.id)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
}
@ViewBuilder
private var rootMessageDetails: some View {
if let message = item.rootMessageDetails.message {
Text(message)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
@ViewBuilder
private var latestMessageDetails: some View {
if let latestMessageDetails = item.latestMessageDetails {
HStack(alignment: .center, spacing: 8) {
Label {
Text("\(item.numberOfReplies)")
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textSecondary)
} icon: {
CompoundIcon(\.threads, size: .small, relativeTo: .compound.bodySMSemibold)
.foregroundColor(.compound.iconSecondary)
}
.labelStyle(.custom(spacing: 4, alignment: .center, iconLayout: .trailing))
LoadableAvatarImage(url: latestMessageDetails.sender.avatarURL,
name: latestMessageDetails.sender.displayName,
contentID: latestMessageDetails.sender.id,
avatarSize: .user(on: .threadSummary),
mediaProvider: mediaProvider)
.accessibilityHidden(true)
if let message = latestMessageDetails.message {
Text(message)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
private var timestamp: some View {
if let latestMessageDetails = item.latestMessageDetails {
Text(latestMessageDetails.timestamp.formattedMinimal())
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
} else {
Text(item.rootMessageDetails.timestamp.formattedTime())
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
}
}
}
// MARK: - Previews
struct RoomThreadListScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = makeViewModel()
static var previews: some View {
RoomThreadListScreen(context: viewModel.context)
}
static func makeViewModel() -> RoomThreadListScreenViewModel {
RoomThreadListScreenViewModel(threadListServiceProxy: RoomThreadListServiceProxyMock(.init()),
mediaProvider: MediaProviderMock(configuration: .init()))
}
}

View File

@@ -1179,6 +1179,14 @@ extension PreviewTests {
}
}
@Test
func roomThreadListScreen() async throws {
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
for (index, preview) in RoomThreadListScreen_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
@Test
func sFNumberedListView() async throws {
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.