diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 4dd135a193..374721869e 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 63; objects = { /* Begin PBXAggregateTarget section */ @@ -485,7 +485,7 @@ 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.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 /* TimelineItemProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8C9BBB729C941BEE0E2A63 /* TimelineItemProviderProtocol.swift */; }; 5EDBDE802761B5ECB54E6787 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711E5996016ABD6EAAEB58A /* LogLevel.swift */; }; 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; 5F0B5797D1BFF2A51084B4C3 /* PinnedEventsTimelineScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D7CD5CA270BFC3EBB450CA /* PinnedEventsTimelineScreenViewModel.swift */; }; @@ -1105,7 +1105,7 @@ D97C782FE0005995C36FA04A /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */; }; D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; - DA03B4F28C4D248EECE3429F /* TimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B248460BD1A3F20318137 /* TimelineProvider.swift */; }; + DA03B4F28C4D248EECE3429F /* TimelineItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B248460BD1A3F20318137 /* TimelineItemProvider.swift */; }; DA10C99BA43A0F1E732F6274 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711E5996016ABD6EAAEB58A /* LogLevel.swift */; }; DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */; }; DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; }; @@ -1395,7 +1395,7 @@ 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; 045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryTests.swift; sourceTree = ""; }; - 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = ""; }; 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = ""; }; @@ -1468,7 +1468,7 @@ 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 12B09A94C519227264A41208 /* RoomMembershipDetailsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembershipDetailsProxy.swift; sourceTree = ""; }; 12FD5280AF55AB7F50F8E47D /* preview_avatar_room.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_room.jpg; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1571,7 +1571,7 @@ 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; - 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; path = PreviewTests.xctestplan; sourceTree = ""; }; + 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PreviewTests.xctestplan; sourceTree = ""; }; 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = ""; }; 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = ""; }; 2711E5996016ABD6EAAEB58A /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; @@ -1648,9 +1648,9 @@ 3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 35FA991289149D31F4286747 /* UserPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreference.swift; sourceTree = ""; }; - 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; - 371B248460BD1A3F20318137 /* TimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProvider.swift; sourceTree = ""; }; + 371B248460BD1A3F20318137 /* TimelineItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProvider.swift; sourceTree = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = ""; }; 37F46CC4FD89ECF4CF26391A /* AuthenticationFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationFlowCoordinatorTests.swift; sourceTree = ""; }; @@ -2062,7 +2062,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = ""; }; 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = ""; }; 8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = ""; }; @@ -2265,7 +2265,7 @@ B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5D829FD8958376614504B18 /* TargetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetConfiguration.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactory.swift; sourceTree = ""; }; @@ -2293,7 +2293,7 @@ BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; - BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; path = test_apple_image.heic; sourceTree = ""; }; + BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_apple_image.heic; sourceTree = ""; }; BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = ""; }; BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; @@ -2394,7 +2394,7 @@ CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; D01FD1171FF40E34D707FD00 /* BigIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigIcon.swift; sourceTree = ""; }; D046ABB22E680F7C5054441B /* SecurityAndPrivacyScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityAndPrivacyScreenViewModelProtocol.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -2457,11 +2457,11 @@ DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = ""; }; DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = ""; }; DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = ""; }; - DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = ""; }; DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = ""; }; DCDAB580109C09A6AA97AF7E /* PollFormScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenTests.swift; sourceTree = ""; }; DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = ""; }; - DD8C9BBB729C941BEE0E2A63 /* TimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProviderProtocol.swift; sourceTree = ""; }; + DD8C9BBB729C941BEE0E2A63 /* TimelineItemProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProviderProtocol.swift; sourceTree = ""; }; DD955A0380C287C418F1A74D /* PhotoLibraryManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryManagerMock.swift; sourceTree = ""; }; DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = ""; }; DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelTests.swift; sourceTree = ""; }; @@ -2500,7 +2500,7 @@ E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; - E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModel.swift; sourceTree = ""; }; @@ -2542,7 +2542,7 @@ ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -6086,8 +6086,8 @@ E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */, 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */, 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */, - 371B248460BD1A3F20318137 /* TimelineProvider.swift */, - DD8C9BBB729C941BEE0E2A63 /* TimelineProviderProtocol.swift */, + 371B248460BD1A3F20318137 /* TimelineItemProvider.swift */, + DD8C9BBB729C941BEE0E2A63 /* TimelineItemProviderProtocol.swift */, F9E543072DE58E751F028998 /* TimelineProxy.swift */, B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */, 3EA31CC7012EA2A5653DAFC9 /* Fixtures */, @@ -6468,7 +6468,6 @@ EC6D0C817B1C21D9D096505A /* XCRemoteSwiftPackageReference "Version" */, EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */, ); - preferredProjectObjectVersion = 54; projectDirPath = ""; projectRoot = ""; targets = ( @@ -7762,9 +7761,9 @@ A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */, 86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */, B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */, - DA03B4F28C4D248EECE3429F /* TimelineProvider.swift in Sources */, + DA03B4F28C4D248EECE3429F /* TimelineItemProvider.swift in Sources */, 4E4EF97B9F9CEFAC726BA72F /* TimelineProviderMock.swift in Sources */, - 5EC046E41755C095DAB1C3FF /* TimelineProviderProtocol.swift in Sources */, + 5EC046E41755C095DAB1C3FF /* TimelineItemProviderProtocol.swift in Sources */, E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */, 16A1F6C703305FCAF4E14EC6 /* TimelineProxyMock.swift in Sources */, 2FEC6652055984389CE1BBEC /* TimelineProxyProtocol.swift in Sources */, @@ -8124,9 +8123,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -8175,9 +8172,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -8203,9 +8198,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -8430,9 +8423,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DDEBUG", - ); + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -8451,9 +8442,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DRELEASE", - ); + OTHER_SWIFT_FLAGS = "-DRELEASE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -8475,9 +8464,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 8fff2377f4..204b0f574a 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -15297,7 +15297,7 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck } } } -class TimelineProviderMock: TimelineProviderProtocol, @unchecked Sendable { +class TimelineItemProviderMock: TimelineItemProviderProtocol, @unchecked Sendable { var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { get { return underlyingUpdatePublisher } set(value) { underlyingUpdatePublisher = value } @@ -15322,11 +15322,11 @@ class TimelineProviderMock: TimelineProviderProtocol, @unchecked Sendable { } class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable { - var timelineProvider: TimelineProviderProtocol { - get { return underlyingTimelineProvider } - set(value) { underlyingTimelineProvider = value } + var timelineItemProvider: TimelineItemProviderProtocol { + get { return underlyingTimelineItemProvider } + set(value) { underlyingTimelineItemProvider = value } } - var underlyingTimelineProvider: TimelineProviderProtocol! + var underlyingTimelineItemProvider: TimelineItemProviderProtocol! //MARK: - subscribeForUpdates diff --git a/ElementX/Sources/Mocks/TimelineProviderMock.swift b/ElementX/Sources/Mocks/TimelineProviderMock.swift index 73f10c0edd..bc05271506 100644 --- a/ElementX/Sources/Mocks/TimelineProviderMock.swift +++ b/ElementX/Sources/Mocks/TimelineProviderMock.swift @@ -10,7 +10,7 @@ import Foundation import MatrixRustSDK @MainActor -class AutoUpdatingTimelineProviderMock: TimelineProvider { +class AutoUpdatingTimelineItemProviderMock: TimelineItemProvider { static var timelineListener: TimelineListener? private let innerPaginationStatePublisher: PassthroughSubject diff --git a/ElementX/Sources/Mocks/TimelineProxyMock.swift b/ElementX/Sources/Mocks/TimelineProxyMock.swift index 08faa97a27..dae96feb27 100644 --- a/ElementX/Sources/Mocks/TimelineProxyMock.swift +++ b/ElementX/Sources/Mocks/TimelineProxyMock.swift @@ -24,12 +24,12 @@ extension TimelineProxyMock { sendReadReceiptForTypeReturnValue = .success(()) if configuration.isAutoUpdating { - underlyingTimelineProvider = AutoUpdatingTimelineProviderMock() + underlyingTimelineItemProvider = AutoUpdatingTimelineItemProviderMock() } else { - let timelineProvider = TimelineProviderMock() - timelineProvider.paginationState = .init(backward: configuration.timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached) - timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher() - underlyingTimelineProvider = timelineProvider + let timelineItemProvider = TimelineItemProviderMock() + timelineItemProvider.paginationState = .init(backward: configuration.timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached) + timelineItemProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher() + underlyingTimelineItemProvider = timelineItemProvider } } } diff --git a/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift b/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift index bdc42a32cc..16bd1e1b17 100644 --- a/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift +++ b/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift @@ -19,7 +19,7 @@ class NetworkMonitor: NetworkMonitorProtocol { } init() { - queue = DispatchQueue(label: "io.element.elementx.networkmonitor", qos: .background) + queue = DispatchQueue(label: "io.element.elementx.network_monitor", qos: .background) pathMonitor = NWPathMonitor() reachabilitySubject = CurrentValueSubject(.reachable) diff --git a/ElementX/Sources/Other/SDKListener.swift b/ElementX/Sources/Other/SDKListener.swift index 65749f9b11..985a217760 100644 --- a/ElementX/Sources/Other/SDKListener.swift +++ b/ElementX/Sources/Other/SDKListener.swift @@ -94,7 +94,7 @@ extension SDKListener: ProgressWatcher where T == Double { } } -// MARK: TimelineProvider +// MARK: TimelineItemProvider extension SDKListener: TimelineListener where T == [TimelineDiff] { func onUpdate(diff: [TimelineDiff]) { onUpdateClosure(diff) } diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 3a89359526..430404bb41 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -28,26 +28,6 @@ struct PinnedEventsTimelineScreen: View { .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 @@ -68,10 +48,7 @@ struct PinnedEventsTimelineScreen: View { .padding(.top, 48) .padding(.horizontal, 16) } else { - TimelineView() - .id(timelineContext.viewState.roomID) - .environmentObject(timelineContext) - .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID) + TimelineView(timelineContext: timelineContext) } } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift index ca8d279dcc..67297a5053 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift @@ -45,7 +45,7 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh } .store(in: &cancellables) - roomProxy.timeline.timelineProvider.membershipChangePublisher.sink { [roomProxy] in + roomProxy.timeline.timelineItemProvider.membershipChangePublisher.sink { [roomProxy] in Task { await roomProxy.updateMembers() } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index ed9544f0ec..939ea8f199 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -20,15 +20,15 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr private let attributedStringBuilder: AttributedStringBuilderProtocol private let appSettings: AppSettings - private var pinnedEventsTimelineProvider: TimelineProviderProtocol? { + private var pinnedEventsTimelineItemProvider: TimelineItemProviderProtocol? { didSet { - guard let pinnedEventsTimelineProvider else { + guard let pinnedEventsTimelineItemProvider else { return } - state.pinnedEventsActionState = .loaded(numberOfItems: pinnedEventsTimelineProvider.itemProxies.filter(\.isEvent).count) + state.pinnedEventsActionState = .loaded(numberOfItems: pinnedEventsTimelineItemProvider.itemProxies.filter(\.isEvent).count) - pinnedEventsTimelineProvider.updatePublisher + pinnedEventsTimelineItemProvider.updatePublisher // When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) .sink { [weak self] updatedItems, _ in @@ -86,7 +86,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr .filter { $0 == .reachable } .receive(on: DispatchQueue.main) .sink { [weak self] _ in - self?.setupPinnedEventsTimelineProviderIfNeeded() + self?.setupPinnedEventsTimelineItemProviderIfNeeded() } .store(in: &cancellables) @@ -439,8 +439,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } } - private func setupPinnedEventsTimelineProviderIfNeeded() { - guard pinnedEventsTimelineProvider == nil else { + private func setupPinnedEventsTimelineItemProviderIfNeeded() { + guard pinnedEventsTimelineItemProvider == nil else { return } @@ -449,8 +449,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr return } - if pinnedEventsTimelineProvider == nil { - pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider + if pinnedEventsTimelineItemProvider == nil { + pinnedEventsTimelineItemProvider = pinnedEventsTimeline.timelineItemProvider } } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index f9de111b42..b267adf908 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -78,7 +78,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe } .store(in: &cancellables) - roomProxy.timeline.timelineProvider.membershipChangePublisher.sink { [roomProxy] _ in + roomProxy.timeline.timelineItemProvider.membershipChangePublisher.sink { [roomProxy] _ in Task { await roomProxy.updateMembers() } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index d8f398eddb..53f2c8131a 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -32,14 +32,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.eraseToAnyPublisher() } - private var pinnedEventsTimelineProvider: TimelineProviderProtocol? { + private var pinnedEventsTimelineItemProvider: TimelineItemProviderProtocol? { didSet { - guard let pinnedEventsTimelineProvider else { + guard let pinnedEventsTimelineItemProvider else { return } - buildPinnedEventContents(timelineItems: pinnedEventsTimelineProvider.itemProxies) - pinnedEventsTimelineProvider.updatePublisher + buildPinnedEventContents(timelineItems: pinnedEventsTimelineItemProvider.itemProxies) + pinnedEventsTimelineItemProvider.updatePublisher // When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) .sink { [weak self] updatedItems, _ in @@ -193,7 +193,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .filter { $0 == .reachable } .receive(on: DispatchQueue.main) .sink { [weak self] _ in - self?.setupPinnedEventsTimelineProviderIfNeeded() + self?.setupPinnedEventsTimelineItemProviderIfNeeded() } .store(in: &cancellables) @@ -347,8 +347,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.canBan = await (try? roomProxy.canUserBan(userID: ownUserID).get()) == true } - private func setupPinnedEventsTimelineProviderIfNeeded() { - guard pinnedEventsTimelineProvider == nil else { + private func setupPinnedEventsTimelineItemProviderIfNeeded() { + guard pinnedEventsTimelineItemProvider == nil else { return } @@ -357,8 +357,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return } - if pinnedEventsTimelineProvider == nil { - pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider + if pinnedEventsTimelineItemProvider == nil { + pinnedEventsTimelineItemProvider = pinnedEventsTimeline.timelineItemProvider } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index ce7e431a1b..e7f3a526a7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -26,7 +26,10 @@ struct RoomScreen: View { } var body: some View { - timeline + TimelineView(timelineContext: timelineContext) + .overlay(alignment: .bottomTrailing) { + scrollToBottomButton + } .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .overlay(alignment: .top) { pinnedItemsBanner @@ -65,38 +68,6 @@ struct RoomScreen: View { .toolbar { toolbar } .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .overlay { loadingIndicator } - .alert(item: $timelineContext.alertInfo) - .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) - } - } - .sheet(item: $timelineContext.reactionSummaryInfo) { - ReactionsSummaryView(reactions: $0.reactions, - members: timelineContext.viewState.members, - mediaProvider: timelineContext.mediaProvider, - selectedReactionKey: $0.selectedKey) - .edgesIgnoringSafeArea([.bottom]) - } - .sheet(item: $timelineContext.readReceiptsSummaryInfo) { - ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) - .environmentObject(timelineContext) - } .timelineMediaPreview(viewModel: $roomContext.mediaPreviewViewModel) .track(screen: .Room) .onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in @@ -110,16 +81,6 @@ struct RoomScreen: View { } .sentryTrace("\(Self.self)") } - - private var timeline: some View { - TimelineView() - .id(timelineContext.viewState.roomID) - .environmentObject(timelineContext) - .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID) - .overlay(alignment: .bottomTrailing) { - scrollToBottomButton - } - } @ViewBuilder private var pinnedItemsBanner: some View { diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift index 1479c8f4c1..c00ca36c4a 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift @@ -13,39 +13,11 @@ struct ThreadTimelineScreen: View { @ObservedObject var timelineContext: TimelineViewModel.Context var body: some View { - content + TimelineView(timelineContext: timelineContext) .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) } } diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 98102ee112..5ee7816895 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -26,6 +26,8 @@ enum TimelineInteractionHandlerAction { case viewInRoomTimeline(eventID: String) } +/// The interaction handler groups logic for dealing with various actions the user can take on a timeline's +/// view that would've normally been part of the ``TimelineViewModel`` @MainActor class TimelineInteractionHandler { private let roomProxy: JoinedRoomProxyProtocol diff --git a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index 17910355cf..1b1b9a9462 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -45,7 +45,7 @@ class TypingMembersObservableObject: ObservableObject { /// extra keyboard handling magic that wasn't playing well with SwiftUI (as of iOS 16.1). /// Also this TableViewController uses a **flipped tableview** class TimelineTableViewController: UIViewController { - private let coordinator: TimelineView.Coordinator + private let coordinator: TimelineViewRepresentable.Coordinator private let tableView = UITableView(frame: .zero, style: .plain) var timelineItemsDictionary = OrderedDictionary() { @@ -168,7 +168,7 @@ class TimelineTableViewController: UIViewController { /// Whether or not the view has been shown on screen yet. private var hasAppearedOnce = false - init(coordinator: TimelineView.Coordinator, + init(coordinator: TimelineViewRepresentable.Coordinator, isScrolledToBottom: Binding, scrollToBottomPublisher: PassthroughSubject) { self.coordinator = coordinator diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 072cae9e6b..8cbffded95 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -8,8 +8,51 @@ import SwiftUI import WysiwygComposer +struct TimelineView: View { + @ObservedObject var timelineContext: TimelineViewModel.Context + + var body: some View { + TimelineViewRepresentable() + .id(timelineContext.viewState.roomID) + .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID) + .alert(item: $timelineContext.alertInfo) + .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) + } + } + .sheet(item: $timelineContext.reactionSummaryInfo) { + ReactionsSummaryView(reactions: $0.reactions, + members: timelineContext.viewState.members, + mediaProvider: timelineContext.mediaProvider, + selectedReactionKey: $0.selectedKey) + .edgesIgnoringSafeArea([.bottom]) + } + .sheet(item: $timelineContext.readReceiptsSummaryInfo) { + ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) + } + // Ensure these are available in the sheets as well. The order is important. + .environmentObject(timelineContext) + .environment(\.timelineContext, timelineContext) + } +} + /// A table view wrapper that displays the timeline of a room. -struct TimelineView: UIViewControllerRepresentable { +struct TimelineViewRepresentable: UIViewControllerRepresentable { @EnvironmentObject private var viewModelContext: TimelineViewModel.Context @Environment(\.openURL) var openURL diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index d8cfb79a37..1a4ac72ef1 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -63,7 +63,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { notificationSettings: NotificationSettingsProxyProtocol, appSettings: AppSettings) { self.roomListService = roomListService - serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider", qos: .default) + serialDispatchQueue = DispatchQueue(label: "io.element.elementx.room_summary_provider", qos: .default) self.eventStringBuilder = eventStringBuilder self.name = name self.shouldUpdateVisibleRange = shouldUpdateVisibleRange diff --git a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift index 810ccfd550..b08e2570df 100644 --- a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift +++ b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift @@ -13,7 +13,7 @@ import MatrixRustSDK final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol { private let roomDirectorySearch: RoomDirectorySearchProtocol private let appSettings: AppSettings - private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomdirectorysearch", qos: .default) + private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.room_directory_search_proxy", qos: .default) private let resultsSubject = CurrentValueSubject<[RoomDirectorySearchResult], Never>([]) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 756991c92e..e7d8ccf057 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -13,19 +13,17 @@ import UIKit class TimelineController: TimelineControllerProtocol { private let roomProxy: JoinedRoomProxyProtocol - private let liveTimelineProvider: TimelineProviderProtocol + private let liveTimelineItemProvider: TimelineItemProviderProtocol private let timelineItemFactory: RoomTimelineItemFactoryProtocol private let mediaProvider: MediaProviderProtocol private let appSettings: AppSettings - private let serialDispatchQueue: DispatchQueue - let callbacks = PassthroughSubject() private var activeTimeline: TimelineProxyProtocol - private var activeTimelineProvider: TimelineProviderProtocol { + private var activeTimelineItemProvider: TimelineItemProviderProtocol { didSet { - configureActiveTimelineProvider() + configureActiveTimelineItemProvider() } } @@ -42,7 +40,7 @@ class TimelineController: TimelineControllerProtocol { } var timelineKind: TimelineKind { - activeTimelineProvider.kind + activeTimelineItemProvider.kind } init(roomProxy: JoinedRoomProxyProtocol, @@ -52,18 +50,16 @@ class TimelineController: TimelineControllerProtocol { mediaProvider: MediaProviderProtocol, appSettings: AppSettings) { self.roomProxy = roomProxy - liveTimelineProvider = timelineProxy.timelineProvider + liveTimelineItemProvider = timelineProxy.timelineItemProvider self.timelineItemFactory = timelineItemFactory self.mediaProvider = mediaProvider self.appSettings = appSettings - serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timelineprovider", qos: .utility) - activeTimeline = timelineProxy - activeTimelineProvider = liveTimelineProvider + activeTimelineItemProvider = liveTimelineItemProvider guard let initialFocussedEventID else { - configureActiveTimelineProvider() + configureActiveTimelineItemProvider() return } @@ -75,7 +71,7 @@ class TimelineController: TimelineControllerProtocol { break case .failure: // Setup the live timeline as a fallback. - configureActiveTimelineProvider() + configureActiveTimelineItemProvider() } } } @@ -85,7 +81,7 @@ class TimelineController: TimelineControllerProtocol { case .success(let timeline): await timeline.subscribeForUpdates() activeTimeline = timeline - activeTimelineProvider = timeline.timelineProvider + activeTimelineItemProvider = timeline.timelineItemProvider return .success(()) case .failure(let error): if case .eventNotFound = error { @@ -98,7 +94,7 @@ class TimelineController: TimelineControllerProtocol { func focusLive() { activeTimeline = roomProxy.timeline - activeTimelineProvider = liveTimelineProvider + activeTimelineItemProvider = liveTimelineItemProvider } func paginateBackwards(requestSize: UInt16) async -> Result { @@ -328,7 +324,7 @@ class TimelineController: TimelineControllerProtocol { // Handle this parallel to the timeline items so we're not forced // to bundle the Rust side objects within them func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo { - for timelineItemProxy in activeTimelineProvider.itemProxies { + for timelineItemProxy in activeTimelineItemProvider.itemProxies { switch timelineItemProxy { case .event(let item): if item.id == itemID { @@ -343,7 +339,7 @@ class TimelineController: TimelineControllerProtocol { } func sendHandle(for itemID: TimelineItemIdentifier) -> SendHandleProxy? { - for timelineItemProxy in activeTimelineProvider.itemProxies { + for timelineItemProxy in activeTimelineItemProvider.itemProxies { switch timelineItemProxy { case .event(let item): if item.id == itemID { @@ -361,24 +357,24 @@ class TimelineController: TimelineControllerProtocol { /// The cancellable used to update the timeline items. private var updateTimelineItemsCancellable: AnyCancellable? - /// The controller is switching the `activeTimelineProvider`. + /// The controller is switching the `activeTimelineItemProvider`. private var isSwitchingTimelines = false /// Configures the controller to listen to `activeTimeline` for events. /// - Parameter clearExistingItems: Whether or not to clear any existing items before loading the timeline's contents. - private func configureActiveTimelineProvider() { + private func configureActiveTimelineItemProvider() { updateTimelineItemsCancellable = nil isSwitchingTimelines = true // Inform the world that the initial items are loading from the store paginationState = PaginationState(backward: .paginating, forward: .paginating) - callbacks.send(.isLive(activeTimelineProvider.kind == .live)) + callbacks.send(.isLive(activeTimelineItemProvider.kind == .live)) - updateTimelineItemsCancellable = Task { [weak self, activeTimelineProvider] in + updateTimelineItemsCancellable = Task { [weak self, activeTimelineItemProvider] in let contentSizeChangePublisher = NotificationCenter.default.publisher(for: UIContentSizeCategory.didChangeNotification) - let timelineUpdates = activeTimelineProvider.updatePublisher.merge(with: contentSizeChangePublisher.map { _ in - (activeTimelineProvider.itemProxies, activeTimelineProvider.paginationState) + let timelineUpdates = activeTimelineItemProvider.updatePublisher.merge(with: contentSizeChangePublisher.map { _ in + (activeTimelineItemProvider.itemProxies, activeTimelineItemProvider.paginationState) }) for await (items, paginationState) in timelineUpdates.values { @@ -506,7 +502,7 @@ class TimelineController: TimelineControllerProtocol { } func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? { - for itemProxy in activeTimelineProvider.itemProxies { + for itemProxy in activeTimelineItemProvider.itemProxies { switch itemProxy { case .event(let eventTimelineItemProxy): if eventTimelineItemProxy.id == itemID { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index c9d492faae..beb741b058 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -33,6 +33,11 @@ enum TimelineControllerError: Error { case eventNotFound } +/// This protocol is a high level abstraction on top of the ``TimelineProxyProtocol`` +/// and its ``TimelineItemProviderProtocol`` that offers an UI layer oriented interface for dealing +/// with Room timelines. +/// It, for example, permits switching from a live timeline to an event focused one, building view specific +/// timeline items, grouping together state events, donating intents to the larger system etc. @MainActor protocol TimelineControllerProtocol { var roomID: String { get } diff --git a/ElementX/Sources/Services/Timeline/TimelineProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift similarity index 98% rename from ElementX/Sources/Services/Timeline/TimelineProvider.swift rename to ElementX/Sources/Services/Timeline/TimelineItemProvider.swift index 2fe37fb41f..89724b2dcc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift @@ -9,7 +9,7 @@ import Combine import Foundation import MatrixRustSDK -class TimelineProvider: TimelineProviderProtocol { +class TimelineItemProvider: TimelineItemProviderProtocol { private var cancellables = Set() private let serialDispatchQueue: DispatchQueue @@ -46,7 +46,7 @@ class TimelineProvider: TimelineProviderProtocol { } init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher) { - serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timelineprovider", qos: .utility) + serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timeline_item_provider", qos: .utility) itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([]) self.kind = kind diff --git a/ElementX/Sources/Services/Timeline/TimelineProviderProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItemProviderProtocol.swift similarity index 80% rename from ElementX/Sources/Services/Timeline/TimelineProviderProtocol.swift rename to ElementX/Sources/Services/Timeline/TimelineItemProviderProtocol.swift index 8e60c7dea1..ce9289ab6a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProviderProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProviderProtocol.swift @@ -27,9 +27,11 @@ struct PaginationState: Equatable { let forward: PaginationStatus } +/// Entities implementing this protocol are responsible for processings diffs coming from the rust timeline +/// and converting them into an array of Element X specific ``TimelineItemProxy``s that will be +/// published as an array together with the pagination state through the ``updatePublisher``. @MainActor -// sourcery: AutoMockable -protocol TimelineProviderProtocol { +protocol TimelineItemProviderProtocol { /// A publisher that signals when ``itemProxies`` or ``paginationState`` are changed. var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { get } /// The current set of items in the timeline. @@ -43,3 +45,6 @@ protocol TimelineProviderProtocol { /// This is temporary and will be replace by a subscription on the room itself. var membershipChangePublisher: AnyPublisher { get } } + +// sourcery: AutoMockable +extension TimelineItemProviderProtocol { } diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 6e418eccc0..ab0d26a258 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -24,9 +24,9 @@ final class TimelineProxy: TimelineProxyProtocol { private let kind: TimelineKind - private var innerTimelineProvider: TimelineProviderProtocol! - var timelineProvider: TimelineProviderProtocol { - innerTimelineProvider + private var innerTimelineItemProvider: TimelineItemProviderProtocol! + var timelineItemProvider: TimelineItemProviderProtocol { + innerTimelineItemProvider } deinit { @@ -39,7 +39,7 @@ final class TimelineProxy: TimelineProxyProtocol { } func subscribeForUpdates() async { - guard innerTimelineProvider == nil else { + guard innerTimelineItemProvider == nil else { MXLog.warning("Timeline already subscribed for updates") return } @@ -51,12 +51,12 @@ final class TimelineProxy: TimelineProxyProtocol { await subscribeToPagination() - let provider = await TimelineProvider(timeline: timeline, kind: kind, paginationStatePublisher: paginationStatePublisher) + let provider = await TimelineItemProvider(timeline: timeline, kind: kind, paginationStatePublisher: paginationStatePublisher) // Make sure the existing items are built so that we have content in the timeline before // determining whether or not the timeline should paginate to load more items. await provider.waitForInitialItems() - innerTimelineProvider = provider + innerTimelineItemProvider = provider Task { await timeline.fetchMembers() @@ -76,7 +76,7 @@ final class TimelineProxy: TimelineProxyProtocol { } func messageEventContent(for timelineItemID: TimelineItemIdentifier) async -> RoomMessageEventContentWithoutRelation? { - guard let content = await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content, + guard let content = await timelineItemProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content, case let .msgLike(messageLikeContent) = content, case let .message(messageContent) = messageLikeContent.kind else { return nil diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 55b28377cd..d6dd4f1ba7 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -37,9 +37,11 @@ enum TimelineProxyError: Error { case failedPaginatingEndReached } -// sourcery: AutoMockable +/// Element X proxies generally wrap the counterpart RustSDK objects while providing platform specific +/// interfaces. In this case it composes methods for interacting with a room's timeline and should be used alongside +/// the ``TimelineItemProviderProtocol`` which offers a reactive interface to timeline changes. protocol TimelineProxyProtocol { - var timelineProvider: TimelineProviderProtocol { get } + var timelineItemProvider: TimelineItemProviderProtocol { get } func subscribeForUpdates() async @@ -125,3 +127,6 @@ protocol TimelineProxyProtocol { html: String?, intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation } + +// sourcery: AutoMockable +extension TimelineProxyProtocol { } diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 0b68be1ff4..b97ecc9dda 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -70,12 +70,12 @@ class RoomScreenViewModelTests: XCTestCase { // setup the loaded pinned events injection in the timeline let pinnedTimelineMock = TimelineProxyMock() - let pinnedTimelineProviderMock = TimelineProviderMock() + let pinnedTimelineItemProviderMock = TimelineItemProviderMock() let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>() - pinnedTimelineProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher() - pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock - pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), - .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2")))] + pinnedTimelineItemProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher() + pinnedTimelineMock.timelineItemProvider = pinnedTimelineItemProviderMock + pinnedTimelineItemProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2")))] // check if the banner is now in a loaded state and is showing the counter deferred = deferFulfillment(viewModel.context.$viewState) { viewState in @@ -111,12 +111,12 @@ class RoomScreenViewModelTests: XCTestCase { let roomProxyMock = JoinedRoomProxyMock(.init()) // setup a way to inject the mock of the pinned events timeline let pinnedTimelineMock = TimelineProxyMock() - let pinnedTimelineProviderMock = TimelineProviderMock() - pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock - pinnedTimelineProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher() - pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), - .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))), - .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))] + let pinnedTimelineItemProviderMock = TimelineItemProviderMock() + pinnedTimelineMock.timelineItemProvider = pinnedTimelineItemProviderMock + pinnedTimelineItemProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher() + pinnedTimelineItemProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))] roomProxyMock.pinnedEventsTimelineReturnValue = .success(pinnedTimelineMock) let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), roomProxy: roomProxyMock,