Add mechanism for opening up a threaded timeline

This commit is contained in:
Stefan Ceriu
2025-04-11 15:41:06 +03:00
committed by Stefan Ceriu
parent 2f938d3cbf
commit 10fb2b5dda
22 changed files with 505 additions and 13 deletions
+36
View File
@@ -69,6 +69,7 @@
08CB4BD12CEEDE6AAE4A18DD /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035177BCD8E8308B098AC3C2 /* WindowManager.swift */; }; 08CB4BD12CEEDE6AAE4A18DD /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035177BCD8E8308B098AC3C2 /* WindowManager.swift */; };
095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; };
095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */; }; 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */; };
09693596AA3CA4E598C0D2F1 /* ThreadTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */; };
09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; }; 09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; };
09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */; }; 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */; };
0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; }; 0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; };
@@ -483,6 +484,7 @@
5DB4334CBBA142376FF5FFEC /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 200626E8353AB2729444F991 /* preview_image.jpg */; }; 5DB4334CBBA142376FF5FFEC /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 200626E8353AB2729444F991 /* preview_image.jpg */; };
5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; };
5DFC2A889D3B39DD47AC63A8 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; }; 5DFC2A889D3B39DD47AC63A8 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; };
5EB116B58533C9A0EBA22717 /* ThreadTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E7CEE6E9314FD69FE0ED9 /* ThreadTimelineScreen.swift */; };
5EC046E41755C095DAB1C3FF /* TimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8C9BBB729C941BEE0E2A63 /* TimelineProviderProtocol.swift */; }; 5EC046E41755C095DAB1C3FF /* TimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8C9BBB729C941BEE0E2A63 /* TimelineProviderProtocol.swift */; };
5EDBDE802761B5ECB54E6787 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711E5996016ABD6EAAEB58A /* LogLevel.swift */; }; 5EDBDE802761B5ECB54E6787 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711E5996016ABD6EAAEB58A /* LogLevel.swift */; };
5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
@@ -586,6 +588,7 @@
71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; };
7254FB2EFDD43BC8BB7A1213 /* SecurityAndPrivacyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AE42C19EDE64B7CB7BE4D0 /* SecurityAndPrivacyScreen.swift */; }; 7254FB2EFDD43BC8BB7A1213 /* SecurityAndPrivacyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AE42C19EDE64B7CB7BE4D0 /* SecurityAndPrivacyScreen.swift */; };
733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; }; 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; };
738288EAEE235CAC0893AB9E /* ThreadTimelineScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9ACDD96F36510C1FC0836B /* ThreadTimelineScreenViewModel.swift */; };
73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; };
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; }; 73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; };
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
@@ -703,6 +706,7 @@
877D3CE8680536DB430DE6A2 /* TimelineItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */; }; 877D3CE8680536DB430DE6A2 /* TimelineItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */; };
878070573C7BF19E735707B4 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */; }; 878070573C7BF19E735707B4 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */; };
87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; }; 87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; };
87E8C31FCF9276F15CB0B408 /* ThreadTimelineScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */; };
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; };
88356DE7F2AD243AB10C7B7A /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; }; 88356DE7F2AD243AB10C7B7A /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
887AC93C523AEFB640EA5EC8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */; }; 887AC93C523AEFB640EA5EC8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */; };
@@ -850,6 +854,7 @@
A3D7110C1E75E7B4A73BE71C /* VoiceMessageRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93C94C30E3135BC9290DE13 /* VoiceMessageRecorderTests.swift */; }; A3D7110C1E75E7B4A73BE71C /* VoiceMessageRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93C94C30E3135BC9290DE13 /* VoiceMessageRecorderTests.swift */; };
A439B456D0761D6541745CC3 /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; A439B456D0761D6541745CC3 /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; };
A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; };
A46B93EA564F539CEC252ECC /* ThreadTimelineScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69AEA8755382DB34892FB7B /* ThreadTimelineScreenModels.swift */; };
A494741843F087881299ACF0 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; A494741843F087881299ACF0 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; };
A4AF12D9D8BA34B3B7B55B08 /* AuthenticationStartScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6663BFB9FDB8752562CD12CA /* AuthenticationStartScreenCoordinator.swift */; }; A4AF12D9D8BA34B3B7B55B08 /* AuthenticationStartScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6663BFB9FDB8752562CD12CA /* AuthenticationStartScreenCoordinator.swift */; };
A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */; }; A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */; };
@@ -1924,6 +1929,7 @@
7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportFlowCoordinator.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>"; }; 739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPreviewProxyProtocol.swift; sourceTree = "<group>"; };
73A5C3F7C9C1DA10CAEC6A98 /* VoiceMessageRecordingComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingComposer.swift; sourceTree = "<group>"; }; 73A5C3F7C9C1DA10CAEC6A98 /* VoiceMessageRecordingComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingComposer.swift; sourceTree = "<group>"; };
73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenCoordinator.swift; sourceTree = "<group>"; };
7447C0AD7EF302CD027D6230 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SAS.strings; sourceTree = "<group>"; }; 7447C0AD7EF302CD027D6230 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SAS.strings; sourceTree = "<group>"; };
7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModelProtocol.swift; sourceTree = "<group>"; };
74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCoordinator.swift; sourceTree = "<group>"; }; 74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -1963,6 +1969,7 @@
7C1AF829F12FDC99717082D9 /* RoomRolesAndPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModel.swift; sourceTree = "<group>"; }; 7C1AF829F12FDC99717082D9 /* RoomRolesAndPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModel.swift; sourceTree = "<group>"; };
7C28B70BEFD3676F11D5D51F /* RoomRolesAndPermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenCoordinator.swift; sourceTree = "<group>"; }; 7C28B70BEFD3676F11D5D51F /* RoomRolesAndPermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenCoordinator.swift; sourceTree = "<group>"; };
7C71B9802433F1B4252291BB /* IdentityConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 7C71B9802433F1B4252291BB /* IdentityConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
7C9ACDD96F36510C1FC0836B /* ThreadTimelineScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModel.swift; sourceTree = "<group>"; };
7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; }; 7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactoryProtocol.swift; sourceTree = "<group>"; }; 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactoryProtocol.swift; sourceTree = "<group>"; };
7D39AF1F659923D77778511E /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 7D39AF1F659923D77778511E /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -2262,6 +2269,7 @@
B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = "<group>"; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = "<group>"; };
B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactory.swift; sourceTree = "<group>"; }; B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactory.swift; sourceTree = "<group>"; };
B68B31232312AFC844440BFE /* DeclineAndBlockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenModels.swift; sourceTree = "<group>"; }; B68B31232312AFC844440BFE /* DeclineAndBlockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenModels.swift; sourceTree = "<group>"; };
B69AEA8755382DB34892FB7B /* ThreadTimelineScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenModels.swift; sourceTree = "<group>"; };
B6A293D06BAB2B7A17D9314B /* VoiceMessageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineView.swift; sourceTree = "<group>"; }; B6A293D06BAB2B7A17D9314B /* VoiceMessageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineView.swift; sourceTree = "<group>"; };
B6C585CE1F721A2770C70D47 /* TimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineControllerProtocol.swift; sourceTree = "<group>"; }; B6C585CE1F721A2770C70D47 /* TimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineControllerProtocol.swift; sourceTree = "<group>"; };
B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHooks.swift; sourceTree = "<group>"; }; B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHooks.swift; sourceTree = "<group>"; };
@@ -2289,6 +2297,7 @@
BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = "<group>"; }; BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = "<group>"; };
BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = "<group>"; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = "<group>"; };
BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = "<group>"; }; BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = "<group>"; };
BC2E7CEE6E9314FD69FE0ED9 /* ThreadTimelineScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreen.swift; sourceTree = "<group>"; };
BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = "<group>"; }; BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = "<group>"; };
BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = "<group>"; }; BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = "<group>"; };
BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = "<group>"; }; BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = "<group>"; };
@@ -2330,6 +2339,7 @@
C33B3F17996DFDF5F0181512 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; }; C33B3F17996DFDF5F0181512 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = "<group>"; }; C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = "<group>"; };
C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManagerProtocol.swift; sourceTree = "<group>"; }; C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManagerProtocol.swift; sourceTree = "<group>"; };
C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModelProtocol.swift; sourceTree = "<group>"; };
C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = "<group>"; }; C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = "<group>"; };
C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; }; C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
C537DE821FED94D23467B6C4 /* PillUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillUtilities.swift; sourceTree = "<group>"; }; C537DE821FED94D23467B6C4 /* PillUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillUtilities.swift; sourceTree = "<group>"; };
@@ -4977,6 +4987,18 @@
path = AppLock; path = AppLock;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9688AEF931DE2C71683ACBC7 /* ThreadTimelineScreen */ = {
isa = PBXGroup;
children = (
73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */,
B69AEA8755382DB34892FB7B /* ThreadTimelineScreenModels.swift */,
7C9ACDD96F36510C1FC0836B /* ThreadTimelineScreenViewModel.swift */,
C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */,
A55B1545241E40A06958DDF7 /* View */,
);
path = ThreadTimelineScreen;
sourceTree = "<group>";
};
99B9B46F2D621380428E68F7 /* ElementX */ = { 99B9B46F2D621380428E68F7 /* ElementX */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -5120,6 +5142,14 @@
path = UnitTests; path = UnitTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A55B1545241E40A06958DDF7 /* View */ = {
isa = PBXGroup;
children = (
BC2E7CEE6E9314FD69FE0ED9 /* ThreadTimelineScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
A6AA0A048CAE428A5CA4CBBB /* LayoutTests */ = { A6AA0A048CAE428A5CA4CBBB /* LayoutTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -5814,6 +5844,7 @@
C59BA103987B953BA374509F /* SecurityAndPrivacyScreen */, C59BA103987B953BA374509F /* SecurityAndPrivacyScreen */,
70B74A432C241E56A7ACE610 /* Settings */, 70B74A432C241E56A7ACE610 /* Settings */,
EC4545C7E37E8294D3FE6800 /* StartChatScreen */, EC4545C7E37E8294D3FE6800 /* StartChatScreen */,
9688AEF931DE2C71683ACBC7 /* ThreadTimelineScreen */,
15D44FCA9475E660B7F56DB9 /* Timeline */, 15D44FCA9475E660B7F56DB9 /* Timeline */,
93C7520ED23C9598BB144DBB /* UserProfileScreen */, 93C7520ED23C9598BB144DBB /* UserProfileScreen */,
); );
@@ -7688,6 +7719,11 @@
53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */, 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */,
D8CFA0EE46376F9FF04EEE45 /* TextRoomTimelineView.swift in Sources */, D8CFA0EE46376F9FF04EEE45 /* TextRoomTimelineView.swift in Sources */,
71643093F87153F633A1B025 /* ThreadDecorator.swift in Sources */, 71643093F87153F633A1B025 /* ThreadDecorator.swift in Sources */,
5EB116B58533C9A0EBA22717 /* ThreadTimelineScreen.swift in Sources */,
09693596AA3CA4E598C0D2F1 /* ThreadTimelineScreenCoordinator.swift in Sources */,
A46B93EA564F539CEC252ECC /* ThreadTimelineScreenModels.swift in Sources */,
738288EAEE235CAC0893AB9E /* ThreadTimelineScreenViewModel.swift in Sources */,
87E8C31FCF9276F15CB0B408 /* ThreadTimelineScreenViewModelProtocol.swift in Sources */,
2AED12987603157C32C2114D /* TimelineBubbleLayout.swift in Sources */, 2AED12987603157C32C2114D /* TimelineBubbleLayout.swift in Sources */,
2BFA4C6D5B3D327B02C66AB0 /* TimelineController.swift in Sources */, 2BFA4C6D5B3D327B02C66AB0 /* TimelineController.swift in Sources */,
7DCFC31B49BDD2BC32184E58 /* TimelineControllerFactory.swift in Sources */, 7DCFC31B49BDD2BC32184E58 /* TimelineControllerFactory.swift in Sources */,
@@ -302,6 +302,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (_, .dismissFlow): case (_, .dismissFlow):
return .complete return .complete
case (_, .presentThread(let itemID)):
return .thread(itemID: itemID)
case (_, .dismissThread):
return .room
case (.initial, .presentRoomDetails): case (.initial, .presentRoomDetails):
return .roomDetails(isRoot: true) return .roomDetails(isRoot: true)
case (.room, .presentRoomDetails): case (.room, .presentRoomDetails):
@@ -462,6 +467,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
case (_, .dismissFlow, .complete): case (_, .dismissFlow, .complete):
dismissFlow(animated: animated) dismissFlow(animated: animated)
case (.room, .presentThread(let itemID), .thread):
Task { await self.presentThread(itemID: itemID) }
case (.thread, .dismissThread, .room):
break
case (.initial, .presentRoomDetails, .roomDetails(let isRoot)), case (.initial, .presentRoomDetails, .roomDetails(let isRoot)),
(.room, .presentRoomDetails, .roomDetails(let isRoot)), (.room, .presentRoomDetails, .roomDetails(let isRoot)),
@@ -618,7 +628,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
presentDeclineAndBlockScreen(userID: userID) presentDeclineAndBlockScreen(userID: userID)
case (.declineAndBlockScreen, .dismissDeclineAndBlockScreen, .joinRoomScreen): case (.declineAndBlockScreen, .dismissDeclineAndBlockScreen, .joinRoomScreen):
break break
// Child flow // Child flow
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild): case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
Task { await self.startChildFlow(for: roomID, via: via, entryPoint: entryPoint) } Task { await self.startChildFlow(for: roomID, via: via, entryPoint: entryPoint) }
@@ -782,6 +792,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle)) stateMachine.tryEvent(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
case .presentKnockRequestsList: case .presentKnockRequestsList:
stateMachine.tryEvent(.presentKnockRequestsListScreen) stateMachine.tryEvent(.presentKnockRequestsListScreen)
case .presentThread(let itemID):
stateMachine.tryEvent(.presentThread(itemID: itemID))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -789,6 +801,41 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return coordinator return coordinator
} }
private func presentThread(itemID: TimelineItemIdentifier) async {
showLoadingIndicator()
defer { hideLoadingIndicator() }
let timelineItemFactory = RoomTimelineItemFactory(userID: userSession.clientProxy.userID,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
guard let eventID = itemID.eventID else {
fatalError("Invalid thread event ID")
}
guard case let .success(timelineController) = await timelineControllerFactory.buildThreadTimelineController(eventID: eventID,
roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider) else {
MXLog.error("Failed presenting media timeline")
return
}
let coordinator = ThreadTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(),
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
appMediator: appMediator,
emojiProvider: emojiProvider,
timelineControllerFactory: timelineControllerFactory,
clientProxy: userSession.clientProxy))
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissThread)
}
}
private func presentJoinRoomScreen(via: [String], animated: Bool) { private func presentJoinRoomScreen(via: [String], animated: Bool) {
let coordinator = JoinRoomScreenCoordinator(parameters: .init(roomID: roomID, let coordinator = JoinRoomScreenCoordinator(parameters: .init(roomID: roomID,
via: via, via: via,
@@ -1747,6 +1794,7 @@ private extension RoomFlowCoordinator {
case initial case initial
case joinRoomScreen case joinRoomScreen
case room case room
case thread(itemID: TimelineItemIdentifier)
case roomDetails(isRoot: Bool) case roomDetails(isRoot: Bool)
case roomDetailsEditScreen case roomDetailsEditScreen
case notificationSettings case notificationSettings
@@ -1790,6 +1838,9 @@ private extension RoomFlowCoordinator {
case presentRoom(presentationAction: PresentationAction?) case presentRoom(presentationAction: PresentationAction?)
case dismissFlow case dismissFlow
case presentThread(itemID: TimelineItemIdentifier)
case dismissThread
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String) case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case dismissReportContent case dismissReportContent
@@ -6438,6 +6438,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
return timelineFocusedOnEventEventIDNumberOfEventsReturnValue return timelineFocusedOnEventEventIDNumberOfEventsReturnValue
} }
} }
//MARK: - threadTimeline
var threadTimelineEventIDUnderlyingCallsCount = 0
var threadTimelineEventIDCallsCount: Int {
get {
if Thread.isMainThread {
return threadTimelineEventIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = threadTimelineEventIDUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
threadTimelineEventIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
threadTimelineEventIDUnderlyingCallsCount = newValue
}
}
}
}
var threadTimelineEventIDCalled: Bool {
return threadTimelineEventIDCallsCount > 0
}
var threadTimelineEventIDReceivedEventID: String?
var threadTimelineEventIDReceivedInvocations: [String] = []
var threadTimelineEventIDUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
var threadTimelineEventIDReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
get {
if Thread.isMainThread {
return threadTimelineEventIDUnderlyingReturnValue
} else {
var returnValue: Result<TimelineProxyProtocol, RoomProxyError>? = nil
DispatchQueue.main.sync {
returnValue = threadTimelineEventIDUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
threadTimelineEventIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
threadTimelineEventIDUnderlyingReturnValue = newValue
}
}
}
}
var threadTimelineEventIDClosure: ((String) async -> Result<TimelineProxyProtocol, RoomProxyError>)?
func threadTimeline(eventID: String) async -> Result<TimelineProxyProtocol, RoomProxyError> {
threadTimelineEventIDCallsCount += 1
threadTimelineEventIDReceivedEventID = eventID
DispatchQueue.main.async {
self.threadTimelineEventIDReceivedInvocations.append(eventID)
}
if let threadTimelineEventIDClosure = threadTimelineEventIDClosure {
return await threadTimelineEventIDClosure(eventID)
} else {
return threadTimelineEventIDReturnValue
}
}
//MARK: - messageFilteredTimeline //MARK: - messageFilteredTimeline
var messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = 0 var messageFilteredTimelineFocusAllowedMessageTypesPresentationUnderlyingCallsCount = 0
@@ -14969,6 +15039,76 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
return buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue return buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue
} }
} }
//MARK: - buildThreadTimelineController
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int {
get {
if Thread.isMainThread {
return buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
}
}
}
}
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderCalled: Bool {
return buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0
}
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (eventID: String, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)?
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(eventID: String, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = []
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>!
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>! {
get {
if Thread.isMainThread {
return buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
} else {
var returnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>? = nil
DispatchQueue.main.sync {
returnValue = buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
}
}
}
}
var buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderClosure: ((String, JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError>)?
func buildThreadTimelineController(eventID: String, roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (eventID: eventID, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
DispatchQueue.main.async {
self.buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((eventID: eventID, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider))
}
if let buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderClosure = buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderClosure {
return await buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderClosure(eventID, roomProxy, timelineItemFactory, mediaProvider)
} else {
return buildThreadTimelineControllerEventIDRoomProxyTimelineItemFactoryMediaProviderReturnValue
}
}
//MARK: - buildPinnedEventsTimelineController //MARK: - buildPinnedEventsTimelineController
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0 var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
@@ -81,7 +81,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline: .displayThread, .composer, .hasScrolled, .viewInRoomTimeline:
break break
} }
} }
@@ -103,7 +103,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline: .displayThread, .composer, .hasScrolled, .viewInRoomTimeline:
break break
} }
} }
@@ -91,7 +91,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
// These other actions will not be handled in this view // These other actions will not be handled in this view
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.displayResolveSendFailure, .composer, .hasScrolled: .displayResolveSendFailure, .displayThread, .composer, .hasScrolled:
// These actions are not handled in this coordinator // These actions are not handled in this coordinator
break break
} }
@@ -44,6 +44,7 @@ enum RoomScreenCoordinatorAction {
case presentPinnedEventsTimeline case presentPinnedEventsTimeline
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case presentKnockRequestsList case presentKnockRequestsList
case presentThread(itemID: TimelineItemIdentifier)
} }
final class RoomScreenCoordinator: CoordinatorProtocol { final class RoomScreenCoordinator: CoordinatorProtocol {
@@ -144,6 +145,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description)) actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description))
case .displayResolveSendFailure(let failure, let sendHandle): case .displayResolveSendFailure(let failure, let sendHandle):
actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle)) actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
case .displayThread(let itemID):
actionsSubject.send(.presentThread(itemID: itemID))
case .composer(let action): case .composer(let action):
composerViewModel.process(timelineAction: action) composerViewModel.process(timelineAction: action)
case .hasScrolled(direction: let direction): case .hasScrolled(direction: let direction):
@@ -0,0 +1,93 @@
//
// Copyright 2022-2024 New Vector 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
struct ThreadTimelineScreenCoordinatorParameters {
let roomProxy: JoinedRoomProxyProtocol
let timelineController: TimelineControllerProtocol
let mediaProvider: MediaProviderProtocol
let mediaPlayerProvider: MediaPlayerProviderProtocol
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
let appMediator: AppMediatorProtocol
let emojiProvider: EmojiProviderProtocol
let timelineControllerFactory: TimelineControllerFactoryProtocol
let clientProxy: ClientProxyProtocol
}
enum ThreadTimelineScreenCoordinatorAction {
case dismiss
case displayUser(userID: String)
case presentLocationViewer(geoURI: GeoURI, description: String?)
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
case displayRoomScreenWithFocussedPin(eventID: String)
}
final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
private let parameters: ThreadTimelineScreenCoordinatorParameters
private let viewModel: ThreadTimelineScreenViewModelProtocol
private let timelineViewModel: TimelineViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<ThreadTimelineScreenCoordinatorAction, Never> = .init()
var actions: AnyPublisher<ThreadTimelineScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: ThreadTimelineScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = ThreadTimelineScreenViewModel()
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
timelineController: parameters.timelineController,
mediaProvider: parameters.mediaProvider,
mediaPlayerProvider: parameters.mediaPlayerProvider,
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: parameters.appMediator,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: parameters.emojiProvider,
timelineControllerFactory: parameters.timelineControllerFactory,
clientProxy: parameters.clientProxy)
}
func start() {
timelineViewModel.actions.sink { [weak self] action in
MXLog.info("Coordinator: received timeline view model action: \(action)")
guard let self else { return }
switch action {
case .displaySenderDetails(let userID):
actionsSubject.send(.displayUser(userID: userID))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
case .displayLocation(_, let geoURI, let description):
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
case .viewInRoomTimeline(let eventID):
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
// These other actions will not be handled in this view
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaPreview,
.displayMediaUploadPreviewScreen, .displayResolveSendFailure, .displayThread, .composer, .hasScrolled:
// These actions are not handled in this coordinator
break
}
}
.store(in: &cancellables)
}
func stop() {
viewModel.stop()
}
func toPresentable() -> AnyView {
AnyView(ThreadTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context))
}
}
@@ -0,0 +1,21 @@
//
// Copyright 2022-2024 New Vector 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 ThreadTimelineScreenViewModelAction { }
struct ThreadTimelineScreenViewState: BindableState {
var bindings = ThreadTimelineScreenViewStateBindings()
}
struct ThreadTimelineScreenViewStateBindings {
/// The view model used to present a QuickLook media preview.
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
}
enum ThreadTimelineScreenViewAction { }
@@ -0,0 +1,31 @@
//
// Copyright 2022-2024 New Vector 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 ThreadTimelineScreenViewModelType = StateStoreViewModel<ThreadTimelineScreenViewState, ThreadTimelineScreenViewAction>
class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTimelineScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<ThreadTimelineScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<ThreadTimelineScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: ThreadTimelineScreenViewState())
}
// MARK: - Public
override func process(viewAction: ThreadTimelineScreenViewAction) { }
func stop() {
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
state.bindings.mediaPreviewViewModel = nil
}
}
@@ -0,0 +1,16 @@
//
// Copyright 2022-2024 New Vector 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 ThreadTimelineScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<ThreadTimelineScreenViewModelAction, Never> { get }
var context: ThreadTimelineScreenViewModelType.Context { get }
func stop()
}
@@ -0,0 +1,51 @@
//
// Copyright 2022-2024 New Vector 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 ThreadTimelineScreen: View {
@ObservedObject var context: ThreadTimelineScreenViewModel.Context
@ObservedObject var timelineContext: TimelineViewModel.Context
var body: some View {
content
.navigationTitle("Thread")
.navigationBarTitleDisplayMode(.inline)
.background(.compound.bgCanvasDefault)
.interactiveDismissDisabled()
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
.sheet(item: $timelineContext.manageMemberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
.sheet(item: $timelineContext.actionMenuInfo) { info in
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf,
canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers,
canCurrentUserPin: timelineContext.viewState.canCurrentUserPin,
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
isDM: timelineContext.viewState.isDirectOneToOneRoom,
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
timelineKind: timelineContext.viewState.timelineKind,
emojiProvider: timelineContext.viewState.emojiProvider)
.makeActions()
if let actions {
TimelineItemMenu(item: info.item, actions: actions)
.environmentObject(timelineContext)
}
}
}
@ViewBuilder
private var content: some View {
TimelineView()
.id(timelineContext.viewState.roomID)
.environmentObject(timelineContext)
.environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID)
}
}
@@ -554,7 +554,7 @@ class TimelineInteractionHandler {
case .pinned: case .pinned:
newTimelineFocus = .pinned newTimelineFocus = .pinned
newTimelinePresentation = .pinnedEventsScreen newTimelinePresentation = .pinnedEventsScreen
case .media: case .media, .thread:
break // We don't need to create a new timeline as it is already filtered. break // We don't need to create a new timeline as it is already filtered.
} }
@@ -24,6 +24,7 @@ enum TimelineViewModelAction {
case displayMediaPreview(TimelineMediaPreviewViewModel) case displayMediaPreview(TimelineMediaPreviewViewModel)
case displayLocation(body: String, geoURI: GeoURI, description: String?) case displayLocation(body: String, geoURI: GeoURI, description: String?)
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case displayThread(itemID: TimelineItemIdentifier)
case composer(action: TimelineComposerAction) case composer(action: TimelineComposerAction)
case hasScrolled(direction: ScrollDirection) case hasScrolled(direction: ScrollDirection)
case viewInRoomTimeline(eventID: String) case viewInRoomTimeline(eventID: String)
@@ -191,8 +191,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
displayReactionSummary(for: itemID, selectedKey: key) displayReactionSummary(for: itemID, selectedKey: key)
case .displayReadReceipts(let itemID): case .displayReadReceipts(let itemID):
displayReadReceipts(for: itemID) displayReadReceipts(for: itemID)
case .displayThread: case .displayThread(let itemID):
break actionsSubject.send(.displayThread(itemID: itemID))
case .handlePasteOrDrop(let provider): case .handlePasteOrDrop(let provider):
timelineInteractionHandler.handlePasteOrDrop(provider) timelineInteractionHandler.handlePasteOrDrop(provider)
case .handlePollAction(let pollAction): case .handlePollAction(let pollAction):
@@ -111,7 +111,7 @@ struct TimelineItemMenuActionProvider {
actions.append(.save) actions.append(.save)
actions = actions.filter(\.canAppearInMediaDetails) actions = actions.filter(\.canAppearInMediaDetails)
secondaryActions = secondaryActions.filter(\.canAppearInMediaDetails) secondaryActions = secondaryActions.filter(\.canAppearInMediaDetails)
case .live, .detached: case .live, .detached, .thread:
break // viewInRoomTimeline is the only non-room item and was added conditionally. break // viewInRoomTimeline is the only non-room item and was added conditionally.
} }
@@ -120,7 +120,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
.onTapGesture { } .onTapGesture { }
} }
if let threadSummary = timelineItem.properties.threadSummary { if context.viewState.timelineKind != .thread, let threadSummary = timelineItem.properties.threadSummary {
TimelineThreadSummaryView(threadSummary: threadSummary) { TimelineThreadSummaryView(threadSummary: threadSummary) {
context.send(viewAction: .displayThread(itemID: timelineItem.id)) context.send(viewAction: .displayThread(itemID: timelineItem.id))
} }
@@ -177,13 +177,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
@ViewBuilder @ViewBuilder
var contentWithReply: some View { var contentWithReply: some View {
TimelineBubbleLayout(spacing: 8) { TimelineBubbleLayout(spacing: 8) {
if timelineItem.properties.isThreaded { if context.viewState.timelineKind != .thread, timelineItem.properties.isThreaded {
ThreadDecorator() ThreadDecorator()
.padding(.leading, 4) .padding(.leading, 4)
.layoutPriority(TimelineBubbleLayout.Priority.regularText) .layoutPriority(TimelineBubbleLayout.Priority.regularText)
} }
if let replyDetails = timelineItem.properties.replyDetails { if shouldShowReplyDetails, let replyDetails = timelineItem.properties.replyDetails {
// The rendered reply bubble with a greedy width. The custom layout prevents // The rendered reply bubble with a greedy width. The custom layout prevents
// the infinite width from increasing the overall width of the view. // the infinite width from increasing the overall width of the view.
@@ -212,6 +212,10 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
} }
} }
private var shouldShowReplyDetails: Bool {
!timelineItem.properties.isThreaded || (timelineItem.properties.isThreaded && context.viewState.timelineKind != .thread)
}
private var messageBubbleTopPadding: CGFloat { private var messageBubbleTopPadding: CGFloat {
guard timelineItem.isOutgoing || isDirectOneToOneRoom else { return 0 } guard timelineItem.isOutgoing || isDirectOneToOneRoom else { return 0 }
return timelineGroupStyle == .single || timelineGroupStyle == .first ? 8 : 0 return timelineGroupStyle == .single || timelineGroupStyle == .first ? 8 : 0
@@ -190,6 +190,24 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
} }
} }
func threadTimeline(eventID: String) async -> Result<TimelineProxyProtocol, RoomProxyError> {
do {
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .thread(rootEventId: eventID, numEvents: 20),
filter: .all,
internalIdPrefix: UUID().uuidString,
dateDividerMode: .daily,
trackReadReceipts: true))
let timeline = TimelineProxy(timeline: sdkTimeline, kind: .thread)
await timeline.subscribeForUpdates()
return .success(timeline)
} catch {
MXLog.error("Unexpected error: \(error)")
return .failure(.sdkError(error))
}
}
func messageFilteredTimeline(focus: TimelineFocus, func messageFilteredTimeline(focus: TimelineFocus,
allowedMessageTypes: [TimelineAllowedMessageType], allowedMessageTypes: [TimelineAllowedMessageType],
presentation: TimelineKind.MediaPresentation) async -> Result<any TimelineProxyProtocol, RoomProxyError> { presentation: TimelineKind.MediaPresentation) async -> Result<any TimelineProxyProtocol, RoomProxyError> {
@@ -197,6 +215,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
let rustFocus: MatrixRustSDK.TimelineFocus = switch focus { let rustFocus: MatrixRustSDK.TimelineFocus = switch focus {
case .live: .live case .live: .live
case .eventID(let eventID): .event(eventId: eventID, numContextEvents: 100) case .eventID(let eventID): .event(eventId: eventID, numContextEvents: 100)
case .thread(let eventID): .thread(rootEventId: eventID, numEvents: 20)
case .pinned: .pinnedEvents(maxEventsToLoad: 100, maxConcurrentRequests: 10) case .pinned: .pinnedEvents(maxEventsToLoad: 100, maxConcurrentRequests: 10)
} }
@@ -83,6 +83,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError> func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError>
func threadTimeline(eventID: String) async -> Result<TimelineProxyProtocol, RoomProxyError>
func messageFilteredTimeline(focus: TimelineFocus, func messageFilteredTimeline(focus: TimelineFocus,
allowedMessageTypes: [TimelineAllowedMessageType], allowedMessageTypes: [TimelineAllowedMessageType],
presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError> presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError>
@@ -21,6 +21,23 @@ struct TimelineControllerFactory: TimelineControllerFactoryProtocol {
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
func buildThreadTimelineController(eventID: String,
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
switch await roomProxy.threadTimeline(eventID: eventID) {
case .success(let timelineProxy):
return .success(TimelineController(roomProxy: roomProxy,
timelineProxy: timelineProxy,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings))
case .failure(let error):
return .failure(.roomProxyError(error))
}
}
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? { mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? {
@@ -19,6 +19,11 @@ protocol TimelineControllerFactoryProtocol {
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> TimelineControllerProtocol mediaProvider: MediaProviderProtocol) -> TimelineControllerProtocol
func buildThreadTimelineController(eventID: String,
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError>
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol?
@@ -97,7 +97,7 @@ final class TimelineProxy: TimelineProxyProtocol {
switch kind { switch kind {
case .live: case .live:
return await paginateBackwardsOnLive(requestSize: requestSize) return await paginateBackwardsOnLive(requestSize: requestSize)
case .detached, .media: case .detached, .media, .thread:
return await focussedPaginate(.backwards, requestSize: requestSize) return await focussedPaginate(.backwards, requestSize: requestSize)
case .pinned: case .pinned:
return .success(()) return .success(())
@@ -596,7 +596,7 @@ final class TimelineProxy: TimelineProxyProtocol {
MXLog.error("Failed to subscribe to back pagination status with error: \(error)") MXLog.error("Failed to subscribe to back pagination status with error: \(error)")
} }
forwardPaginationStatusSubject.send(.timelineEndReached) forwardPaginationStatusSubject.send(.timelineEndReached)
case .detached: case .detached, .thread:
// Detached timelines don't support observation, set the initial state ourself. // Detached timelines don't support observation, set the initial state ourself.
backPaginationStatusSubject.send(.idle) backPaginationStatusSubject.send(.idle)
forwardPaginationStatusSubject.send(.idle) forwardPaginationStatusSubject.send(.idle)
@@ -13,6 +13,7 @@ enum TimelineKind: Equatable {
case live case live
case detached case detached
case pinned case pinned
case thread
enum MediaPresentation { case roomScreenLive, roomScreenDetached, pinnedEventsScreen, mediaFilesScreen } enum MediaPresentation { case roomScreenLive, roomScreenDetached, pinnedEventsScreen, mediaFilesScreen }
case media(MediaPresentation) case media(MediaPresentation)
@@ -21,6 +22,7 @@ enum TimelineKind: Equatable {
enum TimelineFocus { enum TimelineFocus {
case live case live
case eventID(String) case eventID(String)
case thread(eventID: String)
case pinned case pinned
} }