mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
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:
parent
2b82b94500
commit
2a865ce5bf
@ -233,6 +233,7 @@
|
||||
2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; };
|
||||
2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.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 */; };
|
||||
2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.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 */; };
|
||||
4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.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 */; };
|
||||
4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.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 */; };
|
||||
5F35069E13D71DD88633A4B2 /* preview_video.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 45A4B934BA41D6C255900265 /* preview_video.jpg */; };
|
||||
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 */; };
|
||||
601AB75BD52B0B4276CEB84A /* SessionVerificationScreenStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */; };
|
||||
60ED66E63A169E47489348A8 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; };
|
||||
@ -760,6 +763,7 @@
|
||||
97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */; };
|
||||
97BAEDD9054FB5F233EE928B /* EncryptionResetScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.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 */; };
|
||||
9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.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 */; };
|
||||
AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.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 */; };
|
||||
AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.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 */; };
|
||||
FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.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 */; };
|
||||
FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1390,7 +1391,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1569,6 +1569,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2115,6 +2116,7 @@
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
@ -2362,6 +2364,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2408,6 +2411,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -3171,6 +3175,7 @@
|
||||
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */,
|
||||
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */,
|
||||
B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */,
|
||||
DD955A0380C287C418F1A74D /* PhotoLibraryManagerMock.swift */,
|
||||
D38391154120264910D19528 /* PollMock.swift */,
|
||||
894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */,
|
||||
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */,
|
||||
@ -3492,10 +3497,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */,
|
||||
0D3615FD6460BC9C1DEF8659 /* MediaFileManager.swift */,
|
||||
E7495E1119753B06FF2C2279 /* PhotoLibraryManager.swift */,
|
||||
E3A62FBD3007312311C14DD8 /* TimelineMediaPreviewCoordinator.swift */,
|
||||
2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */,
|
||||
0A76B92984638A9B3104840D /* TimelineMediaPreviewView.swift */,
|
||||
53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */,
|
||||
5EC4A8482DA110602FE6DF42 /* View */,
|
||||
);
|
||||
@ -3889,7 +3893,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */,
|
||||
30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */,
|
||||
C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */,
|
||||
AA57E8563C346B13DDE4A6F4 /* TimelineMediaPreviewScreen.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@ -7095,7 +7101,6 @@
|
||||
C11D4A49DC29D89CE2BB31B8 /* MediaEventsTimelineScreenViewModel.swift in Sources */,
|
||||
FD9777315A5D9CDC47458AD1 /* MediaEventsTimelineScreenViewModelProtocol.swift in Sources */,
|
||||
BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */,
|
||||
FAF12EF424E55377816149DB /* MediaFileManager.swift in Sources */,
|
||||
208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */,
|
||||
A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */,
|
||||
4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */,
|
||||
@ -7173,6 +7178,8 @@
|
||||
847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */,
|
||||
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */,
|
||||
BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */,
|
||||
5FA1DCE55973862632961D7C /* PhotoLibraryManager.swift in Sources */,
|
||||
2D38D39B1789B91AE69F477F /* PhotoLibraryManagerMock.swift in Sources */,
|
||||
962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */,
|
||||
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */,
|
||||
899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */,
|
||||
@ -7491,9 +7498,10 @@
|
||||
562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */,
|
||||
FE43747C116CA3D8D6B92F5F /* TimelineMediaPreviewCoordinator.swift in Sources */,
|
||||
12EC6BC99F373FE5C6EB9B64 /* TimelineMediaPreviewDetailsView.swift in Sources */,
|
||||
4ED764A24F2A715C25CF07F1 /* TimelineMediaPreviewFileExportPicker.swift in Sources */,
|
||||
77FB08C303F4C74C0E8577E2 /* TimelineMediaPreviewModels.swift in Sources */,
|
||||
A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */,
|
||||
AE69B349B0011D5EE2C13606 /* TimelineMediaPreviewView.swift in Sources */,
|
||||
9826A4DBBEFA7041A9E0EFAD /* TimelineMediaPreviewScreen.swift in Sources */,
|
||||
86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */,
|
||||
B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */,
|
||||
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */,
|
||||
@ -8539,7 +8547,7 @@
|
||||
repositoryURL = "https://github.com/element-hq/compound-ios";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = 1a70bc7f3420647843b9c18748982c61ef7d2245;
|
||||
revision = 9325643cb4d22150881c5bf79e1e6e3c5a87ea89;
|
||||
};
|
||||
};
|
||||
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||
|
@ -6,8 +6,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/compound-design-tokens",
|
||||
"state" : {
|
||||
"revision" : "f79e05011ec3402c29ded19bcff95b5ead180991",
|
||||
"version" : "2.1.2"
|
||||
"revision" : "a6e96fb4436a4945423a8c068001093af4b7b315",
|
||||
"version" : "3.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -15,7 +15,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/compound-ios",
|
||||
"state" : {
|
||||
"revision" : "1a70bc7f3420647843b9c18748982c61ef7d2245"
|
||||
"revision" : "9325643cb4d22150881c5bf79e1e6e3c5a87ea89"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -2,4 +2,4 @@
|
||||
"NSFaceIDUsageDescription" = "Face ID is used to access your app.";
|
||||
"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.";
|
||||
"NSPhotoLibraryUsageDescription" = "Allows saving photos and videos to your library.";
|
||||
"NSPhotoLibraryUsageDescription" = "This lets you save images and videos to your photo library.";
|
||||
|
@ -274,6 +274,7 @@
|
||||
"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_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_warning" = "Warning";
|
||||
"dialog_unsaved_changes_description_ios" = "Your changes won’t be saved";
|
||||
|
@ -109,7 +109,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private func presentMediaPreview(for previewContext: TimelineMediaPreviewContext) {
|
||||
let parameters = TimelineMediaPreviewCoordinatorParameters(context: previewContext,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
userIndicatorController: userIndicatorController)
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
|
||||
let coordinator = TimelineMediaPreviewCoordinator(parameters: parameters)
|
||||
coordinator.actionsPublisher
|
||||
|
@ -606,6 +606,10 @@ internal enum L10n {
|
||||
}
|
||||
/// 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") }
|
||||
/// %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
|
||||
internal static var dialogTitleConfirmation: String { return L10n.tr("Localizable", "dialog_title_confirmation") }
|
||||
/// Error
|
||||
|
@ -8,6 +8,7 @@ import Combine
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
import MatrixRustSDK
|
||||
import Photos
|
||||
import SwiftUI
|
||||
class AnalyticsClientMock: AnalyticsClientProtocol {
|
||||
var isRunning: Bool {
|
||||
@ -12327,6 +12328,79 @@ class PHGPostHogMock: PHGPostHogProtocol {
|
||||
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 {
|
||||
|
||||
//MARK: - sendPollResponse
|
||||
|
21
ElementX/Sources/Mocks/PhotoLibraryManagerMock.swift
Normal file
21
ElementX/Sources/Mocks/PhotoLibraryManagerMock.swift
Normal 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(())
|
||||
}
|
||||
}
|
@ -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
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ struct TimelineMediaPreviewCoordinatorParameters {
|
||||
let context: TimelineMediaPreviewContext
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
}
|
||||
|
||||
enum TimelineMediaPreviewCoordinatorAction {
|
||||
@ -50,7 +51,9 @@ final class TimelineMediaPreviewCoordinator: CoordinatorProtocol {
|
||||
|
||||
viewModel = TimelineMediaPreviewViewModel(context: parameters.context,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
photoLibraryManager: PhotoLibraryManager(),
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appMediator: parameters.appMediator)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@ -69,6 +72,6 @@ final class TimelineMediaPreviewCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(TimelineMediaPreviewView(context: viewModel.context))
|
||||
AnyView(TimelineMediaPreviewScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,18 @@ struct TimelineMediaPreviewViewState: BindableState {
|
||||
}
|
||||
|
||||
struct TimelineMediaPreviewViewStateBindings {
|
||||
/// A binding that will present the Details view for the specified item.
|
||||
var mediaDetailsItem: TimelineMediaPreviewItem?
|
||||
/// A binding that will present a confirmation to redact the specified item.
|
||||
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.
|
||||
|
@ -13,7 +13,9 @@ typealias TimelineMediaPreviewViewModelType = StateStoreViewModel<TimelineMediaP
|
||||
class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
private let timelineViewModel: TimelineViewModelProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let photoLibraryManager: PhotoLibraryManagerProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appMediator: AppMediatorProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<TimelineMediaPreviewViewModelAction, Never> = .init()
|
||||
var actions: AnyPublisher<TimelineMediaPreviewViewModelAction, Never> {
|
||||
@ -22,12 +24,14 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
|
||||
init(context: TimelineMediaPreviewContext,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
photoLibraryManager: PhotoLibraryManagerProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appMediator: AppMediatorProtocol) {
|
||||
timelineViewModel = context.viewModel
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
// We might not want to inject this, instead creating a new instance with a custom position and colour scheme 🤔
|
||||
self.photoLibraryManager = photoLibraryManager
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appMediator = appMediator
|
||||
|
||||
let currentItem = TimelineMediaPreviewItem(timelineItem: context.item)
|
||||
|
||||
@ -64,10 +68,7 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
MXLog.error("Received unexpected action: \(action)")
|
||||
}
|
||||
case .redactConfirmation(let item):
|
||||
timelineViewModel.context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: .redact))
|
||||
state.bindings.redactConfirmationItem = nil
|
||||
state.bindings.mediaDetailsItem = nil
|
||||
actionsSubject.send(.dismiss)
|
||||
redactItem(item)
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
@ -108,12 +109,43 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
}
|
||||
|
||||
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.")
|
||||
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
|
||||
@ -132,22 +164,38 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
userIndicatorController.retractIndicatorWithId(indicatorID)
|
||||
}
|
||||
|
||||
// FIXME: Add the strings and correct indicator types
|
||||
private func showDownloadErrorIndicator() {
|
||||
// FIXME: Add the correct string and indicator type??
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: downloadErrorIndicatorID,
|
||||
type: .modal,
|
||||
title: L10n.errorUnknown,
|
||||
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() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: errorIndicatorID,
|
||||
type: .modal,
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
|
||||
type: .toast,
|
||||
title: L10n.errorUnknown,
|
||||
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 func makeDownloadIndicatorID(itemID: TimelineItemIdentifier) -> String {
|
||||
"\(Self.self)-Download-\(itemID.uniqueID.id)"
|
||||
|
@ -184,6 +184,8 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
||||
viewModel: TimelineViewModel.mock(timelineKind: timelineKind),
|
||||
namespace: previewNamespace),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
photoLibraryManager: PhotoLibraryManagerMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appMediator: AppMediatorMock())
|
||||
}
|
||||
}
|
||||
|
@ -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) { }
|
||||
}
|
@ -142,6 +142,8 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
|
||||
viewModel: TimelineViewModel.mock,
|
||||
namespace: previewNamespace),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
photoLibraryManager: PhotoLibraryManagerMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appMediator: AppMediatorMock())
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import Compound
|
||||
import QuickLook
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineMediaPreviewView: View {
|
||||
struct TimelineMediaPreviewScreen: View {
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
|
||||
@ -33,6 +33,11 @@ struct TimelineMediaPreviewView: View {
|
||||
.sheet(item: $context.mediaDetailsItem) { item in
|
||||
TimelineMediaPreviewDetailsView(item: item, context: context)
|
||||
}
|
||||
.sheet(item: $context.fileToExport) { file in
|
||||
TimelineMediaPreviewFileExportPicker(file: file)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
.alert(item: $context.alertInfo)
|
||||
.preferredColorScheme(.dark)
|
||||
.zoomTransition(sourceID: currentItem.id, in: context.viewState.transitionNamespace)
|
||||
}
|
||||
@ -102,7 +107,7 @@ struct TimelineMediaPreviewView: View {
|
||||
Spacer()
|
||||
|
||||
Button { context.send(viewAction: .saveCurrentItem) } label: {
|
||||
CompoundIcon(\.download)
|
||||
CompoundIcon(\.downloadIos)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,13 +184,13 @@ private struct QuickLookView: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct TimelineMediaPreviewView_Previews: PreviewProvider {
|
||||
struct TimelineMediaPreviewScreen_Previews: PreviewProvider {
|
||||
@Namespace private static var namespace
|
||||
|
||||
static let viewModel = makeViewModel()
|
||||
|
||||
static var previews: some View {
|
||||
QuickLookView(viewModelContext: viewModel.context)
|
||||
TimelineMediaPreviewScreen(context: viewModel.context)
|
||||
}
|
||||
|
||||
static func makeViewModel() -> TimelineMediaPreviewViewModel {
|
||||
@ -207,6 +212,8 @@ struct TimelineMediaPreviewView_Previews: PreviewProvider {
|
||||
viewModel: TimelineViewModel.mock(timelineKind: .media(.mediaFilesScreen)),
|
||||
namespace: namespace),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
photoLibraryManager: PhotoLibraryManagerMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appMediator: AppMediatorMock())
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ struct CallInviteRoomTimelineView: View {
|
||||
Label {
|
||||
Text(L10n.screenRoomTimelineLegacyCall)
|
||||
} icon: {
|
||||
CompoundIcon(\.voiceCall, size: .medium, relativeTo: .compound.bodyMD)
|
||||
CompoundIcon(\.voiceCallSolid, size: .medium, relativeTo: .compound.bodyMD)
|
||||
}
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
|
@ -73,7 +73,7 @@
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>To record and send messages with audio, $(APP_DISPLAY_NAME) needs to access the microphone.</string>
|
||||
<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>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
|
@ -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.
|
||||
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.
|
||||
NSFaceIDUsageDescription: Face ID is used to access your app.
|
||||
UIBackgroundModes: [
|
||||
|
@ -9,4 +9,4 @@ output:
|
||||
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
|
||||
args:
|
||||
automMockableTestableImports: []
|
||||
autoMockableImports: [AnalyticsEvents, AVFoundation, Combine, Foundation, LocalAuthentication, MatrixRustSDK, SwiftUI]
|
||||
autoMockableImports: [AnalyticsEvents, AVFoundation, Combine, Foundation, LocalAuthentication, MatrixRustSDK, Photos, SwiftUI]
|
||||
|
@ -17,6 +17,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
var viewModel: TimelineMediaPreviewViewModel!
|
||||
var context: TimelineMediaPreviewViewModel.Context { viewModel.context }
|
||||
var mediaProvider: MediaProviderMock!
|
||||
var photoLibraryManager: PhotoLibraryManagerMock!
|
||||
var timelineController: MockRoomTimelineController!
|
||||
|
||||
func testLoadingItem() async throws {
|
||||
@ -27,9 +28,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
XCTAssertNotNil(context.viewState.currentItemActions)
|
||||
|
||||
// When the preview controller sets the current item.
|
||||
let deferred = deferFulfillment(viewModel.state.fileLoadedPublisher) { _ in true }
|
||||
context.send(viewAction: .updateCurrentItem(context.viewState.previewItems[0]))
|
||||
try await deferred.fulfill()
|
||||
try await loadInitialItem()
|
||||
|
||||
// Then the view model should load the item and update its view state.
|
||||
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
@ -82,6 +81,73 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
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 {
|
||||
// Given a view model with a loaded item.
|
||||
try await testLoadingItem()
|
||||
@ -96,30 +162,66 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// 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
|
||||
|
||||
private func setupViewModel() {
|
||||
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))
|
||||
|
||||
private func setupViewModel(initialItemIndex: Int = 0, photoLibraryAuthorizationDenied: Bool = false) {
|
||||
timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
|
||||
timelineController.timelineItems = [item]
|
||||
timelineController.timelineItems = items
|
||||
|
||||
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),
|
||||
timelineController: timelineController),
|
||||
namespace: testNamespace),
|
||||
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))
|
||||
]
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ packages:
|
||||
# path: ../matrix-rust-sdk
|
||||
Compound:
|
||||
url: https://github.com/element-hq/compound-ios
|
||||
revision: 1a70bc7f3420647843b9c18748982c61ef7d2245
|
||||
revision: 9325643cb4d22150881c5bf79e1e6e3c5a87ea89
|
||||
# path: ../compound-ios
|
||||
AnalyticsEvents:
|
||||
url: https://github.com/matrix-org/matrix-analytics-events
|
||||
|
Loading…
x
Reference in New Issue
Block a user