mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Handle alias room permalinks
This commit is contained in:
committed by
Stefan Ceriu
parent
8ba544bc44
commit
69ffd3be46
@@ -190,16 +190,21 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
}
|
}
|
||||||
case .userProfile(let userID):
|
case .userProfile(let userID):
|
||||||
if isExternalURL {
|
if isExternalURL {
|
||||||
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
|
handleAppRoute(route)
|
||||||
} else {
|
} else {
|
||||||
userSessionFlowCoordinator?.handleAppRoute(.roomMemberDetails(userID: userID), animated: true)
|
handleAppRoute(.roomMemberDetails(userID: userID))
|
||||||
}
|
}
|
||||||
case .room(let roomID):
|
case .room(let roomID):
|
||||||
// check that the room is joined here, if not use a joinRoom route.
|
|
||||||
if isExternalURL {
|
if isExternalURL {
|
||||||
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
|
handleAppRoute(route)
|
||||||
} else {
|
} else {
|
||||||
userSessionFlowCoordinator?.handleAppRoute(.childRoom(roomID: roomID), animated: true)
|
handleAppRoute(.childRoom(roomID: roomID))
|
||||||
|
}
|
||||||
|
case .roomAlias(let alias):
|
||||||
|
if isExternalURL {
|
||||||
|
handleAppRoute(route)
|
||||||
|
} else {
|
||||||
|
handleAppRoute(.childRoomAlias(alias))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -255,7 +260,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle here the account switching when available
|
|
||||||
handleAppRoute(.room(roomID: roomID))
|
handleAppRoute(.room(roomID: roomID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,12 @@ enum AppRoute: Equatable {
|
|||||||
case roomList
|
case roomList
|
||||||
/// A room, shown as the root of the stack (popping any child rooms).
|
/// A room, shown as the root of the stack (popping any child rooms).
|
||||||
case room(roomID: String)
|
case room(roomID: String)
|
||||||
|
/// A room, shown as the root of the stack (popping any child rooms).
|
||||||
|
case roomAlias(String)
|
||||||
/// A room, pushed as a child of any existing rooms on the stack.
|
/// A room, pushed as a child of any existing rooms on the stack.
|
||||||
case childRoom(roomID: String)
|
case childRoom(roomID: String)
|
||||||
|
/// A room, pushed as a child of any existing rooms on the stack.
|
||||||
|
case childRoomAlias(String)
|
||||||
/// The information about a particular room.
|
/// The information about a particular room.
|
||||||
case roomDetails(roomID: String)
|
case roomDetails(roomID: String)
|
||||||
/// The profile of a member within the current room.
|
/// The profile of a member within the current room.
|
||||||
@@ -125,6 +129,8 @@ struct MatrixPermalinkParser: URLParser {
|
|||||||
switch parseMatrixEntityFrom(uri: url.absoluteString)?.id {
|
switch parseMatrixEntityFrom(uri: url.absoluteString)?.id {
|
||||||
case .room(let id):
|
case .room(let id):
|
||||||
return .room(roomID: id)
|
return .room(roomID: id)
|
||||||
|
case .roomAlias(let alias):
|
||||||
|
return .roomAlias(alias)
|
||||||
case .user(let id):
|
case .user(let id):
|
||||||
return .userProfile(userID: id)
|
return .userProfile(userID: id)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
} else {
|
} else {
|
||||||
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
||||||
}
|
}
|
||||||
case .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
case .roomAlias, .childRoomAlias, .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,37 +175,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
// MARK: - FlowCoordinatorProtocol
|
// MARK: - FlowCoordinatorProtocol
|
||||||
|
|
||||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||||
clearPresentedSheets(animated: animated) { [weak self] in
|
Task {
|
||||||
guard let self else { return }
|
await asyncHandleAppRoute(appRoute, animated: animated)
|
||||||
|
|
||||||
switch appRoute {
|
|
||||||
case .room(let roomID):
|
|
||||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
|
||||||
case .childRoom(let roomID):
|
|
||||||
if let roomFlowCoordinator {
|
|
||||||
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
|
||||||
} else {
|
|
||||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
|
||||||
}
|
|
||||||
case .roomDetails(let roomID):
|
|
||||||
if stateMachine.state.selectedRoomID == roomID {
|
|
||||||
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
|
||||||
} else {
|
|
||||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated))
|
|
||||||
}
|
|
||||||
case .roomList:
|
|
||||||
roomFlowCoordinator?.clearRoute(animated: animated)
|
|
||||||
case .roomMemberDetails:
|
|
||||||
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
|
||||||
case .userProfile(let userID):
|
|
||||||
stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated))
|
|
||||||
case .genericCallLink(let url):
|
|
||||||
navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
|
|
||||||
case .oidcCallback:
|
|
||||||
break
|
|
||||||
case .settings, .chatBackupSettings:
|
|
||||||
settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +186,60 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
func asyncHandleAppRoute(_ appRoute: AppRoute, animated: Bool) async {
|
||||||
|
showLoadingIndicator(delay: .seconds(0.25))
|
||||||
|
defer {
|
||||||
|
hideLoadingIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
await clearPresentedSheets(animated: animated)
|
||||||
|
|
||||||
|
switch appRoute {
|
||||||
|
case .room(let roomID):
|
||||||
|
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||||
|
case .roomAlias(let alias):
|
||||||
|
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||||
|
case .childRoom(let roomID):
|
||||||
|
if let roomFlowCoordinator {
|
||||||
|
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
|
} else {
|
||||||
|
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||||
|
}
|
||||||
|
case .childRoomAlias(let alias):
|
||||||
|
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let roomFlowCoordinator {
|
||||||
|
roomFlowCoordinator.handleAppRoute(.childRoom(roomID: roomID), animated: animated)
|
||||||
|
} else {
|
||||||
|
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||||
|
}
|
||||||
|
case .roomDetails(let roomID):
|
||||||
|
if stateMachine.state.selectedRoomID == roomID {
|
||||||
|
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
||||||
|
} else {
|
||||||
|
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated))
|
||||||
|
}
|
||||||
|
case .roomList:
|
||||||
|
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||||
|
case .roomMemberDetails:
|
||||||
|
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
||||||
|
case .userProfile(let userID):
|
||||||
|
stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated))
|
||||||
|
case .genericCallLink(let url):
|
||||||
|
navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
|
||||||
|
case .oidcCallback:
|
||||||
|
break
|
||||||
|
case .settings, .chatBackupSettings:
|
||||||
|
settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func attemptStartingOnboarding() {
|
func attemptStartingOnboarding() {
|
||||||
if onboardingFlowCoordinator.shouldStart {
|
if onboardingFlowCoordinator.shouldStart {
|
||||||
clearRoute(animated: false)
|
clearRoute(animated: false)
|
||||||
@@ -222,18 +247,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func clearPresentedSheets(animated: Bool, completion: @escaping () -> Void) {
|
private func clearPresentedSheets(animated: Bool) async {
|
||||||
if navigationSplitCoordinator.sheetCoordinator == nil {
|
if navigationSplitCoordinator.sheetCoordinator == nil {
|
||||||
completion()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationSplitCoordinator.setSheetCoordinator(nil, animated: animated)
|
navigationSplitCoordinator.setSheetCoordinator(nil, animated: animated)
|
||||||
|
|
||||||
// Prevents system crashes when presenting a sheet if another one was already shown
|
// Prevents system crashes when presenting a sheet if another one was already shown
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
try? await Task.sleep(for: .seconds(0.25))
|
||||||
completion()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupStateMachine() {
|
private func setupStateMachine() {
|
||||||
@@ -675,4 +697,20 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
self?.stateMachine.processEvent(.dismissedUserProfileScreen)
|
self?.stateMachine.processEvent(.dismissedUserProfileScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Toasts and loading indicators
|
||||||
|
|
||||||
|
private static let loadingIndicatorIdentifier = "\(UserSessionFlowCoordinator.self)-Loading"
|
||||||
|
|
||||||
|
private func showLoadingIndicator(delay: Duration? = nil) {
|
||||||
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
type: .modal,
|
||||||
|
title: L10n.commonLoading,
|
||||||
|
persistent: true),
|
||||||
|
delay: delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideLoadingIndicator() {
|
||||||
|
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3361,6 +3361,74 @@ class ClientProxyMock: ClientProxyProtocol {
|
|||||||
return roomDirectorySearchProxyReturnValue
|
return roomDirectorySearchProxyReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - resolveRoomAlias
|
||||||
|
|
||||||
|
var resolveRoomAliasUnderlyingCallsCount = 0
|
||||||
|
var resolveRoomAliasCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return resolveRoomAliasUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = resolveRoomAliasUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
resolveRoomAliasUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
resolveRoomAliasUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var resolveRoomAliasCalled: Bool {
|
||||||
|
return resolveRoomAliasCallsCount > 0
|
||||||
|
}
|
||||||
|
var resolveRoomAliasReceivedAlias: String?
|
||||||
|
var resolveRoomAliasReceivedInvocations: [String] = []
|
||||||
|
|
||||||
|
var resolveRoomAliasUnderlyingReturnValue: String?
|
||||||
|
var resolveRoomAliasReturnValue: String? {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return resolveRoomAliasUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: String?? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = resolveRoomAliasUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
resolveRoomAliasUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
resolveRoomAliasUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var resolveRoomAliasClosure: ((String) async -> String?)?
|
||||||
|
|
||||||
|
func resolveRoomAlias(_ alias: String) async -> String? {
|
||||||
|
resolveRoomAliasCallsCount += 1
|
||||||
|
resolveRoomAliasReceivedAlias = alias
|
||||||
|
resolveRoomAliasReceivedInvocations.append(alias)
|
||||||
|
if let resolveRoomAliasClosure = resolveRoomAliasClosure {
|
||||||
|
return await resolveRoomAliasClosure(alias)
|
||||||
|
} else {
|
||||||
|
return resolveRoomAliasReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
//MARK: - ignoreUser
|
//MARK: - ignoreUser
|
||||||
|
|
||||||
var ignoreUserUnderlyingCallsCount = 0
|
var ignoreUserUnderlyingCallsCount = 0
|
||||||
|
|||||||
@@ -597,6 +597,15 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
RoomDirectorySearchProxy(roomDirectorySearch: client.roomDirectorySearch())
|
RoomDirectorySearchProxy(roomDirectorySearch: client.roomDirectorySearch())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveRoomAlias(_ alias: String) async -> String? {
|
||||||
|
do {
|
||||||
|
return try await client.resolveRoomAlias(roomAlias: alias)
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed resolving room alias: \(alias) with error: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Ignored users
|
// MARK: Ignored users
|
||||||
|
|
||||||
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {
|
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
func profile(for userID: String) async -> Result<UserProfileProxy, ClientProxyError>
|
func profile(for userID: String) async -> Result<UserProfileProxy, ClientProxyError>
|
||||||
|
|
||||||
func roomDirectorySearchProxy() -> RoomDirectorySearchProxyProtocol
|
func roomDirectorySearchProxy() -> RoomDirectorySearchProxyProtocol
|
||||||
|
|
||||||
|
func resolveRoomAlias(_ alias: String) async -> String?
|
||||||
|
|
||||||
// MARK: - Ignored users
|
// MARK: - Ignored users
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import Combine
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class UserSessionFlowCoordinatorTests: XCTestCase {
|
class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||||
|
var clientProxy: ClientProxyMock!
|
||||||
var userSessionFlowCoordinator: UserSessionFlowCoordinator!
|
var userSessionFlowCoordinator: UserSessionFlowCoordinator!
|
||||||
var navigationRootCoordinator: NavigationRootCoordinator!
|
var navigationRootCoordinator: NavigationRootCoordinator!
|
||||||
var notificationManager: NotificationManagerMock!
|
var notificationManager: NotificationManagerMock!
|
||||||
@@ -33,7 +34,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
|||||||
|
|
||||||
override func setUp() async throws {
|
override func setUp() async throws {
|
||||||
cancellables.removeAll()
|
cancellables.removeAll()
|
||||||
let clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
||||||
let mediaProvider = MockMediaProvider()
|
let mediaProvider = MockMediaProvider()
|
||||||
let voiceMessageMediaManager = VoiceMessageMediaManagerMock()
|
let voiceMessageMediaManager = VoiceMessageMediaManagerMock()
|
||||||
let userSession = MockUserSession(clientProxy: clientProxy,
|
let userSession = MockUserSession(clientProxy: clientProxy,
|
||||||
@@ -84,6 +85,34 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
|||||||
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, ["1", "1", "2"])
|
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, ["1", "1", "2"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testRoomAliasPresentation() async throws {
|
||||||
|
clientProxy.resolveRoomAliasReturnValue = "1"
|
||||||
|
|
||||||
|
try await process(route: .roomAlias("#alias:matrix.org"), expectedState: .roomList(selectedRoomID: "1"))
|
||||||
|
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
XCTAssertNotNil(detailCoordinator)
|
||||||
|
|
||||||
|
try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil))
|
||||||
|
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||||
|
XCTAssertNil(detailCoordinator)
|
||||||
|
|
||||||
|
try await process(route: .room(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
|
||||||
|
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
XCTAssertNotNil(detailCoordinator)
|
||||||
|
|
||||||
|
clientProxy.resolveRoomAliasReturnValue = "2"
|
||||||
|
|
||||||
|
try await process(route: .room(roomID: "2"), expectedState: .roomList(selectedRoomID: "2"))
|
||||||
|
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
XCTAssertNotNil(detailCoordinator)
|
||||||
|
|
||||||
|
try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil))
|
||||||
|
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||||
|
XCTAssertNil(detailCoordinator)
|
||||||
|
|
||||||
|
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, ["1", "1", "2"])
|
||||||
|
}
|
||||||
|
|
||||||
func testRoomDetailsPresentation() async throws {
|
func testRoomDetailsPresentation() async throws {
|
||||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
|
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
|
||||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||||
|
|||||||
Reference in New Issue
Block a user