Implement the save action when previewing media. (#3630)

* Implement the save action on the media preview.

* Update Compound and use the correct icon.

Also fixes an icon that has been renamed.

* Update the add to photo library usage description to match the designs.

* PR comments.
This commit is contained in:
Doug 2024-12-17 16:35:51 +00:00 committed by GitHub
parent 2b82b94500
commit 2a865ce5bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 403 additions and 68 deletions

View File

@ -233,6 +233,7 @@
2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; }; 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; };
2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */; }; 2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */; };
2D2D8A53B35BE8D8A01449C6 /* PinnedEventsBannerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */; }; 2D2D8A53B35BE8D8A01449C6 /* PinnedEventsBannerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */; };
2D38D39B1789B91AE69F477F /* PhotoLibraryManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD955A0380C287C418F1A74D /* PhotoLibraryManagerMock.swift */; };
2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */; }; 2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */; };
2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */; }; 2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */; };
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; };
@ -395,6 +396,7 @@
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; };
4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; }; 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; };
4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.swift */; }; 4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.swift */; };
4ED764A24F2A715C25CF07F1 /* TimelineMediaPreviewFileExportPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */; };
4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */; }; 4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */; };
4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.swift */; }; 4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.swift */; };
4FE688FE9375B2FBF424146A /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */; }; 4FE688FE9375B2FBF424146A /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */; };
@ -467,6 +469,7 @@
5F0B5797D1BFF2A51084B4C3 /* PinnedEventsTimelineScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D7CD5CA270BFC3EBB450CA /* PinnedEventsTimelineScreenViewModel.swift */; }; 5F0B5797D1BFF2A51084B4C3 /* PinnedEventsTimelineScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D7CD5CA270BFC3EBB450CA /* PinnedEventsTimelineScreenViewModel.swift */; };
5F35069E13D71DD88633A4B2 /* preview_video.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 45A4B934BA41D6C255900265 /* preview_video.jpg */; }; 5F35069E13D71DD88633A4B2 /* preview_video.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 45A4B934BA41D6C255900265 /* preview_video.jpg */; };
5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; };
5FA1DCE55973862632961D7C /* PhotoLibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7495E1119753B06FF2C2279 /* PhotoLibraryManager.swift */; };
5FCD8AFA364206EE32B909A3 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B050A6B233D95807A09289E7 /* Settings.bundle */; }; 5FCD8AFA364206EE32B909A3 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B050A6B233D95807A09289E7 /* Settings.bundle */; };
601AB75BD52B0B4276CEB84A /* SessionVerificationScreenStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */; }; 601AB75BD52B0B4276CEB84A /* SessionVerificationScreenStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */; };
60ED66E63A169E47489348A8 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; }; 60ED66E63A169E47489348A8 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; };
@ -760,6 +763,7 @@
97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */; }; 97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */; };
97BAEDD9054FB5F233EE928B /* EncryptionResetScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.swift */; }; 97BAEDD9054FB5F233EE928B /* EncryptionResetScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.swift */; };
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */; }; 981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */; };
9826A4DBBEFA7041A9E0EFAD /* TimelineMediaPreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA57E8563C346B13DDE4A6F4 /* TimelineMediaPreviewScreen.swift */; };
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; }; 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; };
9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */; }; 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */; };
988BA75A182738150894A23F /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */; }; 988BA75A182738150894A23F /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */; };
@ -866,7 +870,6 @@
AE1160076F663BF14E0E893A /* EffectsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */; }; AE1160076F663BF14E0E893A /* EffectsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */; };
AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */; }; AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */; };
AE5AAD9E32511544FDFA5560 /* WindowManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F27F588F9059128E17C669 /* WindowManagerProtocol.swift */; }; AE5AAD9E32511544FDFA5560 /* WindowManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F27F588F9059128E17C669 /* WindowManagerProtocol.swift */; };
AE69B349B0011D5EE2C13606 /* TimelineMediaPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A76B92984638A9B3104840D /* TimelineMediaPreviewView.swift */; };
AF19D65A9C60C6B2646F3210 /* RedactedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */; }; AF19D65A9C60C6B2646F3210 /* RedactedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */; };
AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */; }; AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */; };
AF33B9044498211C3D82F1E1 /* UNTextInputNotificationResponse+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */; }; AF33B9044498211C3D82F1E1 /* UNTextInputNotificationResponse+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */; };
@ -1222,7 +1225,6 @@
FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; }; FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; };
FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */; }; FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */; };
FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; };
FAF12EF424E55377816149DB /* MediaFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3615FD6460BC9C1DEF8659 /* MediaFileManager.swift */; };
FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; };
FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */; }; FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */; };
FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */; }; FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */; };
@ -1378,7 +1380,6 @@
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; }; 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; }; 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = "<group>"; }; 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = "<group>"; };
0A76B92984638A9B3104840D /* TimelineMediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewView.swift; sourceTree = "<group>"; };
0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = "<group>"; }; 0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = "<group>"; };
0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomModerationRole.swift; sourceTree = "<group>"; }; 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomModerationRole.swift; sourceTree = "<group>"; };
0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; }; 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; };
@ -1390,7 +1391,6 @@
0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = "<group>"; }; 0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = "<group>"; };
0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; }; 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; };
0D3615FD6460BC9C1DEF8659 /* MediaFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFileManager.swift; sourceTree = "<group>"; };
0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenCoordinator.swift; sourceTree = "<group>"; }; 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenCoordinator.swift; sourceTree = "<group>"; };
0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupFlowCoordinator.swift; sourceTree = "<group>"; }; 0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupFlowCoordinator.swift; sourceTree = "<group>"; };
@ -1569,6 +1569,7 @@
303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModel.swift; sourceTree = "<group>"; }; 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModel.swift; sourceTree = "<group>"; };
306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenModels.swift; sourceTree = "<group>"; }; 306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenModels.swift; sourceTree = "<group>"; };
307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreen.swift; sourceTree = "<group>"; }; 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreen.swift; sourceTree = "<group>"; };
30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewFileExportPicker.swift; sourceTree = "<group>"; };
309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = "<group>"; }; 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = "<group>"; };
30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = "<group>"; }; 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = "<group>"; };
314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = "<group>"; }; 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = "<group>"; };
@ -2115,6 +2116,7 @@
A9E88667D393612FD5D84718 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/SAS.strings; sourceTree = "<group>"; }; A9E88667D393612FD5D84718 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/SAS.strings; sourceTree = "<group>"; };
A9FAFE1C2149E6AC8156ED2B /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; }; A9FAFE1C2149E6AC8156ED2B /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
AA19C32BD97F45847724E09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Untranslated.strings; sourceTree = "<group>"; }; AA19C32BD97F45847724E09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Untranslated.strings; sourceTree = "<group>"; };
AA57E8563C346B13DDE4A6F4 /* TimelineMediaPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewScreen.swift; sourceTree = "<group>"; };
AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
AACE9B8E1A4AE79A7E2914F6 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; AACE9B8E1A4AE79A7E2914F6 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfile+Mock.swift"; sourceTree = "<group>"; }; AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfile+Mock.swift"; sourceTree = "<group>"; };
@ -2362,6 +2364,7 @@
DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = "<group>"; }; DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = "<group>"; };
DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = "<group>"; }; DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = "<group>"; };
DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = "<group>"; }; DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = "<group>"; };
DD955A0380C287C418F1A74D /* PhotoLibraryManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryManagerMock.swift; sourceTree = "<group>"; };
DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = "<group>"; }; DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = "<group>"; };
DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelTests.swift; sourceTree = "<group>"; }; DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelTests.swift; sourceTree = "<group>"; };
DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = "<group>"; }; DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = "<group>"; };
@ -2408,6 +2411,7 @@
E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = "<group>"; }; E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = "<group>"; };
E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxy.swift; sourceTree = "<group>"; }; E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxy.swift; sourceTree = "<group>"; };
E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactory.swift; sourceTree = "<group>"; }; E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactory.swift; sourceTree = "<group>"; };
E7495E1119753B06FF2C2279 /* PhotoLibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryManager.swift; sourceTree = "<group>"; };
E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModelProtocol.swift; sourceTree = "<group>"; }; E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModelProtocol.swift; sourceTree = "<group>"; };
E78FC546F28E045A560F2963 /* EncryptionKeyProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProviderProtocol.swift; sourceTree = "<group>"; }; E78FC546F28E045A560F2963 /* EncryptionKeyProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProviderProtocol.swift; sourceTree = "<group>"; };
E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -3171,6 +3175,7 @@
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */, 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */,
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */,
B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */, B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */,
DD955A0380C287C418F1A74D /* PhotoLibraryManagerMock.swift */,
D38391154120264910D19528 /* PollMock.swift */, D38391154120264910D19528 /* PollMock.swift */,
894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */, 894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */,
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */, 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */,
@ -3492,10 +3497,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */, 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */,
0D3615FD6460BC9C1DEF8659 /* MediaFileManager.swift */, E7495E1119753B06FF2C2279 /* PhotoLibraryManager.swift */,
E3A62FBD3007312311C14DD8 /* TimelineMediaPreviewCoordinator.swift */, E3A62FBD3007312311C14DD8 /* TimelineMediaPreviewCoordinator.swift */,
2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */, 2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */,
0A76B92984638A9B3104840D /* TimelineMediaPreviewView.swift */,
53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */, 53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */,
5EC4A8482DA110602FE6DF42 /* View */, 5EC4A8482DA110602FE6DF42 /* View */,
); );
@ -3889,7 +3893,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */, 467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */,
30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */,
C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */, C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */,
AA57E8563C346B13DDE4A6F4 /* TimelineMediaPreviewScreen.swift */,
); );
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
@ -7095,7 +7101,6 @@
C11D4A49DC29D89CE2BB31B8 /* MediaEventsTimelineScreenViewModel.swift in Sources */, C11D4A49DC29D89CE2BB31B8 /* MediaEventsTimelineScreenViewModel.swift in Sources */,
FD9777315A5D9CDC47458AD1 /* MediaEventsTimelineScreenViewModelProtocol.swift in Sources */, FD9777315A5D9CDC47458AD1 /* MediaEventsTimelineScreenViewModelProtocol.swift in Sources */,
BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */, BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */,
FAF12EF424E55377816149DB /* MediaFileManager.swift in Sources */,
208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */, 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */,
A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */, A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */,
4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */, 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */,
@ -7173,6 +7178,8 @@
847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */, 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */,
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */, 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */,
BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */, BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */,
5FA1DCE55973862632961D7C /* PhotoLibraryManager.swift in Sources */,
2D38D39B1789B91AE69F477F /* PhotoLibraryManagerMock.swift in Sources */,
962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */, 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */,
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */, EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */,
899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */, 899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */,
@ -7491,9 +7498,10 @@
562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */, 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */,
FE43747C116CA3D8D6B92F5F /* TimelineMediaPreviewCoordinator.swift in Sources */, FE43747C116CA3D8D6B92F5F /* TimelineMediaPreviewCoordinator.swift in Sources */,
12EC6BC99F373FE5C6EB9B64 /* TimelineMediaPreviewDetailsView.swift in Sources */, 12EC6BC99F373FE5C6EB9B64 /* TimelineMediaPreviewDetailsView.swift in Sources */,
4ED764A24F2A715C25CF07F1 /* TimelineMediaPreviewFileExportPicker.swift in Sources */,
77FB08C303F4C74C0E8577E2 /* TimelineMediaPreviewModels.swift in Sources */, 77FB08C303F4C74C0E8577E2 /* TimelineMediaPreviewModels.swift in Sources */,
A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */, A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */,
AE69B349B0011D5EE2C13606 /* TimelineMediaPreviewView.swift in Sources */, 9826A4DBBEFA7041A9E0EFAD /* TimelineMediaPreviewScreen.swift in Sources */,
86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */, 86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */,
B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */, B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */,
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */, E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */,
@ -8539,7 +8547,7 @@
repositoryURL = "https://github.com/element-hq/compound-ios"; repositoryURL = "https://github.com/element-hq/compound-ios";
requirement = { requirement = {
kind = revision; kind = revision;
revision = 1a70bc7f3420647843b9c18748982c61ef7d2245; revision = 9325643cb4d22150881c5bf79e1e6e3c5a87ea89;
}; };
}; };
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = { F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = {

View File

@ -6,8 +6,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-design-tokens", "location" : "https://github.com/element-hq/compound-design-tokens",
"state" : { "state" : {
"revision" : "f79e05011ec3402c29ded19bcff95b5ead180991", "revision" : "a6e96fb4436a4945423a8c068001093af4b7b315",
"version" : "2.1.2" "version" : "3.0.1"
} }
}, },
{ {
@ -15,7 +15,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-ios", "location" : "https://github.com/element-hq/compound-ios",
"state" : { "state" : {
"revision" : "1a70bc7f3420647843b9c18748982c61ef7d2245" "revision" : "9325643cb4d22150881c5bf79e1e6e3c5a87ea89"
} }
}, },
{ {

View File

@ -2,4 +2,4 @@
"NSFaceIDUsageDescription" = "Face ID is used to access your app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app.";
"NSLocationWhenInUseUsageDescription" = "Grant location access so that Element X can share your location."; "NSLocationWhenInUseUsageDescription" = "Grant location access so that Element X can share your location.";
"NSMicrophoneUsageDescription" = "To record and send messages with audio, Element X needs to access the microphone."; "NSMicrophoneUsageDescription" = "To record and send messages with audio, Element X needs to access the microphone.";
"NSPhotoLibraryUsageDescription" = "Allows saving photos and videos to your library."; "NSPhotoLibraryUsageDescription" = "This lets you save images and videos to your photo library.";

View File

@ -274,6 +274,7 @@
"dialog_permission_microphone_description_ios" = "Grant access so you can record and send messages with audio."; "dialog_permission_microphone_description_ios" = "Grant access so you can record and send messages with audio.";
"dialog_permission_microphone_title_ios" = "%1$@ needs permission to access your microphone."; "dialog_permission_microphone_title_ios" = "%1$@ needs permission to access your microphone.";
"dialog_permission_notification" = "In order to let the application display notifications, please grant the permission in the system settings."; "dialog_permission_notification" = "In order to let the application display notifications, please grant the permission in the system settings.";
"dialog_permission_photo_library_title_ios" = "%1$@ does not have access to your photo library.";
"dialog_title_confirmation" = "Confirmation"; "dialog_title_confirmation" = "Confirmation";
"dialog_title_warning" = "Warning"; "dialog_title_warning" = "Warning";
"dialog_unsaved_changes_description_ios" = "Your changes wont be saved"; "dialog_unsaved_changes_description_ios" = "Your changes wont be saved";

View File

@ -109,7 +109,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
private func presentMediaPreview(for previewContext: TimelineMediaPreviewContext) { private func presentMediaPreview(for previewContext: TimelineMediaPreviewContext) {
let parameters = TimelineMediaPreviewCoordinatorParameters(context: previewContext, let parameters = TimelineMediaPreviewCoordinatorParameters(context: previewContext,
mediaProvider: userSession.mediaProvider, mediaProvider: userSession.mediaProvider,
userIndicatorController: userIndicatorController) userIndicatorController: userIndicatorController,
appMediator: appMediator)
let coordinator = TimelineMediaPreviewCoordinator(parameters: parameters) let coordinator = TimelineMediaPreviewCoordinator(parameters: parameters)
coordinator.actionsPublisher coordinator.actionsPublisher

View File

@ -606,6 +606,10 @@ internal enum L10n {
} }
/// In order to let the application display notifications, please grant the permission in the system settings. /// In order to let the application display notifications, please grant the permission in the system settings.
internal static var dialogPermissionNotification: String { return L10n.tr("Localizable", "dialog_permission_notification") } internal static var dialogPermissionNotification: String { return L10n.tr("Localizable", "dialog_permission_notification") }
/// %1$@ does not have access to your photo library.
internal static func dialogPermissionPhotoLibraryTitleIos(_ p1: Any) -> String {
return L10n.tr("Localizable", "dialog_permission_photo_library_title_ios", String(describing: p1))
}
/// Confirmation /// Confirmation
internal static var dialogTitleConfirmation: String { return L10n.tr("Localizable", "dialog_title_confirmation") } internal static var dialogTitleConfirmation: String { return L10n.tr("Localizable", "dialog_title_confirmation") }
/// Error /// Error

View File

@ -8,6 +8,7 @@ import Combine
import Foundation import Foundation
import LocalAuthentication import LocalAuthentication
import MatrixRustSDK import MatrixRustSDK
import Photos
import SwiftUI import SwiftUI
class AnalyticsClientMock: AnalyticsClientProtocol { class AnalyticsClientMock: AnalyticsClientProtocol {
var isRunning: Bool { var isRunning: Bool {
@ -12327,6 +12328,79 @@ class PHGPostHogMock: PHGPostHogProtocol {
screenPropertiesClosure?(screenTitle, properties) screenPropertiesClosure?(screenTitle, properties)
} }
} }
class PhotoLibraryManagerMock: PhotoLibraryManagerProtocol {
//MARK: - addResource
var addResourceAtUnderlyingCallsCount = 0
var addResourceAtCallsCount: Int {
get {
if Thread.isMainThread {
return addResourceAtUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = addResourceAtUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
addResourceAtUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
addResourceAtUnderlyingCallsCount = newValue
}
}
}
}
var addResourceAtCalled: Bool {
return addResourceAtCallsCount > 0
}
var addResourceAtReceivedArguments: (type: PHAssetResourceType, url: URL)?
var addResourceAtReceivedInvocations: [(type: PHAssetResourceType, url: URL)] = []
var addResourceAtUnderlyingReturnValue: Result<Void, PhotoLibraryManagerError>!
var addResourceAtReturnValue: Result<Void, PhotoLibraryManagerError>! {
get {
if Thread.isMainThread {
return addResourceAtUnderlyingReturnValue
} else {
var returnValue: Result<Void, PhotoLibraryManagerError>? = nil
DispatchQueue.main.sync {
returnValue = addResourceAtUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
addResourceAtUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
addResourceAtUnderlyingReturnValue = newValue
}
}
}
}
var addResourceAtClosure: ((PHAssetResourceType, URL) async -> Result<Void, PhotoLibraryManagerError>)?
func addResource(_ type: PHAssetResourceType, at url: URL) async -> Result<Void, PhotoLibraryManagerError> {
addResourceAtCallsCount += 1
addResourceAtReceivedArguments = (type: type, url: url)
DispatchQueue.main.async {
self.addResourceAtReceivedInvocations.append((type: type, url: url))
}
if let addResourceAtClosure = addResourceAtClosure {
return await addResourceAtClosure(type, url)
} else {
return addResourceAtReturnValue
}
}
}
class PollInteractionHandlerMock: PollInteractionHandlerProtocol { class PollInteractionHandlerMock: PollInteractionHandlerProtocol {
//MARK: - sendPollResponse //MARK: - sendPollResponse

View File

@ -0,0 +1,21 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
extension PhotoLibraryManagerMock {
struct Configuration {
var authorizationDenied = false
}
// swiftlint:disable:next cyclomatic_complexity
convenience init(_ configuration: Configuration) {
self.init()
addResourceAtReturnValue = configuration.authorizationDenied ? .failure(PhotoLibraryManagerError.notAuthorized) : .success(())
}
}

View File

@ -1,8 +0,0 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation

View File

@ -0,0 +1,37 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Photos
enum PhotoLibraryManagerError: Error {
case notAuthorized
case unknown(Error)
}
// sourcery: AutoMockable
protocol PhotoLibraryManagerProtocol {
func addResource(_ type: PHAssetResourceType, at url: URL) async -> Result<Void, PhotoLibraryManagerError>
}
struct PhotoLibraryManager: PhotoLibraryManagerProtocol {
func addResource(_ type: PHAssetResourceType, at url: URL) async -> Result<Void, PhotoLibraryManagerError> {
do {
try await PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
let options = PHAssetResourceCreationOptions()
request.addResource(with: type, fileURL: url, options: options)
}
return .success(())
} catch {
if (error as NSError).code == PHPhotosError.accessUserDenied.rawValue {
return .failure(.notAuthorized)
} else {
return .failure(.unknown(error))
}
}
}
}

View File

@ -27,6 +27,7 @@ struct TimelineMediaPreviewCoordinatorParameters {
let context: TimelineMediaPreviewContext let context: TimelineMediaPreviewContext
let mediaProvider: MediaProviderProtocol let mediaProvider: MediaProviderProtocol
let userIndicatorController: UserIndicatorControllerProtocol let userIndicatorController: UserIndicatorControllerProtocol
let appMediator: AppMediatorProtocol
} }
enum TimelineMediaPreviewCoordinatorAction { enum TimelineMediaPreviewCoordinatorAction {
@ -50,7 +51,9 @@ final class TimelineMediaPreviewCoordinator: CoordinatorProtocol {
viewModel = TimelineMediaPreviewViewModel(context: parameters.context, viewModel = TimelineMediaPreviewViewModel(context: parameters.context,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
userIndicatorController: parameters.userIndicatorController) photoLibraryManager: PhotoLibraryManager(),
userIndicatorController: parameters.userIndicatorController,
appMediator: parameters.appMediator)
} }
func start() { func start() {
@ -69,6 +72,6 @@ final class TimelineMediaPreviewCoordinator: CoordinatorProtocol {
} }
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
AnyView(TimelineMediaPreviewView(context: viewModel.context)) AnyView(TimelineMediaPreviewScreen(context: viewModel.context))
} }
} }

View File

@ -26,8 +26,18 @@ struct TimelineMediaPreviewViewState: BindableState {
} }
struct TimelineMediaPreviewViewStateBindings { struct TimelineMediaPreviewViewStateBindings {
/// A binding that will present the Details view for the specified item.
var mediaDetailsItem: TimelineMediaPreviewItem? var mediaDetailsItem: TimelineMediaPreviewItem?
/// A binding that will present a confirmation to redact the specified item.
var redactConfirmationItem: TimelineMediaPreviewItem? var redactConfirmationItem: TimelineMediaPreviewItem?
/// A binding that will present a document picker to export the specified file.
var fileToExport: TimelineMediaPreviewFileExportPicker.File?
var alertInfo: AlertInfo<TimelineMediaPreviewAlertType>?
}
enum TimelineMediaPreviewAlertType {
case authorizationRequired
} }
/// Wraps a media file and title to be previewed with QuickLook. /// Wraps a media file and title to be previewed with QuickLook.

View File

@ -13,7 +13,9 @@ typealias TimelineMediaPreviewViewModelType = StateStoreViewModel<TimelineMediaP
class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType { class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
private let timelineViewModel: TimelineViewModelProtocol private let timelineViewModel: TimelineViewModelProtocol
private let mediaProvider: MediaProviderProtocol private let mediaProvider: MediaProviderProtocol
private let photoLibraryManager: PhotoLibraryManagerProtocol
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let appMediator: AppMediatorProtocol
private let actionsSubject: PassthroughSubject<TimelineMediaPreviewViewModelAction, Never> = .init() private let actionsSubject: PassthroughSubject<TimelineMediaPreviewViewModelAction, Never> = .init()
var actions: AnyPublisher<TimelineMediaPreviewViewModelAction, Never> { var actions: AnyPublisher<TimelineMediaPreviewViewModelAction, Never> {
@ -22,12 +24,14 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
init(context: TimelineMediaPreviewContext, init(context: TimelineMediaPreviewContext,
mediaProvider: MediaProviderProtocol, mediaProvider: MediaProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) { photoLibraryManager: PhotoLibraryManagerProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
appMediator: AppMediatorProtocol) {
timelineViewModel = context.viewModel timelineViewModel = context.viewModel
self.mediaProvider = mediaProvider self.mediaProvider = mediaProvider
self.photoLibraryManager = photoLibraryManager
// We might not want to inject this, instead creating a new instance with a custom position and colour scheme 🤔
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.appMediator = appMediator
let currentItem = TimelineMediaPreviewItem(timelineItem: context.item) let currentItem = TimelineMediaPreviewItem(timelineItem: context.item)
@ -64,10 +68,7 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
MXLog.error("Received unexpected action: \(action)") MXLog.error("Received unexpected action: \(action)")
} }
case .redactConfirmation(let item): case .redactConfirmation(let item):
timelineViewModel.context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: .redact)) redactItem(item)
state.bindings.redactConfirmationItem = nil
state.bindings.mediaDetailsItem = nil
actionsSubject.send(.dismiss)
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} }
@ -108,12 +109,43 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
} }
private func saveCurrentItem() async { private func saveCurrentItem() async {
guard let url = state.currentItem.fileHandle?.url else { guard let fileURL = state.currentItem.fileHandle?.url else {
MXLog.error("Unable to save an item without a URL, the button shouldn't be visible.") MXLog.error("Unable to save an item without a URL, the button shouldn't be visible.")
return return
} }
showErrorIndicator() do {
switch state.currentItem.timelineItem {
case is AudioRoomTimelineItem, is FileRoomTimelineItem:
state.bindings.fileToExport = .init(url: fileURL)
return // Don't show the indicator.
case is ImageRoomTimelineItem:
try await photoLibraryManager.addResource(.photo, at: fileURL).get()
case is VideoRoomTimelineItem:
try await photoLibraryManager.addResource(.video, at: fileURL).get()
default:
break
}
showSavedIndicator()
} catch PhotoLibraryManagerError.notAuthorized {
MXLog.error("Not authorised to save item to photo library")
state.bindings.alertInfo = .init(id: .authorizationRequired,
title: L10n.dialogPermissionPhotoLibraryTitleIos(InfoPlistReader.main.bundleDisplayName),
primaryButton: .init(title: L10n.commonSettings) { self.appMediator.openAppSettings() },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
} catch {
MXLog.error("Failed saving item: \(error)")
showErrorIndicator()
}
}
private func redactItem(_ item: TimelineMediaPreviewItem) {
timelineViewModel.context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: .redact))
state.bindings.redactConfirmationItem = nil
state.bindings.mediaDetailsItem = nil
actionsSubject.send(.dismiss)
showRedactedIndicator()
} }
// MARK: - Indicators // MARK: - Indicators
@ -132,22 +164,38 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
userIndicatorController.retractIndicatorWithId(indicatorID) userIndicatorController.retractIndicatorWithId(indicatorID)
} }
// FIXME: Add the strings and correct indicator types
private func showDownloadErrorIndicator() { private func showDownloadErrorIndicator() {
// FIXME: Add the correct string and indicator type??
userIndicatorController.submitIndicator(UserIndicator(id: downloadErrorIndicatorID, userIndicatorController.submitIndicator(UserIndicator(id: downloadErrorIndicatorID,
type: .modal, type: .modal,
title: L10n.errorUnknown, title: L10n.errorUnknown,
iconName: "exclamationmark.circle.fill")) iconName: "exclamationmark.circle.fill"))
} }
private func showRedactedIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
type: .toast,
title: "File deleted",
iconName: "checkmark"))
}
private func showSavedIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
type: .toast,
title: "File saved",
iconName: "checkmark"))
}
private func showErrorIndicator() { private func showErrorIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: errorIndicatorID, userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
type: .modal, type: .toast,
title: L10n.errorUnknown, title: L10n.errorUnknown,
iconName: "xmark")) iconName: "xmark"))
} }
private var errorIndicatorID: String { "\(Self.self)-Error" } private var statusIndicatorID: String { "\(Self.self)-Status" }
// Separate indicator IDs for downloads as these can be triggered in the background when swiping between items
private var downloadErrorIndicatorID: String { "\(Self.self)-DownloadError" } private var downloadErrorIndicatorID: String { "\(Self.self)-DownloadError" }
private func makeDownloadIndicatorID(itemID: TimelineItemIdentifier) -> String { private func makeDownloadIndicatorID(itemID: TimelineItemIdentifier) -> String {
"\(Self.self)-Download-\(itemID.uniqueID.id)" "\(Self.self)-Download-\(itemID.uniqueID.id)"

View File

@ -184,6 +184,8 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
viewModel: TimelineViewModel.mock(timelineKind: timelineKind), viewModel: TimelineViewModel.mock(timelineKind: timelineKind),
namespace: previewNamespace), namespace: previewNamespace),
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock()) photoLibraryManager: PhotoLibraryManagerMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
} }
} }

View File

@ -0,0 +1,23 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import SwiftUI
struct TimelineMediaPreviewFileExportPicker: UIViewControllerRepresentable {
struct File: Identifiable {
let url: URL
var id: String { url.absoluteString }
}
let file: File
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
UIDocumentPickerViewController(forExporting: [file.url], asCopy: true)
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { }
}

View File

@ -142,6 +142,8 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
viewModel: TimelineViewModel.mock, viewModel: TimelineViewModel.mock,
namespace: previewNamespace), namespace: previewNamespace),
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock()) photoLibraryManager: PhotoLibraryManagerMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
} }
} }

View File

@ -10,7 +10,7 @@ import Compound
import QuickLook import QuickLook
import SwiftUI import SwiftUI
struct TimelineMediaPreviewView: View { struct TimelineMediaPreviewScreen: View {
@ObservedObject var context: TimelineMediaPreviewViewModel.Context @ObservedObject var context: TimelineMediaPreviewViewModel.Context
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem } private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
@ -33,6 +33,11 @@ struct TimelineMediaPreviewView: View {
.sheet(item: $context.mediaDetailsItem) { item in .sheet(item: $context.mediaDetailsItem) { item in
TimelineMediaPreviewDetailsView(item: item, context: context) TimelineMediaPreviewDetailsView(item: item, context: context)
} }
.sheet(item: $context.fileToExport) { file in
TimelineMediaPreviewFileExportPicker(file: file)
.preferredColorScheme(.dark)
}
.alert(item: $context.alertInfo)
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
.zoomTransition(sourceID: currentItem.id, in: context.viewState.transitionNamespace) .zoomTransition(sourceID: currentItem.id, in: context.viewState.transitionNamespace)
} }
@ -102,7 +107,7 @@ struct TimelineMediaPreviewView: View {
Spacer() Spacer()
Button { context.send(viewAction: .saveCurrentItem) } label: { Button { context.send(viewAction: .saveCurrentItem) } label: {
CompoundIcon(\.download) CompoundIcon(\.downloadIos)
} }
} }
} }
@ -179,13 +184,13 @@ private struct QuickLookView: UIViewControllerRepresentable {
// MARK: - Previews // MARK: - Previews
struct TimelineMediaPreviewView_Previews: PreviewProvider { struct TimelineMediaPreviewScreen_Previews: PreviewProvider {
@Namespace private static var namespace @Namespace private static var namespace
static let viewModel = makeViewModel() static let viewModel = makeViewModel()
static var previews: some View { static var previews: some View {
QuickLookView(viewModelContext: viewModel.context) TimelineMediaPreviewScreen(context: viewModel.context)
} }
static func makeViewModel() -> TimelineMediaPreviewViewModel { static func makeViewModel() -> TimelineMediaPreviewViewModel {
@ -207,6 +212,8 @@ struct TimelineMediaPreviewView_Previews: PreviewProvider {
viewModel: TimelineViewModel.mock(timelineKind: .media(.mediaFilesScreen)), viewModel: TimelineViewModel.mock(timelineKind: .media(.mediaFilesScreen)),
namespace: namespace), namespace: namespace),
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock()) photoLibraryManager: PhotoLibraryManagerMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
} }
} }

View File

@ -16,7 +16,7 @@ struct CallInviteRoomTimelineView: View {
Label { Label {
Text(L10n.screenRoomTimelineLegacyCall) Text(L10n.screenRoomTimelineLegacyCall)
} icon: { } icon: {
CompoundIcon(\.voiceCall, size: .medium, relativeTo: .compound.bodyMD) CompoundIcon(\.voiceCallSolid, size: .medium, relativeTo: .compound.bodyMD)
} }
.font(.compound.bodyMD) .font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary) .foregroundColor(.compound.textSecondary)

View File

@ -73,7 +73,7 @@
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>To record and send messages with audio, $(APP_DISPLAY_NAME) needs to access the microphone.</string> <string>To record and send messages with audio, $(APP_DISPLAY_NAME) needs to access the microphone.</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>Allows saving photos and videos to your library.</string> <string>This lets you save images and videos to your photo library.</string>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>INSendMessageIntent</string> <string>INSendMessageIntent</string>

View File

@ -87,7 +87,7 @@ targets:
] ]
NSCameraUsageDescription: To take pictures or videos and send them as a message $(APP_DISPLAY_NAME) needs access to the camera. NSCameraUsageDescription: To take pictures or videos and send them as a message $(APP_DISPLAY_NAME) needs access to the camera.
NSMicrophoneUsageDescription: To record and send messages with audio, $(APP_DISPLAY_NAME) needs to access the microphone. NSMicrophoneUsageDescription: To record and send messages with audio, $(APP_DISPLAY_NAME) needs to access the microphone.
NSPhotoLibraryAddUsageDescription: Allows saving photos and videos to your library. NSPhotoLibraryAddUsageDescription: This lets you save images and videos to your photo library.
NSLocationWhenInUseUsageDescription: Grant location access so that $(APP_DISPLAY_NAME) can share your location. NSLocationWhenInUseUsageDescription: Grant location access so that $(APP_DISPLAY_NAME) can share your location.
NSFaceIDUsageDescription: Face ID is used to access your app. NSFaceIDUsageDescription: Face ID is used to access your app.
UIBackgroundModes: [ UIBackgroundModes: [

View File

@ -9,4 +9,4 @@ output:
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift ../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
args: args:
automMockableTestableImports: [] automMockableTestableImports: []
autoMockableImports: [AnalyticsEvents, AVFoundation, Combine, Foundation, LocalAuthentication, MatrixRustSDK, SwiftUI] autoMockableImports: [AnalyticsEvents, AVFoundation, Combine, Foundation, LocalAuthentication, MatrixRustSDK, Photos, SwiftUI]

View File

@ -17,6 +17,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
var viewModel: TimelineMediaPreviewViewModel! var viewModel: TimelineMediaPreviewViewModel!
var context: TimelineMediaPreviewViewModel.Context { viewModel.context } var context: TimelineMediaPreviewViewModel.Context { viewModel.context }
var mediaProvider: MediaProviderMock! var mediaProvider: MediaProviderMock!
var photoLibraryManager: PhotoLibraryManagerMock!
var timelineController: MockRoomTimelineController! var timelineController: MockRoomTimelineController!
func testLoadingItem() async throws { func testLoadingItem() async throws {
@ -27,9 +28,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
XCTAssertNotNil(context.viewState.currentItemActions) XCTAssertNotNil(context.viewState.currentItemActions)
// When the preview controller sets the current item. // When the preview controller sets the current item.
let deferred = deferFulfillment(viewModel.state.fileLoadedPublisher) { _ in true } try await loadInitialItem()
context.send(viewAction: .updateCurrentItem(context.viewState.previewItems[0]))
try await deferred.fulfill()
// Then the view model should load the item and update its view state. // Then the view model should load the item and update its view state.
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled) XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
@ -82,6 +81,73 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
XCTAssertTrue(timelineController.redactCalled) XCTAssertTrue(timelineController.redactCalled)
} }
func testSaveImage() async throws {
// Given a view model with a loaded image.
try await testLoadingItem()
XCTAssertEqual(viewModel.state.currentItem.contentType, "JPEG image")
// When choosing to save the image.
let item = context.viewState.currentItem
context.send(viewAction: .saveCurrentItem)
try await Task.sleep(for: .seconds(0.5))
// Then the image should be saved as a photo to the user's photo library.
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .photo)
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, item.fileHandle?.url)
}
func testSaveImageWithoutAuthorization() async throws {
// Given a view model with a loaded image where the user has denied access to the photo library.
setupViewModel(photoLibraryAuthorizationDenied: true)
try await loadInitialItem()
XCTAssertEqual(viewModel.state.currentItem.contentType, "JPEG image")
// When choosing to save the image.
let item = context.viewState.currentItem
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .saveCurrentItem)
try await deferred.fulfill()
// Then the user should be prompted to allow access.
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
XCTAssertEqual(context.alertInfo?.id, .authorizationRequired)
}
func testSaveVideo() async throws {
// Given a view model with a loaded video.
setupViewModel(initialItemIndex: 1)
try await loadInitialItem()
XCTAssertEqual(viewModel.state.currentItem.contentType, "MPEG-4 movie")
// When choosing to save the video.
let item = context.viewState.currentItem
context.send(viewAction: .saveCurrentItem)
try await Task.sleep(for: .seconds(0.5))
// Then the video should be saved as a video in the user's photo library.
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .video)
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, item.fileHandle?.url)
}
func testSaveFile() async throws {
// Given a view model with a loaded file.
setupViewModel(initialItemIndex: 2)
try await loadInitialItem()
XCTAssertEqual(viewModel.state.currentItem.contentType, "PDF document")
// When choosing to save the file.
let item = context.viewState.currentItem
context.send(viewAction: .saveCurrentItem)
try await Task.sleep(for: .seconds(0.5))
// Then the binding should be set for the user to export the file to their specified location.
XCTAssertFalse(photoLibraryManager.addResourceAtCalled)
XCTAssertNotNil(context.fileToExport)
XCTAssertEqual(context.fileToExport?.url, item.fileHandle?.url)
}
func testDismiss() async throws { func testDismiss() async throws {
// Given a view model with a loaded item. // Given a view model with a loaded item.
try await testLoadingItem() try await testLoadingItem()
@ -96,30 +162,66 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
// MARK: - Helpers // MARK: - Helpers
private func loadInitialItem() async throws {
let deferred = deferFulfillment(viewModel.state.fileLoadedPublisher) { _ in true }
context.send(viewAction: .updateCurrentItem(context.viewState.previewItems[0]))
try await deferred.fulfill()
}
@Namespace private var testNamespace @Namespace private var testNamespace
private func setupViewModel() { private func setupViewModel(initialItemIndex: Int = 0, photoLibraryAuthorizationDenied: Bool = false) {
let item = ImageRoomTimelineItem(id: .randomEvent,
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Sally Sanderson"),
content: .init(filename: "Amazing image.jpeg",
caption: "A caption goes right here.",
imageInfo: .mockImage,
thumbnailInfo: .mockThumbnail))
timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen)) timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
timelineController.timelineItems = [item] timelineController.timelineItems = items
mediaProvider = MediaProviderMock(configuration: .init()) mediaProvider = MediaProviderMock(configuration: .init())
viewModel = TimelineMediaPreviewViewModel(context: .init(item: item, photoLibraryManager = PhotoLibraryManagerMock(.init(authorizationDenied: photoLibraryAuthorizationDenied))
viewModel = TimelineMediaPreviewViewModel(context: .init(item: items[initialItemIndex],
viewModel: TimelineViewModel.mock(timelineKind: .media(.mediaFilesScreen), viewModel: TimelineViewModel.mock(timelineKind: .media(.mediaFilesScreen),
timelineController: timelineController), timelineController: timelineController),
namespace: testNamespace), namespace: testNamespace),
mediaProvider: mediaProvider, mediaProvider: mediaProvider,
userIndicatorController: UserIndicatorControllerMock()) photoLibraryManager: photoLibraryManager,
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock())
} }
private let items: [EventBasedMessageTimelineItemProtocol] = [
ImageRoomTimelineItem(id: .randomEvent,
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Sally Sanderson"),
content: .init(filename: "Amazing image.jpeg",
caption: "A caption goes right here.",
imageInfo: .mockImage,
thumbnailInfo: .mockThumbnail,
contentType: .jpeg)),
VideoRoomTimelineItem(id: .randomEvent,
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(filename: "Super video.mp4",
videoInfo: .mockVideo,
thumbnailInfo: .mockThumbnail,
contentType: .mpeg4Movie)),
FileRoomTimelineItem(id: .randomEvent,
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(filename: "Important file.pdf",
source: try? .init(url: .mockMXCFile, mimeType: "document/pdf"),
fileSize: 2453,
thumbnailSource: nil,
contentType: .pdf))
]
} }

View File

@ -65,7 +65,7 @@ packages:
# path: ../matrix-rust-sdk # path: ../matrix-rust-sdk
Compound: Compound:
url: https://github.com/element-hq/compound-ios url: https://github.com/element-hq/compound-ios
revision: 1a70bc7f3420647843b9c18748982c61ef7d2245 revision: 9325643cb4d22150881c5bf79e1e6e3c5a87ea89
# path: ../compound-ios # path: ../compound-ios
AnalyticsEvents: AnalyticsEvents:
url: https://github.com/matrix-org/matrix-analytics-events url: https://github.com/matrix-org/matrix-analytics-events