QuickLook media. (#447)

* Use QL previews for video and present full screen.
* Use URL(staticString:) in more places.
* Fix DesignKit issues.
This commit is contained in:
Doug 2023-01-12 17:37:33 +00:00 committed by GitHub
parent 3153c0f0d9
commit c383029203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 135 additions and 349 deletions

View File

@ -40,6 +40,7 @@ public struct ElementCapsuleButtonStyle: ButtonStyle {
.multilineTextAlignment(.center)
.background(background)
.opacity(configuration.isPressed ? 0.6 : 1)
.contentShape(Capsule())
}
@ViewBuilder

View File

@ -62,6 +62,7 @@ public struct ElementTextFieldStyle: TextFieldStyle {
configuration
.textFieldStyle(BorderedInputFieldStyle(isEditing: isFocused, isError: isError, returnKey: nil))
.focused($isFocused)
.onTapGesture { isFocused = true } // Set focus with taps in the space between the border and text field.
if let footerText {
Text(footerText)

View File

@ -47,7 +47,6 @@
13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; };
14132418A748C988B85B025E /* OnboardingPageIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09199C43BAB209C0BD89A836 /* OnboardingPageIndicator.swift */; };
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; };
1504CE9A609A348D90B69E47 /* VideoPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3004DFA1B10951962787D90 /* VideoPlayerViewModelTests.swift */; };
152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; };
1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; };
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; };
@ -61,7 +60,6 @@
191161FE9E0DA89704301F37 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; };
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; };
19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */; };
19ED6CF7FDBB1158692D101C /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */; };
1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; };
1A8BDEB96C3B2F033FA563F8 /* EmojiPickerHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB785716B9212C093704E767 /* EmojiPickerHeaderView.swift */; };
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; };
@ -183,7 +181,6 @@
5D9F0695DC6C0057F85C12B6 /* UserNotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */; };
5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; };
5E25568E1CDAD983517E58B5 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */; };
5E540CAEF764D7FBD8D80776 /* VideoPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */; };
5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */; };
5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; };
@ -388,12 +385,10 @@
C7B251DC896C0867C51B616D /* AnalyticsPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541542F5AC323709D8563458 /* AnalyticsPrompt.swift */; };
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; };
C8E82786DE1B6A400DA9BA25 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */; };
C94A6048C654B01163AE1BF1 /* VideoPlayerViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5282B7A2DCD076AD2CF27F46 /* VideoPlayerViewModelProtocol.swift */; };
CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */; };
CB137BFB3E083C33E398A6CB /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; };
CB498F4E27AA0545DCEF0F6F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; };
CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; };
CBF64DE774298D773DBD5354 /* VideoPlayerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */; };
CC2A6B71E12DDF1EE6ECD299 /* RoomMembersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118CF7C5548099AACF7E90C /* RoomMembersCoordinator.swift */; };
CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; };
CCAA0671B46EAFD0BB528E2C /* apple_emojis_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */; };
@ -409,7 +404,6 @@
D05A193AE63030F2CFCE2E9C /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */; };
D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; };
D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; };
D3E603A5E9D529CF293E1BF9 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */; };
D59F046B15AA8E971053C1A6 /* RoomDetailsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813B198AE8833FD12E5A9C78 /* RoomDetailsCoordinator.swift */; };
D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; };
D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; };
@ -475,7 +469,7 @@
FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; };
FBCD77D557AACBE9B445133A /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */; };
FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; };
FCB9B475F908765531335859 /* NavigationSplitCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A63A32A0627A03F00A2900FE /* NavigationSplitCoordinator.swift */; };
FCB9B475F908765531335859 /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A63A32A0627A03F00A2900FE /* NavigationSplitCoordinatorTests.swift */; };
FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; };
FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; };
FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA154570F693D93513E584C1 /* RoomMessageFactory.swift */; };
@ -557,7 +551,6 @@
0C88046D6A070D9827181C4D /* OnboardingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingUITests.swift; sourceTree = "<group>"; };
0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = "<group>"; };
0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerScreen.swift; sourceTree = "<group>"; };
0DD16CE9A66C9040B066AD60 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerTests.swift; sourceTree = "<group>"; };
0E7062F88E9D5F79C8A80524 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -590,7 +583,6 @@
18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = "<group>"; };
1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = "<group>"; };
1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; };
1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModels.swift; sourceTree = "<group>"; };
1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = "<group>"; };
1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelProtocol.swift; sourceTree = "<group>"; };
1C429043E986008B97736636 /* ab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ab; path = ab.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -712,7 +704,6 @@
505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = "<group>"; };
51DF91C374901E94D93276F1 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-MX"; path = "es-MX.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreen.swift; sourceTree = "<group>"; };
5282B7A2DCD076AD2CF27F46 /* VideoPlayerViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModelProtocol.swift; sourceTree = "<group>"; };
529513218340CC8419273165 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = "<group>"; };
53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewAdapter.swift; sourceTree = "<group>"; };
@ -859,7 +850,6 @@
A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = "<group>"; };
A2B6433F516F1E6DFA0E2D89 /* vls */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vls; path = vls.lproj/Localizable.strings; sourceTree = "<group>"; };
A3004DFA1B10951962787D90 /* VideoPlayerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModelTests.swift; sourceTree = "<group>"; };
A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = "<group>"; };
A34A814CBD56230BC74FFCF4 /* MXLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLogger.swift; sourceTree = "<group>"; };
A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
@ -869,7 +859,7 @@
A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
A4CF2FC815D26B337E78DA45 /* RoomMembersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersViewModel.swift; sourceTree = "<group>"; };
A5B0B1226DA8DB55918B34CD /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
A63A32A0627A03F00A2900FE /* NavigationSplitCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinator.swift; sourceTree = "<group>"; };
A63A32A0627A03F00A2900FE /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; };
A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = "<group>"; };
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
@ -968,10 +958,8 @@
D06DFD894157A4C93A02D8B5 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Localizable.strings; sourceTree = "<group>"; };
D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = "<group>"; };
D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = "<group>"; };
D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
D31DC8105C6233E5FFD9B84C /* element-x-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "element-x-ios"; path = .; sourceTree = SOURCE_ROOT; };
D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
@ -1252,18 +1240,6 @@
path = Resources;
sourceTree = "<group>";
};
285079C24A5189C48284CC47 /* VideoPlayer */ = {
isa = PBXGroup;
children = (
D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */,
1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */,
D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */,
5282B7A2DCD076AD2CF27F46 /* VideoPlayerViewModelProtocol.swift */,
5E01022071DDDC48EF453374 /* View */,
);
path = VideoPlayer;
sourceTree = "<group>";
};
2D6DC9871FD7173E51D67C73 /* Cache */ = {
isa = PBXGroup;
children = (
@ -1601,14 +1577,6 @@
path = EmojiMart;
sourceTree = "<group>";
};
5E01022071DDDC48EF453374 /* View */ = {
isa = PBXGroup;
children = (
0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
605F8221E52991786397FCC9 /* View */ = {
isa = PBXGroup;
children = (
@ -1723,7 +1691,7 @@
A05707BF550D770168A406DB /* LoginViewModelTests.swift */,
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */,
F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */,
A63A32A0627A03F00A2900FE /* NavigationSplitCoordinator.swift */,
A63A32A0627A03F00A2900FE /* NavigationSplitCoordinatorTests.swift */,
9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */,
00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */,
6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */,
@ -1740,7 +1708,6 @@
1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */,
EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */,
0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */,
A3004DFA1B10951962787D90 /* VideoPlayerViewModelTests.swift */,
53280D2292E6C9C7821773FD /* UserSession */,
7583EAC171059A86B767209F /* MediaProvider */,
7DBC911559934065993A5FF4 /* NotificationManager */,
@ -2361,7 +2328,6 @@
679E9837ECA8D6776079D16E /* RoomScreen */,
D958761758AA1110476DE6A3 /* SessionVerification */,
70B74A432C241E56A7ACE610 /* Settings */,
285079C24A5189C48284CC47 /* VideoPlayer */,
);
path = Screens;
sourceTree = "<group>";
@ -2923,7 +2889,7 @@
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */,
E9631F628251F77A24AA4BB4 /* MockMediaProxy.swift in Sources */,
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */,
FCB9B475F908765531335859 /* NavigationSplitCoordinator.swift in Sources */,
FCB9B475F908765531335859 /* NavigationSplitCoordinatorTests.swift in Sources */,
4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */,
1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */,
F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */,
@ -2946,7 +2912,6 @@
08248D02BACA75CDC3B39A96 /* UserNotificationCenterSpy.swift in Sources */,
8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */,
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */,
1504CE9A609A348D90B69E47 /* VideoPlayerViewModelTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -3249,11 +3214,6 @@
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */,
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */,
AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */,
D3E603A5E9D529CF293E1BF9 /* VideoPlayerCoordinator.swift in Sources */,
5E540CAEF764D7FBD8D80776 /* VideoPlayerModels.swift in Sources */,
CBF64DE774298D773DBD5354 /* VideoPlayerScreen.swift in Sources */,
19ED6CF7FDBB1158692D101C /* VideoPlayerViewModel.swift in Sources */,
C94A6048C654B01163AE1BF1 /* VideoPlayerViewModelProtocol.swift in Sources */,
36C10EDEDC0466E3A9D63132 /* VideoRoomTimelineItem.swift in Sources */,
64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */,
6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */,

View File

@ -90,6 +90,28 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
sheetModule?.coordinator
}
@Published fileprivate var fullScreenCoverModule: NavigationModule? {
didSet {
if let oldValue {
logPresentationChange("Remove fullscreen cover", oldValue)
oldValue.coordinator.stop()
oldValue.dismissalCallback?()
}
if let fullScreenCoverModule {
logPresentationChange("Set fullscreen cover", fullScreenCoverModule)
fullScreenCoverModule.coordinator.start()
}
updateCompactLayoutComponents()
}
}
/// The currently displayed fullscreen cover coordinator
var fullScreenCoverCoordinator: (any CoordinatorProtocol)? {
fullScreenCoverModule?.coordinator
}
@Published fileprivate var compactLayoutRootModule: NavigationModule?
var compactLayoutRootCoordinator: (any CoordinatorProtocol)? {
@ -149,6 +171,19 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
}
/// Present a fullscreen cover on top of the split view
/// - Parameters:
/// - coordinator: the coordinator to display
/// - dismissalCallback: called when the fullscreen cover has been dismissed, programatically or otherwise
func setFullScreenCoverCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
guard let coordinator else {
fullScreenCoverModule = nil
return
}
fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
}
// MARK: - CoordinatorProtocol
func toPresentable() -> AnyView {
@ -301,6 +336,11 @@ private struct NavigationSplitCoordinatorView: View {
// through the NavigationSplitCoordinator as well.
.sheet(item: $navigationSplitCoordinator.sheetModule) { module in
module.coordinator.toPresentable()
.tint(.element.accent)
}
.fullScreenCover(item: $navigationSplitCoordinator.fullScreenCoverModule) { module in
module.coordinator.toPresentable()
.tint(.element.accent)
}
}
@ -389,6 +429,31 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
return sheetModule?.coordinator
}
@Published fileprivate var fullScreenCoverModule: NavigationModule? {
didSet {
if let oldValue {
logPresentationChange("Remove fullscreen cover", oldValue)
oldValue.coordinator.stop()
oldValue.dismissalCallback?()
}
if let fullScreenCoverModule {
logPresentationChange("Set fullscreen cover", fullScreenCoverModule)
fullScreenCoverModule.coordinator.start()
}
}
}
// The currently presented fullscreen cover coordinator
// Fullscreen covers will be presented through the NavigationSplitCoordinator if provided
var fullScreenCoverCoordinator: (any CoordinatorProtocol)? {
if let navigationSplitCoordinator {
return navigationSplitCoordinator.fullScreenCoverCoordinator
}
return fullScreenCoverModule?.coordinator
}
@Published fileprivate var stackModules = [NavigationModule]() {
didSet {
let diffs = stackModules.difference(from: oldValue)
@ -488,6 +553,25 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
}
/// Present a fullscreen cover on top of the stack. If this NavigationStackCoordinator is embedded within a NavigationSplitCoordinator
/// then the presentation will be proxied to the split
/// - Parameters:
/// - coordinator: the coordinator to display
/// - dismissalCallback: called when the fullscreen cover has been dismissed, programatically or otherwise
func setFullScreenCoverCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
if let navigationSplitCoordinator {
navigationSplitCoordinator.setFullScreenCoverCoordinator(coordinator, dismissalCallback: dismissalCallback)
return
}
guard let coordinator else {
fullScreenCoverModule = nil
return
}
fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
}
// MARK: - CoordinatorProtocol
func toPresentable() -> AnyView {
@ -524,6 +608,11 @@ private struct NavigationStackCoordinatorView: View {
}
.sheet(item: $navigationStackCoordinator.sheetModule) { module in
module.coordinator.toPresentable()
.tint(.element.accent)
}
.fullScreenCover(item: $navigationStackCoordinator.fullScreenCoverModule) { module in
module.coordinator.toPresentable()
.tint(.element.accent)
}
}
}

View File

@ -64,10 +64,7 @@ extension LoginHomeserver {
/// A mock homeserver that supports only supports authentication via a single SSO provider.
static var mockOIDC: LoginHomeserver {
guard let issuerURL = URL(string: "https://auth.company.com") else {
fatalError("This shoud never fail parsing")
}
let issuerURL = URL(staticString: "https://auth.company.com")
return LoginHomeserver(address: "company.com", loginMode: .oidc(issuerURL))
}

View File

@ -22,13 +22,15 @@ struct FilePreviewScreen: View {
@ObservedObject var context: FilePreviewViewModel.Context
var body: some View {
PreviewController(fileURL: context.viewState.fileURL, title: context.viewState.title)
.ignoresSafeArea(.all, edges: [.horizontal, .bottom])
.navigationTitle(ElementL10n.attachmentTypeFile)
PreviewView(context: context,
fileURL: context.viewState.fileURL,
title: context.viewState.title)
.ignoresSafeArea()
}
}
private struct PreviewController: UIViewControllerRepresentable {
private struct PreviewView: UIViewControllerRepresentable {
let context: FilePreviewViewModel.Context
let fileURL: URL
let title: String?
@ -36,28 +38,40 @@ private struct PreviewController: UIViewControllerRepresentable {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
let doneButton = UIBarButtonItem(title: "Done",
style: .done,
target: context.coordinator,
action: #selector(Coordinator.done))
controller.navigationItem.rightBarButtonItem = doneButton
return UINavigationController(rootViewController: controller)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
Coordinator(view: self)
}
class Coordinator: QLPreviewControllerDataSource {
let parent: PreviewController
class Coordinator: NSObject, QLPreviewControllerDataSource {
let view: PreviewView
init(parent: PreviewController) {
self.parent = parent
init(view: PreviewView) {
self.view = view
}
@objc func done() {
Task { await view.context.send(viewAction: .cancel) }
}
// MARK: - QLPreviewControllerDataSource
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
PreviewItem(previewItemURL: parent.fileURL, previewItemTitle: parent.title)
PreviewItem(previewItemURL: view.fileURL, previewItemTitle: view.title)
}
}
}

View File

@ -55,9 +55,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
switch result {
case .displayRoomDetails:
self.displayRoomDetails()
case .displayVideo(let videoURL):
self.displayVideo(for: videoURL)
case .displayFile(let fileURL, let title):
case .displayVideo(let fileURL, let title), .displayFile(let fileURL, let title):
self.displayFile(for: fileURL, with: title)
case .displayEmojiPicker(let itemId):
self.displayEmojiPickerScreen(for: itemId)
@ -81,25 +79,14 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
// MARK: - Private
private func displayVideo(for videoURL: URL) {
let params = VideoPlayerCoordinatorParameters(videoURL: videoURL)
let coordinator = VideoPlayerCoordinator(parameters: params)
coordinator.callback = { [weak self] _ in
self?.navigationStackCoordinator.pop()
}
navigationStackCoordinator.push(coordinator)
}
private func displayFile(for fileURL: URL, with title: String?) {
let params = FilePreviewCoordinatorParameters(fileURL: fileURL, title: title)
let coordinator = FilePreviewCoordinator(parameters: params)
coordinator.callback = { [weak self] _ in
self?.navigationStackCoordinator.pop()
self?.navigationStackCoordinator.setFullScreenCoverCoordinator(nil)
}
navigationStackCoordinator.push(coordinator)
navigationStackCoordinator.setFullScreenCoverCoordinator(coordinator)
}
private func displayEmojiPickerScreen(for itemId: String) {

View File

@ -19,7 +19,7 @@ import UIKit
enum RoomScreenViewModelAction {
case displayRoomDetails
case displayVideo(videoURL: URL)
case displayVideo(videoURL: URL, title: String?)
case displayFile(fileURL: URL, title: String?)
case displayEmojiPicker(itemId: String)
}

View File

@ -140,8 +140,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
let action = await timelineController.processItemTap(itemId)
switch action {
case .displayVideo(let videoURL):
callback?(.displayVideo(videoURL: videoURL))
case .displayVideo(let videoURL, let title):
callback?(.displayVideo(videoURL: videoURL, title: title))
case .displayFile(let fileURL, let title):
callback?(.displayFile(fileURL: fileURL, title: title))
case .none:

View File

@ -1,83 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AVKit
import SwiftUI
struct VideoPlayerCoordinatorParameters {
let videoURL: URL
}
enum VideoPlayerCoordinatorAction {
case cancel
}
final class VideoPlayerCoordinator: CoordinatorProtocol {
private let parameters: VideoPlayerCoordinatorParameters
private var viewModel: VideoPlayerViewModelProtocol
var callback: ((VideoPlayerCoordinatorAction) -> Void)?
init(parameters: VideoPlayerCoordinatorParameters) {
self.parameters = parameters
viewModel = VideoPlayerViewModel(videoURL: parameters.videoURL,
autoplay: UIApplication.shared.applicationState == .active)
}
// MARK: - Public
func start() {
configureAudioSession(.sharedInstance())
viewModel.callback = { [weak self] action in
guard let self else { return }
MXLog.debug("VideoPlayerViewModel did complete with result: \(action).")
switch action {
case .cancel:
self.callback?(.cancel)
}
}
}
func stop() {
deconfigureAudioSession(.sharedInstance())
}
func toPresentable() -> AnyView {
AnyView(VideoPlayerScreen(context: viewModel.context))
}
// MARK: - Private
private func configureAudioSession(_ session: AVAudioSession) {
do {
try session.setCategory(.playback,
mode: .default)
try session.setActive(true)
} catch {
MXLog.debug("Configure audio session failed: \(error)")
}
}
private func deconfigureAudioSession(_ session: AVAudioSession) {
do {
try session.setActive(false, options: .notifyOthersOnDeactivation)
} catch {
MXLog.debug("Deconfigure audio session failed: \(error)")
}
}
}

View File

@ -1,30 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum VideoPlayerViewModelAction {
case cancel
}
struct VideoPlayerViewState: BindableState {
let videoURL: URL
let autoplay: Bool
}
enum VideoPlayerViewAction {
case cancel
}

View File

@ -1,35 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
typealias VideoPlayerViewModelType = StateStoreViewModel<VideoPlayerViewState, VideoPlayerViewAction>
class VideoPlayerViewModel: VideoPlayerViewModelType, VideoPlayerViewModelProtocol {
var callback: ((VideoPlayerViewModelAction) -> Void)?
init(videoURL: URL, autoplay: Bool = true) {
super.init(initialViewState: VideoPlayerViewState(videoURL: videoURL,
autoplay: autoplay))
}
override func process(viewAction: VideoPlayerViewAction) async {
switch viewAction {
case .cancel:
callback?(.cancel)
}
}
}

View File

@ -1,23 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@MainActor
protocol VideoPlayerViewModelProtocol {
var callback: ((VideoPlayerViewModelAction) -> Void)? { get set }
var context: VideoPlayerViewModelType.Context { get }
}

View File

@ -1,49 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AVKit
import SwiftUI
struct VideoPlayerScreen: View {
@ObservedObject var context: VideoPlayerViewModel.Context
var body: some View {
VideoPlayer(player: player())
.ignoresSafeArea(.all, edges: [.horizontal, .bottom])
.navigationTitle(ElementL10n.a11yVideo)
}
private func player() -> AVPlayer {
let player = AVPlayer(url: context.viewState.videoURL)
if context.viewState.autoplay {
player.play()
}
return player
}
}
// MARK: - Previews
struct VideoPlayer_Previews: PreviewProvider {
static var previews: some View {
Group {
let viewModel = VideoPlayerViewModel(videoURL: URL(staticString: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"),
autoplay: false)
VideoPlayerScreen(context: viewModel.context)
}
.tint(.element.accent)
}
}

View File

@ -127,7 +127,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return .none
}
if let videoURL = item.cachedVideoURL {
return .displayVideo(videoURL: videoURL)
return .displayVideo(videoURL: videoURL, title: item.text)
}
return .none
case let item as FileRoomTimelineItem:

View File

@ -26,7 +26,7 @@ enum RoomTimelineControllerCallback {
}
enum RoomTimelineControllerAction {
case displayVideo(videoURL: URL)
case displayVideo(videoURL: URL, title: String?)
case displayFile(fileURL: URL, title: String?)
case none
}

View File

@ -168,7 +168,7 @@ final class MediaProviderTests: XCTestCase {
}
func test_whenFileFromSourceWithSource_correctValuesAreReturned() throws {
let expectedURL = try XCTUnwrap(URL(string: "some_url"))
let expectedURL = URL(filePath: "/some/file/path")
fileCache.fileURLToReturn = expectedURL
let url = mediaProvider.fileFromSource(MediaSourceProxy(urlString: "test/test1"), fileExtension: "png")
XCTAssertEqual(fileCache.fileKey, "test1")
@ -177,7 +177,7 @@ final class MediaProviderTests: XCTestCase {
}
func test_whenLoadFileFromSourceAndFileFromSourceExists_urlIsReturned() async throws {
let expectedURL = try XCTUnwrap(URL(string: "some_url"))
let expectedURL = URL(filePath: "/some/file/path")
let expectedResult: Result<URL, MediaProviderError> = .success(expectedURL)
fileCache.fileURLToReturn = expectedURL
let result = await mediaProvider.loadFileFromSource(MediaSourceProxy(urlString: "test/test1"), fileExtension: "png")
@ -185,7 +185,7 @@ final class MediaProviderTests: XCTestCase {
}
func test_whenLoadFileFromSourceAndNoFileFromSourceExists_mediaLoadedFromSource() async throws {
let expectedURL = try XCTUnwrap(URL(string: "some_url"))
let expectedURL = URL(filePath: "/some/file/path")
let expectedResult: Result<URL, MediaProviderError> = .success(expectedURL)
mediaProxy.mediaContentData = try loadTestImage().pngData()
fileCache.storeURLToReturn = expectedURL
@ -217,14 +217,14 @@ final class MediaProviderTests: XCTestCase {
}
func test_whenFileFromURLString_correctURLIsReturned() throws {
let expectedURL = try XCTUnwrap(URL(string: "some_url"))
let expectedURL = URL(filePath: "/some/file/path")
fileCache.fileURLToReturn = expectedURL
let url = mediaProvider.fileFromURLString("test/test1", fileExtension: "png")
XCTAssertEqual(url?.absoluteString, expectedURL.absoluteString)
}
func test_whenLoadFileFromURLString_correctURLIsReturned() async throws {
let expectedURL = try XCTUnwrap(URL(string: "some_url"))
let expectedURL = URL(filePath: "/some/file/path")
let expectedResult: Result<URL, MediaProviderError> = .success(expectedURL)
fileCache.fileURLToReturn = expectedURL
let result = await mediaProvider.loadFileFromURLString("test/test1", fileExtension: "png")

View File

@ -1,44 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import ElementX
@MainActor
class VideoPlayerScreenViewModelTests: XCTestCase {
var viewModel: VideoPlayerViewModelProtocol!
var context: VideoPlayerViewModelType.Context!
@MainActor override func setUpWithError() throws {
viewModel = VideoPlayerViewModel(videoURL: URL(staticString: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"), autoplay: true)
context = viewModel.context
}
@MainActor func testCancel() async throws {
var correctResult = false
viewModel.callback = { result in
switch result {
case .cancel:
correctResult = true
}
}
context.send(viewAction: .cancel)
await Task.yield()
XCTAssert(correctResult)
}
}

1
changelog.d/418.change Normal file
View File

@ -0,0 +1 @@
Use QuickLook previews for video and present previews full screen (doesn't address gestures yet).