Handle alias room permalinks

This commit is contained in:
Stefan Ceriu 2024-04-22 18:05:18 +03:00 committed by Stefan Ceriu
parent 8ba544bc44
commit 69ffd3be46
8 changed files with 200 additions and 44 deletions

View File

@ -190,16 +190,21 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
case .userProfile(let userID):
if isExternalURL {
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
handleAppRoute(route)
} else {
userSessionFlowCoordinator?.handleAppRoute(.roomMemberDetails(userID: userID), animated: true)
handleAppRoute(.roomMemberDetails(userID: userID))
}
case .room(let roomID):
// check that the room is joined here, if not use a joinRoom route.
if isExternalURL {
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
handleAppRoute(route)
} 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:
break
@ -255,7 +260,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
return
}
// Handle here the account switching when available
handleAppRoute(.room(roomID: roomID))
}

View File

@ -24,8 +24,12 @@ enum AppRoute: Equatable {
case roomList
/// A room, shown as the root of the stack (popping any child rooms).
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.
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.
case roomDetails(roomID: String)
/// The profile of a member within the current room.
@ -125,6 +129,8 @@ struct MatrixPermalinkParser: URLParser {
switch parseMatrixEntityFrom(uri: url.absoluteString)?.id {
case .room(let id):
return .room(roomID: id)
case .roomAlias(let alias):
return .roomAlias(alias)
case .user(let id):
return .userProfile(userID: id)
default:

View File

@ -138,7 +138,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} else {
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
}
}

View File

@ -175,11 +175,33 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - FlowCoordinatorProtocol
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
clearPresentedSheets(animated: animated) { [weak self] in
guard let self else { return }
Task {
await asyncHandleAppRoute(appRoute, animated: animated)
}
}
func clearRoute(animated: Bool) {
roomFlowCoordinator?.clearRoute(animated: animated)
}
// 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 {
@ -187,6 +209,16 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
} 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)
@ -207,13 +239,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
}
}
}
func clearRoute(animated: Bool) {
roomFlowCoordinator?.clearRoute(animated: animated)
}
// MARK: - Private
func attemptStartingOnboarding() {
if onboardingFlowCoordinator.shouldStart {
@ -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 {
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()
}
try? await Task.sleep(for: .seconds(0.25))
}
private func setupStateMachine() {
@ -675,4 +697,20 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
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)
}
}

View File

@ -3361,6 +3361,74 @@ class ClientProxyMock: ClientProxyProtocol {
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
var ignoreUserUnderlyingCallsCount = 0

View File

@ -597,6 +597,15 @@ class ClientProxy: ClientProxyProtocol {
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
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {

View File

@ -161,6 +161,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func roomDirectorySearchProxy() -> RoomDirectorySearchProxyProtocol
func resolveRoomAlias(_ alias: String) async -> String?
// MARK: - Ignored users
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError>

View File

@ -21,6 +21,7 @@ import Combine
@MainActor
class UserSessionFlowCoordinatorTests: XCTestCase {
var clientProxy: ClientProxyMock!
var userSessionFlowCoordinator: UserSessionFlowCoordinator!
var navigationRootCoordinator: NavigationRootCoordinator!
var notificationManager: NotificationManagerMock!
@ -33,7 +34,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
override func setUp() async throws {
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 voiceMessageMediaManager = VoiceMessageMediaManagerMock()
let userSession = MockUserSession(clientProxy: clientProxy,
@ -84,6 +85,34 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
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 {
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)