Skip to content

Commit

Permalink
Handle external links to a user. (#2690)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixlwave authored Apr 15, 2024
1 parent e0138ee commit e7af7fb
Show file tree
Hide file tree
Showing 22 changed files with 449 additions and 153 deletions.
4 changes: 4 additions & 0 deletions ElementX.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@
7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; };
7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; };
73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; };
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; };
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; };
74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */; };
Expand Down Expand Up @@ -1576,6 +1577,7 @@
71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreen.swift; sourceTree = "<group>"; };
71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = "<group>"; };
71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModelTests.swift; sourceTree = "<group>"; };
72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = "<group>"; };
7310D8DFE01AF45F0689C3AA /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = "<group>"; };
7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportFlowCoordinator.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3620,6 +3622,7 @@
EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */,
2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */,
BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */,
71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */,
4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */,
283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */,
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */,
Expand Down Expand Up @@ -5819,6 +5822,7 @@
E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */,
A1DF0E1E526A981ED6D5DF44 /* UserIndicatorControllerTests.swift in Sources */,
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */,
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */,
627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */,
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */,
21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */,
Expand Down
8 changes: 6 additions & 2 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
} else {
navigationRootCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)))
}
case .roomMemberDetails:
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
case .userProfile(let userID):
if isExternalURL {
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
} else {
userSessionFlowCoordinator?.handleAppRoute(.roomMemberDetails(userID: userID), animated: true)
}
case .room(let roomID):
// check that the room is joined here, if not use a joinRoom route.
if isExternalURL {
Expand Down
2 changes: 2 additions & 0 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ final class AppSettings {
let encryptionURL: URL = "https://element.io/help#encryption"
/// A URL where users can go read more about the chat backup.
let chatBackupDetailsURL: URL = "https://element.io/help#encryption5"
/// Any domains that Element web may be hosted on - used for handling links.
let elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"]

@UserPreference(key: UserDefaultsKeys.appAppearance, defaultValue: .system, storageType: .userDefaults(store))
var appAppearance: AppAppearance
Expand Down
49 changes: 37 additions & 12 deletions ElementX/Sources/Application/Navigation/AppRoutes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ enum AppRoute: Equatable {
/// The information about a particular room.
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)
/// The profile of a matrix user (outside of a room).
case userProfile(userID: String)
/// An Element Call link generated outside of a chat room.
case genericCallLink(url: URL)
/// The settings screen.
Expand All @@ -45,6 +46,7 @@ struct AppRouteURLParser {
init(appSettings: AppSettings) {
urlParsers = [
MatrixPermalinkParser(),
ElementWebURLParser(domains: appSettings.elementWebHosts),
OIDCCallbackURLParser(appSettings: appSettings),
ElementCallURLParser()
]
Expand All @@ -64,9 +66,6 @@ struct AppRouteURLParser {
/// Represents a type that can parse a `URL` into an `AppRoute`.
///
/// The following Universal Links are missing parsers.
/// - app.element.io
/// - staging.element.io
/// - develop.element.io
/// - mobile.element.io
protocol URLParser {
func route(from url: URL) -> AppRoute?
Expand Down Expand Up @@ -123,17 +122,43 @@ struct ElementCallURLParser: URLParser {

struct MatrixPermalinkParser: URLParser {
func route(from url: URL) -> AppRoute? {
guard let matrixEntity = parseMatrixEntityFrom(uri: url.absoluteString) else {
switch parseMatrixEntityFrom(uri: url.absoluteString)?.id {
case .room(let id):
return .room(roomID: id)
case .user(let id):
return .userProfile(userID: id)
default:
return nil
}
}
}

struct ElementWebURLParser: URLParser {
let domains: [String]
let paths = ["room", "user"]

private let permalinkParser = MatrixPermalinkParser()

func route(from url: URL) -> AppRoute? {
guard let matrixToURL = buildMatrixToURL(from: url) else { return nil }
return permalinkParser.route(from: matrixToURL)
}

private func buildMatrixToURL(from url: URL) -> URL? {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return url
}

switch matrixEntity.id {
case .user(let userID):
return .roomMemberDetails(userID: userID)
case .room(let roomID):
return .room(roomID: roomID)
default:
return nil
for domain in domains where domain == url.host {
components.host = "matrix.to"
for path in paths {
components.fragment?.replace("/\(path)", with: "")
}

guard let matrixToURL = components.url else { continue }
return matrixToURL
}

return url
}
}
50 changes: 12 additions & 38 deletions ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} else {
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
}
case .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
case .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
break
}
}
Expand Down Expand Up @@ -875,16 +875,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomProxy: roomProxy,
clientProxy: userSession.clientProxy,
mediaProvider: userSession.mediaProvider,
userIndicatorController: userIndicatorController)
userIndicatorController: userIndicatorController,
analytics: analytics)
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params)

coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .openUserProfile:
stateMachine.tryEvent(.presentUserProfile(userID: userID))
case .openDirectChat(let displayName):
openDirectChat(with: userID, displayName: displayName)
case .openDirectChat(let roomID):
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
}
}
.store(in: &cancellables)
Expand All @@ -896,16 +897,20 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {

private func replaceRoomMemberDetailsWithUserProfile(userID: String) {
let parameters = UserProfileScreenCoordinatorParameters(userID: userID,
isPresentedModally: false,
clientProxy: userSession.clientProxy,
mediaProvider: userSession.mediaProvider,
userIndicatorController: userIndicatorController)
userIndicatorController: userIndicatorController,
analytics: analytics)
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }

switch action {
case .openDirectChat(let displayName):
openDirectChat(with: userID, displayName: displayName)
case .openDirectChat(let roomID):
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
case .dismiss:
break // Not supported when pushed.
}
}
.store(in: &cancellables)
Expand All @@ -917,37 +922,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}

private func openDirectChat(with userID: String, displayName: String?) {
let loadingIndicatorIdentifier = "OpenDirectChatLoadingIndicator"

userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier,
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false),
title: L10n.commonLoading,
persistent: true))

Task { [weak self] in
guard let self else { return }

let currentDirectRoom = await userSession.clientProxy.directRoomForUserID(userID)
switch currentDirectRoom {
case .success(.some(let roomID)):
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
case .success(nil):
switch await userSession.clientProxy.createDirectRoom(with: userID, expectedRoomName: displayName) {
case .success(let roomID):
analytics.trackCreatedRoom(isDM: true)
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
case .failure:
userIndicatorController.alertInfo = .init(id: UUID())
}
case .failure:
userIndicatorController.alertInfo = .init(id: UUID())
}

userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
}
}

private func presentMessageForwarding(for itemID: TimelineItemIdentifier) {
guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider, let eventID = itemID.eventID else {
fatalError()
Expand Down
39 changes: 39 additions & 0 deletions ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
case .roomList, .roomMemberDetails:
self.roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
case .userProfile(let userID):
stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated))
case .genericCallLink(let url):
self.navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
case .oidcCallback:
Expand Down Expand Up @@ -302,6 +304,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentRoomDirectorySearch()
case (.roomDirectorySearchScreen, .dismissedRoomDirectorySearchScreen, .roomList):
dismissRoomDirectorySearch()

case (_, .showUserProfileScreen(let userID), .userProfileScreen):
presentUserProfileScreen(userID: userID, animated: animated)
case (.userProfileScreen, .dismissedUserProfileScreen, .roomList):
break

default:
fatalError("Unknown transition: \(context)")
Expand Down Expand Up @@ -654,4 +661,36 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private func dismissRoomDirectorySearch() {
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
}

// MARK: User Profile

private func presentUserProfileScreen(userID: String, animated: Bool) {
clearRoute(animated: animated)

let navigationStackCoordinator = NavigationStackCoordinator()
let parameters = UserProfileScreenCoordinatorParameters(userID: userID,
isPresentedModally: true,
clientProxy: userSession.clientProxy,
mediaProvider: userSession.mediaProvider,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: analytics)
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }

switch action {
case .openDirectChat(let roomID):
navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
case .dismiss:
navigationSplitCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)

navigationStackCoordinator.setRootCoordinator(coordinator, animated: false)
navigationSplitCoordinator.setSheetCoordinator(navigationStackCoordinator, animated: animated) { [weak self] in
self?.stateMachine.processEvent(.dismissedUserProfileScreen)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class UserSessionFlowCoordinatorStateMachine {
/// Showing the home screen. The `selectedRoomID` represents the timeline shown on the detail panel (if any)
case roomList(selectedRoomID: String?)

/// Showing the session verification flows
/// Showing the feedback screen.
case feedbackScreen(selectedRoomID: String?)

/// Showing the settings screen
Expand All @@ -45,10 +45,13 @@ class UserSessionFlowCoordinatorStateMachine {
/// Showing Room Directory Search screen
case roomDirectorySearchScreen(selectedRoomID: String?)

/// Showing the user profile screen. This screen clears the navigation.
case userProfileScreen

/// The selected room ID from the state if available.
var selectedRoomID: String? {
switch self {
case .initial:
case .initial, .userProfileScreen:
nil
case .roomList(let selectedRoomID),
.feedbackScreen(let selectedRoomID),
Expand Down Expand Up @@ -102,9 +105,15 @@ class UserSessionFlowCoordinatorStateMachine {
/// Logout has been cancelled
case dismissedLogoutConfirmationScreen

/// Request presentation of the room directory search screen.
case showRoomDirectorySearchScreen

/// The room directory search screen has been dismissed.
case dismissedRoomDirectorySearchScreen

/// Request presentation of the user profile screen.
case showUserProfileScreen(userID: String)
/// The user profile screen has been dismissed.
case dismissedUserProfileScreen
}

private let stateMachine: StateMachine<State, Event>
Expand Down Expand Up @@ -169,6 +178,11 @@ class UserSessionFlowCoordinatorStateMachine {
return .roomDirectorySearchScreen(selectedRoomID: selectedRoomID)
case (.roomDirectorySearchScreen(let selectedRoomID), .dismissedRoomDirectorySearchScreen):
return .roomList(selectedRoomID: selectedRoomID)

case (_, .showUserProfileScreen):
return .userProfileScreen
case (.userProfileScreen, .dismissedUserProfileScreen):
return .roomList(selectedRoomID: nil)

default:
return nil
Expand Down
Loading

0 comments on commit e7af7fb

Please sign in to comment.