mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Tweak internal/external deeplink handling (#2664)
* Add a childRoom AppRoute. * Add a tests for child room routes.
This commit is contained in:
@@ -176,7 +176,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeepLink(_ url: URL) -> Bool {
|
func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool {
|
||||||
// Parse into an AppRoute to redirect these in a type safe way.
|
// Parse into an AppRoute to redirect these in a type safe way.
|
||||||
|
|
||||||
if let route = appRouteURLParser.route(from: url) {
|
if let route = appRouteURLParser.route(from: url) {
|
||||||
@@ -193,8 +193,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
} else {
|
} else {
|
||||||
navigationRootCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)))
|
navigationRootCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)))
|
||||||
}
|
}
|
||||||
case .roomMemberDetails, .room:
|
case .roomMemberDetails:
|
||||||
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
|
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
|
||||||
|
case .room(let roomID):
|
||||||
|
if isExternalURL {
|
||||||
|
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
|
||||||
|
} else {
|
||||||
|
userSessionFlowCoordinator?.handleAppRoute(.childRoom(roomID: roomID), animated: true)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ import Foundation
|
|||||||
|
|
||||||
protocol AppCoordinatorProtocol: CoordinatorProtocol {
|
protocol AppCoordinatorProtocol: CoordinatorProtocol {
|
||||||
var windowManager: WindowManagerProtocol { get }
|
var windowManager: WindowManagerProtocol { get }
|
||||||
@discardableResult func handleDeepLink(_ url: URL) -> Bool
|
@discardableResult func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ struct Application: App {
|
|||||||
appCoordinator.toPresentable()
|
appCoordinator.toPresentable()
|
||||||
.statusBarHidden(shouldHideStatusBar)
|
.statusBarHidden(shouldHideStatusBar)
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
if appCoordinator.handleDeepLink(url) {
|
if appCoordinator.handleDeepLink(url, isExternalURL: false) {
|
||||||
return .handled
|
return .handled
|
||||||
}
|
}
|
||||||
|
|
||||||
return .systemAction
|
return .systemAction
|
||||||
})
|
})
|
||||||
.onOpenURL {
|
.onOpenURL {
|
||||||
if !appCoordinator.handleDeepLink($0) {
|
if !appCoordinator.handleDeepLink($0, isExternalURL: true) {
|
||||||
openURLInSystemBrowser($0)
|
openURLInSystemBrowser($0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,24 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum AppRoute: Equatable {
|
enum AppRoute: Equatable {
|
||||||
|
/// The callback used to complete login with OIDC.
|
||||||
case oidcCallback(url: URL)
|
case oidcCallback(url: URL)
|
||||||
|
/// The app's home screen.
|
||||||
case roomList
|
case roomList
|
||||||
|
/// A room, shown as the root of the stack (popping any child rooms).
|
||||||
case room(roomID: String)
|
case room(roomID: String)
|
||||||
|
/// A room, pushed as a child of any existing rooms on the stack.
|
||||||
|
case childRoom(roomID: String)
|
||||||
|
/// The information about a particular room.
|
||||||
case roomDetails(roomID: String)
|
case roomDetails(roomID: String)
|
||||||
|
/// The profile of a member within the current room.
|
||||||
|
/// (This can be specialised into 2 routes when we support user permalinks).
|
||||||
case roomMemberDetails(userID: String)
|
case roomMemberDetails(userID: String)
|
||||||
|
/// An Element Call link generated outside of a chat room.
|
||||||
case genericCallLink(url: URL)
|
case genericCallLink(url: URL)
|
||||||
|
/// The settings screen.
|
||||||
case settings
|
case settings
|
||||||
|
/// The setting screen for key backup.
|
||||||
case chatBackupSettings
|
case chatBackupSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,13 +109,24 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .room(let roomID):
|
case .room(let roomID):
|
||||||
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
||||||
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
|
||||||
|
case .childRoom(let roomID):
|
||||||
|
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||||
|
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
|
} else {
|
||||||
|
stateMachine.tryEvent(.presentChildRoom(roomID: roomID), userInfo: EventUserInfo(animated: animated))
|
||||||
|
}
|
||||||
case .roomDetails(let roomID):
|
case .roomDetails(let roomID):
|
||||||
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
||||||
stateMachine.tryEvent(.presentRoomDetails, userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoomDetails, userInfo: EventUserInfo(animated: animated))
|
||||||
case .roomList:
|
case .roomList:
|
||||||
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
||||||
case .roomMemberDetails(let userID):
|
case .roomMemberDetails(let userID):
|
||||||
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
// Always assume this will be presented on the child, external permalinks to a user aren't for a room member.
|
||||||
|
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||||
|
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
|
} else {
|
||||||
|
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
||||||
|
}
|
||||||
case .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
case .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -136,7 +147,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch (fromState, event) {
|
switch (fromState, event) {
|
||||||
case (_, .presentRoom):
|
case (_, .presentRoom):
|
||||||
return .room
|
return .room
|
||||||
case (.room, .dismissRoom):
|
case (_, .dismissRoom):
|
||||||
return .complete
|
return .complete
|
||||||
|
|
||||||
case (.initial, .presentRoomDetails):
|
case (.initial, .presentRoomDetails):
|
||||||
@@ -145,8 +156,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
return .roomDetails(isRoot: false)
|
return .roomDetails(isRoot: false)
|
||||||
case (.roomDetails, .dismissRoomDetails):
|
case (.roomDetails, .dismissRoomDetails):
|
||||||
return .room
|
return .room
|
||||||
case (.roomDetails, .dismissRoom):
|
|
||||||
return .complete
|
|
||||||
|
|
||||||
case (.roomDetails, .presentRoomDetailsEditScreen):
|
case (.roomDetails, .presentRoomDetailsEditScreen):
|
||||||
return .roomDetailsEditScreen
|
return .roomDetailsEditScreen
|
||||||
@@ -254,7 +263,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch (context.fromState, context.event, context.toState) {
|
switch (context.fromState, context.event, context.toState) {
|
||||||
case (_, .presentRoom, .room):
|
case (_, .presentRoom, .room):
|
||||||
Task { await self.presentRoom(animated: animated) }
|
Task { await self.presentRoom(animated: animated) }
|
||||||
case (.room, .dismissRoom, .complete):
|
case (_, .dismissRoom, .complete):
|
||||||
dismissFlow(animated: animated)
|
dismissFlow(animated: animated)
|
||||||
|
|
||||||
case (.initial, .presentRoomDetails, .roomDetails(let isRoot)),
|
case (.initial, .presentRoomDetails, .roomDetails(let isRoot)),
|
||||||
@@ -263,8 +272,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
Task { await self.presentRoomDetails(isRoot: isRoot, animated: animated) }
|
Task { await self.presentRoomDetails(isRoot: isRoot, animated: animated) }
|
||||||
case (.roomDetails, .dismissRoomDetails, .room):
|
case (.roomDetails, .dismissRoomDetails, .room):
|
||||||
break
|
break
|
||||||
case (.roomDetails, .dismissRoom, .complete):
|
|
||||||
dismissFlow(animated: animated)
|
|
||||||
|
|
||||||
case (.roomDetails, .presentRoomDetailsEditScreen, .roomDetailsEditScreen):
|
case (.roomDetails, .presentRoomDetailsEditScreen, .roomDetailsEditScreen):
|
||||||
presentRoomDetailsEditScreen()
|
presentRoomDetailsEditScreen()
|
||||||
|
|||||||
@@ -179,6 +179,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch appRoute {
|
switch appRoute {
|
||||||
case .room(let roomID):
|
case .room(let roomID):
|
||||||
Task { await self.handleRoomRoute(roomID: roomID, animated: animated) }
|
Task { await self.handleRoomRoute(roomID: roomID, animated: animated) }
|
||||||
|
case .childRoom(let roomID):
|
||||||
|
if let roomFlowCoordinator {
|
||||||
|
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
|
} else {
|
||||||
|
Task { await self.handleRoomRoute(roomID: roomID, animated: animated) }
|
||||||
|
}
|
||||||
case .roomDetails(let roomID):
|
case .roomDetails(let roomID):
|
||||||
if stateMachine.state.selectedRoomID == roomID {
|
if stateMachine.state.selectedRoomID == roomID {
|
||||||
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate {
|
|||||||
navigationRootCoordinator.toPresentable()
|
navigationRootCoordinator.toPresentable()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeepLink(_ url: URL) -> Bool {
|
func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool {
|
||||||
fatalError("Not implemented.")
|
fatalError("Not implemented.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class UnitTestsAppCoordinator: AppCoordinatorProtocol {
|
|||||||
AnyView(ProgressView("Running Unit Tests"))
|
AnyView(ProgressView("Running Unit Tests"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeepLink(_ url: URL) -> Bool {
|
func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool {
|
||||||
fatalError("Not implemented.")
|
fatalError("Not implemented.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,30 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomDetailsScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomDetailsScreenCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testChildRoomFlow() async throws {
|
||||||
|
await setupViewModel()
|
||||||
|
|
||||||
|
try await process(route: .room(roomID: "1"))
|
||||||
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||||
|
|
||||||
|
try await process(route: .childRoom(roomID: "2"))
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||||
|
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||||
|
|
||||||
|
try await process(route: .childRoom(roomID: "3"))
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2)
|
||||||
|
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||||
|
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||||
|
|
||||||
|
try await process(route: .roomList, expectedAction: .finished)
|
||||||
|
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||||
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests the child flow teardown in isolation of it's parent.
|
||||||
func testChildFlowTearDown() async throws {
|
func testChildFlowTearDown() async throws {
|
||||||
await setupViewModel(asChildFlow: true)
|
await setupViewModel(asChildFlow: true)
|
||||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||||
@@ -90,6 +114,24 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator, "A child room flow should leave its parent to clean up the stack.")
|
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator, "A child room flow should leave its parent to clean up the stack.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testChildRoomMemberDetails() async throws {
|
||||||
|
await setupViewModel()
|
||||||
|
|
||||||
|
try await process(route: .room(roomID: "1"))
|
||||||
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||||
|
|
||||||
|
try await process(route: .childRoom(roomID: "2"))
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||||
|
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||||
|
|
||||||
|
try await process(route: .roomMemberDetails(userID: RoomMemberProxyMock.mockMe.userID))
|
||||||
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2)
|
||||||
|
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||||
|
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomMemberDetailsScreenCoordinator)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func process(route: AppRoute) async throws {
|
private func process(route: AppRoute) async throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user