Video playback (#308)
* Create media player screen * Introduce `FileCache` to cache message attachments * Add file loading functionality into the media provider * Process tap action on timeline items * Pass item taps to view model * Navigate to media player on view model callback * Commit project file * Add changelog * Rename media to video * Add a loader when large videos being processed * Add back button explicitly on video screen, fixes for light scheme * Handle right swipe to dismiss video
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -91,10 +91,12 @@
|
||||
33CAC1226DFB8B5D8447D286 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; };
|
||||
344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; };
|
||||
34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; };
|
||||
34C258B9CDFC07F7D9BD00E8 /* VideoPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8482665982D664BDDA644F /* VideoPlayerViewModelTests.swift */; };
|
||||
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; };
|
||||
3588F34D05B4D731A73214C6 /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */; };
|
||||
35C57543D245E82CBFE15DF0 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; };
|
||||
35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; };
|
||||
3603946B7A65EAE18FF5AB63 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 749C42DE5951C0695312EBFD /* FileCache.swift */; };
|
||||
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; };
|
||||
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; };
|
||||
36C10EDEDC0466E3A9D63132 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */; };
|
||||
@@ -119,6 +121,8 @@
|
||||
462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; };
|
||||
46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; };
|
||||
4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */; };
|
||||
46F8817A235DC41228128BE7 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B7BF5D0705F3CB70E7B2D7 /* VideoPlayerViewModel.swift */; };
|
||||
485A7A97076C7D19104BDC1D /* VideoPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBE603A7EB2C93E81BA6415 /* VideoPlayerModels.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 */; };
|
||||
@@ -155,6 +159,7 @@
|
||||
63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; };
|
||||
64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */; };
|
||||
64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; };
|
||||
656427D3C59554E03ECD898E /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C2348F84A80F682E3A68D0 /* VideoPlayerCoordinator.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 */; };
|
||||
@@ -201,6 +206,7 @@
|
||||
7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */; };
|
||||
7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C483956FA3D665E3842E319A /* SettingsScreen.swift */; };
|
||||
8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; };
|
||||
80997E933A5B2C0868D80B45 /* VideoPlayerViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6410F8C03DC4AA46991A6B02 /* VideoPlayerViewModelProtocol.swift */; };
|
||||
80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; };
|
||||
80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */; };
|
||||
83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; };
|
||||
@@ -217,6 +223,7 @@
|
||||
8B807DC963D1D4155A241BCC /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F9E67AAB66638C69626866C /* UserSessionFlowCoordinator.swift */; };
|
||||
8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */; };
|
||||
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; };
|
||||
8D332A24CD23B4216E33EC5C /* VideoPlayerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447A6399BC5EDE7AF7713267 /* VideoPlayerScreen.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 */; };
|
||||
@@ -349,6 +356,8 @@
|
||||
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; };
|
||||
EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; };
|
||||
EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; };
|
||||
ECDDA9CD291E6F4100FC93E6 /* FileCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDDA9CC291E6F4100FC93E6 /* FileCacheTests.swift */; };
|
||||
ECDDA9D3292233B600FC93E6 /* Swipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDDA9D2292233B600FC93E6 /* Swipe.swift */; };
|
||||
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; };
|
||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; };
|
||||
@@ -482,6 +491,7 @@
|
||||
2CF9FE7E0CF9F40D1509E63A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginTests.swift; sourceTree = "<group>"; };
|
||||
2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProxy.swift; sourceTree = "<group>"; };
|
||||
2D8482665982D664BDDA644F /* VideoPlayerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModelTests.swift; sourceTree = "<group>"; };
|
||||
2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertInfo.swift; sourceTree = "<group>"; };
|
||||
2F1B28C596DE541DA0AFD16C /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lo; path = lo.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
304FFD608DB6E612075AB1B4 /* WeakDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionary.swift; sourceTree = "<group>"; };
|
||||
@@ -520,9 +530,11 @@
|
||||
3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; };
|
||||
40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenLoadingViewPresenter.swift; sourceTree = "<group>"; };
|
||||
41C2348F84A80F682E3A68D0 /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||
422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = "<group>"; };
|
||||
434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewPresenter.swift; sourceTree = "<group>"; };
|
||||
447A6399BC5EDE7AF7713267 /* VideoPlayerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerScreen.swift; sourceTree = "<group>"; };
|
||||
4488F5F92A64A137665C96CD /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
44AEEE13AC1BF303AE48CBF8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationTests.swift; sourceTree = "<group>"; };
|
||||
@@ -562,7 +574,6 @@
|
||||
541542F5AC323709D8563458 /* AnalyticsPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPrompt.swift; sourceTree = "<group>"; };
|
||||
5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
||||
54E438DBCBDC7A41B95DDDD9 /* MXLogObjcWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogObjcWrapper.m; sourceTree = "<group>"; };
|
||||
551DAED7F623AA5366E79927 /* repository */ = {isa = PBXFileReference; lastKnownFileType = folder; name = repository; path = .; sourceTree = SOURCE_ROOT; };
|
||||
55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
55F30E764BED111C81739844 /* SoftLogoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutUITests.swift; sourceTree = "<group>"; };
|
||||
@@ -589,6 +600,7 @@
|
||||
6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleToastView.swift; sourceTree = "<group>"; };
|
||||
624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
6410F8C03DC4AA46991A6B02 /* VideoPlayerViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = "<group>"; };
|
||||
6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -618,6 +630,7 @@
|
||||
72D03D36422177EF01905D20 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = "<group>"; };
|
||||
73FC861755C6388F62B9280A /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = "<group>"; };
|
||||
749C42DE5951C0695312EBFD /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
|
||||
752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterType.swift; sourceTree = "<group>"; };
|
||||
799A3A11C434296ED28F87C8 /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = iw; path = iw.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = "<group>"; };
|
||||
@@ -652,17 +665,18 @@
|
||||
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
|
||||
8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = "<group>"; };
|
||||
8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; path = LICENSE; sourceTree = "<group>"; };
|
||||
8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = "<group>"; };
|
||||
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
|
||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||
8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
||||
9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = "<group>"; };
|
||||
90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; };
|
||||
9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreen.swift; sourceTree = "<group>"; };
|
||||
92B7BF5D0705F3CB70E7B2D7 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = "<group>"; };
|
||||
9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = "<group>"; };
|
||||
938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
@@ -750,6 +764,7 @@
|
||||
BA7B2E9CC5DC3B76ADC35A43 /* AnalyticsPromptCheckmarkItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptCheckmarkItem.swift; sourceTree = "<group>"; };
|
||||
BB3073CCD77D906B330BC1D6 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = "<group>"; };
|
||||
BC9B05D6B293A039EB963CA7 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
BCBE603A7EB2C93E81BA6415 /* VideoPlayerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModels.swift; sourceTree = "<group>"; };
|
||||
BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = "<group>"; };
|
||||
BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = "<group>"; };
|
||||
BEE6BF9BA63FF42F8AF6EEEA /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -793,6 +808,7 @@
|
||||
D0ADFDC712027931F2216668 /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = "<group>"; };
|
||||
D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
|
||||
D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.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>"; };
|
||||
D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
|
||||
D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
@@ -828,6 +844,8 @@
|
||||
E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = "<group>"; };
|
||||
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
ECDDA9CC291E6F4100FC93E6 /* FileCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCacheTests.swift; sourceTree = "<group>"; };
|
||||
ECDDA9D2292233B600FC93E6 /* Swipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swipe.swift; sourceTree = "<group>"; };
|
||||
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
|
||||
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
|
||||
@@ -955,6 +973,7 @@
|
||||
0787F81684E503024BD0C051 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECDDA9C9291E5EE800FC93E6 /* Cache */,
|
||||
4BF8D11D9ED15CFC373D0119 /* Analytics */,
|
||||
AAFDD509929A0CCF8BCE51EB /* Authentication */,
|
||||
EBBEB5471737E9D116DF4738 /* Background */,
|
||||
@@ -1155,6 +1174,7 @@
|
||||
A40C19719687984FD9478FBE /* Task.swift */,
|
||||
287FC98AF2664EAD79C0D902 /* UIDevice.swift */,
|
||||
227AC5D71A4CE43512062243 /* URL.swift */,
|
||||
ECDDA9D2292233B600FC93E6 /* Swipe.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -1379,6 +1399,8 @@
|
||||
7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */,
|
||||
2CEBCB9676FCD1D0F13188DD /* StringTests.swift */,
|
||||
EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */,
|
||||
2D8482665982D664BDDA644F /* VideoPlayerViewModelTests.swift */,
|
||||
ECDDA9CC291E6F4100FC93E6 /* FileCacheTests.swift */,
|
||||
AF552BB969DC98A4BB8CF8D5 /* UserIndicators */,
|
||||
);
|
||||
path = Sources;
|
||||
@@ -1520,7 +1542,7 @@
|
||||
9413F680ECDFB2B0DDB0DEF2 /* Packages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
551DAED7F623AA5366E79927 /* repository */,
|
||||
D31DC8105C6233E5FFD9B84C /* element-x-ios */,
|
||||
);
|
||||
name = Packages;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
@@ -1608,6 +1630,14 @@
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A253B36CAD2059B6D8C130CD /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
447A6399BC5EDE7AF7713267 /* VideoPlayerScreen.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A312471EA62EFB0FD94E60DC /* Style */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1807,6 +1837,18 @@
|
||||
path = Vendor;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D3E07C2F92EC8C5659601744 /* VideoPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
41C2348F84A80F682E3A68D0 /* VideoPlayerCoordinator.swift */,
|
||||
BCBE603A7EB2C93E81BA6415 /* VideoPlayerModels.swift */,
|
||||
92B7BF5D0705F3CB70E7B2D7 /* VideoPlayerViewModel.swift */,
|
||||
6410F8C03DC4AA46991A6B02 /* VideoPlayerViewModelProtocol.swift */,
|
||||
A253B36CAD2059B6D8C130CD /* View */,
|
||||
);
|
||||
path = VideoPlayer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D958761758AA1110476DE6A3 /* SessionVerification */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1847,6 +1889,7 @@
|
||||
E74CD7681375AD2EAA34D66B /* Authentication */,
|
||||
4009BE2E791C16AC6EE39A7E /* BugReport */,
|
||||
B53CA9BECD3F97805E1432D0 /* HomeScreen */,
|
||||
D3E07C2F92EC8C5659601744 /* VideoPlayer */,
|
||||
679E9837ECA8D6776079D16E /* RoomScreen */,
|
||||
D958761758AA1110476DE6A3 /* SessionVerification */,
|
||||
70B74A432C241E56A7ACE610 /* Settings */,
|
||||
@@ -1903,6 +1946,14 @@
|
||||
path = Background;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECDDA9C9291E5EE800FC93E6 /* Cache */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
749C42DE5951C0695312EBFD /* FileCache.swift */,
|
||||
);
|
||||
path = Cache;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F8474EB69289112888B65518 /* UserIndicators */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2354,11 +2405,13 @@
|
||||
9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */,
|
||||
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */,
|
||||
0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */,
|
||||
ECDDA9CD291E6F4100FC93E6 /* FileCacheTests.swift in Sources */,
|
||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */,
|
||||
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
|
||||
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
|
||||
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */,
|
||||
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */,
|
||||
34C258B9CDFC07F7D9BD00E8 /* VideoPlayerViewModelTests.swift in Sources */,
|
||||
27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */,
|
||||
46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */,
|
||||
EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */,
|
||||
@@ -2471,10 +2524,17 @@
|
||||
BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */,
|
||||
49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */,
|
||||
2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */,
|
||||
ECDDA9D3292233B600FC93E6 /* Swipe.swift in Sources */,
|
||||
B94368839BDB69172E28E245 /* MXLog.swift in Sources */,
|
||||
2A90D9F91A836E30B7D78838 /* MXLogObjcWrapper.m in Sources */,
|
||||
BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */,
|
||||
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */,
|
||||
3603946B7A65EAE18FF5AB63 /* FileCache.swift in Sources */,
|
||||
656427D3C59554E03ECD898E /* VideoPlayerCoordinator.swift in Sources */,
|
||||
485A7A97076C7D19104BDC1D /* VideoPlayerModels.swift in Sources */,
|
||||
8D332A24CD23B4216E33EC5C /* VideoPlayerScreen.swift in Sources */,
|
||||
46F8817A235DC41228128BE7 /* VideoPlayerViewModel.swift in Sources */,
|
||||
80997E933A5B2C0868D80B45 /* VideoPlayerViewModelProtocol.swift in Sources */,
|
||||
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */,
|
||||
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */,
|
||||
62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */,
|
||||
|
||||
34
ElementX/Sources/Other/Extensions/Swipe.swift
Normal file
34
ElementX/Sources/Other/Extensions/Swipe.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// 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
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func onSwipeGesture(minimumDistance: CGFloat,
|
||||
up: @escaping (() -> Void) = { },
|
||||
down: @escaping (() -> Void) = { },
|
||||
left: @escaping (() -> Void) = { },
|
||||
right: @escaping (() -> Void) = { }) -> some View {
|
||||
gesture(DragGesture(minimumDistance: minimumDistance, coordinateSpace: .local)
|
||||
.onEnded { value in
|
||||
if value.translation.width < 0 { left() }
|
||||
if value.translation.width > 0 { right() }
|
||||
if value.translation.height < 0 { up() }
|
||||
if value.translation.height > 0 { down() }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RoomScreenCoordinatorParameters {
|
||||
let navigationRouter: NavigationRouterType
|
||||
let timelineController: RoomTimelineControllerProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let roomName: String?
|
||||
@@ -31,6 +32,7 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
private let parameters: RoomScreenCoordinatorParameters
|
||||
private let roomScreenHostingController: UIViewController
|
||||
private var roomScreenViewModel: RoomScreenViewModelProtocol
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -55,7 +57,17 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() { }
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
roomScreenViewModel.callback = { [weak self] result in
|
||||
guard let self else { return }
|
||||
MXLog.debug("RoomScreenViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
case .displayVideo(let videoURL):
|
||||
self.displayVideo(for: videoURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
roomScreenHostingController
|
||||
@@ -64,4 +76,22 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
func stop() {
|
||||
roomScreenViewModel.stop()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func displayVideo(for videoURL: URL) {
|
||||
let params = VideoPlayerCoordinatorParameters(videoURL: videoURL)
|
||||
let coordinator = VideoPlayerCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self, weak coordinator] _ in
|
||||
guard let self, let coordinator = coordinator else { return }
|
||||
self.navigationRouter.popModule(animated: true)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum RoomScreenViewModelAction { }
|
||||
enum RoomScreenViewModelAction {
|
||||
case displayVideo(videoURL: URL)
|
||||
}
|
||||
|
||||
enum RoomScreenComposerMode: Equatable {
|
||||
case `default`
|
||||
@@ -29,6 +31,7 @@ enum RoomScreenViewAction {
|
||||
case loadPreviousPage
|
||||
case itemAppeared(id: String)
|
||||
case itemDisappeared(id: String)
|
||||
case itemTapped(id: String)
|
||||
case linkClicked(url: URL)
|
||||
case sendMessage
|
||||
case sendReaction(key: String, eventID: String)
|
||||
@@ -42,6 +45,7 @@ struct RoomScreenViewState: BindableState {
|
||||
var roomAvatar: UIImage?
|
||||
var items: [RoomTimelineViewProvider] = []
|
||||
var isBackPaginating = false
|
||||
var showLoading = false
|
||||
var bindings: RoomScreenViewStateBindings
|
||||
|
||||
var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?
|
||||
|
||||
@@ -81,14 +81,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
var callback: ((RoomScreenViewModelAction) -> Void)?
|
||||
|
||||
override func process(viewAction: RoomScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .loadPreviousPage:
|
||||
guard !state.isBackPaginating else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
|
||||
default:
|
||||
#warning("Treat errors")
|
||||
@@ -97,6 +95,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
await timelineController.processItemAppearance(id)
|
||||
case .itemDisappeared(let id):
|
||||
await timelineController.processItemDisappearance(id)
|
||||
case .itemTapped(let id):
|
||||
await itemTapped(with: id)
|
||||
case .linkClicked(let url):
|
||||
MXLog.warning("Link clicked: \(url)")
|
||||
case .sendMessage:
|
||||
@@ -118,6 +118,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func itemTapped(with itemId: String) async {
|
||||
state.showLoading = true
|
||||
let action = await timelineController.processItemTap(itemId)
|
||||
|
||||
switch action {
|
||||
case .displayVideo(let videoURL):
|
||||
callback?(.displayVideo(videoURL: videoURL))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
state.showLoading = false
|
||||
}
|
||||
|
||||
private func buildTimelineViews() {
|
||||
let stateItems = timelineController.timelineItems.map { item in
|
||||
|
||||
@@ -18,6 +18,7 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol RoomScreenViewModelProtocol {
|
||||
var callback: ((RoomScreenViewModelAction) -> Void)? { get set }
|
||||
var context: RoomScreenViewModelType.Context { get }
|
||||
|
||||
func stop()
|
||||
|
||||
@@ -20,30 +20,41 @@ struct RoomScreen: View {
|
||||
@ObservedObject var context: RoomScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0.0) {
|
||||
TimelineView()
|
||||
.environmentObject(context)
|
||||
|
||||
MessageComposer(text: $context.composerText,
|
||||
focused: $context.composerFocused,
|
||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||
type: context.viewState.composerMode) {
|
||||
sendMessage()
|
||||
} replyCancellationAction: {
|
||||
context.send(viewAction: .cancelReply)
|
||||
} editCancellationAction: {
|
||||
context.send(viewAction: .cancelEdit)
|
||||
ZStack {
|
||||
VStack(spacing: 0.0) {
|
||||
TimelineView()
|
||||
.environmentObject(context)
|
||||
|
||||
MessageComposer(text: $context.composerText,
|
||||
focused: $context.composerFocused,
|
||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||
type: context.viewState.composerMode) {
|
||||
sendMessage()
|
||||
} replyCancellationAction: {
|
||||
context.send(viewAction: .cancelReply)
|
||||
} editCancellationAction: {
|
||||
context.send(viewAction: .cancelEdit)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
RoomHeaderView(context: context)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
RoomHeaderView(context: context)
|
||||
}
|
||||
}
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
|
||||
|
||||
if context.viewState.showLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.tint(.element.primaryContent)
|
||||
.padding(16)
|
||||
.background(Color.element.quinaryContent)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
||||
@@ -76,6 +76,9 @@ struct TimelineItemList: View {
|
||||
context.send(viewAction: .linkClicked(url: url))
|
||||
return .systemAction
|
||||
})
|
||||
.onTapGesture {
|
||||
context.send(viewAction: .itemTapped(id: item.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +176,9 @@ struct TimelineItemList: View {
|
||||
}
|
||||
|
||||
private func requestBackPagination() {
|
||||
guard !context.viewState.isBackPaginating else {
|
||||
return
|
||||
}
|
||||
context.send(viewAction: .loadPreviousPage)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// 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 VideoPlayerCoordinatorParameters {
|
||||
let videoURL: URL
|
||||
}
|
||||
|
||||
enum VideoPlayerCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class VideoPlayerCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: VideoPlayerCoordinatorParameters
|
||||
private let videoPlayerHostingController: UIViewController
|
||||
private var videoPlayerViewModel: VideoPlayerViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((VideoPlayerCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: VideoPlayerCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = VideoPlayerViewModel(videoURL: parameters.videoURL)
|
||||
let view = VideoPlayerScreen(context: viewModel.context)
|
||||
videoPlayerViewModel = viewModel
|
||||
videoPlayerHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: videoPlayerHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
videoPlayerViewModel.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 toPresentable() -> UIViewController {
|
||||
videoPlayerHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
}
|
||||
}
|
||||
36
ElementX/Sources/Screens/VideoPlayer/VideoPlayerModels.swift
Normal file
36
ElementX/Sources/Screens/VideoPlayer/VideoPlayerModels.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum VideoPlayerViewModelAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct VideoPlayerViewState: BindableState {
|
||||
var videoURL: URL
|
||||
var autoplay: Bool
|
||||
}
|
||||
|
||||
enum VideoPlayerViewAction {
|
||||
case cancel
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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 {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((VideoPlayerViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(videoURL: URL, autoplay: Bool = true) {
|
||||
super.init(initialViewState: VideoPlayerViewState(videoURL: videoURL,
|
||||
autoplay: autoplay))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: VideoPlayerViewAction) async {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// 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 }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// 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 {
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var context: VideoPlayerViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
VideoPlayer(player: player())
|
||||
.ignoresSafeArea()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
context.send(viewAction: .cancel)
|
||||
} label: {
|
||||
Image(systemName: "chevron.backward")
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSwipeGesture(minimumDistance: 3.0, right: {
|
||||
context.send(viewAction: .cancel)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
89
ElementX/Sources/Services/Cache/FileCache.swift
Normal file
89
ElementX/Sources/Services/Cache/FileCache.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
// MARK: - FileCacheProtocol
|
||||
|
||||
protocol FileCacheProtocol {
|
||||
func file(forKey key: String, fileExtension: String) -> URL?
|
||||
func store(_ data: Data, with fileExtension: String, forKey key: String) throws -> URL
|
||||
func remove(forKey key: String, fileExtension: String) throws
|
||||
func removeAll() throws
|
||||
}
|
||||
|
||||
// MARK: - FileCache
|
||||
|
||||
/// Implementation of `FileCacheProtocol` under `FileManager.default.temporaryDirectory`.
|
||||
class FileCache {
|
||||
private let fileManager = FileManager.default
|
||||
private let folder: URL
|
||||
|
||||
/// Default instance. Uses `FileCache` as the folder name.
|
||||
static let `default` = FileCache(folderName: "FileCache")
|
||||
|
||||
init(folderName: String) {
|
||||
folder = fileManager.temporaryDirectory.appending(path: folderName, directoryHint: .isDirectory)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private func filePath(forKey key: String, fileExtension: String) -> URL {
|
||||
folder.appending(path: key, directoryHint: .notDirectory).appendingPathExtension(fileExtension)
|
||||
}
|
||||
|
||||
private func folderExists() -> Bool {
|
||||
var isDirectory: ObjCBool = false
|
||||
guard fileManager.fileExists(atPath: folder.path(), isDirectory: &isDirectory) else {
|
||||
return false
|
||||
}
|
||||
return isDirectory.boolValue
|
||||
}
|
||||
|
||||
private func createFolderIfNeeded() throws {
|
||||
guard !folderExists() else {
|
||||
return
|
||||
}
|
||||
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FileCacheProtocol
|
||||
|
||||
extension FileCache: FileCacheProtocol {
|
||||
func file(forKey key: String, fileExtension: String) -> URL? {
|
||||
let url = filePath(forKey: key, fileExtension: fileExtension)
|
||||
return fileManager.isReadableFile(atPath: url.path()) ? url : nil
|
||||
}
|
||||
|
||||
func store(_ data: Data, with fileExtension: String, forKey key: String) throws -> URL {
|
||||
try createFolderIfNeeded()
|
||||
let url = filePath(forKey: key, fileExtension: fileExtension)
|
||||
try data.write(to: url)
|
||||
return url
|
||||
}
|
||||
|
||||
func remove(forKey key: String, fileExtension: String) throws {
|
||||
try fileManager.removeItem(at: filePath(forKey: key, fileExtension: fileExtension))
|
||||
}
|
||||
|
||||
func removeAll() throws {
|
||||
guard folderExists() else {
|
||||
return
|
||||
}
|
||||
try fileManager.removeItem(at: folder)
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,16 @@ import UIKit
|
||||
struct MediaProvider: MediaProviderProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
private let fileCache: FileCache
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
init(clientProxy: ClientProxyProtocol,
|
||||
imageCache: Kingfisher.ImageCache,
|
||||
fileCache: FileCache,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
self.imageCache = imageCache
|
||||
self.fileCache = fileCache
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
@@ -88,6 +91,51 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
return .failure(.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? {
|
||||
guard let source else {
|
||||
return nil
|
||||
}
|
||||
let cacheKey = fileCacheKeyForURLString(source.underlyingSource.url())
|
||||
return fileCache.file(forKey: cacheKey, fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
if let url = fileFromSource(source, fileExtension: fileExtension) {
|
||||
return .success(url)
|
||||
}
|
||||
|
||||
let loadFileBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)")
|
||||
defer {
|
||||
loadFileBgTask?.stop()
|
||||
}
|
||||
|
||||
let cacheKey = fileCacheKeyForURLString(source.url)
|
||||
|
||||
do {
|
||||
let data = try await clientProxy.loadMediaContentForSource(source.underlyingSource)
|
||||
|
||||
let url = try fileCache.store(data, with: fileExtension, forKey: cacheKey)
|
||||
return .success(url)
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving file with error: \(error)")
|
||||
return .failure(.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
func fileFromURLString(_ urlString: String?, fileExtension: String) -> URL? {
|
||||
guard let urlString else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
|
||||
fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
await loadFileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)),
|
||||
fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@@ -98,6 +146,13 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
return urlString
|
||||
}
|
||||
}
|
||||
|
||||
private func fileCacheKeyForURLString(_ urlString: String) -> String {
|
||||
guard let component = urlString.split(separator: "/").last else {
|
||||
return urlString
|
||||
}
|
||||
return String(component)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ImageCache {
|
||||
|
||||
@@ -19,6 +19,7 @@ import UIKit
|
||||
|
||||
enum MediaProviderError: Error {
|
||||
case failedRetrievingImage
|
||||
case failedRetrievingFile
|
||||
case invalidImageData
|
||||
}
|
||||
|
||||
@@ -30,6 +31,14 @@ protocol MediaProviderProtocol {
|
||||
func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL?
|
||||
|
||||
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError>
|
||||
|
||||
func fileFromURLString(_ urlString: String?, fileExtension: String) -> URL?
|
||||
|
||||
@discardableResult func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result<URL, MediaProviderError>
|
||||
}
|
||||
|
||||
extension MediaProviderProtocol {
|
||||
|
||||
@@ -49,4 +49,20 @@ struct MockMediaProvider: MediaProviderProtocol {
|
||||
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? {
|
||||
nil
|
||||
}
|
||||
|
||||
@discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
.failure(.failedRetrievingFile)
|
||||
}
|
||||
|
||||
func fileFromURLString(_ urlString: String?, fileExtension: String) -> URL? {
|
||||
nil
|
||||
}
|
||||
|
||||
@discardableResult func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
.failure(.failedRetrievingFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
func processItemAppearance(_ itemId: String) async { }
|
||||
|
||||
func processItemDisappearance(_ itemId: String) async { }
|
||||
|
||||
func processItemTap(_ itemId: String) async -> RoomTimelineControllerAction { .none }
|
||||
|
||||
func sendMessage(_ message: String) async { }
|
||||
|
||||
|
||||
@@ -98,15 +98,36 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
switch timelineItem {
|
||||
case let item as ImageRoomTimelineItem:
|
||||
await loadImageForImageTimelineItem(item)
|
||||
await loadThumbnailForImageTimelineItem(item)
|
||||
case let item as VideoRoomTimelineItem:
|
||||
await loadImageForVideoTimelineItem(item)
|
||||
await loadThumbnailForVideoTimelineItem(item)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func processItemDisappearance(_ itemId: String) { }
|
||||
|
||||
func processItemTap(_ itemId: String) async -> RoomTimelineControllerAction {
|
||||
guard let timelineItem = timelineItems.first(where: { $0.id == itemId }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
switch timelineItem {
|
||||
case let item as VideoRoomTimelineItem:
|
||||
await loadVideoForTimelineItem(item)
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == itemId }),
|
||||
let item = timelineItems[index] as? VideoRoomTimelineItem else {
|
||||
return .none
|
||||
}
|
||||
if let videoURL = item.cachedVideoURL {
|
||||
return .displayVideo(videoURL: videoURL)
|
||||
}
|
||||
return .none
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String) async {
|
||||
switch await timelineProvider.sendMessage(message) {
|
||||
@@ -244,7 +265,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return .middle
|
||||
}
|
||||
|
||||
private func loadImageForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
|
||||
private func loadThumbnailForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
@@ -268,7 +289,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImageForVideoTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
private func loadThumbnailForVideoTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
@@ -291,6 +312,32 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
if timelineItem.cachedVideoURL != nil {
|
||||
// already cached
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
// This is not great. We could better estimate file extension from the mimetype.
|
||||
let fileExtension = String(timelineItem.text.split(separator: ".").last ?? "mp4")
|
||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: fileExtension) {
|
||||
case .success(let fileURL):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? VideoRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.cachedVideoURL = fileURL
|
||||
timelineItems[index] = item
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadUserAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async {
|
||||
if timelineItem.shouldShowSenderDetails == false {
|
||||
|
||||
@@ -24,6 +24,11 @@ enum RoomTimelineControllerCallback {
|
||||
case finishedBackPaginating
|
||||
}
|
||||
|
||||
enum RoomTimelineControllerAction {
|
||||
case displayVideo(videoURL: URL)
|
||||
case none
|
||||
}
|
||||
|
||||
enum RoomTimelineControllerError: Error {
|
||||
case generic
|
||||
}
|
||||
@@ -38,6 +43,8 @@ protocol RoomTimelineControllerProtocol {
|
||||
func processItemAppearance(_ itemId: String) async
|
||||
|
||||
func processItemDisappearance(_ itemId: String) async
|
||||
|
||||
func processItemTap(_ itemId: String) async -> RoomTimelineControllerAction
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError>
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
||||
let source: MediaSource?
|
||||
let thumbnailSource: MediaSource?
|
||||
var image: UIImage?
|
||||
var cachedVideoURL: URL?
|
||||
|
||||
var width: CGFloat?
|
||||
var height: CGFloat?
|
||||
|
||||
@@ -158,13 +158,15 @@ class UserSessionFlowCoordinator: Coordinator {
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||
roomAvatarUrl: roomProxy.avatarURL)
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.stateMachine.processEvent(.dismissedRoomScreen)
|
||||
|
||||
@@ -60,6 +60,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
return .success(UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy,
|
||||
imageCache: .onlyInMemory,
|
||||
fileCache: .default,
|
||||
backgroundTaskService: backgroundTaskService)))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
@@ -78,6 +79,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
return .success(UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy,
|
||||
imageCache: .onlyInMemory,
|
||||
fileCache: .default,
|
||||
backgroundTaskService: backgroundTaskService)))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed creating user session with error: \(error)")
|
||||
|
||||
@@ -115,13 +115,15 @@ class MockScreen: Identifiable {
|
||||
case .splash:
|
||||
return SplashScreenCoordinator()
|
||||
case .roomPlainNoAvatar:
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(),
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
roomName: "Some room name",
|
||||
roomAvatarUrl: nil)
|
||||
return RoomScreenCoordinator(parameters: parameters)
|
||||
case .roomEncryptedWithAvatar:
|
||||
let parameters = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(),
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
roomName: "Some room name",
|
||||
roomAvatarUrl: "mock_url")
|
||||
|
||||
78
UnitTests/Sources/FileCacheTests.swift
Normal file
78
UnitTests/Sources/FileCacheTests.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// 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
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
class FileCacheTests: XCTestCase {
|
||||
private var cache: FileCache!
|
||||
|
||||
override func setUp() {
|
||||
cache = .default
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
try cache.removeAll()
|
||||
}
|
||||
|
||||
func testExistence() throws {
|
||||
let data = Data(repeating: 1, count: 32)
|
||||
let key = "some_key"
|
||||
let fileExtension = "mp4"
|
||||
|
||||
let url1 = try cache.store(data, with: fileExtension, forKey: key)
|
||||
let url2 = cache.file(forKey: key, fileExtension: fileExtension)
|
||||
|
||||
XCTAssertEqual(url1, url2)
|
||||
}
|
||||
|
||||
func testRemove() throws {
|
||||
let data = Data(repeating: 1, count: 32)
|
||||
let key = "some_key"
|
||||
let fileExtension = "mp4"
|
||||
|
||||
_ = try cache.store(data, with: fileExtension, forKey: key)
|
||||
try cache.remove(forKey: key, fileExtension: fileExtension)
|
||||
let url = cache.file(forKey: key, fileExtension: fileExtension)
|
||||
|
||||
XCTAssertNil(url)
|
||||
}
|
||||
|
||||
func testRemoveAll() throws {
|
||||
let data1 = Data(repeating: 1, count: 32)
|
||||
let key1 = "some_key_1"
|
||||
let fileExtension1 = "mp4"
|
||||
|
||||
let data2 = Data(repeating: 1, count: 64)
|
||||
let key2 = "some_key_2"
|
||||
let fileExtension2 = "mp4"
|
||||
|
||||
_ = try cache.store(data1, with: fileExtension1, forKey: key1)
|
||||
_ = try cache.store(data2, with: fileExtension2, forKey: key2)
|
||||
try cache.removeAll()
|
||||
let url1 = cache.file(forKey: key1, fileExtension: fileExtension1)
|
||||
let url2 = cache.file(forKey: key2, fileExtension: fileExtension2)
|
||||
|
||||
XCTAssertNil(url1)
|
||||
XCTAssertNil(url2)
|
||||
}
|
||||
|
||||
func testRemoveAllWhenEmpty() throws {
|
||||
XCTAssertNoThrow(try cache.removeAll())
|
||||
}
|
||||
}
|
||||
44
UnitTests/Sources/VideoPlayerViewModelTests.swift
Normal file
44
UnitTests/Sources/VideoPlayerViewModelTests.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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/238.feature
Normal file
1
changelog.d/238.feature
Normal file
@@ -0,0 +1 @@
|
||||
Timeline: Add playback support for video items.
|
||||
Reference in New Issue
Block a user