From 4318449200f6b67285e1b76e3a8a8ba21d9343b4 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 28 Nov 2022 14:19:40 +0300 Subject: [PATCH] Image viewer (#338) * Resume other app's music when video playback finished * Remove old media player * Add `isModallyPresented` into the video player screen * Create image viewer screen * Add test screen identifier * Display image viewer when message tapped * Fix template script unit test path * Tweaks on scaling * Commit project file * Add changelog * Ignore safe areas on the file preview screen * Display images in preview * Remove image viewer screen Co-authored-by: Stefan Ceriu --- ElementX.xcodeproj/project.pbxproj | 66 +++++++++---------- .../FilePreview/View/FilePreviewScreen.swift | 7 +- .../MediaPlayer/MediaPlayerCoordinator.swift | 56 ---------------- .../MediaPlayer/MediaPlayerViewModel.swift | 35 ---------- .../MediaPlayerViewModelProtocol.swift | 23 ------- .../MediaPlayer/View/MediaPlayerScreen.swift | 48 -------------- .../ActivityCoordinator.swift} | 21 +++--- .../RoomScreen/RoomScreenCoordinator.swift | 22 +++++-- .../VideoPlayer/VideoPlayerCoordinator.swift | 18 ++++- .../VideoPlayer/VideoPlayerModels.swift | 5 +- .../VideoPlayer/VideoPlayerViewModel.swift | 5 +- .../VideoPlayer/View/VideoPlayerScreen.swift | 34 ++++++---- .../Timeline/RoomTimelineController.swift | 38 +++++++++++ .../RoomTimelineControllerProtocol.swift | 1 + .../Items/ImageRoomTimelineItem.swift | 1 + Tools/Scripts/createScreen.sh | 2 +- .../Sources/MediaPlayerViewModelTests.swift | 44 ------------- changelog.d/244.feature | 1 + 18 files changed, 143 insertions(+), 284 deletions(-) delete mode 100644 ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift delete mode 100644 ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModel.swift delete mode 100644 ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModelProtocol.swift delete mode 100644 ElementX/Sources/Screens/MediaPlayer/View/MediaPlayerScreen.swift rename ElementX/Sources/Screens/{MediaPlayer/MediaPlayerModels.swift => Other/ActivityCoordinator.swift} (67%) delete mode 100644 UnitTests/Sources/MediaPlayerViewModelTests.swift create mode 100644 changelog.d/244.feature diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 886f040cc..d60dc539e 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 086C2FA7750378EB2BFD0BEE /* UITestsRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D751BB69BB7C38FD247517B4 /* UITestsRootCoordinator.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; 09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; }; + 098CE03C6CC71A31F263FA33 /* ActivityCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9D14D6F914324865C7DB9F /* ActivityCoordinator.swift */; }; 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; @@ -132,9 +133,6 @@ 44AE0752E001D1D10605CD88 /* Swipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FDA5344F7C4C6E4E863E13 /* Swipe.swift */; }; 457465EC436703E8C76133A4 /* WeakDictionaryReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7955B20E2E6DA68E5BC0AB9 /* WeakDictionaryReference.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; - 46F8817A235DC41228128BE7 /* MediaPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B7BF5D0705F3CB70E7B2D7 /* MediaPlayerViewModel.swift */; }; - 483507026FDCA2E16E5197A6 /* MediaPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C444092DB0E4AB393067AC36 /* MediaPlayerViewModelTests.swift */; }; - 485A7A97076C7D19104BDC1D /* MediaPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBE603A7EB2C93E81BA6415 /* MediaPlayerModels.swift */; }; 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */; }; 492274DA6691EE985C2FCCAA /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; 49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D58333B377888012740101 /* LoginViewModel.swift */; }; @@ -181,7 +179,6 @@ 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */; }; 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; }; 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1423AB065857FA546444DB15 /* NotificationManager.swift */; }; - 656427D3C59554E03ECD898E /* MediaPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C2348F84A80F682E3A68D0 /* MediaPlayerCoordinator.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; @@ -234,7 +231,6 @@ 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */; }; 7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C483956FA3D665E3842E319A /* SettingsScreen.swift */; }; 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; - 80997E933A5B2C0868D80B45 /* MediaPlayerViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6410F8C03DC4AA46991A6B02 /* MediaPlayerViewModelProtocol.swift */; }; 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; 8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */; }; 834DD9E41FC42A509BAD52E3 /* NavigationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */; }; @@ -254,7 +250,6 @@ 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */; }; 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; - 8D332A24CD23B4216E33EC5C /* MediaPlayerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447A6399BC5EDE7AF7713267 /* MediaPlayerScreen.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D9F646387DF656EF91EE4CB /* RoomMessageFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */; }; 8F2FAA98457750D9D664136F /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; @@ -613,11 +608,9 @@ 3FDFF4C1153D263BAB93C1F3 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = ""; }; 40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; - 41C2348F84A80F682E3A68D0 /* MediaPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerCoordinator.swift; sourceTree = ""; }; 41F3B445BD6EF1C751806B22 /* SlidingSyncViewProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingSyncViewProxy.swift; sourceTree = ""; }; 422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; 434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; - 447A6399BC5EDE7AF7713267 /* MediaPlayerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerScreen.swift; sourceTree = ""; }; 4488F5F92A64A137665C96CD /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Localizable.strings; sourceTree = ""; }; 44AEEE13AC1BF303AE48CBF8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationTests.swift; sourceTree = ""; }; @@ -683,7 +676,6 @@ 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 62BDF0FF4F59AF6EA858B70B /* FilePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModel.swift; sourceTree = ""; }; 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; - 6410F8C03DC4AA46991A6B02 /* MediaPlayerViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerViewModelProtocol.swift; sourceTree = ""; }; 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationPresenter.swift; sourceTree = ""; }; 653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = ""; }; @@ -753,7 +745,6 @@ 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserNotificationController.swift; sourceTree = ""; }; 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreen.swift; sourceTree = ""; }; - 92B7BF5D0705F3CB70E7B2D7 /* MediaPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerViewModel.swift; sourceTree = ""; }; 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = ""; }; 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = ""; }; 938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -846,7 +837,6 @@ BB3073CCD77D906B330BC1D6 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModels.swift; sourceTree = ""; }; BC9B05D6B293A039EB963CA7 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; - BCBE603A7EB2C93E81BA6415 /* MediaPlayerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerModels.swift; sourceTree = ""; }; BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = ""; }; BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = ""; }; BEE6BF9BA63FF42F8AF6EEEA /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -857,7 +847,6 @@ C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelProtocol.swift; sourceTree = ""; }; - C444092DB0E4AB393067AC36 /* MediaPlayerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerViewModelTests.swift; sourceTree = ""; }; C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; @@ -871,6 +860,7 @@ C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptViewModelProtocol.swift; sourceTree = ""; }; CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + CA9D14D6F914324865C7DB9F /* ActivityCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCoordinator.swift; sourceTree = ""; }; CAAE4A709C0A2144C103AA0F /* ang */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ang; path = ang.lproj/Localizable.strings; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CBA95E52C4C6EE8769A63E57 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Localizable.strings; sourceTree = ""; }; @@ -1571,7 +1561,6 @@ 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, A05707BF550D770168A406DB /* LoginViewModelTests.swift */, F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, - C444092DB0E4AB393067AC36 /* MediaPlayerViewModelTests.swift */, 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */, 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, @@ -1848,14 +1837,6 @@ path = Localizations; sourceTree = ""; }; - A253B36CAD2059B6D8C130CD /* View */ = { - isa = PBXGroup; - children = ( - 447A6399BC5EDE7AF7713267 /* MediaPlayerScreen.swift */, - ); - path = View; - sourceTree = ""; - }; A312471EA62EFB0FD94E60DC /* Style */ = { isa = PBXGroup; children = ( @@ -1870,6 +1851,7 @@ A448A3A8F764174C60CD0CA1 /* Other */ = { isa = PBXGroup; children = ( + CA9D14D6F914324865C7DB9F /* ActivityCoordinator.swift */, 1F7AB0A148FCCAC28681C190 /* InviteFriendsCoordinator.swift */, 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */, 4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */, @@ -1931,6 +1913,14 @@ path = NSE; sourceTree = ""; }; + B3E78735F63FA93FAAAF700A /* MockUserNotificationController.swift~refs */ = { + isa = PBXGroup; + children = ( + F798CDE87F83A94B8BC2E18A /* remotes */, + ); + path = "MockUserNotificationController.swift~refs"; + sourceTree = ""; + }; B442FCF47E0A6F28D7D50A4D /* FilePreview */ = { isa = PBXGroup; children = ( @@ -2018,6 +2008,13 @@ path = UITests; sourceTree = ""; }; + C5A8A8B1C16BBFEA4B9D5988 /* origin */ = { + isa = PBXGroup; + children = ( + ); + path = origin; + sourceTree = ""; + }; CA555F7C7CA382ACACF0D82B /* Keychain */ = { isa = PBXGroup; children = ( @@ -2075,16 +2072,11 @@ path = Vendor; sourceTree = ""; }; - D3E07C2F92EC8C5659601744 /* MediaPlayer */ = { + D14F980E72A97D6169A499E8 /* ImageViewer */ = { isa = PBXGroup; children = ( - 41C2348F84A80F682E3A68D0 /* MediaPlayerCoordinator.swift */, - BCBE603A7EB2C93E81BA6415 /* MediaPlayerModels.swift */, - 92B7BF5D0705F3CB70E7B2D7 /* MediaPlayerViewModel.swift */, - 6410F8C03DC4AA46991A6B02 /* MediaPlayerViewModelProtocol.swift */, - A253B36CAD2059B6D8C130CD /* View */, ); - path = MediaPlayer; + path = ImageViewer; sourceTree = ""; }; D958761758AA1110476DE6A3 /* SessionVerification */ = { @@ -2110,6 +2102,7 @@ CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */, 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */, F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */, + B3E78735F63FA93FAAAF700A /* MockUserNotificationController.swift~refs */, ); path = UserNotifications; sourceTree = ""; @@ -2150,7 +2143,7 @@ 4009BE2E791C16AC6EE39A7E /* BugReport */, B442FCF47E0A6F28D7D50A4D /* FilePreview */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, - D3E07C2F92EC8C5659601744 /* MediaPlayer */, + D14F980E72A97D6169A499E8 /* ImageViewer */, 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */, A448A3A8F764174C60CD0CA1 /* Other */, 679E9837ECA8D6776079D16E /* RoomScreen */, @@ -2208,6 +2201,14 @@ path = Background; sourceTree = ""; }; + F798CDE87F83A94B8BC2E18A /* remotes */ = { + isa = PBXGroup; + children = ( + C5A8A8B1C16BBFEA4B9D5988 /* origin */, + ); + path = remotes; + sourceTree = ""; + }; FCDF06BDB123505F0334B4F9 /* Timeline */ = { isa = PBXGroup; children = ( @@ -2688,7 +2689,6 @@ 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, - 483507026FDCA2E16E5197A6 /* MediaPlayerViewModelTests.swift in Sources */, 834DD9E41FC42A509BAD52E3 /* NavigationControllerTests.swift in Sources */, F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, @@ -2712,6 +2712,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 098CE03C6CC71A31F263FA33 /* ActivityCoordinator.swift in Sources */, 7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */, A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */, 39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */, @@ -2810,11 +2811,6 @@ B94368839BDB69172E28E245 /* MXLog.swift in Sources */, B66757D0254843162595B25D /* MXLogger.swift in Sources */, 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */, - 656427D3C59554E03ECD898E /* MediaPlayerCoordinator.swift in Sources */, - 485A7A97076C7D19104BDC1D /* MediaPlayerModels.swift in Sources */, - 8D332A24CD23B4216E33EC5C /* MediaPlayerScreen.swift in Sources */, - 46F8817A235DC41228128BE7 /* MediaPlayerViewModel.swift in Sources */, - 80997E933A5B2C0868D80B45 /* MediaPlayerViewModelProtocol.swift in Sources */, EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */, 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */, FBCD77D557AACBE9B445133A /* MediaProxy.swift in Sources */, diff --git a/ElementX/Sources/Screens/FilePreview/View/FilePreviewScreen.swift b/ElementX/Sources/Screens/FilePreview/View/FilePreviewScreen.swift index 45d38a318..b37310b5c 100644 --- a/ElementX/Sources/Screens/FilePreview/View/FilePreviewScreen.swift +++ b/ElementX/Sources/Screens/FilePreview/View/FilePreviewScreen.swift @@ -19,16 +19,11 @@ import SwiftUI import UIKit struct FilePreviewScreen: View { - @Environment(\.colorScheme) private var colorScheme - - var counterColor: Color { - colorScheme == .light ? .element.secondaryContent : .element.tertiaryContent - } - @ObservedObject var context: FilePreviewViewModel.Context var body: some View { PreviewController(fileURL: context.viewState.fileURL, title: context.viewState.title) + .ignoresSafeArea() } } diff --git a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift b/ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift deleted file mode 100644 index 5ed6b3275..000000000 --- a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift +++ /dev/null @@ -1,56 +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 - -struct MediaPlayerCoordinatorParameters { - let mediaURL: URL -} - -enum MediaPlayerCoordinatorAction { - case cancel -} - -final class MediaPlayerCoordinator: CoordinatorProtocol { - private let parameters: MediaPlayerCoordinatorParameters - private var viewModel: MediaPlayerViewModelProtocol - - var callback: ((MediaPlayerCoordinatorAction) -> Void)? - - init(parameters: MediaPlayerCoordinatorParameters) { - self.parameters = parameters - - viewModel = MediaPlayerViewModel(mediaURL: parameters.mediaURL) - } - - // MARK: - Public - - func start() { - MXLog.debug("Did start.") - viewModel.callback = { [weak self] action in - guard let self else { return } - MXLog.debug("MediaPlayerViewModel did complete with result: \(action).") - switch action { - case .cancel: - self.callback?(.cancel) - } - } - } - - func toPresentable() -> AnyView { - AnyView(MediaPlayerScreen(context: viewModel.context)) - } -} diff --git a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModel.swift b/ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModel.swift deleted file mode 100644 index 5a3261235..000000000 --- a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModel.swift +++ /dev/null @@ -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 MediaPlayerViewModelType = StateStoreViewModel - -class MediaPlayerViewModel: MediaPlayerViewModelType, MediaPlayerViewModelProtocol { - var callback: ((MediaPlayerViewModelAction) -> Void)? - - init(mediaURL: URL, autoplay: Bool = true) { - super.init(initialViewState: MediaPlayerViewState(mediaURL: mediaURL, - autoplay: autoplay)) - } - - override func process(viewAction: MediaPlayerViewAction) async { - switch viewAction { - case .cancel: - callback?(.cancel) - } - } -} diff --git a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModelProtocol.swift b/ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModelProtocol.swift deleted file mode 100644 index 99911ff41..000000000 --- a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerViewModelProtocol.swift +++ /dev/null @@ -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 MediaPlayerViewModelProtocol { - var callback: ((MediaPlayerViewModelAction) -> Void)? { get set } - var context: MediaPlayerViewModelType.Context { get } -} diff --git a/ElementX/Sources/Screens/MediaPlayer/View/MediaPlayerScreen.swift b/ElementX/Sources/Screens/MediaPlayer/View/MediaPlayerScreen.swift deleted file mode 100644 index e80fc6700..000000000 --- a/ElementX/Sources/Screens/MediaPlayer/View/MediaPlayerScreen.swift +++ /dev/null @@ -1,48 +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 MediaPlayerScreen: View { - @ObservedObject var context: MediaPlayerViewModel.Context - - var body: some View { - VideoPlayer(player: player()) - .ignoresSafeArea() - } - - private func player() -> AVPlayer { - let player = AVPlayer(url: context.viewState.mediaURL) - if context.viewState.autoplay { - player.play() - } - return player - } -} - -// MARK: - Previews - -struct MediaPlayer_Previews: PreviewProvider { - static var previews: some View { - Group { - let viewModel = MediaPlayerViewModel(mediaURL: URL(staticString: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"), - autoplay: false) - MediaPlayerScreen(context: viewModel.context) - } - .tint(.element.accent) - } -} diff --git a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerModels.swift b/ElementX/Sources/Screens/Other/ActivityCoordinator.swift similarity index 67% rename from ElementX/Sources/Screens/MediaPlayer/MediaPlayerModels.swift rename to ElementX/Sources/Screens/Other/ActivityCoordinator.swift index dc5a3ba20..849259bad 100644 --- a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerModels.swift +++ b/ElementX/Sources/Screens/Other/ActivityCoordinator.swift @@ -14,17 +14,14 @@ // limitations under the License. // -import Foundation +import SwiftUI -enum MediaPlayerViewModelAction { - case cancel -} - -struct MediaPlayerViewState: BindableState { - let mediaURL: URL - let autoplay: Bool -} - -enum MediaPlayerViewAction { - case cancel +struct ActivityCoordinator: CoordinatorProtocol { + let items: [Any] + + func toPresentable() -> AnyView { + return AnyView(UIActivityViewControllerWrapper(activityItems: items) + .presentationDetents([.medium]) + .ignoresSafeArea()) + } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index e650e71c7..e9003f0a4 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -66,13 +66,25 @@ final class RoomScreenCoordinator: CoordinatorProtocol { // MARK: - Private private func displayVideo(for videoURL: URL) { - let params = VideoPlayerCoordinatorParameters(videoURL: videoURL) + let params = VideoPlayerCoordinatorParameters(videoURL: videoURL, isModallyPresented: false) let coordinator = VideoPlayerCoordinator(parameters: params) - coordinator.callback = { [weak self] _ in - self?.navigationController.pop() - } - navigationController.push(coordinator) + if params.isModallyPresented { + coordinator.callback = { [weak self] _ in + self?.navigationController.dismissSheet() + } + + let controller = NavigationController() + controller.setRootCoordinator(coordinator) + + navigationController.presentSheet(controller) + } else { + coordinator.callback = { [weak self] _ in + self?.navigationController.pop() + } + + navigationController.push(coordinator) + } } private func displayFile(for fileURL: URL, with title: String?) { diff --git a/ElementX/Sources/Screens/VideoPlayer/VideoPlayerCoordinator.swift b/ElementX/Sources/Screens/VideoPlayer/VideoPlayerCoordinator.swift index 4e3e3aaa4..4374410c5 100644 --- a/ElementX/Sources/Screens/VideoPlayer/VideoPlayerCoordinator.swift +++ b/ElementX/Sources/Screens/VideoPlayer/VideoPlayerCoordinator.swift @@ -19,6 +19,7 @@ import SwiftUI struct VideoPlayerCoordinatorParameters { let videoURL: URL + let isModallyPresented: Bool } enum VideoPlayerCoordinatorAction { @@ -34,7 +35,8 @@ final class VideoPlayerCoordinator: CoordinatorProtocol { init(parameters: VideoPlayerCoordinatorParameters) { self.parameters = parameters - viewModel = VideoPlayerViewModel(videoURL: parameters.videoURL) + viewModel = VideoPlayerViewModel(videoURL: parameters.videoURL, + isModallyPresented: parameters.isModallyPresented) } // MARK: - Public @@ -51,6 +53,10 @@ final class VideoPlayerCoordinator: CoordinatorProtocol { } } } + + func stop() { + deconfigureAudioSession(.sharedInstance()) + } func toPresentable() -> AnyView { AnyView(VideoPlayerScreen(context: viewModel.context)) @@ -62,10 +68,18 @@ final class VideoPlayerCoordinator: CoordinatorProtocol { do { try session.setCategory(.playback, mode: .default, - options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]) + options: [.allowBluetooth, .allowBluetoothA2DP]) 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)") + } + } } diff --git a/ElementX/Sources/Screens/VideoPlayer/VideoPlayerModels.swift b/ElementX/Sources/Screens/VideoPlayer/VideoPlayerModels.swift index 384185774..8cf878d46 100644 --- a/ElementX/Sources/Screens/VideoPlayer/VideoPlayerModels.swift +++ b/ElementX/Sources/Screens/VideoPlayer/VideoPlayerModels.swift @@ -21,8 +21,9 @@ enum VideoPlayerViewModelAction { } struct VideoPlayerViewState: BindableState { - var videoURL: URL - var autoplay: Bool + let videoURL: URL + let autoplay: Bool + let isModallyPresented: Bool } enum VideoPlayerViewAction { diff --git a/ElementX/Sources/Screens/VideoPlayer/VideoPlayerViewModel.swift b/ElementX/Sources/Screens/VideoPlayer/VideoPlayerViewModel.swift index abb18c176..aa9177eb9 100644 --- a/ElementX/Sources/Screens/VideoPlayer/VideoPlayerViewModel.swift +++ b/ElementX/Sources/Screens/VideoPlayer/VideoPlayerViewModel.swift @@ -21,9 +21,10 @@ typealias VideoPlayerViewModelType = StateStoreViewModel Void)? - init(videoURL: URL, autoplay: Bool = true) { + init(videoURL: URL, autoplay: Bool = true, isModallyPresented: Bool = true) { super.init(initialViewState: VideoPlayerViewState(videoURL: videoURL, - autoplay: autoplay)) + autoplay: autoplay, + isModallyPresented: isModallyPresented)) } override func process(viewAction: VideoPlayerViewAction) async { diff --git a/ElementX/Sources/Screens/VideoPlayer/View/VideoPlayerScreen.swift b/ElementX/Sources/Screens/VideoPlayer/View/VideoPlayerScreen.swift index fe094c86a..f12f5eb4a 100644 --- a/ElementX/Sources/Screens/VideoPlayer/View/VideoPlayerScreen.swift +++ b/ElementX/Sources/Screens/VideoPlayer/View/VideoPlayerScreen.swift @@ -22,25 +22,33 @@ struct VideoPlayerScreen: View { var body: some View { VideoPlayer(player: player()) - .ignoresSafeArea() + .background(Color.black.ignoresSafeArea()) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden() - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button { - context.send(viewAction: .cancel) - } label: { - Image(systemName: "chevron.backward") - .foregroundColor(.white) - .fontWeight(.semibold) - } + .toolbar { toolbar } + .onSwipeGesture(minimumDistance: 3.0, down: { + if context.viewState.isModallyPresented { + context.send(viewAction: .cancel) + } + }, right: { + if !context.viewState.isModallyPresented { + context.send(viewAction: .cancel) } - } - .onSwipeGesture(minimumDistance: 3.0, right: { - context.send(viewAction: .cancel) }) } + @ToolbarContentBuilder + var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button { context.send(viewAction: .cancel) } label: { + Image(systemName: context.viewState.isModallyPresented ? "xmark" : "chevron.backward") + .foregroundColor(.white) + .fontWeight(.semibold) + } + .accessibilityIdentifier("dismissButton") + } + } + private func player() -> AVPlayer { let player = AVPlayer(url: context.viewState.videoURL) if context.viewState.autoplay { diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index 7228ac47d..48265c914 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -114,6 +114,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } switch timelineItem { + case let item as ImageRoomTimelineItem: + await loadFileForImageTimelineItem(item) + guard let index = timelineItems.firstIndex(where: { $0.id == itemId }), + let item = timelineItems[index] as? ImageRoomTimelineItem else { + return .none + } + if let fileURL = item.cachedFileURL { + return .displayFile(fileURL: fileURL, title: item.text) + } + return .none case let item as VideoRoomTimelineItem: await loadVideoForTimelineItem(item) guard let index = timelineItems.firstIndex(where: { $0.id == itemId }), @@ -349,6 +359,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } + private func loadFileForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async { + if timelineItem.cachedFileURL != nil { + // already cached + return + } + + guard let source = timelineItem.source else { + return + } + + // This is not great. We could better estimate file extension from the mimetype. + guard let fileExtension = timelineItem.text.split(separator: ".").last else { + return + } + switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) { + case .success(let fileURL): + guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }), + var item = timelineItems[index] as? ImageRoomTimelineItem else { + return + } + + item.cachedFileURL = fileURL + timelineItems[index] = item + case .failure: + break + } + } + private func loadFileForTimelineItem(_ timelineItem: FileRoomTimelineItem) async { if timelineItem.cachedFileURL != nil { // already cached diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift index c0f9b76f1..446935e45 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift @@ -16,6 +16,7 @@ import Combine import Foundation +import UIKit enum RoomTimelineControllerCallback { case updatedTimelineItems diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift index 38242b75c..edb5a35a6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift @@ -31,6 +31,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa let source: MediaSourceProxy? var image: UIImage? + var cachedFileURL: URL? var width: CGFloat? var height: CGFloat? diff --git a/Tools/Scripts/createScreen.sh b/Tools/Scripts/createScreen.sh index d2f813df4..a9ddc1e07 100755 --- a/Tools/Scripts/createScreen.sh +++ b/Tools/Scripts/createScreen.sh @@ -26,7 +26,7 @@ echo "Copying tests" cp -R "Templates/SimpleScreenExample/Tests/UI/" $UI_TESTS_DIR/ -cp -R "Templates/SimpleScreenExample/Tests/Unit" $UNIT_TESTS_DIR/ +cp -R "Templates/SimpleScreenExample/Tests/Unit/" $UNIT_TESTS_DIR/ SCREEN_NAME=$2 SCREEN_VAR_NAME=`echo $SCREEN_NAME | awk '{ print tolower(substr($0, 1, 1)) substr($0, 2) }'` diff --git a/UnitTests/Sources/MediaPlayerViewModelTests.swift b/UnitTests/Sources/MediaPlayerViewModelTests.swift deleted file mode 100644 index a0926d8d9..000000000 --- a/UnitTests/Sources/MediaPlayerViewModelTests.swift +++ /dev/null @@ -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 MediaPlayerScreenViewModelTests: XCTestCase { - var viewModel: MediaPlayerViewModelProtocol! - var context: MediaPlayerViewModelType.Context! - - @MainActor override func setUpWithError() throws { - viewModel = MediaPlayerViewModel(mediaURL: 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) - } -} diff --git a/changelog.d/244.feature b/changelog.d/244.feature new file mode 100644 index 000000000..d7c354e63 --- /dev/null +++ b/changelog.d/244.feature @@ -0,0 +1 @@ +Timeline: Display images fullscreen when tapped.