mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
Update the timeline media QuickLook modifier. (#3593)
Not hooked up to any flows yet.
This commit is contained in:
parent
7254b6e029
commit
b5605a52e3
@ -105,6 +105,7 @@
|
||||
12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; };
|
||||
12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */; };
|
||||
12E6D052D055531A6783E21B /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */; };
|
||||
12EC6BC99F373FE5C6EB9B64 /* TimelineMediaPreviewDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */; };
|
||||
1307268DC41730E5BCF7D9A0 /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638790D3F915F0909315C47A /* PollView.swift */; };
|
||||
1318721F4E5F307586D98112 /* VoiceMessageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */; };
|
||||
13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; };
|
||||
@ -575,6 +576,7 @@
|
||||
77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */; };
|
||||
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; };
|
||||
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; };
|
||||
77FB08C303F4C74C0E8577E2 /* TimelineMediaPreviewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */; };
|
||||
7807B1DEE32617896886A8E5 /* FormattingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E6FAA3719E9B7A2D5510B /* FormattingToolbar.swift */; };
|
||||
784592335560C2E91D32D177 /* DeveloperOptionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B098A612DCB5A7358EECD5 /* DeveloperOptionsScreenModels.swift */; };
|
||||
785613C0C092B532198EB3BB /* TimelineStartRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44ECC9D66400727DFFEE12E8 /* TimelineStartRoomTimelineView.swift */; };
|
||||
@ -650,6 +652,7 @@
|
||||
8658F5034EAD7357CE7F9AC7 /* MatrixUserShareLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */; };
|
||||
865DD5CA474C6AE6C2BC008E /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; };
|
||||
86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */; };
|
||||
86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */; };
|
||||
8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; };
|
||||
86DFA58FBBEB0AF671D2A1E1 /* HomeScreenKnockedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */; };
|
||||
86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */; };
|
||||
@ -793,6 +796,7 @@
|
||||
A2172B5A26976F9174228B8A /* AppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */; };
|
||||
A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
|
||||
A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
|
||||
A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */; };
|
||||
A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
|
||||
A36AD251013402EDBD666C75 /* AppMediatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAC027034248429A438886B /* AppMediatorMock.swift */; };
|
||||
A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; };
|
||||
@ -931,6 +935,7 @@
|
||||
C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; };
|
||||
C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */; };
|
||||
C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 622D09D4ECE759189009AEAF /* MapLibreMapView.swift */; };
|
||||
C20ADD95496C3328E8E22F36 /* TimelineMediaQuickLook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D1DF63E340E81E45EAA68D /* TimelineMediaQuickLook.swift */; };
|
||||
C26DB49C06C00B5DF1A991A5 /* InviteUsersScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */; };
|
||||
C2879369106A419A5071F1F8 /* VoiceMessageRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */; };
|
||||
C32765D740C81AD4C42E8F50 /* CreateRoomFlowParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935C2FB18EFB8EEE96B26330 /* CreateRoomFlowParameters.swift */; };
|
||||
@ -951,6 +956,7 @@
|
||||
C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */; };
|
||||
C58E305C380D3ADDF7912180 /* StickerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */; };
|
||||
C5A07E2D88BE7D51DCECD166 /* LoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */; };
|
||||
C62B99D50A5A9BFD504B6774 /* TimelineMediaPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626271BD45A5CE50A4567C89 /* TimelineMediaPreviewController.swift */; };
|
||||
C67FCC854F3A6FC7A2EC04D0 /* MediaUploadPreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */; };
|
||||
C6C06DDA8881260303FBA3A0 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; };
|
||||
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; };
|
||||
@ -1174,6 +1180,7 @@
|
||||
F66BBBE51B258BBB0B918C68 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C79D91A7F9F378CECEF64B5A /* MatrixRustSDK */; };
|
||||
F66BCCC825D6CA51724A94D0 /* MediaPlayerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */; };
|
||||
F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */; };
|
||||
F6BF52CB027393EE03CEC523 /* TimelineMediaPreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */; };
|
||||
F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; };
|
||||
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
|
||||
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; };
|
||||
@ -1501,6 +1508,7 @@
|
||||
2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationFlowCoordinatorUITests.swift; sourceTree = "<group>"; };
|
||||
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = "<group>"; };
|
||||
2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewModels.swift; sourceTree = "<group>"; };
|
||||
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
|
||||
2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = "<group>"; };
|
||||
2AC3FDB58F57386741A4FC7F /* DeactivateAccountScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
@ -1633,6 +1641,7 @@
|
||||
45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterProtocol.swift; sourceTree = "<group>"; };
|
||||
4629710C0337ADD9C8909542 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenModels.swift; sourceTree = "<group>"; };
|
||||
467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewDetailsView.swift; sourceTree = "<group>"; };
|
||||
4691B8DE1D51DE152680098A /* HomeScreenSlidingSyncMigrationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenSlidingSyncMigrationBanner.swift; sourceTree = "<group>"; };
|
||||
46A2AD86F7E618F468F6FAF5 /* VoiceMessageRecordingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingButton.swift; sourceTree = "<group>"; };
|
||||
46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = "<group>"; };
|
||||
@ -1656,6 +1665,7 @@
|
||||
4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
||||
4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenModels.swift; sourceTree = "<group>"; };
|
||||
49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetLabelStyle.swift; sourceTree = "<group>"; };
|
||||
49D1DF63E340E81E45EAA68D /* TimelineMediaQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaQuickLook.swift; sourceTree = "<group>"; };
|
||||
49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
|
||||
@ -1698,6 +1708,7 @@
|
||||
5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
536C0E2178949B290776EA4E /* QRCodeLoginServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
536E72DCBEEC4A1FE66CFDCE /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
|
||||
53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewViewModel.swift; sourceTree = "<group>"; };
|
||||
53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = test_animated_image.gif; sourceTree = "<group>"; };
|
||||
542D4F49FABA056DEEEB3400 /* RustTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = "<group>"; };
|
||||
5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
||||
@ -1732,6 +1743,7 @@
|
||||
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewViewModelTests.swift; sourceTree = "<group>"; };
|
||||
5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5CEEAE1BFAACD6C96B6DB731 /* PHGPostHogProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogProtocol.swift; sourceTree = "<group>"; };
|
||||
5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = "<group>"; };
|
||||
@ -1754,6 +1766,7 @@
|
||||
61B33F23681660E940BA57F4 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||
622D09D4ECE759189009AEAF /* MapLibreMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreMapView.swift; sourceTree = "<group>"; };
|
||||
624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
626271BD45A5CE50A4567C89 /* TimelineMediaPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewController.swift; sourceTree = "<group>"; };
|
||||
627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListEmptyStateView.swift; sourceTree = "<group>"; };
|
||||
62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderTests.swift; sourceTree = "<group>"; };
|
||||
62B07B296D7A9D2F09120853 /* OrderedSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSet.swift; sourceTree = "<group>"; };
|
||||
@ -2215,6 +2228,7 @@
|
||||
C715CFE00686DACA59D836EA /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||
C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenModels.swift; sourceTree = "<group>"; };
|
||||
C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewRedactConfirmationView.swift; sourceTree = "<group>"; };
|
||||
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = "<group>"; };
|
||||
C7D851A10FDA55579960DC61 /* WebRegistrationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
|
||||
@ -3428,6 +3442,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */,
|
||||
626271BD45A5CE50A4567C89 /* TimelineMediaPreviewController.swift */,
|
||||
2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */,
|
||||
53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */,
|
||||
49D1DF63E340E81E45EAA68D /* TimelineMediaQuickLook.swift */,
|
||||
5EC4A8482DA110602FE6DF42 /* View */,
|
||||
);
|
||||
path = FilePreviewScreen;
|
||||
sourceTree = "<group>";
|
||||
@ -3801,6 +3820,15 @@
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5EC4A8482DA110602FE6DF42 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */,
|
||||
C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5F6CB68B44F6C587E463A934 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -4107,6 +4135,7 @@
|
||||
2CEBCB9676FCD1D0F13188DD /* StringTests.swift */,
|
||||
2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */,
|
||||
9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */,
|
||||
5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */,
|
||||
6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */,
|
||||
1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */,
|
||||
76310030C831D4610A705603 /* URLComponentsTests.swift */,
|
||||
@ -6550,6 +6579,7 @@
|
||||
E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */,
|
||||
3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */,
|
||||
0D4EB2ABAA5FE8CB10FDBCB8 /* TimelineItemFactoryTests.swift in Sources */,
|
||||
F6BF52CB027393EE03CEC523 /* TimelineMediaPreviewViewModelTests.swift in Sources */,
|
||||
2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */,
|
||||
282A5F3375DDC774AE09B0C3 /* TracingConfigurationTests.swift in Sources */,
|
||||
8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */,
|
||||
@ -7358,6 +7388,12 @@
|
||||
1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */,
|
||||
EFBBD44C0A16F017C32D2099 /* TimelineItemStatusView.swift in Sources */,
|
||||
562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */,
|
||||
C62B99D50A5A9BFD504B6774 /* TimelineMediaPreviewController.swift in Sources */,
|
||||
12EC6BC99F373FE5C6EB9B64 /* TimelineMediaPreviewDetailsView.swift in Sources */,
|
||||
77FB08C303F4C74C0E8577E2 /* TimelineMediaPreviewModels.swift in Sources */,
|
||||
A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */,
|
||||
86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */,
|
||||
C20ADD95496C3328E8E22F36 /* TimelineMediaQuickLook.swift in Sources */,
|
||||
B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */,
|
||||
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */,
|
||||
16A1F6C703305FCAF4E14EC6 /* TimelineProxyMock.swift in Sources */,
|
||||
|
@ -142,6 +142,7 @@
|
||||
"common_developer_options" = "Developer options";
|
||||
"common_device_id" = "Device ID";
|
||||
"common_direct_chat" = "Direct chat";
|
||||
"common_downloading" = "Downloading";
|
||||
"common_edited_suffix" = "(edited)";
|
||||
"common_editing" = "Editing";
|
||||
"common_editing_caption" = "Editing caption";
|
||||
@ -389,6 +390,8 @@
|
||||
"screen_knock_requests_list_title" = "Requests to join";
|
||||
"screen_media_details_file_format" = "File format";
|
||||
"screen_media_details_filename" = "File name";
|
||||
"screen_media_details_redact_confirmation_message" = "This file will be removed from the room and members won’t have access to it.";
|
||||
"screen_media_details_redact_confirmation_title" = "Delete file?";
|
||||
"screen_media_details_uploaded_by" = "Uploaded by";
|
||||
"screen_media_details_uploaded_on" = "Uploaded on";
|
||||
"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps.";
|
||||
|
@ -322,6 +322,8 @@ internal enum L10n {
|
||||
internal static var commonDeviceId: String { return L10n.tr("Localizable", "common_device_id") }
|
||||
/// Direct chat
|
||||
internal static var commonDirectChat: String { return L10n.tr("Localizable", "common_direct_chat") }
|
||||
/// Downloading
|
||||
internal static var commonDownloading: String { return L10n.tr("Localizable", "common_downloading") }
|
||||
/// (edited)
|
||||
internal static var commonEditedSuffix: String { return L10n.tr("Localizable", "common_edited_suffix") }
|
||||
/// Editing
|
||||
@ -1378,6 +1380,10 @@ internal enum L10n {
|
||||
internal static var screenMediaDetailsFileFormat: String { return L10n.tr("Localizable", "screen_media_details_file_format") }
|
||||
/// File name
|
||||
internal static var screenMediaDetailsFilename: String { return L10n.tr("Localizable", "screen_media_details_filename") }
|
||||
/// This file will be removed from the room and members won’t have access to it.
|
||||
internal static var screenMediaDetailsRedactConfirmationMessage: String { return L10n.tr("Localizable", "screen_media_details_redact_confirmation_message") }
|
||||
/// Delete file?
|
||||
internal static var screenMediaDetailsRedactConfirmationTitle: String { return L10n.tr("Localizable", "screen_media_details_redact_confirmation_title") }
|
||||
/// Uploaded by
|
||||
internal static var screenMediaDetailsUploadedBy: String { return L10n.tr("Localizable", "screen_media_details_uploaded_by") }
|
||||
/// Uploaded on
|
||||
|
@ -61,7 +61,13 @@ extension MediaProviderMock {
|
||||
return .success(data)
|
||||
}
|
||||
|
||||
loadFileFromSourceFilenameReturnValue = .failure(.failedRetrievingFile)
|
||||
loadFileFromSourceFilenameClosure = { _, _ in
|
||||
guard let url = Bundle.main.url(forResource: "preview_image", withExtension: "jpg") else {
|
||||
return .failure(.failedRetrievingFile)
|
||||
}
|
||||
|
||||
return .success(.unmanaged(url: url))
|
||||
}
|
||||
|
||||
loadImageRetryingOnReconnectionSizeClosure = { _, _ in
|
||||
Task {
|
||||
|
@ -75,6 +75,7 @@ enum UserAvatarSizeOnScreen {
|
||||
case knockingUsersBannerStack
|
||||
case knockingUserBanner
|
||||
case knockingUserList
|
||||
case mediaPreviewDetails
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
@ -110,6 +111,8 @@ enum UserAvatarSizeOnScreen {
|
||||
return 32
|
||||
case .knockingUserList:
|
||||
return 52
|
||||
case .mediaPreviewDetails:
|
||||
return 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,6 @@ extension Date {
|
||||
|
||||
/// A fixed date used for mocks, previews etc.
|
||||
static var mock: Date {
|
||||
Calendar.current.startOfDay(for: .now).addingTimeInterval((9 * 60 * 60) + (41 * 60)) // 9:41 am
|
||||
DateComponents(calendar: .current, year: 2007, month: 1, day: 9, hour: 9, minute: 41).date ?? .now
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,149 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Compound
|
||||
import QuickLook
|
||||
import SwiftUI
|
||||
|
||||
class TimelineMediaPreviewController: QLPreviewController, QLPreviewControllerDataSource {
|
||||
private let viewModel: TimelineMediaPreviewViewModel
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
private let headerHostingController: UIHostingController<HeaderView>
|
||||
private let captionHostingController: UIHostingController<CaptionView>
|
||||
private let detailsHostingController: UIHostingController<TimelineMediaPreviewDetailsView>
|
||||
|
||||
private var navigationBar: UINavigationBar? { view.subviews.first?.subviews.first { $0 is UINavigationBar } as? UINavigationBar }
|
||||
private var toolbar: UIToolbar? { view.subviews.first?.subviews.last { $0 is UIToolbar } as? UIToolbar }
|
||||
private var captionView: UIView { captionHostingController.view }
|
||||
|
||||
init(viewModel: TimelineMediaPreviewViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
headerHostingController = UIHostingController(rootView: HeaderView(context: viewModel.context))
|
||||
headerHostingController.view.backgroundColor = .clear
|
||||
captionHostingController = UIHostingController(rootView: CaptionView(context: viewModel.context))
|
||||
captionHostingController.view.backgroundColor = .clear
|
||||
detailsHostingController = UIHostingController(rootView: TimelineMediaPreviewDetailsView(context: viewModel.context))
|
||||
detailsHostingController.view.backgroundColor = .compound.bgCanvasDefault
|
||||
|
||||
// let materialView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||
// captionHostingController.view.insertMatchedSubview(materialView, at: 0)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
view.addSubview(captionView)
|
||||
|
||||
// Observation of currentPreviewItem doesn't work, so use the index instead.
|
||||
publisher(for: \.currentPreviewItemIndex)
|
||||
.sink { [weak self] _ in
|
||||
guard let self, let currentPreviewItem = currentPreviewItem as? TimelineMediaPreviewItem else { return }
|
||||
Task { await self.viewModel.updateCurrentItem(currentPreviewItem) }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.actions
|
||||
.sink { [weak self] action in
|
||||
switch action {
|
||||
case .loadedMediaFile:
|
||||
self?.refreshCurrentPreviewItem()
|
||||
case .viewInTimeline:
|
||||
self?.dismiss(animated: true) // Dismiss the details sheet.
|
||||
// Errrr, hmmmmm, do something else here.
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
dataSource = self
|
||||
}
|
||||
|
||||
@available(*, unavailable) required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
|
||||
overrideUserInterfaceStyle = .dark
|
||||
|
||||
if let toolbar {
|
||||
captionView.isHidden = toolbar.alpha == 0
|
||||
|
||||
if captionView.constraints.isEmpty {
|
||||
captionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
captionView.bottomAnchor.constraint(equalTo: toolbar.topAnchor),
|
||||
captionView.leadingAnchor.constraint(equalTo: toolbar.leadingAnchor),
|
||||
captionView.trailingAnchor.constraint(equalTo: toolbar.trailingAnchor)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
navigationBar?.topItem?.titleView = headerHostingController.view
|
||||
|
||||
if navigationBar?.topItem?.rightBarButtonItems?.count == 1 {
|
||||
navigationBar?.topItem?.rightBarButtonItems?.append(UIBarButtonItem(image: UIImage(systemSymbol: .infoCircle), style: .plain, target: self, action: #selector(presentMediaDetails)))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: QLPreviewControllerDataSource
|
||||
|
||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||
viewModel.state.previewItems.count
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
viewModel.state.previewItems[index]
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@objc func presentMediaDetails() {
|
||||
detailsHostingController.overrideUserInterfaceStyle = .dark
|
||||
detailsHostingController.sheetPresentationController?.detents = [.medium()]
|
||||
detailsHostingController.sheetPresentationController?.prefersGrabberVisible = true
|
||||
|
||||
present(detailsHostingController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private struct HeaderView: View {
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Text(context.viewState.currentItem?.sender.displayName ?? context.viewState.currentItem?.sender.id ?? L10n.commonLoading)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
Text(context.viewState.currentItem?.timestamp.formatted(date: .abbreviated, time: .omitted) ?? "")
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CaptionView: View {
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
if let caption = context.viewState.currentItem?.caption {
|
||||
Text(caption)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.lineLimit(5)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(16)
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import QuickLook
|
||||
|
||||
enum TimelineMediaPreviewViewModelAction {
|
||||
case loadedMediaFile
|
||||
case viewInTimeline
|
||||
}
|
||||
|
||||
struct TimelineMediaPreviewViewState: BindableState {
|
||||
var previewItems: [TimelineMediaPreviewItem]
|
||||
var currentItem: TimelineMediaPreviewItem?
|
||||
}
|
||||
|
||||
/// Wraps a media file and title to be previewed with QuickLook.
|
||||
class TimelineMediaPreviewItem: NSObject, QLPreviewItem {
|
||||
private let timelineItem: EventBasedMessageTimelineItemProtocol
|
||||
var fileHandle: MediaFileHandleProxy?
|
||||
|
||||
init(timelineItem: EventBasedMessageTimelineItemProtocol) {
|
||||
self.timelineItem = timelineItem
|
||||
}
|
||||
|
||||
var id: TimelineItemIdentifier { timelineItem.id }
|
||||
|
||||
// MARK: QLPreviewItem
|
||||
|
||||
var previewItemURL: URL? {
|
||||
fileHandle?.url
|
||||
}
|
||||
|
||||
var previewItemTitle: String? {
|
||||
switch timelineItem {
|
||||
case let audioItem as AudioRoomTimelineItem:
|
||||
audioItem.content.filename
|
||||
case let fileItem as FileRoomTimelineItem:
|
||||
fileItem.content.filename
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
imageItem.content.filename
|
||||
case let videoItem as VideoRoomTimelineItem:
|
||||
videoItem.content.filename
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Event details
|
||||
|
||||
var sender: TimelineItemSender {
|
||||
timelineItem.sender
|
||||
}
|
||||
|
||||
var timestamp: Date {
|
||||
timelineItem.timestamp
|
||||
}
|
||||
|
||||
// MARK: Media details
|
||||
|
||||
var mediaSource: MediaSourceProxy? {
|
||||
switch timelineItem {
|
||||
case let audioItem as AudioRoomTimelineItem:
|
||||
audioItem.content.source
|
||||
case let fileItem as FileRoomTimelineItem:
|
||||
fileItem.content.source
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
imageItem.content.imageInfo.source
|
||||
case let videoItem as VideoRoomTimelineItem:
|
||||
videoItem.content.videoInfo.source
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
var thumbnailMediaSource: MediaSourceProxy? {
|
||||
switch timelineItem {
|
||||
case let fileItem as FileRoomTimelineItem:
|
||||
fileItem.content.thumbnailSource
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
imageItem.content.thumbnailInfo?.source
|
||||
case let videoItem as VideoRoomTimelineItem:
|
||||
videoItem.content.thumbnailInfo?.source
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
var filename: String? {
|
||||
switch timelineItem {
|
||||
case let audioItem as AudioRoomTimelineItem:
|
||||
audioItem.content.filename
|
||||
case let fileItem as FileRoomTimelineItem:
|
||||
fileItem.content.filename
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
imageItem.content.filename
|
||||
case let videoItem as VideoRoomTimelineItem:
|
||||
videoItem.content.filename
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
var fileSize: Double? {
|
||||
previewItemURL.flatMap { try? FileManager.default.sizeForItem(at: $0) } ?? expectedFileSize
|
||||
}
|
||||
|
||||
private var expectedFileSize: Double? {
|
||||
let fileSize: UInt? = switch timelineItem {
|
||||
case let audioItem as AudioRoomTimelineItem:
|
||||
audioItem.content.fileSize
|
||||
case let fileItem as FileRoomTimelineItem:
|
||||
fileItem.content.fileSize
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
imageItem.content.imageInfo.fileSize
|
||||
case let videoItem as VideoRoomTimelineItem:
|
||||
videoItem.content.videoInfo.fileSize
|
||||
default:
|
||||
nil
|
||||
}
|
||||
|
||||
return fileSize.map(Double.init)
|
||||
}
|
||||
|
||||
var caption: String? {
|
||||
timelineItem.mediaCaption
|
||||
}
|
||||
|
||||
var contentType: String? {
|
||||
switch timelineItem {
|
||||
case let audioItem as AudioRoomTimelineItem:
|
||||
audioItem.content.contentType?.localizedDescription
|
||||
case let fileItem as FileRoomTimelineItem:
|
||||
fileItem.content.contentType?.localizedDescription
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
imageItem.content.contentType?.localizedDescription
|
||||
case let videoItem as VideoRoomTimelineItem:
|
||||
videoItem.content.contentType?.localizedDescription
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
var blurhash: String? {
|
||||
switch timelineItem {
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
imageItem.content.blurhash
|
||||
case let videoItem as VideoRoomTimelineItem:
|
||||
videoItem.content.blurhash
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TimelineMediaPreviewViewAction {
|
||||
case viewInTimeline
|
||||
case redact
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
typealias TimelineMediaPreviewViewModelType = StateStoreViewModel<TimelineMediaPreviewViewState, TimelineMediaPreviewViewAction>
|
||||
|
||||
class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<TimelineMediaPreviewViewModelAction, Never> = .init()
|
||||
var actions: AnyPublisher<TimelineMediaPreviewViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(previewItems: [EventBasedMessageTimelineItemProtocol], mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
// We might not want to inject this, instead creating a new instance with a custom position and colour scheme 🤔
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: TimelineMediaPreviewViewState(previewItems: previewItems.map(TimelineMediaPreviewItem.init)), mediaProvider: mediaProvider)
|
||||
}
|
||||
|
||||
override func process(viewAction: TimelineMediaPreviewViewAction) {
|
||||
switch viewAction {
|
||||
case .viewInTimeline:
|
||||
actionsSubject.send(.viewInTimeline)
|
||||
case .redact:
|
||||
break // Do it here??
|
||||
}
|
||||
}
|
||||
|
||||
func updateCurrentItem(_ previewItem: TimelineMediaPreviewItem) async {
|
||||
state.currentItem = previewItem
|
||||
|
||||
if previewItem.fileHandle == nil, let source = previewItem.mediaSource {
|
||||
showDownloadingIndicator(itemID: previewItem.id)
|
||||
defer { hideDownloadingIndicator(itemID: previewItem.id) }
|
||||
|
||||
switch await mediaProvider.loadFileFromSource(source) {
|
||||
case .success(let handle):
|
||||
previewItem.fileHandle = handle
|
||||
actionsSubject.send(.loadedMediaFile)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed loading media: \(error)")
|
||||
#warning("Show the error!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showDownloadingIndicator(itemID: TimelineItemIdentifier) {
|
||||
let indicatorID = makeDownloadIndicatorID(itemID: itemID)
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: indicatorID,
|
||||
type: .toast(progress: .indeterminate),
|
||||
title: L10n.commonDownloading,
|
||||
persistent: true),
|
||||
delay: .seconds(0.1)) // Don't show the indicator when the SDK loads the file from the store.
|
||||
}
|
||||
|
||||
private func hideDownloadingIndicator(itemID: TimelineItemIdentifier) {
|
||||
let indicatorID = makeDownloadIndicatorID(itemID: itemID)
|
||||
userIndicatorController.retractIndicatorWithId(indicatorID)
|
||||
}
|
||||
|
||||
private func makeDownloadIndicatorID(itemID: TimelineItemIdentifier) -> String {
|
||||
"\(TimelineMediaPreviewViewModel.self)-Download-\(itemID.uniqueID.id)"
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import QuickLook
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
/// Preview a media file using a QuickLook Preview Controller. The preview is interactive with
|
||||
/// the dismiss gesture working as expected if it was presented from UIKit.
|
||||
func timelineMediaQuickLook(viewModel: Binding<TimelineMediaPreviewViewModel?>) -> some View {
|
||||
modifier(TimelineMediaQuickLookModifier(viewModel: viewModel))
|
||||
}
|
||||
}
|
||||
|
||||
private struct TimelineMediaQuickLookModifier: ViewModifier {
|
||||
@Binding var viewModel: TimelineMediaPreviewViewModel?
|
||||
|
||||
@State private var dismissalPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.background {
|
||||
if let viewModel {
|
||||
EmbeddedQuickLookPresenter(viewModel: viewModel, dismissalPublisher: dismissalPublisher) {
|
||||
self.viewModel = nil
|
||||
}
|
||||
} else {
|
||||
// Work around QLPreviewController dismissal issues, see below.
|
||||
let _ = dismissalPublisher.send(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When this view is put in the background of a SwiftUI view hierarchy,
|
||||
/// it will present a QLPreviewController on top of the entire app.
|
||||
private struct EmbeddedQuickLookPresenter: UIViewControllerRepresentable {
|
||||
let viewModel: TimelineMediaPreviewViewModel
|
||||
let dismissalPublisher: PassthroughSubject<Void, Never>
|
||||
let onDismiss: () -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> PresentingController {
|
||||
PresentingController(viewModel: viewModel, dismissalPublisher: dismissalPublisher, onDismiss: onDismiss)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PresentingController, context: Context) { }
|
||||
|
||||
/// A view controller that hosts the QuickLook preview.
|
||||
///
|
||||
/// This wrapper somehow allows the preview controller to do presentation/dismissal
|
||||
/// animations and interactions which don't work if you represent it directly to SwiftUI 🤷♂️
|
||||
class PresentingController: UIViewController, QLPreviewControllerDelegate {
|
||||
private let previewController: QLPreviewController
|
||||
private let sourceView = UIView()
|
||||
|
||||
private var hasPresented = false
|
||||
private let onDismiss: () -> Void
|
||||
private var dismissalObserver: AnyCancellable?
|
||||
|
||||
init(viewModel: TimelineMediaPreviewViewModel,
|
||||
dismissalPublisher: PassthroughSubject<Void, Never>,
|
||||
onDismiss: @escaping () -> Void) {
|
||||
previewController = TimelineMediaPreviewController(viewModel: viewModel)
|
||||
self.onDismiss = onDismiss
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
// The QLPreviewController will not automatically dismiss itself when the underlying view is removed
|
||||
// (e.g. switching rooms from a notification) and it continues to hold on to the whole hierarcy.
|
||||
// Manually tell it to dismiss itself here.
|
||||
dismissalObserver = dismissalPublisher.sink { [weak self] _ in
|
||||
// Dispatching on main.async with weak self we avoid doing an extra dismiss if the view is presented on top of another modal
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
view.backgroundColor = .clear
|
||||
view.addSubview(sourceView)
|
||||
|
||||
sourceView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
sourceView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
sourceView.centerYAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
sourceView.widthAnchor.constraint(equalToConstant: 200),
|
||||
sourceView.heightAnchor.constraint(equalToConstant: 200)
|
||||
])
|
||||
}
|
||||
|
||||
// Don't use viewWillAppear due to the following warning:
|
||||
// Presenting view controller <QLPreviewController> from detached view controller <HostingController> is not supported,
|
||||
// and may result in incorrect safe area insets and a corrupt root presentation. Make sure <HostingController> is in
|
||||
// the view controller hierarchy before presenting from it. Will become a hard exception in a future release.
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
guard !hasPresented else { return }
|
||||
|
||||
previewController.delegate = self
|
||||
present(previewController, animated: true)
|
||||
hasPresented = true
|
||||
}
|
||||
|
||||
// MARK: QLPreviewControllerDelegate
|
||||
|
||||
func previewController(_ controller: QLPreviewController, transitionViewFor item: any QLPreviewItem) -> UIView? {
|
||||
sourceView
|
||||
}
|
||||
|
||||
func previewControllerDidDismiss(_ controller: QLPreviewController) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct TimelineMediaQuickLook_Previews: PreviewProvider {
|
||||
static let viewModel = makeViewModel()
|
||||
|
||||
static var previews: some View {
|
||||
EmbeddedQuickLookPresenter(viewModel: viewModel, dismissalPublisher: .init()) { }
|
||||
}
|
||||
|
||||
static func makeViewModel() -> TimelineMediaPreviewViewModel {
|
||||
let previewItem = FileRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "", displayName: "Sally Sanderson"),
|
||||
content: .init(filename: "Important document.pdf",
|
||||
caption: "A caption goes right here.",
|
||||
source: try? .init(url: .mockMXCFile, mimeType: nil),
|
||||
fileSize: 3 * 1024 * 1024,
|
||||
thumbnailSource: nil,
|
||||
contentType: .pdf))
|
||||
|
||||
return TimelineMediaPreviewViewModel(previewItems: [previewItem],
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineMediaPreviewDetailsView: View {
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
@State private var isPresentingRedactConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
details
|
||||
actions
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.top, 19) // For the drag indicator
|
||||
.sheet(isPresented: $isPresentingRedactConfirmation) {
|
||||
TimelineMediaPreviewRedactConfirmationView(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
private var details: some View {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
DetailsRow(title: L10n.screenMediaDetailsUploadedBy) {
|
||||
HStack(spacing: 8) {
|
||||
if let sender = context.viewState.currentItem?.sender {
|
||||
LoadableAvatarImage(url: sender.avatarURL,
|
||||
name: sender.displayName,
|
||||
contentID: sender.id,
|
||||
avatarSize: .user(on: .mediaPreviewDetails),
|
||||
mediaProvider: context.mediaProvider)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let displayName = sender.displayName {
|
||||
Text(displayName)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundStyle(.compound.decorativeColor(for: sender.id).text)
|
||||
}
|
||||
|
||||
Text(sender.id)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
} else {
|
||||
Text(L10n.commonLoading)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DetailsRow(title: L10n.screenMediaDetailsUploadedOn) {
|
||||
Text(context.viewState.currentItem?.timestamp.formatted(date: .abbreviated, time: .shortened) ?? "")
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
|
||||
DetailsRow(title: L10n.screenMediaDetailsFilename) {
|
||||
Text(context.viewState.currentItem?.filename ?? "")
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
|
||||
if let contentType = context.viewState.currentItem?.contentType {
|
||||
DetailsRow(title: L10n.screenMediaDetailsFileFormat) {
|
||||
Group {
|
||||
if let fileSize = context.viewState.currentItem?.fileSize {
|
||||
Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file)))
|
||||
} else {
|
||||
Text(contentType)
|
||||
}
|
||||
}
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 24)
|
||||
.padding(.bottom, 32)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
private var actions: some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
|
||||
Button { context.send(viewAction: .viewInTimeline) } label: {
|
||||
Label(L10n.actionViewInTimeline, icon: \.visibilityOn)
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
|
||||
Button(role: .destructive) { isPresentingRedactConfirmation = true } label: {
|
||||
Label(L10n.actionRemove, icon: \.delete)
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DetailsRow<Content: View>: View {
|
||||
let title: String
|
||||
let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.textCase(.uppercase)
|
||||
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel(contentType: .jpeg)
|
||||
static let unknownTypeViewModel = makeViewModel()
|
||||
|
||||
static var previews: some View {
|
||||
TimelineMediaPreviewDetailsView(context: viewModel.context)
|
||||
.previewDisplayName("Image")
|
||||
TimelineMediaPreviewDetailsView(context: unknownTypeViewModel.context)
|
||||
.previewDisplayName("Unknown type")
|
||||
}
|
||||
|
||||
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
|
||||
let previewItems = [
|
||||
ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: .mockMXCUserAvatar),
|
||||
content: .init(filename: "Amazing Image.jpeg",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail,
|
||||
contentType: contentType))
|
||||
]
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
viewModel.state.currentItem = viewModel.state.previewItems.first
|
||||
|
||||
return viewModel
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
preview
|
||||
buttons
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
.padding(.top, 19) // For the drag indicator
|
||||
.presentationBackground(.compound.bgCanvasDefault)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(spacing: 16) {
|
||||
BigIcon(icon: \.delete, style: .alertSolid)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenMediaDetailsRedactConfirmationTitle)
|
||||
.font(.compound.headingMDBold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(L10n.screenMediaDetailsRedactConfirmationMessage)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
.padding(.top, 24)
|
||||
.padding(.bottom, 32)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var preview: some View {
|
||||
if let currentItem = context.viewState.currentItem {
|
||||
HStack(spacing: 12) {
|
||||
if let mediaSource = currentItem.thumbnailMediaSource {
|
||||
Color.clear
|
||||
.scaledFrame(size: 40)
|
||||
.background {
|
||||
LoadableImage(mediaSource: mediaSource,
|
||||
mediaType: .timelineItem(uniqueID: currentItem.id.uniqueID.id),
|
||||
blurhash: currentItem.blurhash,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
Color.compound.bgSubtleSecondary
|
||||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(currentItem.filename ?? "")
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
|
||||
if let contentType = currentItem.contentType {
|
||||
Group {
|
||||
if let fileSize = currentItem.fileSize {
|
||||
Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file)))
|
||||
} else {
|
||||
Text(contentType)
|
||||
}
|
||||
}
|
||||
.font(.compound.bodySM)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
|
||||
private var buttons: some View {
|
||||
VStack(spacing: 16) {
|
||||
Button(L10n.actionRemove, role: .destructive) {
|
||||
context.send(viewAction: .redact)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text(L10n.actionCancel)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.compound(.plain))
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel(contentType: .jpeg)
|
||||
|
||||
static var previews: some View {
|
||||
TimelineMediaPreviewRedactConfirmationView(context: viewModel.context)
|
||||
}
|
||||
|
||||
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
|
||||
let previewItems = [
|
||||
ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: .mockMXCUserAvatar),
|
||||
content: .init(filename: "Amazing Image.jpeg",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail,
|
||||
contentType: contentType))
|
||||
]
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
viewModel.state.currentItem = viewModel.state.previewItems.first
|
||||
|
||||
return viewModel
|
||||
}
|
||||
}
|
@ -96,6 +96,7 @@ struct RoomScreen: View {
|
||||
.environmentObject(timelineContext)
|
||||
}
|
||||
.interactiveQuickLook(item: $timelineContext.mediaPreviewItem)
|
||||
.timelineMediaQuickLook(viewModel: $timelineContext.mediaPreviewViewModel)
|
||||
.track(screen: .Room)
|
||||
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
|
||||
guard let provider = providers.first,
|
||||
|
@ -127,6 +127,8 @@ struct TimelineViewStateBindings {
|
||||
/// A media item that will be previewed with QuickLook.
|
||||
var mediaPreviewItem: MediaPreviewItem?
|
||||
|
||||
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
|
||||
|
||||
var alertInfo: AlertInfo<RoomScreenAlertInfoType>?
|
||||
|
||||
var debugInfo: TimelineItemDebugInfo?
|
||||
|
@ -221,8 +221,9 @@ struct VideoInfoProxy: Hashable {
|
||||
private(set) var size: CGSize?
|
||||
private(set) var aspectRatio: CGFloat?
|
||||
private(set) var mimeType: String?
|
||||
private(set) var fileSize: UInt?
|
||||
|
||||
init(source: MediaSource, duration: TimeInterval, width: UInt64?, height: UInt64?, mimeType: String?) {
|
||||
init(source: MediaSource, duration: TimeInterval, width: UInt64?, height: UInt64?, mimeType: String?, fileSize: UInt?) {
|
||||
self.source = MediaSourceProxy(source: source, mimeType: mimeType)
|
||||
self.duration = duration
|
||||
|
||||
@ -230,16 +231,18 @@ struct VideoInfoProxy: Hashable {
|
||||
size = mediaInfo.size
|
||||
aspectRatio = mediaInfo.aspectRatio
|
||||
self.mimeType = mediaInfo.mimeType
|
||||
self.fileSize = fileSize
|
||||
}
|
||||
|
||||
// MARK: - Mocks
|
||||
|
||||
private init(source: MediaSourceProxy, duration: TimeInterval, size: CGSize?, aspectRatio: CGFloat?, mimeType: String?) {
|
||||
private init(source: MediaSourceProxy, duration: TimeInterval, size: CGSize?, aspectRatio: CGFloat?, mimeType: String?, fileSize: UInt?) {
|
||||
self.source = source
|
||||
self.duration = duration
|
||||
self.size = size
|
||||
self.aspectRatio = aspectRatio
|
||||
self.mimeType = mimeType
|
||||
self.fileSize = fileSize
|
||||
}
|
||||
|
||||
static var mockVideo: VideoInfoProxy {
|
||||
@ -251,7 +254,8 @@ struct VideoInfoProxy: Hashable {
|
||||
duration: 100,
|
||||
size: .init(width: 1920, height: 1080),
|
||||
aspectRatio: 1.78,
|
||||
mimeType: nil)
|
||||
mimeType: nil,
|
||||
fileSize: 45_167_000)
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,35 +264,38 @@ struct ImageInfoProxy: Hashable {
|
||||
private(set) var size: CGSize?
|
||||
private(set) var aspectRatio: CGFloat?
|
||||
private(set) var mimeType: String?
|
||||
private(set) var fileSize: UInt?
|
||||
|
||||
init?(source: MediaSource?, width: UInt64?, height: UInt64?, mimeType: String?) {
|
||||
init?(source: MediaSource?, width: UInt64?, height: UInt64?, mimeType: String?, fileSize: UInt?) {
|
||||
guard let source else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType)
|
||||
self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType, fileSize: fileSize)
|
||||
}
|
||||
|
||||
init(source: MediaSource, width: UInt64?, height: UInt64?, mimeType: String?) {
|
||||
self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType)
|
||||
init(source: MediaSource, width: UInt64?, height: UInt64?, mimeType: String?, fileSize: UInt?) {
|
||||
self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType, fileSize: fileSize)
|
||||
}
|
||||
|
||||
init(source: MediaSourceProxy, width: UInt64?, height: UInt64?, mimeType: String?) {
|
||||
init(source: MediaSourceProxy, width: UInt64?, height: UInt64?, mimeType: String?, fileSize: UInt?) {
|
||||
self.source = source
|
||||
|
||||
let mediaInfo = MediaInfoProxy(width: width, height: height, mimeType: mimeType)
|
||||
size = mediaInfo.size
|
||||
aspectRatio = mediaInfo.aspectRatio
|
||||
self.mimeType = mediaInfo.mimeType
|
||||
self.fileSize = fileSize
|
||||
}
|
||||
|
||||
// MARK: - Mocks
|
||||
|
||||
private init(source: MediaSourceProxy, size: CGSize?, aspectRatio: CGFloat?) {
|
||||
private init(source: MediaSourceProxy, size: CGSize?, aspectRatio: CGFloat?, fileSize: UInt?) {
|
||||
self.source = source
|
||||
self.size = size
|
||||
self.aspectRatio = aspectRatio
|
||||
mimeType = source.mimeType
|
||||
self.fileSize = fileSize
|
||||
}
|
||||
|
||||
static var mockImage: ImageInfoProxy {
|
||||
@ -296,7 +303,7 @@ struct ImageInfoProxy: Hashable {
|
||||
fatalError("Invalid mock media source URL")
|
||||
}
|
||||
|
||||
return .init(source: mediaSource, size: .init(width: 2730, height: 2048), aspectRatio: 4 / 3)
|
||||
return .init(source: mediaSource, size: .init(width: 2730, height: 2048), aspectRatio: 4 / 3, fileSize: 717_000)
|
||||
}
|
||||
|
||||
static var mockThumbnail: ImageInfoProxy {
|
||||
@ -304,7 +311,7 @@ struct ImageInfoProxy: Hashable {
|
||||
fatalError("Invalid mock media source URL")
|
||||
}
|
||||
|
||||
return .init(source: mediaSource, size: .init(width: 800, height: 600), aspectRatio: 4 / 3)
|
||||
return .init(source: mediaSource, size: .init(width: 800, height: 600), aspectRatio: 4 / 3, fileSize: 84000)
|
||||
}
|
||||
|
||||
static var mockVideoThumbnail: ImageInfoProxy {
|
||||
@ -312,11 +319,11 @@ struct ImageInfoProxy: Hashable {
|
||||
fatalError("Invalid mock media source URL")
|
||||
}
|
||||
|
||||
return .init(source: mediaSource, size: .init(width: 800, height: 450), aspectRatio: 16 / 9)
|
||||
return .init(source: mediaSource, size: .init(width: 800, height: 450), aspectRatio: 16 / 9, fileSize: 98000)
|
||||
}
|
||||
}
|
||||
|
||||
struct MediaInfoProxy: Hashable {
|
||||
private struct MediaInfoProxy: Hashable {
|
||||
private(set) var size: CGSize?
|
||||
private(set) var mimeType: String?
|
||||
private(set) var aspectRatio: CGFloat?
|
||||
|
@ -116,7 +116,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ info: MatrixRustSDK.ImageInfo,
|
||||
_ mediaSource: MediaSource,
|
||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
let imageInfo = ImageInfoProxy(source: mediaSource, width: info.width, height: info.height, mimeType: info.mimetype)
|
||||
let imageInfo = ImageInfoProxy(source: mediaSource, width: info.width, height: info.height, mimeType: info.mimetype, fileSize: info.size.map(UInt.init))
|
||||
|
||||
return StickerRoomTimelineItem(id: eventItemProxy.id,
|
||||
body: body,
|
||||
@ -518,12 +518,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
let thumbnailInfo = ImageInfoProxy(source: messageContent.info?.thumbnailSource,
|
||||
width: messageContent.info?.thumbnailInfo?.width,
|
||||
height: messageContent.info?.thumbnailInfo?.height,
|
||||
mimeType: messageContent.info?.thumbnailInfo?.mimetype)
|
||||
mimeType: messageContent.info?.thumbnailInfo?.mimetype,
|
||||
fileSize: messageContent.info?.size.map(UInt.init))
|
||||
|
||||
let imageInfo = ImageInfoProxy(source: messageContent.source,
|
||||
width: messageContent.info?.width,
|
||||
height: messageContent.info?.height,
|
||||
mimeType: messageContent.info?.mimetype)
|
||||
mimeType: messageContent.info?.mimetype,
|
||||
fileSize: messageContent.info?.size.map(UInt.init))
|
||||
|
||||
return .init(filename: messageContent.filename,
|
||||
caption: messageContent.caption,
|
||||
@ -542,13 +544,15 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
let thumbnailInfo = ImageInfoProxy(source: messageContent.info?.thumbnailSource,
|
||||
width: messageContent.info?.thumbnailInfo?.width,
|
||||
height: messageContent.info?.thumbnailInfo?.height,
|
||||
mimeType: messageContent.info?.thumbnailInfo?.mimetype)
|
||||
mimeType: messageContent.info?.thumbnailInfo?.mimetype,
|
||||
fileSize: messageContent.info?.size.map(UInt.init))
|
||||
|
||||
let videoInfo = VideoInfoProxy(source: messageContent.source,
|
||||
duration: messageContent.info?.duration ?? 0,
|
||||
width: messageContent.info?.width,
|
||||
height: messageContent.info?.height,
|
||||
mimeType: messageContent.info?.mimetype)
|
||||
mimeType: messageContent.info?.mimetype,
|
||||
fileSize: messageContent.info?.size.map(UInt.init))
|
||||
|
||||
return .init(filename: messageContent.filename,
|
||||
caption: messageContent.caption,
|
||||
|
@ -911,6 +911,18 @@ extension PreviewTests {
|
||||
}
|
||||
}
|
||||
|
||||
func test_timelineMediaPreviewDetailsView() {
|
||||
for preview in TimelineMediaPreviewDetailsView_Previews._allPreviews {
|
||||
assertSnapshots(matching: preview)
|
||||
}
|
||||
}
|
||||
|
||||
func test_timelineMediaPreviewRedactConfirmationView() {
|
||||
for preview in TimelineMediaPreviewRedactConfirmationView_Previews._allPreviews {
|
||||
assertSnapshots(matching: preview)
|
||||
}
|
||||
}
|
||||
|
||||
func test_timelineReactionView() {
|
||||
for preview in TimelineReactionView_Previews._allPreviews {
|
||||
assertSnapshots(matching: preview)
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Image.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Image.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Unknown-type.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-en-GB.Unknown-type.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Image.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Image.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Unknown-type.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPad-pseudo.Unknown-type.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Image.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Image.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Unknown-type.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-en-GB.Unknown-type.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Image.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Image.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Unknown-type.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewDetailsView-iPhone-16-pseudo.Unknown-type.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPad-en-GB.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPad-en-GB.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPad-pseudo.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPad-pseudo.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineMediaPreviewRedactConfirmationView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
56
UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift
Normal file
56
UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
var viewModel: TimelineMediaPreviewViewModel!
|
||||
var context: TimelineMediaPreviewViewModel.Context { viewModel.context }
|
||||
var mediaProvider: MediaProviderMock!
|
||||
|
||||
func testLoadingItem() async throws {
|
||||
// Given a fresh view model.
|
||||
setupViewModel()
|
||||
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
XCTAssertNil(context.viewState.currentItem)
|
||||
|
||||
// When setting the current item.
|
||||
await viewModel.updateCurrentItem(context.viewState.previewItems[0])
|
||||
|
||||
// Then the view model should load the item and update its view state.
|
||||
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
XCTAssertEqual(context.viewState.currentItem, context.viewState.previewItems[0])
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel() {
|
||||
let previewItems = [
|
||||
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))
|
||||
]
|
||||
|
||||
mediaProvider = MediaProviderMock(configuration: .init())
|
||||
viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems,
|
||||
mediaProvider: mediaProvider,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
@ -22,6 +22,8 @@ class VoiceMessageMediaManagerTests: XCTestCase {
|
||||
override func setUp() async throws {
|
||||
voiceMessageCache = VoiceMessageCacheMock()
|
||||
mediaProvider = MediaProviderMock(configuration: .init())
|
||||
mediaProvider.loadFileFromSourceFilenameClosure = nil
|
||||
mediaProvider.loadFileFromSourceFilenameReturnValue = .failure(.failedRetrievingFile)
|
||||
voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider,
|
||||
voiceMessageCache: voiceMessageCache)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user