mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Generic element call link support (#1643)
* Remove swift-url-routing and replace it with a custom implementation. It fails parsing the fragments in app.element.io links and it doesn't bring any value * Add handling for opening generic element call links * Add applinks support for call.element.io * Enable VoIP and Audio playback background modes
This commit is contained in:
@@ -119,6 +119,7 @@
|
||||
292827744227DF61C930BDDB /* CreateRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */; };
|
||||
29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */; };
|
||||
2955F4C160CFD7794D819C64 /* EffectsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024F7398C5FC12586FB10E9D /* EffectsScene.swift */; };
|
||||
296B988AB2280207D558C5BF /* AppRoutesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8CA15700B6BA365624AA7C /* AppRoutesTests.swift */; };
|
||||
29EE1791E0AFA1ABB7F23D2F /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; };
|
||||
2A90DD14DE5C891BFA433950 /* TimelineReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */; };
|
||||
2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */; };
|
||||
@@ -157,11 +158,12 @@
|
||||
34C752A73717C691582DC6C7 /* UnsupportedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */; };
|
||||
34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */; };
|
||||
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; };
|
||||
355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; };
|
||||
35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; };
|
||||
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; };
|
||||
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; };
|
||||
36AD4DD4C798E22584ED3200 /* URLRouting in Frameworks */ = {isa = PBXBuildFile; productRef = E9BAB8A793FE3B54CDD47102 /* URLRouting */; };
|
||||
36CD6E11B37396E14F032CB6 /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; };
|
||||
36AD4DD4C798E22584ED3200 /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; };
|
||||
36CD6E11B37396E14F032CB6 /* Emojibase in Frameworks */ = {isa = PBXBuildFile; productRef = C05729B1684C331F5FFE9232 /* Emojibase */; };
|
||||
37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; };
|
||||
383055C6ABE5BE058CEE1DDB /* WelcomeScreenScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FE5EF0AFFE360C66420AAE /* WelcomeScreenScreenCoordinator.swift */; };
|
||||
38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; };
|
||||
@@ -200,7 +202,6 @@
|
||||
43F35A7E5703D64DB0519C59 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */; };
|
||||
440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; };
|
||||
44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */; };
|
||||
44F0E1B576C7599DF8022071 /* WysiwygComposer in Frameworks */ = {isa = PBXBuildFile; productRef = CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */; };
|
||||
46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; };
|
||||
46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; };
|
||||
46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; };
|
||||
@@ -484,7 +485,7 @@
|
||||
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
|
||||
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
|
||||
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; };
|
||||
A0D7E5BD0298A97DCBDCE40B /* Emojibase in Frameworks */ = {isa = PBXBuildFile; productRef = C05729B1684C331F5FFE9232 /* Emojibase */; };
|
||||
A0D7E5BD0298A97DCBDCE40B /* WysiwygComposer in Frameworks */ = {isa = PBXBuildFile; productRef = CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */; };
|
||||
A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */; };
|
||||
A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; };
|
||||
A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; };
|
||||
@@ -555,6 +556,7 @@
|
||||
B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */; };
|
||||
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; };
|
||||
B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */; };
|
||||
B53D292A5CA61E371C4CD785 /* GenericCallLinkCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514923AA9640C34F39E0500A /* GenericCallLinkCoordinator.swift */; };
|
||||
B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFECCE59967018204876D0A5 /* LocationMarkerView.swift */; };
|
||||
B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */; };
|
||||
B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; };
|
||||
@@ -789,7 +791,6 @@
|
||||
FCDA202B246F75BA28E10C5F /* MapTilerAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */; };
|
||||
FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; };
|
||||
FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; };
|
||||
FF149F0A3550A54C50ECBE7A /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843CF833BF6485B64AC87E1 /* AppRouter.swift */; };
|
||||
FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4552D3466B1453F287223ADA /* SwipeRightAction.swift */; };
|
||||
FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -964,6 +965,7 @@
|
||||
260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = "<group>"; };
|
||||
277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigurationScreenViewStateTests.swift; sourceTree = "<group>"; };
|
||||
27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
|
||||
27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
|
||||
27B8315A340B46F98B9C5AF0 /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||
27EA0F71A3A400A202E15318 /* CreatePollScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreen.swift; sourceTree = "<group>"; };
|
||||
287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = "<group>"; };
|
||||
@@ -1079,6 +1081,7 @@
|
||||
5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = "<group>"; };
|
||||
514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
514923AA9640C34F39E0500A /* GenericCallLinkCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCallLinkCoordinator.swift; sourceTree = "<group>"; };
|
||||
51C2BCE0BC1FC69C1B36E688 /* BugReportScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenModels.swift; sourceTree = "<group>"; };
|
||||
51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileProxy.swift; sourceTree = "<group>"; };
|
||||
52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenModels.swift; sourceTree = "<group>"; };
|
||||
@@ -1285,6 +1288,7 @@
|
||||
9E685274772980BDEFF6691E /* UNUserNotificationCenter+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNUserNotificationCenter+Settings.swift"; sourceTree = "<group>"; };
|
||||
9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = "<group>"; };
|
||||
9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = "<group>"; };
|
||||
9F8CA15700B6BA365624AA7C /* AppRoutesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutesTests.swift; sourceTree = "<group>"; };
|
||||
A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = "<group>"; };
|
||||
A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -1416,7 +1420,6 @@
|
||||
C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
|
||||
C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
C843CF833BF6485B64AC87E1 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = "<group>"; };
|
||||
C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = "<group>"; };
|
||||
@@ -1649,10 +1652,9 @@
|
||||
EAC6FE2CD4F50A43068ADCD8 /* SwiftState in Frameworks */,
|
||||
754602A7B2AAD443C4228ED4 /* GZIP in Frameworks */,
|
||||
B0CB16349B96262AA65A04AF /* Sentry in Frameworks */,
|
||||
36AD4DD4C798E22584ED3200 /* URLRouting in Frameworks */,
|
||||
36CD6E11B37396E14F032CB6 /* Version in Frameworks */,
|
||||
A0D7E5BD0298A97DCBDCE40B /* Emojibase in Frameworks */,
|
||||
44F0E1B576C7599DF8022071 /* WysiwygComposer in Frameworks */,
|
||||
36AD4DD4C798E22584ED3200 /* Version in Frameworks */,
|
||||
36CD6E11B37396E14F032CB6 /* Emojibase in Frameworks */,
|
||||
A0D7E5BD0298A97DCBDCE40B /* WysiwygComposer in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2572,6 +2574,7 @@
|
||||
children = (
|
||||
58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */,
|
||||
C687844F60BFF532D49A994C /* AnalyticsTests.swift */,
|
||||
9F8CA15700B6BA365624AA7C /* AppRoutesTests.swift */,
|
||||
893777A4997BBDB68079D4F5 /* ArrayTests.swift */,
|
||||
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */,
|
||||
37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */,
|
||||
@@ -2678,7 +2681,7 @@
|
||||
780F74C73E826685A9DB289B /* Navigation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C843CF833BF6485B64AC87E1 /* AppRouter.swift */,
|
||||
27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */,
|
||||
B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */,
|
||||
9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */,
|
||||
CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */,
|
||||
@@ -3139,6 +3142,7 @@
|
||||
A448A3A8F764174C60CD0CA1 /* Other */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
514923AA9640C34F39E0500A /* GenericCallLinkCoordinator.swift */,
|
||||
BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */,
|
||||
854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */,
|
||||
);
|
||||
@@ -3932,7 +3936,6 @@
|
||||
9573B94B1C86C6DF751AF3FD /* SwiftState */,
|
||||
997C7385E1A07E061D7E2100 /* GZIP */,
|
||||
7731767AE437BA3BD2CC14A8 /* Sentry */,
|
||||
E9BAB8A793FE3B54CDD47102 /* URLRouting */,
|
||||
A05AF81DDD14AD58CB0E1B9B /* Version */,
|
||||
C05729B1684C331F5FFE9232 /* Emojibase */,
|
||||
CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */,
|
||||
@@ -4066,7 +4069,6 @@
|
||||
E9C4F3A12AA1F65C13A8C8EB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
|
||||
6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */,
|
||||
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
0020F10A9DA1895036A72013 /* XCRemoteSwiftPackageReference "swift-url-routing" */,
|
||||
EC6D0C817B1C21D9D096505A /* XCRemoteSwiftPackageReference "Version" */,
|
||||
945C99D66CE013061F6A3C71 /* XCRemoteSwiftPackageReference "matrix-wysiwyg-composer-swift" */,
|
||||
);
|
||||
@@ -4287,6 +4289,7 @@
|
||||
files = (
|
||||
A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */,
|
||||
890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */,
|
||||
296B988AB2280207D558C5BF /* AppRoutesTests.swift in Sources */,
|
||||
3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */,
|
||||
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */,
|
||||
5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */,
|
||||
@@ -4411,7 +4414,7 @@
|
||||
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */,
|
||||
4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */,
|
||||
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */,
|
||||
FF149F0A3550A54C50ECBE7A /* AppRouter.swift in Sources */,
|
||||
355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */,
|
||||
12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */,
|
||||
9462C62798F47E39DCC182D2 /* Application.swift in Sources */,
|
||||
74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */,
|
||||
@@ -4525,6 +4528,7 @@
|
||||
85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */,
|
||||
46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */,
|
||||
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */,
|
||||
B53D292A5CA61E371C4CD785 /* GenericCallLinkCoordinator.swift in Sources */,
|
||||
4295E5F850897710A51AE114 /* GeoURI.swift in Sources */,
|
||||
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
||||
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */,
|
||||
@@ -5526,14 +5530,6 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
0020F10A9DA1895036A72013 /* XCRemoteSwiftPackageReference "swift-url-routing" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pointfreeco/swift-url-routing";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 0.5.0;
|
||||
};
|
||||
};
|
||||
0CBF57301AA172C21F76CE86 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/maplibre/maplibre-gl-native-distribution";
|
||||
@@ -5953,11 +5949,6 @@
|
||||
package = D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
E9BAB8A793FE3B54CDD47102 /* URLRouting */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0020F10A9DA1895036A72013 /* XCRemoteSwiftPackageReference "swift-url-routing" */;
|
||||
productName = URLRouting;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = AC22997D58D612146053154D /* Project object */;
|
||||
|
||||
@@ -178,15 +178,6 @@
|
||||
"version" : "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-case-paths",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-case-paths",
|
||||
"state" : {
|
||||
"revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984",
|
||||
"version" : "0.14.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -205,15 +196,6 @@
|
||||
"version" : "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-parsing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-parsing",
|
||||
"state" : {
|
||||
"revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70",
|
||||
"version" : "0.12.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-snapshot-testing",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -223,15 +205,6 @@
|
||||
"version" : "1.11.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-url-routing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-url-routing",
|
||||
"state" : {
|
||||
"revision" : "2f4f0404b3de0a0711feb7190f724d8a80bc1cfd",
|
||||
"version" : "0.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftstate",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -258,15 +231,6 @@
|
||||
"revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25",
|
||||
"version" : "2.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xctest-dynamic-overlay",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state" : {
|
||||
"revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865",
|
||||
"version" : "0.9.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
||||
@@ -138,6 +138,21 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
func handleUniversalLink(_ url: URL) {
|
||||
// Parse into an AppRoute to redirect these in a type safe way.
|
||||
|
||||
if let route = AppRouteURLParser.route(from: url) {
|
||||
switch route {
|
||||
case .genericCallLink(let url):
|
||||
if let userSessionFlowCoordinator {
|
||||
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
||||
} else {
|
||||
navigationRootCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Until we have an OIDC callback AppRoute, handle it manually.
|
||||
if url.absoluteString.starts(with: appSettings.oidcRedirectURL.absoluteString) {
|
||||
MXLog.error("OIDC callback through Universal Links not implemented.")
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// Copyright 2023 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 URLRouting
|
||||
|
||||
enum AppRoute {
|
||||
case roomList
|
||||
case room(roomID: String)
|
||||
case roomDetails(roomID: String)
|
||||
case invites
|
||||
}
|
||||
|
||||
struct AppRouterManager {
|
||||
private let deeplinkRouter = OneOf {
|
||||
Route(.case(AppRoute.room(roomID:))) {
|
||||
// Check with product if this is the expect path
|
||||
Path { "room" }
|
||||
Query {
|
||||
Field("id") { Parse(.string) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let permalinkRouter = OneOf {
|
||||
Route(.case(AppRoute.room(roomID:))) {
|
||||
Host("matrix.to")
|
||||
Path {
|
||||
"#"
|
||||
Parse(.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func route(from url: URL) -> AppRoute? {
|
||||
var route: AppRoute?
|
||||
if let deeplinkRoute = try? deeplinkRouter.match(url: url) {
|
||||
route = deeplinkRoute
|
||||
} else if let permalinkRoute = try? permalinkRouter.match(url: url) {
|
||||
route = permalinkRoute
|
||||
}
|
||||
return route
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AppRoute: Equatable {
|
||||
case roomList
|
||||
case room(roomID: String)
|
||||
case roomDetails(roomID: String)
|
||||
case invites
|
||||
case genericCallLink(url: URL)
|
||||
}
|
||||
|
||||
enum AppRouteURLParser {
|
||||
private enum KnownHosts: String, CaseIterable {
|
||||
case elementIo = "element.io"
|
||||
case appElementIo = "app.element.io"
|
||||
case stagingElementIo = "staging.element.io"
|
||||
case developElementIo = "develop.element.io"
|
||||
case mobileElementIo = "mobile.element.io"
|
||||
case callElementIo = "call.element.io"
|
||||
}
|
||||
|
||||
static func route(from url: URL) -> AppRoute? {
|
||||
guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let host = urlComponents.host else {
|
||||
MXLog.error("Failed parsing URL: \(url)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard KnownHosts.allCases.map(\.rawValue).contains(host) else {
|
||||
return .genericCallLink(url: url)
|
||||
}
|
||||
|
||||
if host == KnownHosts.callElementIo.rawValue {
|
||||
return .genericCallLink(url: url)
|
||||
}
|
||||
|
||||
// Deep linking not supported at the moment
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,26 @@ class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomSt
|
||||
rootModule?.coordinator
|
||||
}
|
||||
|
||||
@Published fileprivate var sheetModule: NavigationModule? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
logPresentationChange("Remove sheet", oldValue)
|
||||
oldValue.tearDown()
|
||||
}
|
||||
|
||||
if let sheetModule {
|
||||
logPresentationChange("Set sheet", sheetModule)
|
||||
sheetModule.coordinator?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The currently presented sheet coordinator
|
||||
// Sheets will be presented through the NavigationSplitCoordinator if provided
|
||||
var sheetCoordinator: (any CoordinatorProtocol)? {
|
||||
sheetModule?.coordinator
|
||||
}
|
||||
|
||||
/// Sets or replaces the presented coordinator
|
||||
/// - Parameter coordinator: the coordinator to display
|
||||
func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
@@ -45,6 +65,25 @@ class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomSt
|
||||
|
||||
rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
|
||||
/// - dismissalCallback: called when the sheet has been dismissed, programatically or otherwise
|
||||
func setSheetCoordinator(_ coordinator: (any CoordinatorProtocol)?, animated: Bool = true, dismissalCallback: (() -> Void)? = nil) {
|
||||
guard let coordinator else {
|
||||
sheetModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
if sheetModule?.coordinator === coordinator {
|
||||
fatalError("Cannot use the same coordinator more than once")
|
||||
}
|
||||
|
||||
var transaction = Transaction()
|
||||
transaction.disablesAnimations = !animated
|
||||
|
||||
withTransaction(transaction) {
|
||||
sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CoordinatorProtocol
|
||||
|
||||
@@ -79,5 +118,8 @@ private struct NavigationRootCoordinatorView: View {
|
||||
rootCoordinator.rootModule?.coordinator?.toPresentable()
|
||||
}
|
||||
.animation(.elementDefault, value: rootCoordinator.rootModule)
|
||||
.sheet(item: $rootCoordinator.sheetModule) { module in
|
||||
module.coordinator?.toPresentable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
||||
case .invites:
|
||||
break
|
||||
case .genericCallLink:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,27 +105,22 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
// MARK: - FlowCoordinatorProtocol
|
||||
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
// Tidy up any state before applying the new route.
|
||||
switch stateMachine.state {
|
||||
case .initial, .migration:
|
||||
return // Not ready to handle a route.
|
||||
case .roomList:
|
||||
break // Nothing to tidy up on the home screen.
|
||||
case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen, .invitesScreen, .welcomeScreen:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil, animated: animated)
|
||||
}
|
||||
|
||||
// Apply the route.
|
||||
switch appRoute {
|
||||
case .room, .roomDetails, .roomList:
|
||||
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
case .invites:
|
||||
if UIDevice.current.isPhone {
|
||||
roomFlowCoordinator.clearRoute(animated: animated)
|
||||
clearPresentedSheets(animated: animated) { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
switch appRoute {
|
||||
case .room, .roomDetails, .roomList:
|
||||
self.roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
case .invites:
|
||||
if UIDevice.current.isPhone {
|
||||
self.roomFlowCoordinator.clearRoute(animated: animated)
|
||||
}
|
||||
self.stateMachine.processEvent(.showInvitesScreen, userInfo: .init(animated: animated))
|
||||
case .genericCallLink(let url):
|
||||
self.navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
|
||||
}
|
||||
stateMachine.processEvent(.showInvitesScreen, userInfo: .init(animated: animated))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +130,20 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func clearPresentedSheets(animated: Bool, completion: @escaping () -> Void) {
|
||||
if navigationSplitCoordinator.sheetCoordinator == nil {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil, animated: animated)
|
||||
|
||||
// Prevents system crashes when presenting a sheet if another one was already shown
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupStateMachine() {
|
||||
stateMachine.addTransitionHandler { [weak self] context in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -198,7 +198,7 @@ struct CollapsibleReactionLayout: Layout {
|
||||
}
|
||||
var secondLastRow = rows[rows.count - 2]
|
||||
let collapseButton = secondLastRow.removeLast()
|
||||
lastRow.prepend(collapseButton)
|
||||
lastRow.insert(collapseButton, at: 0)
|
||||
rows[rows.count - 2] = secondLastRow
|
||||
rows[rows.count - 1] = lastRow
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Copyright 2023 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
|
||||
import WebKit
|
||||
|
||||
struct GenericLinkCoordinatorParameters {
|
||||
let url: URL
|
||||
}
|
||||
|
||||
class GenericCallLinkCoordinator: CoordinatorProtocol {
|
||||
private let parameters: GenericLinkCoordinatorParameters
|
||||
|
||||
init(parameters: GenericLinkCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(
|
||||
WebView(url: parameters.url)
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.presentationDragIndicator(.visible)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct WebView: UIViewRepresentable {
|
||||
let url: URL
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
context.coordinator.webView
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(initialURL: url)
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate {
|
||||
private let initialURL: URL
|
||||
private(set) var webView: WKWebView!
|
||||
|
||||
init(initialURL: URL) {
|
||||
self.initialURL = initialURL
|
||||
super.init()
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.allowsPictureInPictureMediaPlayback = true
|
||||
|
||||
webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
webView.uiDelegate = self
|
||||
}
|
||||
|
||||
// MARK: - WKUIDelegate
|
||||
|
||||
func webView(_ webView: WKWebView, decideMediaCapturePermissionsFor origin: WKSecurityOrigin, initiatedBy frame: WKFrameInfo, type: WKMediaCaptureType) async -> WKPermissionDecision {
|
||||
// Don't allow permissions for domains different than what the call was started on
|
||||
guard origin.host == initialURL.host else {
|
||||
return .deny
|
||||
}
|
||||
|
||||
return .grant
|
||||
}
|
||||
|
||||
// MARK: - WKNavigationDelegate
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
|
||||
// Allow any content from the main URL.
|
||||
if navigationAction.request.url?.host == initialURL.host {
|
||||
return .allow
|
||||
}
|
||||
|
||||
// Additionally allow any embedded content such as captchas.
|
||||
if let targetFrame = navigationAction.targetFrame, !targetFrame.isMainFrame {
|
||||
return .allow
|
||||
}
|
||||
|
||||
// Otherwise the request is invalid.
|
||||
return .cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
<string>applinks:staging.element.io</string>
|
||||
<string>applinks:develop.element.io</string>
|
||||
<string>applinks:mobile.element.io</string>
|
||||
<string>applinks:call.element.io</string>
|
||||
<string>webcredentials:*.element.io</string>
|
||||
</array>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>audio</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
|
||||
@@ -75,7 +75,9 @@ targets:
|
||||
NSPhotoLibraryAddUsageDescription: Allows saving photos and videos to your library.
|
||||
NSLocationWhenInUseUsageDescription: When you share your location to people, $(APP_DISPLAY_NAME) needs access to show them a map.
|
||||
UIBackgroundModes: [
|
||||
fetch
|
||||
fetch,
|
||||
audio,
|
||||
voip
|
||||
]
|
||||
BGTaskSchedulerPermittedIdentifiers: [
|
||||
io.element.elementx.background.refresh
|
||||
@@ -175,7 +177,6 @@ targets:
|
||||
- package: SwiftState
|
||||
- package: GZIP
|
||||
- package: Sentry
|
||||
- package: URLRouting
|
||||
- package: Version
|
||||
- package: Emojibase
|
||||
- package: WysiwygComposer
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Copyright 2023 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
|
||||
|
||||
class AppRouterTests: XCTestCase {
|
||||
func testRoutes() {
|
||||
guard let url = URL(string: "https://call.element.io/test") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(AppRouteURLParser.route(from: url), AppRoute.genericCallLink(url: url))
|
||||
}
|
||||
}
|
||||
@@ -106,9 +106,6 @@ packages:
|
||||
SwiftUIIntrospect:
|
||||
url: https://github.com/siteline/SwiftUI-Introspect
|
||||
minorVersion: 0.9.0
|
||||
URLRouting:
|
||||
url: https://github.com/pointfreeco/swift-url-routing
|
||||
minorVersion: 0.5.0
|
||||
Version:
|
||||
url: https://github.com/mxcl/Version
|
||||
minorVersion: 2.0.0
|
||||
|
||||
Reference in New Issue
Block a user