Media gallery - part 1(#3588)

* Introduce a `MediaEventsTimelineFlowCoordinator`
* Update SDK API and architecture
* Add a feature flag, add translations
* Move the media events timeline presentation under the room flow coordinator state machine
* Rename `TimelineViewState.timelineViewState` of type `TimelineState` to `timelineState`
* Enabled SwiftLint's `trailing_closure` rule and fix the warnings.
This commit is contained in:
Stefan Ceriu 2024-12-06 16:58:14 +02:00 committed by GitHub
parent a9e4837b62
commit caaa89af62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 1247 additions and 279 deletions

View File

@ -9,6 +9,7 @@ opt_in_rules:
- private_action - private_action
- explicit_init - explicit_init
- shorthand_optional_binding - shorthand_optional_binding
- trailing_closure
included: included:
- ElementX - ElementX

View File

@ -637,6 +637,7 @@
847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */; }; 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */; };
84C631E734FD2555B39B681C /* RoomRolesAndPermissionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */; }; 84C631E734FD2555B39B681C /* RoomRolesAndPermissionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */; };
84CAE3E96D93194DA06B9194 /* CallScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9AD6AE5FC868962F090740 /* CallScreenViewModelProtocol.swift */; }; 84CAE3E96D93194DA06B9194 /* CallScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9AD6AE5FC868962F090740 /* CallScreenViewModelProtocol.swift */; };
84E514915DF0C168B08A3A0A /* MediaEventsTimelineFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2178B951602AA921A5FD9DC8 /* MediaEventsTimelineFlowCoordinator.swift */; };
84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; }; 84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; };
8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */; }; 8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */; };
854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */; }; 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */; };
@ -885,6 +886,7 @@
B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; };
B79E8AB83EBBDCD476D0362F /* PollFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622EC7898469BB1D0881CDD /* PollFormScreen.swift */; }; B79E8AB83EBBDCD476D0362F /* PollFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622EC7898469BB1D0881CDD /* PollFormScreen.swift */; };
B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28146817C61423CACCF942F5 /* CallScreenModels.swift */; }; B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28146817C61423CACCF942F5 /* CallScreenModels.swift */; };
B7F58D6903F9D509EDAB9E4F /* MediaEventsTimelineScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033218DA395B003F7AB29A2 /* MediaEventsTimelineScreenModels.swift */; };
B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */; }; B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */; };
B855AF29D7D8FC8DAAA73D4A /* test_voice_message.m4a in Resources */ = {isa = PBXBuildFile; fileRef = DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */; }; B855AF29D7D8FC8DAAA73D4A /* test_voice_message.m4a in Resources */ = {isa = PBXBuildFile; fileRef = DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */; };
B879446FD8E65A711EF8F9F7 /* AdvancedSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */; }; B879446FD8E65A711EF8F9F7 /* AdvancedSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */; };
@ -915,6 +917,7 @@
BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; }; BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; };
BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; }; BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */; }; BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */; };
BEC6DFEA506085D3027E353C /* MediaEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */; };
BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */; }; BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */; };
C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; }; C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; };
C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
@ -922,6 +925,7 @@
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; }; C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; };
C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */; }; C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */; };
C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */; }; C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */; };
C11D4A49DC29D89CE2BB31B8 /* MediaEventsTimelineScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976ED77B772F50C4BAD757E7 /* MediaEventsTimelineScreenViewModel.swift */; };
C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */; }; C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */; };
C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */; }; C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */; };
C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; }; C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; };
@ -960,6 +964,7 @@
C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; }; C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; };
C8C7AF33AADF88B306CD2695 /* QRCodeLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4427AF4B7FB7EF3E3D424C7 /* QRCodeLoginService.swift */; }; C8C7AF33AADF88B306CD2695 /* QRCodeLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4427AF4B7FB7EF3E3D424C7 /* QRCodeLoginService.swift */; };
C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; }; C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; };
C8E1E4E06B7C7A3A8246FC9B /* MediaEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */; };
C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; };
C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */; }; C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */; };
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; };
@ -1203,6 +1208,7 @@
FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; }; FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; };
FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; };
FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; };
FD9777315A5D9CDC47458AD1 /* MediaEventsTimelineScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A175D0FDEDBFA44C47FE13AE /* MediaEventsTimelineScreenViewModelProtocol.swift */; };
FDC67E8C0EDCB00ABC66C859 /* landscape_test_video.mov in Resources */ = {isa = PBXBuildFile; fileRef = 78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */; }; FDC67E8C0EDCB00ABC66C859 /* landscape_test_video.mov in Resources */ = {isa = PBXBuildFile; fileRef = 78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */; };
FDD5B4B616D9FF4DE3E9A418 /* QRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DB574F954CC2B40F7BE892 /* QRCodeScannerView.swift */; }; FDD5B4B616D9FF4DE3E9A418 /* QRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DB574F954CC2B40F7BE892 /* QRCodeScannerView.swift */; };
FDE47D4686BA0F86BB584633 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = CAA3B9DF998B397C9EE64E8B /* Collections */; }; FDE47D4686BA0F86BB584633 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = CAA3B9DF998B397C9EE64E8B /* Collections */; };
@ -1287,6 +1293,7 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreen.swift; sourceTree = "<group>"; };
00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = "<group>"; }; 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = "<group>"; };
00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreen.swift; sourceTree = "<group>"; }; 00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreen.swift; sourceTree = "<group>"; };
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = "<group>"; }; 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
@ -1445,6 +1452,7 @@
20E69F67D2A70ABD08CA6D54 /* NotificationPermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 20E69F67D2A70ABD08CA6D54 /* NotificationPermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
2141693488CE5446BB391964 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; }; 2141693488CE5446BB391964 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItemContent.swift; sourceTree = "<group>"; }; 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItemContent.swift; sourceTree = "<group>"; };
2178B951602AA921A5FD9DC8 /* MediaEventsTimelineFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineFlowCoordinator.swift; sourceTree = "<group>"; };
218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = "<group>"; }; 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = "<group>"; };
21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
21DD8599815136EFF5B73F38 /* UserFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFlowTests.swift; sourceTree = "<group>"; }; 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFlowTests.swift; sourceTree = "<group>"; };
@ -1802,6 +1810,7 @@
6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsBannerStateTests.swift; sourceTree = "<group>"; }; 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsBannerStateTests.swift; sourceTree = "<group>"; };
6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceProtocol.swift; sourceTree = "<group>"; }; 6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceProtocol.swift; sourceTree = "<group>"; };
7033218DA395B003F7AB29A2 /* MediaEventsTimelineScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenModels.swift; sourceTree = "<group>"; };
7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModel.swift; sourceTree = "<group>"; }; 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModel.swift; sourceTree = "<group>"; };
70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = "<group>"; }; 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = "<group>"; };
713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@ -1892,6 +1901,7 @@
84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModel.swift; sourceTree = "<group>"; }; 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModel.swift; sourceTree = "<group>"; };
84AF32E4136FD6F159D86C2C /* RoomDirectorySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchView.swift; sourceTree = "<group>"; }; 84AF32E4136FD6F159D86C2C /* RoomDirectorySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchView.swift; sourceTree = "<group>"; };
84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunnerTests.swift; sourceTree = "<group>"; }; 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunnerTests.swift; sourceTree = "<group>"; };
8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenCoordinator.swift; sourceTree = "<group>"; };
85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
851B95BB98649B8E773D6790 /* AppLockService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockService.swift; sourceTree = "<group>"; }; 851B95BB98649B8E773D6790 /* AppLockService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockService.swift; sourceTree = "<group>"; };
8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; }; 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
@ -1975,6 +1985,7 @@
96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = "<group>"; }; 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = "<group>"; };
97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreen.swift; sourceTree = "<group>"; }; 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreen.swift; sourceTree = "<group>"; };
974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPermissions.swift; sourceTree = "<group>"; }; 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPermissions.swift; sourceTree = "<group>"; };
976ED77B772F50C4BAD757E7 /* MediaEventsTimelineScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenViewModel.swift; sourceTree = "<group>"; };
9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = "<group>"; };
97B2ACA28A854E41AE3AC9AD /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = "<group>"; }; 97B2ACA28A854E41AE3AC9AD /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = "<group>"; };
97C8E13A1FBA717B0C277ECC /* ProgressCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressCursorModifier.swift; sourceTree = "<group>"; }; 97C8E13A1FBA717B0C277ECC /* ProgressCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressCursorModifier.swift; sourceTree = "<group>"; };
@ -2021,6 +2032,7 @@
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; }; A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = "<group>"; }; A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = "<group>"; };
A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenCoordinator.swift; sourceTree = "<group>"; }; A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenCoordinator.swift; sourceTree = "<group>"; };
A175D0FDEDBFA44C47FE13AE /* MediaEventsTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = "<group>"; }; A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = "<group>"; };
A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = "<group>"; }; A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = "<group>"; };
A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; }; A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
@ -2908,6 +2920,18 @@
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
26397A1EDB867FD573821532 /* MediaEventsTimelineScreen */ = {
isa = PBXGroup;
children = (
8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */,
7033218DA395B003F7AB29A2 /* MediaEventsTimelineScreenModels.swift */,
976ED77B772F50C4BAD757E7 /* MediaEventsTimelineScreenViewModel.swift */,
A175D0FDEDBFA44C47FE13AE /* MediaEventsTimelineScreenViewModelProtocol.swift */,
DB180A1068D7B85489E13E3F /* View */,
);
path = MediaEventsTimelineScreen;
sourceTree = "<group>";
};
26C16326BCCCED74A85A0F48 /* View */ = { 26C16326BCCCED74A85A0F48 /* View */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3704,6 +3728,7 @@
7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */, 7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */,
A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */, A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */,
ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */, ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */,
2178B951602AA921A5FD9DC8 /* MediaEventsTimelineFlowCoordinator.swift */,
C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */, C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */,
A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */, A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */,
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */, 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */,
@ -5371,6 +5396,14 @@
path = RoomChangePermissionsScreen; path = RoomChangePermissionsScreen;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
DB180A1068D7B85489E13E3F /* View */ = {
isa = PBXGroup;
children = (
002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
DD96B3F20F354494DECBC4F7 /* View */ = { DD96B3F20F354494DECBC4F7 /* View */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -5469,6 +5502,7 @@
BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */, BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */,
948DD12A5533BE1BC260E437 /* LocationSharing */, 948DD12A5533BE1BC260E437 /* LocationSharing */,
73E032ADD008D63812791D97 /* LogViewerScreen */, 73E032ADD008D63812791D97 /* LogViewerScreen */,
26397A1EDB867FD573821532 /* MediaEventsTimelineScreen */,
87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */, 87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */,
23605DD08620BE6558242469 /* MediaUploadPreviewScreen */, 23605DD08620BE6558242469 /* MediaUploadPreviewScreen */,
3348D14DBDB54E72FC67E2F3 /* MessageForwardingScreen */, 3348D14DBDB54E72FC67E2F3 /* MessageForwardingScreen */,
@ -6933,6 +6967,12 @@
BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */, BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */,
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */, 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */,
8658F5034EAD7357CE7F9AC7 /* MatrixUserShareLink.swift in Sources */, 8658F5034EAD7357CE7F9AC7 /* MatrixUserShareLink.swift in Sources */,
84E514915DF0C168B08A3A0A /* MediaEventsTimelineFlowCoordinator.swift in Sources */,
BEC6DFEA506085D3027E353C /* MediaEventsTimelineScreen.swift in Sources */,
C8E1E4E06B7C7A3A8246FC9B /* MediaEventsTimelineScreenCoordinator.swift in Sources */,
B7F58D6903F9D509EDAB9E4F /* MediaEventsTimelineScreenModels.swift in Sources */,
C11D4A49DC29D89CE2BB31B8 /* MediaEventsTimelineScreenViewModel.swift in Sources */,
FD9777315A5D9CDC47458AD1 /* MediaEventsTimelineScreenViewModelProtocol.swift in Sources */,
BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */, BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */,
208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */, 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */,
A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */, A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */,

View File

@ -1048,7 +1048,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
.actionsPublisher .actionsPublisher
.filter(\.isSyncUpdate) .filter(\.isSyncUpdate)
.collect(.byTimeOrCount(DispatchQueue.main, .seconds(10), 10)) .collect(.byTimeOrCount(DispatchQueue.main, .seconds(10), 10))
.sink(receiveValue: { [weak self] _ in .sink { [weak self] _ in
guard let self else { return } guard let self else { return }
MXLog.info("Background app refresh finished") MXLog.info("Background app refresh finished")
backgroundRefreshSyncObserver?.cancel() backgroundRefreshSyncObserver?.cancel()
@ -1059,6 +1059,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
MXLog.info("Marking Background app refresh task as complete.") MXLog.info("Marking Background app refresh task as complete.")
task.setTaskCompleted(success: true) task.setTaskCompleted(success: true)
} }
}) }
} }
} }

View File

@ -48,6 +48,7 @@ final class AppSettings {
case enableOnlySignedDeviceIsolationMode case enableOnlySignedDeviceIsolationMode
case knockingEnabled case knockingEnabled
case createMediaCaptionsEnabled case createMediaCaptionsEnabled
case mediaBrowserEnabled
} }
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
@ -288,6 +289,9 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.createMediaCaptionsEnabled, defaultValue: false, storageType: .userDefaults(store)) @UserPreference(key: UserDefaultsKeys.createMediaCaptionsEnabled, defaultValue: false, storageType: .userDefaults(store))
var createMediaCaptionsEnabled var createMediaCaptionsEnabled
@UserPreference(key: UserDefaultsKeys.mediaBrowserEnabled, defaultValue: false, storageType: .userDefaults(store))
var mediaBrowserEnabled
#endif #endif
// MARK: - Shared // MARK: - Shared

View File

@ -42,11 +42,11 @@ struct Application: App {
openURLInSystemBrowser($0) openURLInSystemBrowser($0)
} }
} }
.onContinueUserActivity("INStartVideoCallIntent", perform: { userActivity in .onContinueUserActivity("INStartVideoCallIntent") { userActivity in
// `INStartVideoCallIntent` is to be replaced with `INStartCallIntent` // `INStartVideoCallIntent` is to be replaced with `INStartCallIntent`
// but calls from Recents still send it ¯\_()_/¯ // but calls from Recents still send it ¯\_()_/¯
appCoordinator.handleUserActivity(userActivity) appCoordinator.handleUserActivity(userActivity)
}) }
.task { .task {
appCoordinator.start() appCoordinator.start()
} }

View File

@ -0,0 +1,97 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import Foundation
enum MediaEventsTimelineFlowCoordinatorAction {
case finished
}
class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
private let navigationStackCoordinator: NavigationStackCoordinator
private let userSession: UserSessionProtocol
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let appMediator: AppMediatorProtocol
private let emojiProvider: EmojiProviderProtocol
private let actionsSubject: PassthroughSubject<MediaEventsTimelineFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<MediaEventsTimelineFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
private var cancellables = Set<AnyCancellable>()
init(navigationStackCoordinator: NavigationStackCoordinator,
userSession: UserSessionProtocol,
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
roomProxy: JoinedRoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
appMediator: AppMediatorProtocol,
emojiProvider: EmojiProviderProtocol) {
self.navigationStackCoordinator = navigationStackCoordinator
self.userSession = userSession
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
self.appMediator = appMediator
self.emojiProvider = emojiProvider
}
func start() {
Task { await presentMediaEventsTimeline() }
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
fatalError()
}
func clearRoute(animated: Bool) {
fatalError()
}
// MARK: - Private
private func presentMediaEventsTimeline() async {
let timelineItemFactory = RoomTimelineItemFactory(userID: userSession.clientProxy.userID,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
guard case let .success(mediaTimelineController) = await roomTimelineControllerFactory.buildMessageFilteredRoomTimelineController(allowedMessageTypes: [.image, .video],
roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider) else {
MXLog.error("Failed presenting media timeline")
return
}
guard case let .success(filesTimelineController) = await roomTimelineControllerFactory.buildMessageFilteredRoomTimelineController(allowedMessageTypes: [.file, .audio],
roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider) else {
MXLog.error("Failed presenting media timeline")
return
}
let parameters = MediaEventsTimelineScreenCoordinatorParameters(roomProxy: roomProxy,
mediaTimelineController: mediaTimelineController,
filesTimelineController: filesTimelineController,
mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(),
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
appMediator: appMediator,
emojiProvider: emojiProvider)
let coordinator = MediaEventsTimelineScreenCoordinator(parameters: parameters)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.actionsSubject.send(.finished)
}
}
}

View File

@ -65,7 +65,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, guard let timelineController = await roomTimelineControllerFactory.buildPinnedEventsRoomTimelineController(roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory, timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider) else { mediaProvider: userSession.mediaProvider) else {
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil") fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")

View File

@ -50,11 +50,6 @@ struct FocusEvent: Hashable {
let shouldSetPin: Bool let shouldSetPin: Bool
} }
private enum PinnedEventsTimelineSource: Hashable {
case room
case details(isRoot: Bool)
}
private enum PresentationAction: Hashable { private enum PresentationAction: Hashable {
case eventFocus(FocusEvent) case eventFocus(FocusEvent)
case share(ShareExtensionPayload) case share(ShareExtensionPayload)
@ -102,6 +97,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
// periphery:ignore - used to avoid deallocation // periphery:ignore - used to avoid deallocation
private var pinnedEventsTimelineFlowCoordinator: PinnedEventsTimelineFlowCoordinator? private var pinnedEventsTimelineFlowCoordinator: PinnedEventsTimelineFlowCoordinator?
// periphery:ignore - used to avoid deallocation // periphery:ignore - used to avoid deallocation
private var mediaEventsTimelineFlowCoordinator: MediaEventsTimelineFlowCoordinator?
// periphery:ignore - used to avoid deallocation
private var childRoomFlowCoordinator: RoomFlowCoordinator? private var childRoomFlowCoordinator: RoomFlowCoordinator?
private let stateMachine: StateMachine<State, Event> = .init(state: .initial) private let stateMachine: StateMachine<State, Event> = .init(state: .initial)
@ -149,7 +146,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
fatalError("This flow coordinator expect a route") fatalError("This flow coordinator expect a route")
} }
// swiftlint:disable:next cyclomatic_complexity
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
guard stateMachine.state != .complete else { guard stateMachine.state != .complete else {
fatalError("This flow coordinator is `finished` ☠️") fatalError("This flow coordinator is `finished` ☠️")
@ -369,16 +365,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return .room return .room
case (.room, .presentPinnedEventsTimeline): case (.room, .presentPinnedEventsTimeline):
return .pinnedEventsTimeline(previousState: .room) return .pinnedEventsTimeline(previousState: fromState)
case (.roomDetails(let isRoot), .presentPinnedEventsTimeline): case (.roomDetails, .presentPinnedEventsTimeline):
return .pinnedEventsTimeline(previousState: .details(isRoot: isRoot)) return .pinnedEventsTimeline(previousState: fromState)
case (.pinnedEventsTimeline(let previousState), .dismissPinnedEventsTimeline): case (.pinnedEventsTimeline(let previousState), .dismissPinnedEventsTimeline):
switch previousState { return previousState
case .room:
return .room
case .details(let isRoot):
return .roomDetails(isRoot: isRoot)
}
case (.roomDetails, .presentPollsHistory): case (.roomDetails, .presentPollsHistory):
return .pollsHistory return .pollsHistory
@ -400,8 +391,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.resolveSendFailure, .dismissResolveSendFailure): case (.resolveSendFailure, .dismissResolveSendFailure):
return .room return .room
// Child flow
case (_, .startChildFlow(let roomID, _, _)): case (_, .startChildFlow(let roomID, _, _)):
return .presentingChild(childRoomID: roomID, previousState: fromState) return .presentingChild(childRoomID: roomID, previousState: fromState)
case (.presentingChild(_, let previousState), .dismissChildFlow): case (.presentingChild(_, let previousState), .dismissChildFlow):
@ -412,6 +401,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen): case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen):
return previousState return previousState
case (.roomDetails, .presentMediaEventsTimeline):
return .mediaEventsTimeline(previousState: fromState)
case (.mediaEventsTimeline(let previousState), .dismissMediaEventsTimeline):
return previousState
default: default:
return nil return nil
} }
@ -527,7 +521,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
break break
case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline): case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
presentPinnedEventsTimeline() startPinnedEventsTimelineFlow()
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room): case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room):
break break
@ -537,7 +531,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
break break
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline): case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
presentPinnedEventsTimeline() startPinnedEventsTimelineFlow()
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails): case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails):
break break
@ -573,6 +567,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.knockRequestsList, .dismissKnockRequestsListScreen, .room): case (.knockRequestsList, .dismissKnockRequestsListScreen, .room):
break break
case (.roomDetails, .presentMediaEventsTimeline, .mediaEventsTimeline):
Task { await self.startMediaEventsTimelineFlow() }
case (.mediaEventsTimeline, .dismissMediaEventsTimeline, .roomDetails):
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) }
@ -848,6 +847,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentPinnedEventsTimeline) stateMachine.tryEvent(.presentPinnedEventsTimeline)
case .presentKnockingRequestsListScreen: case .presentKnockingRequestsListScreen:
stateMachine.tryEvent(.presentKnockRequestsListScreen) stateMachine.tryEvent(.presentKnockRequestsListScreen)
case .presentMediaEventsTimeline:
stateMachine.tryEvent(.presentMediaEventsTimeline)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@ -1432,44 +1433,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
coordinator.start() coordinator.start()
} }
private func presentPinnedEventsTimeline() {
let stackCoordinator = NavigationStackCoordinator()
let coordinator = PinnedEventsTimelineFlowCoordinator(navigationStackCoordinator: stackCoordinator,
userSession: userSession,
roomTimelineControllerFactory: roomTimelineControllerFactory,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
appMediator: appMediator,
emojiProvider: emojiProvider)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else {
return
}
switch action {
case .finished:
navigationStackCoordinator.setSheetCoordinator(nil)
case .displayUser(let userID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
case .forwardedMessageToRoom(let roomID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
case .displayRoomScreenWithFocussedPin(let eventID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true))))
}
}
.store(in: &cancellables)
pinnedEventsTimelineFlowCoordinator = coordinator
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
}
coordinator.start()
}
private func presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) { private func presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) {
let coordinator = ResolveVerifiedUserSendFailureScreenCoordinator(parameters: .init(failure: failure, let coordinator = ResolveVerifiedUserSendFailureScreenCoordinator(parameters: .init(failure: failure,
sendHandle: sendHandle, sendHandle: sendHandle,
@ -1490,7 +1453,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
// MARK: - Child Flow // MARK: - Other flows
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async { private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
let coordinator = await RoomFlowCoordinator(roomID: roomID, let coordinator = await RoomFlowCoordinator(roomID: roomID,
@ -1528,6 +1491,71 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
coordinator.handleAppRoute(.share(payload), animated: true) coordinator.handleAppRoute(.share(payload), animated: true)
} }
} }
private func startPinnedEventsTimelineFlow() {
let stackCoordinator = NavigationStackCoordinator()
let flowCoordinator = PinnedEventsTimelineFlowCoordinator(navigationStackCoordinator: stackCoordinator,
userSession: userSession,
roomTimelineControllerFactory: roomTimelineControllerFactory,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
appMediator: appMediator,
emojiProvider: emojiProvider)
flowCoordinator.actionsPublisher.sink { [weak self] action in
guard let self else {
return
}
switch action {
case .finished:
navigationStackCoordinator.setSheetCoordinator(nil)
case .displayUser(let userID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
case .forwardedMessageToRoom(let roomID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
case .displayRoomScreenWithFocussedPin(let eventID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true))))
}
}
.store(in: &cancellables)
pinnedEventsTimelineFlowCoordinator = flowCoordinator
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
}
flowCoordinator.start()
}
private func startMediaEventsTimelineFlow() async {
let flowCoordinator = MediaEventsTimelineFlowCoordinator(navigationStackCoordinator: navigationStackCoordinator,
userSession: userSession,
roomTimelineControllerFactory: roomTimelineControllerFactory,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
appMediator: appMediator,
emojiProvider: emojiProvider)
flowCoordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .finished:
stateMachine.tryEvent(.dismissMediaEventsTimeline)
}
}
.store(in: &cancellables)
mediaEventsTimelineFlowCoordinator = flowCoordinator
flowCoordinator.start()
}
} }
private extension RoomFlowCoordinator { private extension RoomFlowCoordinator {
@ -1565,9 +1593,10 @@ private extension RoomFlowCoordinator {
case pollsHistory case pollsHistory
case pollsHistoryForm case pollsHistoryForm
case rolesAndPermissions case rolesAndPermissions
case pinnedEventsTimeline(previousState: PinnedEventsTimelineSource) case pinnedEventsTimeline(previousState: State)
case resolveSendFailure case resolveSendFailure
case knockRequestsList(previousState: State) case knockRequestsList(previousState: State)
case mediaEventsTimeline(previousState: State)
/// A child flow is in progress. /// A child flow is in progress.
case presentingChild(childRoomID: String, previousState: State) case presentingChild(childRoomID: String, previousState: State)
@ -1643,12 +1672,14 @@ private extension RoomFlowCoordinator {
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case dismissResolveSendFailure case dismissResolveSendFailure
// Child room flow events
case startChildFlow(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) case startChildFlow(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint)
case dismissChildFlow case dismissChildFlow
case presentKnockRequestsListScreen case presentKnockRequestsListScreen
case dismissKnockRequestsListScreen case dismissKnockRequestsListScreen
case presentMediaEventsTimeline
case dismissMediaEventsTimeline
} }
} }

View File

@ -6147,6 +6147,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
return timelineFocusedOnEventEventIDNumberOfEventsReturnValue return timelineFocusedOnEventEventIDNumberOfEventsReturnValue
} }
} }
//MARK: - messageFilteredTimeline
var messageFilteredTimelineAllowedMessageTypesUnderlyingCallsCount = 0
var messageFilteredTimelineAllowedMessageTypesCallsCount: Int {
get {
if Thread.isMainThread {
return messageFilteredTimelineAllowedMessageTypesUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = messageFilteredTimelineAllowedMessageTypesUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
messageFilteredTimelineAllowedMessageTypesUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
messageFilteredTimelineAllowedMessageTypesUnderlyingCallsCount = newValue
}
}
}
}
var messageFilteredTimelineAllowedMessageTypesCalled: Bool {
return messageFilteredTimelineAllowedMessageTypesCallsCount > 0
}
var messageFilteredTimelineAllowedMessageTypesReceivedAllowedMessageTypes: [RoomMessageEventMessageType]?
var messageFilteredTimelineAllowedMessageTypesReceivedInvocations: [[RoomMessageEventMessageType]] = []
var messageFilteredTimelineAllowedMessageTypesUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
var messageFilteredTimelineAllowedMessageTypesReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
get {
if Thread.isMainThread {
return messageFilteredTimelineAllowedMessageTypesUnderlyingReturnValue
} else {
var returnValue: Result<TimelineProxyProtocol, RoomProxyError>? = nil
DispatchQueue.main.sync {
returnValue = messageFilteredTimelineAllowedMessageTypesUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
messageFilteredTimelineAllowedMessageTypesUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
messageFilteredTimelineAllowedMessageTypesUnderlyingReturnValue = newValue
}
}
}
}
var messageFilteredTimelineAllowedMessageTypesClosure: (([RoomMessageEventMessageType]) async -> Result<TimelineProxyProtocol, RoomProxyError>)?
func messageFilteredTimeline(allowedMessageTypes: [RoomMessageEventMessageType]) async -> Result<TimelineProxyProtocol, RoomProxyError> {
messageFilteredTimelineAllowedMessageTypesCallsCount += 1
messageFilteredTimelineAllowedMessageTypesReceivedAllowedMessageTypes = allowedMessageTypes
DispatchQueue.main.async {
self.messageFilteredTimelineAllowedMessageTypesReceivedInvocations.append(allowedMessageTypes)
}
if let messageFilteredTimelineAllowedMessageTypesClosure = messageFilteredTimelineAllowedMessageTypesClosure {
return await messageFilteredTimelineAllowedMessageTypesClosure(allowedMessageTypes)
} else {
return messageFilteredTimelineAllowedMessageTypesReturnValue
}
}
//MARK: - redact //MARK: - redact
var redactUnderlyingCallsCount = 0 var redactUnderlyingCallsCount = 0
@ -12602,17 +12672,17 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol {
return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue
} }
} }
//MARK: - buildRoomPinnedTimelineController //MARK: - buildPinnedEventsRoomTimelineController
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0 var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int { var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount return buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount returnValue = buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
} }
return returnValue! return returnValue!
@ -12620,29 +12690,29 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
} }
} }
} }
} }
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCalled: Bool { var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCalled: Bool {
return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0 return buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0
} }
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)? var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)?
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = [] var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = []
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: RoomTimelineControllerProtocol? var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: RoomTimelineControllerProtocol?
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: RoomTimelineControllerProtocol? { var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: RoomTimelineControllerProtocol? {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue return buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
} else { } else {
var returnValue: RoomTimelineControllerProtocol?? = nil var returnValue: RoomTimelineControllerProtocol?? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue returnValue = buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
} }
return returnValue! return returnValue!
@ -12650,26 +12720,96 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
} }
} }
} }
} }
var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> RoomTimelineControllerProtocol?)? var buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> RoomTimelineControllerProtocol?)?
func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? { func buildPinnedEventsRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? {
buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1 buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider) buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
DispatchQueue.main.async { DispatchQueue.main.async {
self.buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)) self.buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider))
} }
if let buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure { if let buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure = buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure {
return await buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure(roomProxy, timelineItemFactory, mediaProvider) return await buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure(roomProxy, timelineItemFactory, mediaProvider)
} else { } else {
return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue return buildPinnedEventsRoomTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue
}
}
//MARK: - buildMessageFilteredRoomTimelineController
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int {
get {
if Thread.isMainThread {
return buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue
}
}
}
}
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderCalled: Bool {
return buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0
}
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (allowedMessageTypes: [RoomMessageEventMessageType], roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)?
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(allowedMessageTypes: [RoomMessageEventMessageType], roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = []
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError>!
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError>! {
get {
if Thread.isMainThread {
return buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
} else {
var returnValue: Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError>? = nil
DispatchQueue.main.sync {
returnValue = buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue
}
}
}
}
var buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderClosure: (([RoomMessageEventMessageType], JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError>)?
func buildMessageFilteredRoomTimelineController(allowedMessageTypes: [RoomMessageEventMessageType], roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError> {
buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (allowedMessageTypes: allowedMessageTypes, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
DispatchQueue.main.async {
self.buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((allowedMessageTypes: allowedMessageTypes, roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider))
}
if let buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderClosure = buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderClosure {
return await buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderClosure(allowedMessageTypes, roomProxy, timelineItemFactory, mediaProvider)
} else {
return buildMessageFilteredRoomTimelineControllerAllowedMessageTypesRoomProxyTimelineItemFactoryMediaProviderReturnValue
} }
} }
} }

View File

@ -8248,6 +8248,71 @@ open class MediaSourceSDKMock: MatrixRustSDK.MediaSource {
{ {
} }
//MARK: - toJson
var toJsonUnderlyingCallsCount = 0
open var toJsonCallsCount: Int {
get {
if Thread.isMainThread {
return toJsonUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = toJsonUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
toJsonUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
toJsonUnderlyingCallsCount = newValue
}
}
}
}
open var toJsonCalled: Bool {
return toJsonCallsCount > 0
}
var toJsonUnderlyingReturnValue: String!
open var toJsonReturnValue: String! {
get {
if Thread.isMainThread {
return toJsonUnderlyingReturnValue
} else {
var returnValue: String? = nil
DispatchQueue.main.sync {
returnValue = toJsonUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
toJsonUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
toJsonUnderlyingReturnValue = newValue
}
}
}
}
open var toJsonClosure: (() -> String)?
open override func toJson() -> String {
toJsonCallsCount += 1
if let toJsonClosure = toJsonClosure {
return toJsonClosure()
} else {
return toJsonReturnValue
}
}
//MARK: - url //MARK: - url
var urlUnderlyingCallsCount = 0 var urlUnderlyingCallsCount = 0
@ -12813,6 +12878,81 @@ open class RoomSDKMock: MatrixRustSDK.Room {
} }
} }
//MARK: - messageFilteredTimeline
open var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesThrowableError: Error?
var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingCallsCount = 0
open var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesCallsCount: Int {
get {
if Thread.isMainThread {
return messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingCallsCount = newValue
}
}
}
}
open var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesCalled: Bool {
return messageFilteredTimelineInternalIdPrefixAllowedMessageTypesCallsCount > 0
}
open var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesReceivedArguments: (internalIdPrefix: String?, allowedMessageTypes: [RoomMessageEventMessageType])?
open var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesReceivedInvocations: [(internalIdPrefix: String?, allowedMessageTypes: [RoomMessageEventMessageType])] = []
var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingReturnValue: Timeline!
open var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesReturnValue: Timeline! {
get {
if Thread.isMainThread {
return messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingReturnValue
} else {
var returnValue: Timeline? = nil
DispatchQueue.main.sync {
returnValue = messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
messageFilteredTimelineInternalIdPrefixAllowedMessageTypesUnderlyingReturnValue = newValue
}
}
}
}
open var messageFilteredTimelineInternalIdPrefixAllowedMessageTypesClosure: ((String?, [RoomMessageEventMessageType]) async throws -> Timeline)?
open override func messageFilteredTimeline(internalIdPrefix: String?, allowedMessageTypes: [RoomMessageEventMessageType]) async throws -> Timeline {
if let error = messageFilteredTimelineInternalIdPrefixAllowedMessageTypesThrowableError {
throw error
}
messageFilteredTimelineInternalIdPrefixAllowedMessageTypesCallsCount += 1
messageFilteredTimelineInternalIdPrefixAllowedMessageTypesReceivedArguments = (internalIdPrefix: internalIdPrefix, allowedMessageTypes: allowedMessageTypes)
DispatchQueue.main.async {
self.messageFilteredTimelineInternalIdPrefixAllowedMessageTypesReceivedInvocations.append((internalIdPrefix: internalIdPrefix, allowedMessageTypes: allowedMessageTypes))
}
if let messageFilteredTimelineInternalIdPrefixAllowedMessageTypesClosure = messageFilteredTimelineInternalIdPrefixAllowedMessageTypesClosure {
return try await messageFilteredTimelineInternalIdPrefixAllowedMessageTypesClosure(internalIdPrefix, allowedMessageTypes)
} else {
return messageFilteredTimelineInternalIdPrefixAllowedMessageTypesReturnValue
}
}
//MARK: - ownUserId //MARK: - ownUserId
var ownUserIdUnderlyingCallsCount = 0 var ownUserIdUnderlyingCallsCount = 0

View File

@ -10,6 +10,7 @@ import SwiftUI
extension MediaProviderMock { extension MediaProviderMock {
struct Configuration { } struct Configuration { }
// swiftlint:disable:next cyclomatic_complexity
convenience init(configuration: Configuration) { convenience init(configuration: Configuration) {
self.init() self.init()

View File

@ -106,12 +106,12 @@ struct VoiceMessageButton_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
HStack(spacing: 8) { HStack(spacing: 8) {
VoiceMessageButton(state: .paused, size: .small, action: { }) VoiceMessageButton(state: .paused, size: .small) { }
VoiceMessageButton(state: .paused, size: .medium, action: { }) VoiceMessageButton(state: .paused, size: .medium) { }
} }
HStack(spacing: 8) { HStack(spacing: 8) {
VoiceMessageButton(state: .playing, size: .small, action: { }) VoiceMessageButton(state: .playing, size: .small) { }
VoiceMessageButton(state: .playing, size: .medium, action: { }) VoiceMessageButton(state: .playing, size: .medium) { }
} }
} }
.padding() .padding()

View File

@ -34,7 +34,7 @@ struct BlockedUsersScreen: View {
ForEach(context.viewState.blockedUsers, id: \.self) { user in ForEach(context.viewState.blockedUsers, id: \.self) { user in
ListRow(label: .avatar(title: user.displayName ?? user.userID, icon: avatar(for: user)), ListRow(label: .avatar(title: user.displayName ?? user.userID, icon: avatar(for: user)),
details: .isWaiting(context.viewState.processingUserID == user.userID), details: .isWaiting(context.viewState.processingUserID == user.userID),
kind: .button(action: { context.send(viewAction: .unblockUser(user)) })) kind: .button { context.send(viewAction: .unblockUser(user)) })
} }
} }
} }

View File

@ -34,14 +34,14 @@ class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewMo
title: L10n.screenEditPollDeleteConfirmationTitle, title: L10n.screenEditPollDeleteConfirmationTitle,
message: L10n.screenEditPollDeleteConfirmation, message: L10n.screenEditPollDeleteConfirmation,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionOk, action: { self.actionsSubject.send(.delete) })) secondaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.delete) })
case .cancel: case .cancel:
if state.formContentHasChanged { if state.formContentHasChanged {
state.bindings.alertInfo = .init(id: .init(), state.bindings.alertInfo = .init(id: .init(),
title: L10n.screenCreatePollCancelConfirmationTitleIos, title: L10n.screenCreatePollCancelConfirmationTitleIos,
message: L10n.screenCreatePollCancelConfirmationContentIos, message: L10n.screenCreatePollCancelConfirmationContentIos,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionOk, action: { self.actionsSubject.send(.cancel) })) secondaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.cancel) })
} else { } else {
actionsSubject.send(.cancel) actionsSubject.send(.cancel)
} }

View File

@ -43,9 +43,9 @@ class DeactivateAccountScreenViewModel: DeactivateAccountScreenViewModelType, De
state.bindings.alertInfo = .init(id: .confirmation, state.bindings.alertInfo = .init(id: .confirmation,
title: L10n.screenDeactivateAccountTitle, title: L10n.screenDeactivateAccountTitle,
message: L10n.screenDeactivateAccountConfirmationDialogContent, message: L10n.screenDeactivateAccountConfirmationDialogContent,
primaryButton: .init(title: L10n.actionDeactivate, action: { primaryButton: .init(title: L10n.actionDeactivate) {
Task { await self.deactivateAccount() } Task { await self.deactivateAccount() }
}), },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
} }

View File

@ -38,10 +38,10 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp
state.bindings.alertInfo = .init(id: UUID(), state.bindings.alertInfo = .init(id: UUID(),
title: L10n.screenResetEncryptionConfirmationAlertTitle, title: L10n.screenResetEncryptionConfirmationAlertTitle,
message: L10n.screenResetEncryptionConfirmationAlertSubtitle, message: L10n.screenResetEncryptionConfirmationAlertSubtitle,
primaryButton: .init(title: L10n.screenResetEncryptionConfirmationAlertAction, role: .destructive, action: { [weak self] in primaryButton: .init(title: L10n.screenResetEncryptionConfirmationAlertAction, role: .destructive) { [weak self] in
guard let self else { return } guard let self else { return }
Task { await self.startResetFlow() } Task { await self.startResetFlow() }
})) })
case .cancel: case .cancel:
actionsSubject.send(.cancel) actionsSubject.send(.cancel)
} }

View File

@ -326,11 +326,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.state.bindings.alertInfo = AlertInfo(id: UUID(), self.state.bindings.alertInfo = AlertInfo(id: UUID(),
title: L10n.bannerMigrateToNativeSlidingSyncForceLogoutTitle, title: L10n.bannerMigrateToNativeSlidingSyncForceLogoutTitle,
primaryButton: .init(title: L10n.bannerMigrateToNativeSlidingSyncAction, primaryButton: .init(title: L10n.bannerMigrateToNativeSlidingSyncAction) { [weak self] in
action: { [weak self] in
self?.appSettings.slidingSyncDiscovery = .native self?.appSettings.slidingSyncDiscovery = .native
self?.actionsSubject.send(.logoutWithoutConfirmation) self?.actionsSubject.send(.logoutWithoutConfirmation)
})) })
} }
} }
} }
@ -433,7 +432,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
title: title, title: title,
message: message, message: message,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionDecline, role: .destructive, action: { Task { await self.declineInvite(roomID: room.id) } })) secondaryButton: .init(title: L10n.actionDecline, role: .destructive) { Task { await self.declineInvite(roomID: room.id) } })
} }
private func declineInvite(roomID: String) async { private func declineInvite(roomID: String) async {

View File

@ -63,7 +63,7 @@ struct HomeScreenEmptyStateLayout: Layout {
} }
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let mainView = subviews.first(where: { $0.priority > 0 }) let mainView = subviews.first { $0.priority > 0 }
let topViews = subviews.filter { $0 != mainView } let topViews = subviews.filter { $0 != mainView }
var y: CGFloat = bounds.minY var y: CGFloat = bounds.minY

View File

@ -50,7 +50,7 @@ struct InviteUsersScreenSelectedItem_Previews: PreviewProvider, TestablePreview
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack(spacing: 28) { HStack(spacing: 28) {
ForEach(people, id: \.userID) { user in ForEach(people, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: MediaProviderMock(configuration: .init()), dismissAction: { }) InviteUsersScreenSelectedItem(user: user, mediaProvider: MediaProviderMock(configuration: .init())) { }
.frame(width: 72) .frame(width: 72)
} }
} }

View File

@ -221,7 +221,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
title: L10n.screenInvitesDeclineChatTitle, title: L10n.screenInvitesDeclineChatTitle,
message: L10n.screenInvitesDeclineChatMessage(roomName), message: L10n.screenInvitesDeclineChatMessage(roomName),
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionDecline, role: .destructive, action: { Task { await self.declineInvite() } })) secondaryButton: .init(title: L10n.actionDecline, role: .destructive) { Task { await self.declineInvite() } })
} }
private func showCancelKnockConfirmationAlert() { private func showCancelKnockConfirmationAlert() {
@ -229,7 +229,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
title: L10n.screenJoinRoomCancelKnockAlertTitle, title: L10n.screenJoinRoomCancelKnockAlertTitle,
message: L10n.screenJoinRoomCancelKnockAlertDescription, message: L10n.screenJoinRoomCancelKnockAlertDescription,
primaryButton: .init(title: L10n.actionNo, role: .cancel, action: nil), primaryButton: .init(title: L10n.actionNo, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.screenJoinRoomCancelKnockAlertConfirmation, role: .destructive, action: { Task { await self.cancelKnock() } })) secondaryButton: .init(title: L10n.screenJoinRoomCancelKnockAlertConfirmation, role: .destructive) { Task { await self.cancelKnock() } })
} }
private func declineInvite() async { private func declineInvite() async {

View File

@ -261,7 +261,7 @@ struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview {
topic: "“Science and technology were the only keys to opening the door to the future, and people approached science with the faith and sincerity of elementary school students.”", topic: "“Science and technology were the only keys to opening the door to the future, and people approached science with the faith and sincerity of elementary school students.”",
avatarURL: .mockMXCAvatar, avatarURL: .mockMXCAvatar,
memberCount: UInt(100), memberCount: UInt(100),
isHistoryWorldReadable: false, isHistoryWorldReadable: nil,
isJoined: membership.isJoined, isJoined: membership.isJoined,
isInvited: membership.isInvited, isInvited: membership.isInvited,
isPublic: membership.isPublic, isPublic: membership.isPublic,

View File

@ -177,19 +177,19 @@ struct KnockRequestCell_Previews: PreviewProvider, TestablePreview {
static let aliceWithNoName = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: nil, avatarURL: nil, timestamp: "20 Nov 2024", reason: nil) static let aliceWithNoName = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: nil, avatarURL: nil, timestamp: "20 Nov 2024", reason: nil)
static var previews: some View { static var previews: some View {
KnockRequestCell(cellInfo: aliceWithLongReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) KnockRequestCell(cellInfo: aliceWithLongReason) { _ in } onDecline: { _ in } onDeclineAndBan: { _ in }
.previewDisplayName("Long reason") .previewDisplayName("Long reason")
KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) KnockRequestCell(cellInfo: aliceWithShortReason) { _ in } onDecline: { _ in } onDeclineAndBan: { _ in }
.previewDisplayName("Short reason") .previewDisplayName("Short reason")
KnockRequestCell(cellInfo: aliceWithNoReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) KnockRequestCell(cellInfo: aliceWithNoReason) { _ in } onDecline: { _ in } onDeclineAndBan: { _ in }
.previewDisplayName("No reason") .previewDisplayName("No reason")
KnockRequestCell(cellInfo: aliceWithNoName, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) KnockRequestCell(cellInfo: aliceWithNoName) { _ in } onDecline: { _ in } onDeclineAndBan: { _ in }
.previewDisplayName("No name") .previewDisplayName("No name")
KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: nil, onDecline: { _ in }, onDeclineAndBan: { _ in }) // KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: nil) onDecline: { _ in } onDeclineAndBan: { _ in }
.previewDisplayName("No Accept") // .previewDisplayName("No Accept")
KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: nil, onDecline: nil, onDeclineAndBan: { _ in }) // KnockRequestCell(cellInfo: aliceWithShortReason) onDeclineAndBan: { _ in }
.previewDisplayName("No Accept and Decline") // .previewDisplayName("No Accept and Decline")
KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: nil) // KnockRequestCell(cellInfo: aliceWithShortReason) { _ in } onDecline: { _ in })
.previewDisplayName("No Ban") // .previewDisplayName("No Ban")
} }
} }

View File

@ -0,0 +1,68 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import SwiftUI
struct MediaEventsTimelineScreenCoordinatorParameters {
let roomProxy: JoinedRoomProxyProtocol
let mediaTimelineController: RoomTimelineControllerProtocol
let filesTimelineController: RoomTimelineControllerProtocol
let mediaProvider: MediaProviderProtocol
let mediaPlayerProvider: MediaPlayerProviderProtocol
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
let appMediator: AppMediatorProtocol
let emojiProvider: EmojiProviderProtocol
}
enum MediaEventsTimelineScreenCoordinatorAction { }
final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
private let parameters: MediaEventsTimelineScreenCoordinatorParameters
private let viewModel: MediaEventsTimelineScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<MediaEventsTimelineScreenCoordinatorAction, Never> = .init()
var actions: AnyPublisher<MediaEventsTimelineScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: MediaEventsTimelineScreenCoordinatorParameters) {
self.parameters = parameters
let mediaTimelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
timelineController: parameters.mediaTimelineController,
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)
let filesTimelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
timelineController: parameters.filesTimelineController,
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)
viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel,
filesTimelineViewModel: filesTimelineViewModel,
mediaProvider: parameters.mediaProvider)
}
func toPresentable() -> AnyView {
AnyView(MediaEventsTimelineScreen(context: viewModel.context))
}
}

View File

@ -0,0 +1,32 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
enum MediaEventsTimelineScreenViewModelAction { }
enum MediaEventsTimelineScreenMode {
case media
case files
}
struct MediaEventsTimelineScreenViewState: BindableState {
var isBackPaginating = false
var items = [RoomTimelineItemViewState]()
var bindings: MediaEventsTimelineScreenViewStateBindings
}
struct MediaEventsTimelineScreenViewStateBindings {
var screenMode: MediaEventsTimelineScreenMode
}
enum MediaEventsTimelineScreenViewAction {
case changedScreenMode
case oldestItemDidAppear
case oldestItemDidDisappear
}

View File

@ -0,0 +1,102 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import SwiftUI
typealias MediaEventsTimelineScreenViewModelType = StateStoreViewModel<MediaEventsTimelineScreenViewState, MediaEventsTimelineScreenViewAction>
class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType, MediaEventsTimelineScreenViewModelProtocol {
private let mediaTimelineViewModel: TimelineViewModelProtocol
private let filesTimelineViewModel: TimelineViewModelProtocol
private var isOldestItemVisible = false
private var activeTimelineViewModel: TimelineViewModelProtocol {
switch state.bindings.screenMode {
case .media:
mediaTimelineViewModel
case .files:
filesTimelineViewModel
}
}
private let actionsSubject: PassthroughSubject<MediaEventsTimelineScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<MediaEventsTimelineScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(mediaTimelineViewModel: TimelineViewModelProtocol,
filesTimelineViewModel: TimelineViewModelProtocol,
mediaProvider: MediaProviderProtocol,
screenMode: MediaEventsTimelineScreenMode = .media) {
self.mediaTimelineViewModel = mediaTimelineViewModel
self.filesTimelineViewModel = filesTimelineViewModel
super.init(initialViewState: .init(bindings: .init(screenMode: screenMode)), mediaProvider: mediaProvider)
mediaTimelineViewModel.context.$viewState.sink { [weak self] timelineViewState in
guard let self, state.bindings.screenMode == .media else {
return
}
updateWithTimelineViewState(timelineViewState)
}
.store(in: &cancellables)
filesTimelineViewModel.context.$viewState.sink { [weak self] timelineViewState in
guard let self, state.bindings.screenMode == .files else {
return
}
updateWithTimelineViewState(timelineViewState)
}
.store(in: &cancellables)
updateWithTimelineViewState(activeTimelineViewModel.context.viewState)
}
// MARK: - Public
override func process(viewAction: MediaEventsTimelineScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .changedScreenMode:
updateWithTimelineViewState(activeTimelineViewModel.context.viewState)
case .oldestItemDidAppear:
isOldestItemVisible = true
backPaginateIfNecessary(paginationStatus: activeTimelineViewModel.context.viewState.timelineState.paginationState.backward)
case .oldestItemDidDisappear:
isOldestItemVisible = false
}
}
// MARK: - Private
private func updateWithTimelineViewState(_ timelineViewState: TimelineViewState) {
state.items = timelineViewState.timelineState.itemViewStates.filter { itemViewState in
switch itemViewState.type {
case .image, .video:
state.bindings.screenMode == .media
case .audio, .file:
state.bindings.screenMode == .files
default:
false
}
}.reversed()
state.isBackPaginating = (timelineViewState.timelineState.paginationState.backward == .paginating)
backPaginateIfNecessary(paginationStatus: timelineViewState.timelineState.paginationState.backward)
}
private func backPaginateIfNecessary(paginationStatus: PaginationStatus) {
if paginationStatus == .idle, isOldestItemVisible {
activeTimelineViewModel.context.send(viewAction: .paginateBackwards)
}
}
}

View File

@ -0,0 +1,14 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
@MainActor
protocol MediaEventsTimelineScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<MediaEventsTimelineScreenViewModelAction, Never> { get }
var context: MediaEventsTimelineScreenViewModelType.Context { get }
}

View File

@ -0,0 +1,173 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Compound
import SwiftUI
struct MediaEventsTimelineScreen: View {
@ObservedObject var context: MediaEventsTimelineScreenViewModel.Context
var body: some View {
content
.navigationBarTitleDisplayMode(.inline)
.background(.compound.bgCanvasDefault)
// Doesn't play well with the transformed scrollView
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .principal) {
Picker("", selection: $context.screenMode) {
Text(L10n.screenMediaBrowserListModeMedia)
.padding()
.tag(MediaEventsTimelineScreenMode.media)
Text(L10n.screenMediaBrowserListModeFiles)
.padding()
.tag(MediaEventsTimelineScreenMode.files)
}
.pickerStyle(.segmented)
}
}
}
@ViewBuilder
private var content: some View {
ScrollView {
Group {
let columns = [GridItem(.adaptive(minimum: 80, maximum: 150), spacing: 1)]
LazyVGrid(columns: columns, alignment: .center, spacing: 1) {
ForEach(context.viewState.items) { item in
Color.clear // Let the image aspect fill in place
.aspectRatio(1, contentMode: .fill)
.overlay {
viewForTimelineItem(item)
}
.clipped()
.scaleEffect(.init(width: 1, height: -1))
}
}
// Needs to be wrapped in a LazyStack otherwise appearance calls don't trigger
LazyVStack(spacing: 0) {
Rectangle()
.frame(height: 44)
.foregroundStyle(.compound.bgCanvasDefault)
.overlay {
if context.viewState.isBackPaginating {
ProgressView()
}
}
.onAppear {
context.send(viewAction: .oldestItemDidAppear)
}
.onDisappear {
context.send(viewAction: .oldestItemDidDisappear)
}
}
}
}
.scaleEffect(.init(width: 1, height: -1))
.onChange(of: context.screenMode) { _, _ in
context.send(viewAction: .changedScreenMode)
}
}
@ViewBuilder func viewForTimelineItem(_ item: RoomTimelineItemViewState) -> some View {
switch item.type {
case .image(let timelineItem):
#warning("Make this work for gifs")
LoadableImage(mediaSource: timelineItem.content.thumbnailInfo?.source ?? timelineItem.content.imageInfo.source,
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id),
blurhash: timelineItem.content.blurhash,
size: timelineItem.content.thumbnailInfo?.size ?? timelineItem.content.imageInfo.size,
mediaProvider: context.mediaProvider) {
placeholder
}
.mediaItemAspectRatio(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo)
case .video(let timelineItem):
if let thumbnailSource = timelineItem.content.thumbnailInfo?.source {
LoadableImage(mediaSource: thumbnailSource,
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id),
blurhash: timelineItem.content.blurhash,
size: timelineItem.content.thumbnailInfo?.size,
mediaProvider: context.mediaProvider) { imageView in
imageView
.overlay { playIcon }
} placeholder: {
placeholder
}
.mediaItemAspectRatio(imageInfo: timelineItem.content.thumbnailInfo)
} else {
playIcon
}
case .separator(let timelineItem):
Text(timelineItem.text)
default:
EmptyView()
}
}
private var playIcon: some View {
Image(systemName: "play.circle.fill")
.resizable()
.frame(width: 50, height: 50)
.background(.ultraThinMaterial, in: Circle())
.foregroundColor(.white)
}
private var placeholder: some View {
Rectangle()
.foregroundColor(.compound._bgBubbleIncoming)
.opacity(0.3)
}
}
extension View {
/// Constrains the max height of a media item in the timeline, whilst preserving its aspect ratio.
@ViewBuilder
func mediaItemAspectRatio(imageInfo: ImageInfoProxy?) -> some View {
aspectRatio(imageInfo?.aspectRatio, contentMode: .fill)
}
}
// MARK: - Previews
struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
static let timelineViewModel: TimelineViewModel = {
let timelineController = MockRoomTimelineController(timelineKind: .media)
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
}()
static let mediaViewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: timelineViewModel,
filesTimelineViewModel: timelineViewModel,
mediaProvider: MediaProviderMock(configuration: .init()),
screenMode: .media)
static let filesViewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: timelineViewModel,
filesTimelineViewModel: timelineViewModel,
mediaProvider: MediaProviderMock(configuration: .init()),
screenMode: .files)
static var previews: some View {
NavigationStack {
MediaEventsTimelineScreen(context: mediaViewModel.context)
.previewDisplayName("Media")
}
NavigationStack {
MediaEventsTimelineScreen(context: filesViewModel.context)
.previewDisplayName("Files")
}
}
}

View File

@ -69,7 +69,7 @@ struct PinnedEventsTimelineScreen: View {
TimelineView() TimelineView()
.id(timelineContext.viewState.roomID) .id(timelineContext.viewState.roomID)
.environmentObject(timelineContext) .environmentObject(timelineContext)
.environment(\.focussedEventID, timelineContext.viewState.timelineViewState.focussedEvent?.eventID) .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID)
} }
} }

View File

@ -60,8 +60,7 @@ struct RoomChangeRolesScreenSelectedItem_Previews: PreviewProvider, TestablePrev
HStack(spacing: 12) { HStack(spacing: 12) {
ForEach(members, id: \.id) { member in ForEach(members, id: \.id) { member in
RoomChangeRolesScreenSelectedItem(member: member, RoomChangeRolesScreenSelectedItem(member: member,
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init())) { }
dismissAction: { })
.frame(width: 72) .frame(width: 72)
} }
} }

View File

@ -29,6 +29,7 @@ enum RoomDetailsScreenCoordinatorAction {
case presentRolesAndPermissionsScreen case presentRolesAndPermissionsScreen
case presentCall case presentCall
case presentPinnedEventsTimeline case presentPinnedEventsTimeline
case presentMediaEventsTimeline
case presentKnockingRequestsListScreen case presentKnockingRequestsListScreen
} }
@ -80,6 +81,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentCall) actionsSubject.send(.presentCall)
case .displayPinnedEventsTimeline: case .displayPinnedEventsTimeline:
actionsSubject.send(.presentPinnedEventsTimeline) actionsSubject.send(.presentPinnedEventsTimeline)
case .displayMediaEventsTimeline:
actionsSubject.send(.presentMediaEventsTimeline)
case .displayKnockingRequests: case .displayKnockingRequests:
actionsSubject.send(.presentKnockingRequestsListScreen) actionsSubject.send(.presentKnockingRequestsListScreen)
} }

View File

@ -22,6 +22,7 @@ enum RoomDetailsScreenViewModelAction {
case requestRolesAndPermissionsPresentation case requestRolesAndPermissionsPresentation
case startCall case startCall
case displayPinnedEventsTimeline case displayPinnedEventsTimeline
case displayMediaEventsTimeline
case displayKnockingRequests case displayKnockingRequests
} }
@ -48,13 +49,15 @@ struct RoomDetailsScreenViewState: BindableState {
var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading
var canJoinCall = false var canJoinCall = false
var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading
var knockingEnabled = false var knockingEnabled = false
var isKnockableRoom = false var isKnockableRoom = false
var canSeeKnockingRequests: Bool { var canSeeKnockingRequests: Bool {
knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers) knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers)
} }
var mediaBrowserEnabled = false
var canEdit: Bool { var canEdit: Bool {
!isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar) !isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
} }
@ -197,6 +200,7 @@ enum RoomDetailsScreenViewAction {
case processTapRolesAndPermissions case processTapRolesAndPermissions
case processTapCall case processTapCall
case processTapPinnedEvents case processTapPinnedEvents
case processTapMediaEvents
case processTapRequestsToJoin case processTapRequestsToJoin
} }

View File

@ -79,6 +79,10 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.weakAssign(to: \.state.knockingEnabled, on: self) .weakAssign(to: \.state.knockingEnabled, on: self)
.store(in: &cancellables) .store(in: &cancellables)
appSettings.$mediaBrowserEnabled
.weakAssign(to: \.state.mediaBrowserEnabled, on: self)
.store(in: &cancellables)
appMediator.networkMonitor.reachabilityPublisher appMediator.networkMonitor.reachabilityPublisher
.filter { $0 == .reachable } .filter { $0 == .reachable }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -164,6 +168,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
case .processTapPinnedEvents: case .processTapPinnedEvents:
analyticsService.trackInteraction(name: .PinnedMessageRoomInfoButton) analyticsService.trackInteraction(name: .PinnedMessageRoomInfoButton)
actionsSubject.send(.displayPinnedEventsTimeline) actionsSubject.send(.displayPinnedEventsTimeline)
case .processTapMediaEvents:
actionsSubject.send(.displayMediaEventsTimeline)
case .processTapRequestsToJoin: case .processTapRequestsToJoin:
actionsSubject.send(.displayKnockingRequests) actionsSubject.send(.displayKnockingRequests)
} }
@ -207,8 +213,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self, ownUserID = roomProxy.ownUserID] members in .sink { [weak self, ownUserID = roomProxy.ownUserID] members in
guard let self else { return } guard let self else { return }
let accountOwner = members.first(where: { $0.userID == ownUserID }) let accountOwner = members.first { $0.userID == ownUserID }
let dmRecipient = members.first(where: { $0.userID != ownUserID }) let dmRecipient = members.first { $0.userID != ownUserID }
self.dmRecipient = dmRecipient self.dmRecipient = dmRecipient
self.state.dmRecipient = dmRecipient.map(RoomMemberDetails.init(withProxy:)) self.state.dmRecipient = dmRecipient.map(RoomMemberDetails.init(withProxy:))
self.state.accountOwner = accountOwner.map(RoomMemberDetails.init(withProxy:)) self.state.accountOwner = accountOwner.map(RoomMemberDetails.init(withProxy:))

View File

@ -164,9 +164,9 @@ struct RoomDetailsScreen: View {
ListRow(label: .default(title: L10n.screenRoomDetailsPinnedEventsRowTitle, ListRow(label: .default(title: L10n.screenRoomDetailsPinnedEventsRowTitle,
icon: \.pin), icon: \.pin),
details: context.viewState.pinnedEventsActionState.isLoading ? .isWaiting(true) : .title(context.viewState.pinnedEventsActionState.count), details: context.viewState.pinnedEventsActionState.isLoading ? .isWaiting(true) : .title(context.viewState.pinnedEventsActionState.count),
kind: context.viewState.pinnedEventsActionState.isLoading ? .label : .navigationLink(action: { kind: context.viewState.pinnedEventsActionState.isLoading ? .label : .navigationLink {
context.send(viewAction: .processTapPinnedEvents) context.send(viewAction: .processTapPinnedEvents)
})) })
.disabled(context.viewState.pinnedEventsActionState.isLoading) .disabled(context.viewState.pinnedEventsActionState.isLoading)
if context.viewState.canSeeKnockingRequests { if context.viewState.canSeeKnockingRequests {
@ -184,6 +184,13 @@ struct RoomDetailsScreen: View {
context.send(viewAction: .processTapPolls) context.send(viewAction: .processTapPolls)
}) })
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.pollsHistory) .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.pollsHistory)
if context.viewState.mediaBrowserEnabled {
ListRow(label: .default(title: L10n.screenMediaBrowserTitle, icon: \.image),
kind: .navigationLink {
context.send(viewAction: .processTapMediaEvents)
})
}
} }
} }

View File

@ -46,8 +46,7 @@ struct RoomMemberDetailsScreen: View {
AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID),
isVerified: context.viewState.showVerifiedBadge, isVerified: context.viewState.showVerifiedBadge,
avatarSize: .user(on: .memberDetails), avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider, mediaProvider: context.mediaProvider) { }
footer: { })
} }
} }

View File

@ -586,14 +586,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
private func makeCreateWithTextAlertInfo(urlBinding: Binding<String>, textBinding: Binding<String>) -> AlertInfo<UUID> { private func makeCreateWithTextAlertInfo(urlBinding: Binding<String>, textBinding: Binding<String>) -> AlertInfo<UUID> {
AlertInfo(id: UUID(), AlertInfo(id: UUID(),
title: L10n.richTextEditorCreateLink, title: L10n.richTextEditorCreateLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel, action: { primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel) {
self.restoreComposerSelectedRange() self.restoreComposerSelectedRange()
}), },
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave, action: { secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave) {
self.restoreComposerSelectedRange() self.restoreComposerSelectedRange()
self.createLinkWithText() self.createLinkWithText()
}), },
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.commonText, textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.commonText,
text: textBinding, text: textBinding,
autoCapitalization: .never, autoCapitalization: .never,
@ -607,14 +607,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
private func makeSetUrlAlertInfo(urlBinding: Binding<String>, isEdit: Bool) -> AlertInfo<UUID> { private func makeSetUrlAlertInfo(urlBinding: Binding<String>, isEdit: Bool) -> AlertInfo<UUID> {
AlertInfo(id: UUID(), AlertInfo(id: UUID(),
title: isEdit ? L10n.richTextEditorEditLink : L10n.richTextEditorCreateLink, title: isEdit ? L10n.richTextEditorEditLink : L10n.richTextEditorCreateLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel, action: { primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionCancel) {
self.restoreComposerSelectedRange() self.restoreComposerSelectedRange()
}), },
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave, action: { secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave) {
self.restoreComposerSelectedRange() self.restoreComposerSelectedRange()
self.setLink() self.setLink()
}), },
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder, textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder,
text: urlBinding, text: urlBinding,
autoCapitalization: .never, autoCapitalization: .never,
@ -624,16 +624,16 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
private func makeEditChoiceAlertInfo(urlBinding: Binding<String>) -> AlertInfo<UUID> { private func makeEditChoiceAlertInfo(urlBinding: Binding<String>) -> AlertInfo<UUID> {
AlertInfo(id: UUID(), AlertInfo(id: UUID(),
title: L10n.richTextEditorEditLink, title: L10n.richTextEditorEditLink,
primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionRemove, role: .destructive, action: { primaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionRemove, role: .destructive) {
self.restoreComposerSelectedRange() self.restoreComposerSelectedRange()
self.removeLinks() self.removeLinks()
}), },
verticalButtons: [AlertInfo<UUID>.AlertButton(title: L10n.actionEdit, action: { verticalButtons: [AlertInfo<UUID>.AlertButton(title: L10n.actionEdit) {
self.state.bindings.alertInfo = nil self.state.bindings.alertInfo = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.state.bindings.alertInfo = self.makeSetUrlAlertInfo(urlBinding: urlBinding, isEdit: true) self.state.bindings.alertInfo = self.makeSetUrlAlertInfo(urlBinding: urlBinding, isEdit: true)
} }
})]) }])
} }
private func restoreComposerSelectedRange() { private func restoreComposerSelectedRange() {

View File

@ -102,9 +102,9 @@ private struct SingleKnockRequestBannerContent: View {
Button(L10n.screenRoomSingleKnockRequestViewButtonTitle, action: onViewAll) Button(L10n.screenRoomSingleKnockRequestViewButtonTitle, action: onViewAll)
.buttonStyle(.compound(.secondary, size: .medium)) .buttonStyle(.compound(.secondary, size: .medium))
if let onAccept { if let onAccept {
Button(L10n.screenRoomSingleKnockRequestAcceptButtonTitle, action: { Button(L10n.screenRoomSingleKnockRequestAcceptButtonTitle) {
onAccept(request.userID) onAccept(request.userID)
}) }
.buttonStyle(.compound(.primary, size: .medium)) .buttonStyle(.compound(.primary, size: .medium))
} }
} }
@ -166,9 +166,9 @@ private struct KnockRequestsBannerDismissButton: View {
CompoundIcon(\.close, size: .medium, relativeTo: .compound.bodySMSemibold) CompoundIcon(\.close, size: .medium, relativeTo: .compound.bodySMSemibold)
.foregroundColor(.compound.iconTertiary) .foregroundColor(.compound.iconTertiary)
} }
.alignmentGuide(.top, computeValue: { _ in .alignmentGuide(.top) { _ in
3 3
}) }
} }
} }
@ -188,15 +188,16 @@ struct KnockRequestsBannerView_Previews: PreviewProvider, TestablePreview {
] ]
static var previews: some View { static var previews: some View {
KnockRequestsBannerView(requests: singleRequest, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) KnockRequestsBannerView(requests: singleRequest) { } onAccept: { _ in } onViewAll: { }
.previewDisplayName("Single Request") .previewDisplayName("Single Request")
// swiftlint:disable:next trailing_closure
KnockRequestsBannerView(requests: singleRequest, onDismiss: { }, onAccept: nil, onViewAll: { }) KnockRequestsBannerView(requests: singleRequest, onDismiss: { }, onAccept: nil, onViewAll: { })
.previewDisplayName("Single Request, no accept action") .previewDisplayName("Single Request, no accept action")
KnockRequestsBannerView(requests: singleRequestWithReason, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) KnockRequestsBannerView(requests: singleRequestWithReason) { } onAccept: { _ in } onViewAll: { }
.previewDisplayName("Single Request with reason") .previewDisplayName("Single Request with reason")
KnockRequestsBannerView(requests: singleRequestNoDisplayName, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) KnockRequestsBannerView(requests: singleRequestNoDisplayName) { } onAccept: { _ in } onViewAll: { }
.previewDisplayName("Single Request, No Display Name") .previewDisplayName("Single Request, No Display Name")
KnockRequestsBannerView(requests: multipleRequests, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) KnockRequestsBannerView(requests: multipleRequests) { } onAccept: { _ in } onViewAll: { }
.previewDisplayName("Multiple Requests") .previewDisplayName("Multiple Requests")
} }
} }

View File

@ -113,7 +113,7 @@ struct RoomScreen: View {
TimelineView() TimelineView()
.id(timelineContext.viewState.roomID) .id(timelineContext.viewState.roomID)
.environmentObject(timelineContext) .environmentObject(timelineContext)
.environment(\.focussedEventID, timelineContext.viewState.timelineViewState.focussedEvent?.eventID) .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID)
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {
scrollToBottomButton scrollToBottomButton
} }
@ -183,7 +183,7 @@ struct RoomScreen: View {
} }
private var isAtBottomAndLive: Bool { private var isAtBottomAndLive: Bool {
timelineContext.isScrolledToBottom && timelineContext.viewState.timelineViewState.isLive timelineContext.isScrolledToBottom && timelineContext.viewState.timelineState.isLive
} }
@ViewBuilder @ViewBuilder

View File

@ -51,6 +51,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var elementCallBaseURLOverride: URL? { get set } var elementCallBaseURLOverride: URL? { get set }
var knockingEnabled: Bool { get set } var knockingEnabled: Bool { get set }
var createMediaCaptionsEnabled: Bool { get set } var createMediaCaptionsEnabled: Bool { get set }
var mediaBrowserEnabled: Bool { get set }
} }
extension AppSettings: DeveloperOptionsProtocol { } extension AppSettings: DeveloperOptionsProtocol { }

View File

@ -49,7 +49,7 @@ struct DeveloperOptionsScreen: View {
} }
} }
Section("Timeline") { Section("Room") {
Toggle(isOn: $context.hideTimelineMedia) { Toggle(isOn: $context.hideTimelineMedia) {
Text("Hide image & video previews") Text("Hide image & video previews")
} }
@ -57,6 +57,10 @@ struct DeveloperOptionsScreen: View {
Toggle(isOn: $context.createMediaCaptionsEnabled) { Toggle(isOn: $context.createMediaCaptionsEnabled) {
Text("Allow creation of media captions") Text("Allow creation of media captions")
} }
Toggle(isOn: $context.mediaBrowserEnabled) {
Text("Enable the media browser")
}
} }
Section("Join rules") { Section("Join rules") {

View File

@ -123,7 +123,7 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie
guard !Task.isCancelled else { return } guard !Task.isCancelled else { return }
let filteredRoomsSummary = roomSummaryProvider.roomListPublisher.value.filter { summary in let filteredRoomsSummary = roomSummaryProvider.roomListPublisher.value.filter { summary in
roomsWithUserDefinedRules.contains(where: { summary.id == $0 }) roomsWithUserDefinedRules.contains { summary.id == $0 }
} }
var roomsWithUserDefinedMode: [NotificationSettingsEditScreenRoom] = [] var roomsWithUserDefinedMode: [NotificationSettingsEditScreenRoom] = []
@ -142,7 +142,7 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie
} }
// Sort the room list // Sort the room list
roomsWithUserDefinedMode.sort(by: { $0.name.localizedCompare($1.name) == .orderedAscending }) roomsWithUserDefinedMode.sort { $0.name.localizedCompare($1.name) == .orderedAscending }
state.roomsWithUserDefinedMode = roomsWithUserDefinedMode state.roomsWithUserDefinedMode = roomsWithUserDefinedMode
} }

View File

@ -391,7 +391,6 @@ class TimelineInteractionHandler {
// MARK: Audio Playback // MARK: Audio Playback
// swiftlint:disable:next cyclomatic_complexity
func playPauseAudio(for itemID: TimelineItemIdentifier) async { func playPauseAudio(for itemID: TimelineItemIdentifier) async {
MXLog.info("Toggle play/pause audio for itemID \(itemID)") MXLog.info("Toggle play/pause audio for itemID \(itemID)")
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else {

View File

@ -92,7 +92,7 @@ struct TimelineViewState: BindableState {
var showLoading = false var showLoading = false
var showReadReceipts = false var showReadReceipts = false
var isEncryptedOneToOneRoom = false var isEncryptedOneToOneRoom = false
var timelineViewState: TimelineState // check the doc before changing this var timelineState: TimelineState // check the doc before changing this
var ownUserID: String var ownUserID: String
var canCurrentUserRedactOthers = false var canCurrentUserRedactOthers = false

View File

@ -78,7 +78,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: timelineController.timelineKind == .pinned, super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: timelineController.timelineKind == .pinned,
roomID: roomProxy.id, roomID: roomProxy.id,
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), timelineState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
ownUserID: roomProxy.ownUserID, ownUserID: roomProxy.ownUserID,
isViewSourceEnabled: appSettings.viewSourceEnabled, isViewSourceEnabled: appSettings.viewSourceEnabled,
isCreateMediaCaptionsEnabled: appSettings.createMediaCaptionsEnabled, isCreateMediaCaptionsEnabled: appSettings.createMediaCaptionsEnabled,
@ -107,6 +107,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
return self.timelineInteractionHandler.audioPlayerState(for: itemID) return self.timelineInteractionHandler.audioPlayerState(for: itemID)
} }
state.timelineState.paginationState = timelineController.paginationState
buildTimelineViews(timelineItems: timelineController.timelineItems) buildTimelineViews(timelineItems: timelineController.timelineItems)
updateMembers(roomProxy.membersPublisher.value) updateMembers(roomProxy.membersPublisher.value)
@ -174,7 +175,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .scrolledToFocussedItem: case .scrolledToFocussedItem:
didScrollToFocussedItem() didScrollToFocussedItem()
case .hasSwitchedTimeline: case .hasSwitchedTimeline:
Task { state.timelineViewState.isSwitchingTimelines = false } Task { state.timelineState.isSwitchingTimelines = false }
case let .hasScrolled(direction): case let .hasScrolled(direction):
actionsSubject.send(.hasScrolled(direction: direction)) actionsSubject.send(.hasScrolled(direction: direction))
case .setOpenURLAction(let action): case .setOpenURLAction(let action):
@ -215,8 +216,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} }
func focusOnEvent(eventID: String) async { func focusOnEvent(eventID: String) async {
if state.timelineViewState.hasLoadedItem(with: eventID) { if state.timelineState.hasLoadedItem(with: eventID) {
state.timelineViewState.focussedEvent = .init(eventID: eventID, appearance: .animated) state.timelineState.focussedEvent = .init(eventID: eventID, appearance: .animated)
return return
} }
@ -225,7 +226,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
switch await timelineController.focusOnEvent(eventID, timelineSize: Constants.detachedTimelineSize) { switch await timelineController.focusOnEvent(eventID, timelineSize: Constants.detachedTimelineSize) {
case .success: case .success:
state.timelineViewState.focussedEvent = .init(eventID: eventID, appearance: .immediate) state.timelineState.focussedEvent = .init(eventID: eventID, appearance: .immediate)
case .failure(let error): case .failure(let error):
MXLog.error("Failed to focus on event \(eventID)") MXLog.error("Failed to focus on event \(eventID)")
@ -244,9 +245,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} }
private func didScrollToFocussedItem() { private func didScrollToFocussedItem() {
if var focussedEvent = state.timelineViewState.focussedEvent { if var focussedEvent = state.timelineState.focussedEvent {
focussedEvent.appearance = .hasAppeared focussedEvent.appearance = .hasAppeared
state.timelineViewState.focussedEvent = focussedEvent state.timelineState.focussedEvent = focussedEvent
hideFocusLoadingIndicator() hideFocusLoadingIndicator()
} }
} }
@ -362,16 +363,16 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .updatedTimelineItems(let updatedItems, let isSwitchingTimelines): case .updatedTimelineItems(let updatedItems, let isSwitchingTimelines):
buildTimelineViews(timelineItems: updatedItems, isSwitchingTimelines: isSwitchingTimelines) buildTimelineViews(timelineItems: updatedItems, isSwitchingTimelines: isSwitchingTimelines)
case .paginationState(let paginationState): case .paginationState(let paginationState):
if state.timelineViewState.paginationState != paginationState { if state.timelineState.paginationState != paginationState {
state.timelineViewState.paginationState = paginationState state.timelineState.paginationState = paginationState
} }
case .isLive(let isLive): case .isLive(let isLive):
if state.timelineViewState.isLive != isLive { if state.timelineState.isLive != isLive {
state.timelineViewState.isLive = isLive state.timelineState.isLive = isLive
// Remove the event highlight *only* when transitioning from non-live to live. // Remove the event highlight *only* when transitioning from non-live to live.
if isLive, state.timelineViewState.focussedEvent != nil { if isLive, state.timelineState.focussedEvent != nil {
state.timelineViewState.focussedEvent = nil state.timelineState.focussedEvent = nil
} }
} }
} }
@ -516,7 +517,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
break break
} }
if state.timelineViewState.paginationState.forward == .timelineEndReached { if state.timelineState.paginationState.forward == .timelineEndReached {
focusLive() focusLive()
} }
@ -525,8 +526,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} }
private func scrollToBottom() { private func scrollToBottom() {
if state.timelineViewState.isLive { if state.timelineState.isLive {
state.timelineViewState.scrollToBottomPublisher.send(()) state.timelineState.scrollToBottomPublisher.send(())
} else { } else {
focusLive() focusLive()
} }
@ -703,14 +704,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} }
if isSwitchingTimelines { if isSwitchingTimelines {
state.timelineViewState.isSwitchingTimelines = true state.timelineState.isSwitchingTimelines = true
} }
state.timelineViewState.itemsDictionary = timelineItemsDictionary state.timelineState.itemsDictionary = timelineItemsDictionary
} }
private func updateViewState(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState { private func updateViewState(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState {
if let timelineItemViewState = state.timelineViewState.itemsDictionary[item.id.uniqueID] { if let timelineItemViewState = state.timelineState.itemsDictionary[item.id.uniqueID] {
timelineItemViewState.groupStyle = groupStyle timelineItemViewState.groupStyle = groupStyle
timelineItemViewState.type = .init(item: item) timelineItemViewState.type = .init(item: item)
return timelineItemViewState return timelineItemViewState
@ -744,7 +745,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
userIndicatorController.alertInfo = .init(id: .init(), userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenRoomInviteAgainAlertTitle, title: L10n.screenRoomInviteAgainAlertTitle,
message: L10n.screenRoomInviteAgainAlertMessage, message: L10n.screenRoomInviteAgainAlertMessage,
primaryButton: .init(title: L10n.actionInvite, action: { [weak self] in self?.inviteOtherDMUserBack() }), primaryButton: .init(title: L10n.actionInvite) { [weak self] in self?.inviteOtherDMUserBack() },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
} }
@ -830,14 +831,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
state.bindings.alertInfo = .init(id: type, state.bindings.alertInfo = .init(id: type,
title: L10n.dialogPermissionMicrophoneTitleIos(InfoPlistReader.main.bundleDisplayName), title: L10n.dialogPermissionMicrophoneTitleIos(InfoPlistReader.main.bundleDisplayName),
message: L10n.dialogPermissionMicrophoneDescriptionIos, message: L10n.dialogPermissionMicrophoneDescriptionIos,
primaryButton: .init(title: L10n.commonSettings, action: { [weak self] in self?.appMediator.openAppSettings() }), primaryButton: .init(title: L10n.commonSettings) { [weak self] in self?.appMediator.openAppSettings() },
secondaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil)) secondaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil))
case .pollEndConfirmation(let pollStartID): case .pollEndConfirmation(let pollStartID):
state.bindings.alertInfo = .init(id: type, state.bindings.alertInfo = .init(id: type,
title: L10n.actionEndPoll, title: L10n.actionEndPoll,
message: L10n.commonPollEndConfirmation, message: L10n.commonPollEndConfirmation,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionOk, action: { self.timelineInteractionHandler.endPoll(pollStartID: pollStartID) })) secondaryButton: .init(title: L10n.actionOk) { self.timelineInteractionHandler.endPoll(pollStartID: pollStartID) })
case .sendingFailed: case .sendingFailed:
state.bindings.alertInfo = .init(id: type, state.bindings.alertInfo = .init(id: type,
title: L10n.commonSendingFailed, title: L10n.commonSendingFailed,

View File

@ -400,7 +400,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
static var mockTimeline: some View { static var mockTimeline: some View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.state.timelineViewState.itemViewStates) { viewState in ForEach(viewModel.state.timelineState.itemViewStates) { viewState in
RoomTimelineItemView(viewState: viewState) RoomTimelineItemView(viewState: viewState)
} }
} }

View File

@ -79,7 +79,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
}() }()
static let sendingLast: TextRoomTimelineItem = { static let sendingLast: TextRoomTimelineItem = {
let id = viewModel.state.timelineViewState.uniqueIDs.last ?? .init(id: UUID().uuidString) let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(id: UUID().uuidString)
var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)), var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)),
timestamp: .mock, timestamp: .mock,
isOutgoing: true, isOutgoing: true,
@ -99,7 +99,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
}() }()
static let sentLast: TextRoomTimelineItem = { static let sentLast: TextRoomTimelineItem = {
let id = viewModel.state.timelineViewState.uniqueIDs.last ?? .init(id: UUID().uuidString) let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(id: UUID().uuidString)
let result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)), let result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)),
timestamp: .mock, timestamp: .mock,
isOutgoing: true, isOutgoing: true,

View File

@ -14,7 +14,7 @@ struct TimelineItemStatusView: View {
@EnvironmentObject private var context: TimelineViewModel.Context @EnvironmentObject private var context: TimelineViewModel.Context
private var isLastOutgoingMessage: Bool { private var isLastOutgoingMessage: Bool {
timelineItem.isOutgoing && context.viewState.timelineViewState.uniqueIDs.last == timelineItem.id.uniqueID timelineItem.isOutgoing && context.viewState.timelineState.uniqueIDs.last == timelineItem.id.uniqueID
} }
var body: some View { var body: some View {

View File

@ -16,7 +16,7 @@ struct TimelineView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> TimelineTableViewController { func makeUIViewController(context: Context) -> TimelineTableViewController {
let tableViewController = TimelineTableViewController(coordinator: context.coordinator, let tableViewController = TimelineTableViewController(coordinator: context.coordinator,
isScrolledToBottom: $viewModelContext.isScrolledToBottom, isScrolledToBottom: $viewModelContext.isScrolledToBottom,
scrollToBottomPublisher: viewModelContext.viewState.timelineViewState.scrollToBottomPublisher) scrollToBottomPublisher: viewModelContext.viewState.timelineState.scrollToBottomPublisher)
// Needs to be dispatched on main asynchronously otherwise we get a runtime warning // Needs to be dispatched on main asynchronously otherwise we get a runtime warning
DispatchQueue.main.async { DispatchQueue.main.async {
viewModelContext.send(viewAction: .setOpenURLAction(openURL)) viewModelContext.send(viewAction: .setOpenURLAction(openURL))
@ -44,21 +44,21 @@ struct TimelineView: UIViewControllerRepresentable {
/// Updates the specified table view's properties from the current view state. /// Updates the specified table view's properties from the current view state.
func update(tableViewController: TimelineTableViewController) { func update(tableViewController: TimelineTableViewController) {
if tableViewController.isSwitchingTimelines != context.viewState.timelineViewState.isSwitchingTimelines { if tableViewController.isSwitchingTimelines != context.viewState.timelineState.isSwitchingTimelines {
// Must come before timelineItemsDictionary in order to disable animations. // Must come before timelineItemsDictionary in order to disable animations.
tableViewController.isSwitchingTimelines = context.viewState.timelineViewState.isSwitchingTimelines tableViewController.isSwitchingTimelines = context.viewState.timelineState.isSwitchingTimelines
} }
if tableViewController.timelineItemsDictionary != context.viewState.timelineViewState.itemsDictionary { if tableViewController.timelineItemsDictionary != context.viewState.timelineState.itemsDictionary {
tableViewController.timelineItemsDictionary = context.viewState.timelineViewState.itemsDictionary tableViewController.timelineItemsDictionary = context.viewState.timelineState.itemsDictionary
} }
if tableViewController.paginationState != context.viewState.timelineViewState.paginationState { if tableViewController.paginationState != context.viewState.timelineState.paginationState {
tableViewController.paginationState = context.viewState.timelineViewState.paginationState tableViewController.paginationState = context.viewState.timelineState.paginationState
} }
if tableViewController.isLive != context.viewState.timelineViewState.isLive { if tableViewController.isLive != context.viewState.timelineState.isLive {
tableViewController.isLive = context.viewState.timelineViewState.isLive tableViewController.isLive = context.viewState.timelineState.isLive
} }
if tableViewController.focussedEvent != context.viewState.timelineViewState.focussedEvent { if tableViewController.focussedEvent != context.viewState.timelineState.focussedEvent {
tableViewController.focussedEvent = context.viewState.timelineViewState.focussedEvent tableViewController.focussedEvent = context.viewState.timelineState.focussedEvent
} }
if tableViewController.hideTimelineMedia != context.viewState.hideTimelineMedia { if tableViewController.hideTimelineMedia != context.viewState.hideTimelineMedia {
tableViewController.hideTimelineMedia = context.viewState.hideTimelineMedia tableViewController.hideTimelineMedia = context.viewState.hideTimelineMedia

View File

@ -43,8 +43,7 @@ struct UserProfileScreen: View {
AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID),
isVerified: context.viewState.showVerifiedBadge, isVerified: context.viewState.showVerifiedBadge,
avatarSize: .user(on: .memberDetails), avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider, mediaProvider: context.mediaProvider) { }
footer: { })
} }
} }

View File

@ -506,7 +506,7 @@ class ClientProxy: ClientProxyProtocol {
} }
if !roomSummaryProvider.statePublisher.value.isLoaded { if !roomSummaryProvider.statePublisher.value.isLoaded {
_ = await roomSummaryProvider.statePublisher.values.first(where: { $0.isLoaded }) _ = await roomSummaryProvider.statePublisher.values.first { $0.isLoaded }
} }
if shouldAwait { if shouldAwait {

View File

@ -298,11 +298,11 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
.infoPublisher .infoPublisher
.compactMap { ($0.hasRoomCall, $0.activeRoomCallParticipants) } .compactMap { ($0.hasRoomCall, $0.activeRoomCallParticipants) }
.removeDuplicates { $0 == $1 } .removeDuplicates { $0 == $1 }
.drop(while: { hasRoomCall, _ in .drop { hasRoomCall, _ in
// Filter all updates before hasRoomCall becomes `true`. Then we can correctly // Filter all updates before hasRoomCall becomes `true`. Then we can correctly
// detect its change to `false` to stop ringing when the caller hangs up. // detect its change to `false` to stop ringing when the caller hangs up.
!hasRoomCall !hasRoomCall
}) }
.sink { [weak self] hasOngoingCall, activeRoomCallParticipants in .sink { [weak self] hasOngoingCall, activeRoomCallParticipants in
guard let self else { return } guard let self else { return }

View File

@ -120,7 +120,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol {
// as in this case no API call is made by the RustSDK and the push rules are therefore not updated. // as in this case no API call is made by the RustSDK and the push rules are therefore not updated.
_ = await callbacks _ = await callbacks
.timeout(.seconds(2.0), scheduler: DispatchQueue.main, options: nil, customError: nil) .timeout(.seconds(2.0), scheduler: DispatchQueue.main, options: nil, customError: nil)
.values.first(where: { $0 == .settingsDidChange }) .values.first { $0 == .settingsDidChange }
} }
@MainActor @MainActor

View File

@ -166,6 +166,19 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
} }
} }
func messageFilteredTimeline(allowedMessageTypes: [RoomMessageEventMessageType]) async -> Result<any TimelineProxyProtocol, RoomProxyError> {
do {
let timeline = try await TimelineProxy(timeline: room.messageFilteredTimeline(internalIdPrefix: nil, allowedMessageTypes: allowedMessageTypes),
kind: .media)
await timeline.subscribeForUpdates()
return .success(timeline)
} catch {
MXLog.error("Failed retrieving media events timeline with error: \(error)")
return .failure(.sdkError(error))
}
}
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> { func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
do { do {
try await room.redact(eventId: eventID, reason: nil) try await room.redact(eventId: eventID, reason: nil)

View File

@ -70,6 +70,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 messageFilteredTimeline(allowedMessageTypes: [RoomMessageEventMessageType]) async -> Result<TimelineProxyProtocol, RoomProxyError>
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError> func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError>

View File

@ -23,6 +23,12 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>() let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
var paginationState: PaginationState = .initial {
didSet {
callbacks.send(.paginationState(paginationState))
}
}
var timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default var timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default
var timelineItemsTimestamp: [TimelineItemIdentifier: Date] = [:] var timelineItemsTimestamp: [TimelineItemIdentifier: Date] = [:]
@ -30,9 +36,18 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
init(timelineKind: TimelineKind = .live, listenForSignals: Bool = false) { init(timelineKind: TimelineKind = .live, listenForSignals: Bool = false) {
self.timelineKind = timelineKind self.timelineKind = timelineKind
callbacks.send(.paginationState(PaginationState(backward: .idle, forward: .timelineEndReached))) paginationState = PaginationState(backward: .idle, forward: .timelineEndReached)
callbacks.send(.isLive(true)) callbacks.send(.isLive(true))
switch timelineKind {
case .media:
timelineItems = (0..<5).reduce([]) { partialResult, _ in
partialResult + RoomTimelineItemFixtures.mediaChunk
}
default:
break
}
guard listenForSignals else { return } guard listenForSignals else { return }
do { do {
@ -56,7 +71,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
} }
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> { func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
callbacks.send(.paginationState(PaginationState(backward: .paginating, forward: .timelineEndReached))) paginationState = PaginationState(backward: .paginating, forward: .timelineEndReached)
if client == nil { if client == nil {
try? await simulateBackPagination() try? await simulateBackPagination()
@ -170,8 +185,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
/// Prepends the next chunk of items to the `timelineItems` array. /// Prepends the next chunk of items to the `timelineItems` array.
private func simulateBackPagination() async throws { private func simulateBackPagination() async throws {
defer { defer {
callbacks.send(.paginationState(PaginationState(backward: backPaginationResponses.isEmpty ? .timelineEndReached : .idle, paginationState = PaginationState(backward: backPaginationResponses.isEmpty ? .timelineEndReached : .idle,
forward: .timelineEndReached))) forward: .timelineEndReached)
} }
guard !backPaginationResponses.isEmpty else { return } guard !backPaginationResponses.isEmpty else { return }

View File

@ -31,6 +31,12 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
private(set) var timelineItems = [RoomTimelineItemProtocol]() private(set) var timelineItems = [RoomTimelineItemProtocol]()
private(set) var paginationState: PaginationState = .initial {
didSet {
callbacks.send(.paginationState(paginationState))
}
}
var roomID: String { var roomID: String {
roomProxy.id roomProxy.id
} }
@ -64,7 +70,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
Task { Task {
callbacks.send(.paginationState(PaginationState(backward: .paginating, forward: .paginating))) paginationState = PaginationState(backward: .paginating, forward: .paginating)
switch await focusOnEvent(initialFocussedEventID, timelineSize: 100) { switch await focusOnEvent(initialFocussedEventID, timelineSize: 100) {
case .success: case .success:
break break
@ -368,7 +375,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
isSwitchingTimelines = true isSwitchingTimelines = true
// Inform the world that the initial items are loading from the store // Inform the world that the initial items are loading from the store
callbacks.send(.paginationState(.init(backward: .paginating, forward: .paginating))) paginationState = PaginationState(backward: .paginating, forward: .paginating)
callbacks.send(.isLive(activeTimelineProvider.kind == .live)) callbacks.send(.isLive(activeTimelineProvider.kind == .live))
updateTimelineItemsCancellable = activeTimelineProvider updateTimelineItemsCancellable = activeTimelineProvider
@ -446,7 +453,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
callbacks.send(.updatedTimelineItems(timelineItems: newTimelineItems, isSwitchingTimelines: isNewTimeline)) callbacks.send(.updatedTimelineItems(timelineItems: newTimelineItems, isSwitchingTimelines: isNewTimeline))
callbacks.send(.paginationState(paginationState)) self.paginationState = paginationState
} }
private func buildTimelineItem(for itemProxy: TimelineItemProxy) -> RoomTimelineItemProtocol? { private func buildTimelineItem(for itemProxy: TimelineItemProxy) -> RoomTimelineItemProtocol? {

View File

@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import MatrixRustSDK
struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
@ -20,12 +21,13 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, func buildPinnedEventsRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? { mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? {
guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else { guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else {
return nil return nil
} }
return RoomTimelineController(roomProxy: roomProxy, return RoomTimelineController(roomProxy: roomProxy,
timelineProxy: pinnedEventsTimeline, timelineProxy: pinnedEventsTimeline,
initialFocussedEventID: nil, initialFocussedEventID: nil,
@ -33,4 +35,21 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
mediaProvider: mediaProvider, mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
func buildMessageFilteredRoomTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError> {
switch await roomProxy.messageFilteredTimeline(allowedMessageTypes: allowedMessageTypes) {
case .success(let timelineProxy):
return .success(RoomTimelineController(roomProxy: roomProxy,
timelineProxy: timelineProxy,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings))
case .failure(let error):
return .failure(.roomProxyError(error))
}
}
} }

View File

@ -6,6 +6,11 @@
// //
import Foundation import Foundation
import MatrixRustSDK
enum RoomTimelineFactoryControllerError: Error {
case roomProxyError(RoomProxyError)
}
@MainActor @MainActor
protocol RoomTimelineControllerFactoryProtocol { protocol RoomTimelineControllerFactoryProtocol {
@ -13,9 +18,15 @@ protocol RoomTimelineControllerFactoryProtocol {
initialFocussedEventID: String?, initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol
func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol,
func buildPinnedEventsRoomTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol?
func buildMessageFilteredRoomTimelineController(allowedMessageTypes: [RoomMessageEventMessageType],
roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> Result<RoomTimelineControllerProtocol, RoomTimelineFactoryControllerError>
} }
// sourcery: AutoMockable // sourcery: AutoMockable

View File

@ -31,7 +31,12 @@ protocol RoomTimelineControllerProtocol {
var roomID: String { get } var roomID: String { get }
var timelineKind: TimelineKind { get } var timelineKind: TimelineKind { get }
/// The currently known items, use only for setting up the intial state.
var timelineItems: [RoomTimelineItemProtocol] { get } var timelineItems: [RoomTimelineItemProtocol] { get }
/// The current pagination state, use only for setting up the intial state
var paginationState: PaginationState { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get } var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
func processItemAppearance(_ itemID: TimelineItemIdentifier) async func processItemAppearance(_ itemID: TimelineItemIdentifier) async

View File

@ -42,7 +42,7 @@ extension AggregatedReaction {
/// Whether to highlight the reaction, indicating that the current user sent this reaction. /// Whether to highlight the reaction, indicating that the current user sent this reaction.
var isHighlighted: Bool { var isHighlighted: Bool {
senders.contains(where: { $0.id == accountOwnerID }) senders.contains { $0.id == accountOwnerID }
} }
/// The key to be displayed on screen. See `maxDisplayChars`. /// The key to be displayed on screen. See `maxDisplayChars`.

View File

@ -13,7 +13,7 @@ import MatrixRustSDK
/// Its value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances. /// Its value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances.
/// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or /// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or
/// a local transaction id, never both or none. /// a local transaction id, never both or none.
enum TimelineItemIdentifier: Hashable { enum TimelineItemIdentifier: Hashable, Sendable {
case event(uniqueID: TimelineUniqueId, eventOrTransactionID: EventOrTransactionId) case event(uniqueID: TimelineUniqueId, eventOrTransactionID: EventOrTransactionId)
case virtual(uniqueID: TimelineUniqueId) case virtual(uniqueID: TimelineUniqueId)

View File

@ -61,8 +61,9 @@ struct VoiceMessageRoomPlaybackView: View {
if let url = playerState.fileURL { if let url = playerState.fileURL {
WaveformView(audioURL: url, WaveformView(audioURL: url,
configuration: .init(style: .striped(.init(color: .black, width: waveformLineWidth, spacing: waveformLinePadding)), configuration: .init(style: .striped(.init(color: .black, width: waveformLineWidth, spacing: waveformLinePadding)),
verticalScalingFactor: 1.0), verticalScalingFactor: 1.0)) {
placeholder: { estimatedWaveformView }) estimatedWaveformView
}
.progressMask(progress: playerState.progress) .progressMask(progress: playerState.progress)
} else { } else {
estimatedWaveformView estimatedWaveformView

View File

@ -92,7 +92,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: case .detached, .media:
return await focussedPaginate(.backwards, requestSize: requestSize) return await focussedPaginate(.backwards, requestSize: requestSize)
case .pinned: case .pinned:
return .success(()) return .success(())
@ -319,7 +319,6 @@ final class TimelineProxy: TimelineProxyProtocol {
return .success(()) return .success(())
} }
// swiftlint:disable:next function_parameter_count
func sendVideo(url: URL, func sendVideo(url: URL,
thumbnailURL: URL, thumbnailURL: URL,
videoInfo: VideoInfo, videoInfo: VideoInfo,
@ -580,7 +579,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, .media:
// 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)

View File

@ -13,6 +13,7 @@ enum TimelineKind {
case live case live
case detached case detached
case pinned case pinned
case media
} }
enum TimelineProxyError: Error { enum TimelineProxyError: Error {

View File

@ -232,7 +232,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
private func finalizeRecording() async -> Result<Void, VoiceMessageRecorderError> { private func finalizeRecording() async -> Result<Void, VoiceMessageRecorderError> {
MXLog.info("finalize audio recording") MXLog.info("finalize audio recording")
guard let url = audioRecorder.audioFileURL, audioRecorder.currentTime > 0 else { guard audioRecorder.audioFileURL != nil, audioRecorder.currentTime > 0 else {
return .failure(.previewNotAvailable) return .failure(.previewNotAvailable)
} }

View File

@ -377,6 +377,12 @@ extension PreviewTests {
} }
} }
func test_mediaEventsTimelineScreen() {
for preview in MediaEventsTimelineScreen_Previews._allPreviews {
assertSnapshots(matching: preview)
}
}
func test_mediaUploadPreviewScreen() { func test_mediaUploadPreviewScreen() {
for preview in MediaUploadPreviewScreen_Previews._allPreviews { for preview in MediaUploadPreviewScreen_Previews._allPreviews {
assertSnapshots(matching: preview) assertSnapshots(matching: preview)

View File

@ -79,7 +79,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3) XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org") XCTAssertEqual(link?.host, "www.matrix.org")
} }
@ -96,7 +96,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3) XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org") XCTAssertEqual(link?.host, "www.matrix.org")
} }
@ -113,7 +113,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3) XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org") XCTAssertEqual(link?.host, "www.matrix.org")
} }
@ -130,7 +130,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(attributedString.runs.count, 3) XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first(where: { $0.link != nil })?.link let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link, "https://matrix.org") XCTAssertEqual(link, "https://matrix.org")
} }

View File

@ -20,7 +20,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.blockedUsers.contains(where: { $0.displayName != nil }) } let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.blockedUsers.contains { $0.displayName != nil } }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty)
@ -35,7 +35,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
let deferred = deferFulfillment(viewModel.context.$viewState) { $0.blockedUsers.contains(where: { $0.displayName != nil }) } let deferred = deferFulfillment(viewModel.context.$viewState) { $0.blockedUsers.contains { $0.displayName != nil } }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty)

View File

@ -54,10 +54,10 @@ class ComposerToolbarViewModelTests: XCTestCase {
.map(\.composerMode) .map(\.composerMode)
.removeDuplicates() .removeDuplicates()
.dropFirst() .dropFirst()
.sink(receiveValue: { composerMode in .sink { composerMode in
XCTAssertEqual(composerMode, mode) XCTAssertEqual(composerMode, mode)
expectation.fulfill() expectation.fulfill()
}) }
viewModel.process(timelineAction: .setMode(mode: mode)) viewModel.process(timelineAction: .setMode(mode: mode))

View File

@ -84,7 +84,7 @@ class JoinRoomScreenViewModelTests: XCTestCase {
topic: nil, topic: nil,
avatarURL: nil, avatarURL: nil,
memberCount: 0, memberCount: 0,
isHistoryWorldReadable: false, isHistoryWorldReadable: nil,
isJoined: false, isJoined: false,
isInvited: false, isInvited: false,
isPublic: false, isPublic: false,

View File

@ -34,7 +34,7 @@ class MessageForwardingScreenViewModelTests: XCTestCase {
} }
func testInitialState() { func testInitialState() {
XCTAssertNil(context.viewState.rooms.first(where: { $0.id == forwardingItem.roomID }), "The source room ID shouldn't be shown") XCTAssertNil(context.viewState.rooms.first { $0.id == forwardingItem.roomID }, "The source room ID shouldn't be shown")
} }
func testRoomSelection() { func testRoomSelection() {

View File

@ -29,7 +29,7 @@ class PollFormScreenViewModelTests: XCTestCase {
XCTAssertFalse(context.viewState.bindings.isUndisclosed) XCTAssertFalse(context.viewState.bindings.isUndisclosed)
// Cancellation should work without confirmation // Cancellation should work without confirmation
let deferred = deferFulfillment(viewModel.actions, until: { _ in true }) let deferred = deferFulfillment(viewModel.actions) { _ in true }
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
let action = try await deferred.fulfill() let action = try await deferred.fulfill()
XCTAssertNil(context.alertInfo) XCTAssertNil(context.alertInfo)
@ -45,7 +45,7 @@ class PollFormScreenViewModelTests: XCTestCase {
XCTAssertFalse(context.viewState.bindings.isUndisclosed) XCTAssertFalse(context.viewState.bindings.isUndisclosed)
// Cancellation should work without confirmation // Cancellation should work without confirmation
let deferred = deferFulfillment(viewModel.actions, until: { _ in true }) let deferred = deferFulfillment(viewModel.actions) { _ in true }
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
let action = try await deferred.fulfill() let action = try await deferred.fulfill()
XCTAssertNil(context.alertInfo) XCTAssertNil(context.alertInfo)

View File

@ -161,8 +161,8 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
// Then no warning should be shown, and the call to update the users should be made straight away. // Then no warning should be shown, and the call to update the users should be made straight away.
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 2) XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 2)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains(where: { $0.userID == existingModerator.id && $0.powerLevel == 0 }), true) XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == existingModerator.id && $0.powerLevel == 0 }, true)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains(where: { $0.userID == firstUser.id && $0.powerLevel == 50 }), true) XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 50 }, true)
} }
func testSavePromotedAdministrator() async throws { func testSavePromotedAdministrator() async throws {
@ -189,7 +189,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
// Then the user should be made into an administrator. // Then the user should be made into an administrator.
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 1) XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 1)
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains(where: { $0.userID == firstUser.id && $0.powerLevel == 100 }), true) XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 100 }, true)
} }
private func setupViewModel(mode: RoomMemberDetails.Role) { private func setupViewModel(mode: RoomMemberDetails.Role) {

View File

@ -322,7 +322,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
topic: nil, topic: nil,
avatarURL: nil, avatarURL: nil,
memberCount: 0, memberCount: 0,
isHistoryWorldReadable: false, isHistoryWorldReadable: nil,
isJoined: false, isJoined: false,
isInvited: true, isInvited: true,
isPublic: false, isPublic: false,

View File

@ -49,7 +49,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase {
context.send(viewAction: .editOwnUserRole) context.send(viewAction: .editOwnUserRole)
XCTAssertNotNil(context.alertInfo) XCTAssertNotNil(context.alertInfo)
context.alertInfo?.verticalButtons?.first(where: { $0.title.localizedStandardContains("moderator") })?.action?() context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("moderator") }?.action?()
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
@ -64,7 +64,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase {
context.send(viewAction: .editOwnUserRole) context.send(viewAction: .editOwnUserRole)
XCTAssertNotNil(context.alertInfo) XCTAssertNotNil(context.alertInfo)
context.alertInfo?.verticalButtons?.first(where: { $0.title.localizedStandardContains("member") })?.action?() context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("member") }?.action?()
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))

View File

@ -45,9 +45,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
// Then the messages should be grouped together. // Then the messages should be grouped together.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the middle message from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the middle message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
} }
func testMessageGroupingMultipleSenders() { func testMessageGroupingMultipleSenders() {
@ -73,12 +73,12 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
// Then the messages should be grouped by sender. // Then the messages should be grouped by sender.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "A message should not be grouped when the sender changes.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .single, "A message should not be grouped when the sender changes.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[3].groupStyle, .last, "A group should be ended when the sender changes in the next message.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[3].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[4].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[4].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[5].groupStyle, .last, "A group should be ended when the sender changes in the next message.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[5].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
} }
func testMessageGroupingWithLeadingReactions() { func testMessageGroupingWithLeadingReactions() {
@ -99,9 +99,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
// Then the first message should not be grouped but the other two should. // Then the first message should not be grouped but the other two should.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "When the first message has reactions it should not be grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .single, "When the first message has reactions it should not be grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .first, "A new group should be made when the preceding message has reactions.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .first, "A new group should be made when the preceding message has reactions.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
} }
func testMessageGroupingWithInnerReactions() { func testMessageGroupingWithInnerReactions() {
@ -122,9 +122,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
// Then the first and second messages should be grouped and the last one should not. // Then the first and second messages should be grouped and the last one should not.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .last, "When the message has reactions, the group should end here.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .last, "When the message has reactions, the group should end here.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .single, "The last message should not be grouped when the preceding message has reactions.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .single, "The last message should not be grouped when the preceding message has reactions.")
} }
func testMessageGroupingWithTrailingReactions() { func testMessageGroupingWithTrailingReactions() {
@ -145,9 +145,9 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
// Then the messages should be grouped together. // Then the messages should be grouped together.
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the second message from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the second message from being grouped.")
XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.") XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.")
} }
// MARK: - Focussing // MARK: - Focussing
@ -162,18 +162,18 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
XCTAssertEqual(timelineController.focusOnEventCallCount, 0) XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEvent) XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
// When focussing on an item that isn't loaded. // When focussing on an item that isn't loaded.
let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineViewState.isLive } let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineState.isLive }
await viewModel.focusOnEvent(eventID: "t4") await viewModel.focusOnEvent(eventID: "t4")
try await deferred.fulfill() try await deferred.fulfill()
// Then a new timeline should be loaded and the room focussed on that event. // Then a new timeline should be loaded and the room focussed on that event.
XCTAssertEqual(timelineController.focusOnEventCallCount, 1) XCTAssertEqual(timelineController.focusOnEventCallCount, 1)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive) XCTAssertFalse(viewModel.context.viewState.timelineState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t4", appearance: .immediate)) XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
} }
func testFocusLoadedItem() async throws { func testFocusLoadedItem() async throws {
@ -186,18 +186,18 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
XCTAssertEqual(timelineController.focusOnEventCallCount, 0) XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEvent) XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
// When focussing on a loaded item. // When focussing on a loaded item.
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.timelineViewState.isLive } let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.timelineState.isLive }
await viewModel.focusOnEvent(eventID: "t1") await viewModel.focusOnEvent(eventID: "t1")
try await deferred.fulfill() try await deferred.fulfill()
// Then the timeline should remain live and the item should be focussed. // Then the timeline should remain live and the item should be focussed.
XCTAssertEqual(timelineController.focusOnEventCallCount, 0) XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t1", appearance: .animated)) XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t1", appearance: .animated))
} }
func testFocusLive() async throws { func testFocusLive() async throws {
@ -210,30 +210,30 @@ class TimelineViewModelTests: XCTestCase {
let viewModel = makeViewModel(timelineController: timelineController) let viewModel = makeViewModel(timelineController: timelineController)
var deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineViewState.isLive } var deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineState.isLive }
await viewModel.focusOnEvent(eventID: "t4") await viewModel.focusOnEvent(eventID: "t4")
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(timelineController.focusLiveCallCount, 0) XCTAssertEqual(timelineController.focusLiveCallCount, 0)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive) XCTAssertFalse(viewModel.context.viewState.timelineState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t4", appearance: .immediate)) XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
// When switching back to a live timeline. // When switching back to a live timeline.
deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineViewState.isLive } deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineState.isLive }
viewModel.context.send(viewAction: .focusLive) viewModel.context.send(viewAction: .focusLive)
try await deferred.fulfill() try await deferred.fulfill()
// Then the timeline should switch back to being live and the event focus should be removed. // Then the timeline should switch back to being live and the event focus should be removed.
XCTAssertEqual(timelineController.focusLiveCallCount, 1) XCTAssertEqual(timelineController.focusLiveCallCount, 1)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEvent) XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
} }
func testInitialFocusViewState() async throws { func testInitialFocusViewState() async throws {
let timelineController = MockRoomTimelineController() let timelineController = MockRoomTimelineController()
let viewModel = makeViewModel(focussedEventID: "t10", timelineController: timelineController) let viewModel = makeViewModel(focussedEventID: "t10", timelineController: timelineController)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEvent, .init(eventID: "t10", appearance: .immediate)) XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t10", appearance: .immediate))
} }
// MARK: - Read Receipts // MARK: - Read Receipts