Skip to content

Commit

Permalink
Generic element call link support (#1643)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stefanceriu committed Sep 7, 2023
1 parent 9022837 commit 62b6cd5
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 146 deletions.
45 changes: 18 additions & 27 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
58 changes: 0 additions & 58 deletions ElementX/Sources/Application/Navigation/AppRouter.swift

This file was deleted.

55 changes: 55 additions & 0 deletions ElementX/Sources/Application/Navigation/AppRoutes.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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

Expand Down Expand Up @@ -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()
}
}
}
2 changes: 2 additions & 0 deletions ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
case .invites:
break
case .genericCallLink:
break
}
}

Expand Down
47 changes: 28 additions & 19 deletions ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand All @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 62b6cd5

Please sign in to comment.