mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
Handle external links to a user. (#2690)
This commit is contained in:
parent
e0138eeb35
commit
e7af7fb59c
@ -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 */; };
|
||||
@ -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>"; };
|
||||
@ -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 */,
|
||||
@ -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 */,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
@ -45,6 +46,7 @@ struct AppRouteURLParser {
|
||||
init(appSettings: AppSettings) {
|
||||
urlParsers = [
|
||||
MatrixPermalinkParser(),
|
||||
ElementWebURLParser(domains: appSettings.elementWebHosts),
|
||||
OIDCCallbackURLParser(appSettings: appSettings),
|
||||
ElementCallURLParser()
|
||||
]
|
||||
@ -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?
|
||||
@ -123,17 +122,43 @@ struct ElementCallURLParser: URLParser {
|
||||
|
||||
struct MatrixPermalinkParser: URLParser {
|
||||
func route(from url: URL) -> AppRoute? {
|
||||
guard let matrixEntity = parseMatrixEntityFrom(uri: url.absoluteString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch matrixEntity.id {
|
||||
case .user(let userID):
|
||||
return .roomMemberDetails(userID: userID)
|
||||
case .room(let roomID):
|
||||
return .room(roomID: roomID)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
@ -875,7 +875,8 @@ 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
|
||||
@ -883,8 +884,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
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)
|
||||
@ -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)
|
||||
@ -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()
|
||||
|
@ -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:
|
||||
@ -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)")
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -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),
|
||||
@ -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>
|
||||
@ -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
|
||||
|
@ -2077,6 +2077,74 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
return accountURLActionReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - createDirectRoomIfNeeded
|
||||
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameUnderlyingCallsCount = 0
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return createDirectRoomIfNeededWithExpectedRoomNameUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = createDirectRoomIfNeededWithExpectedRoomNameUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
createDirectRoomIfNeededWithExpectedRoomNameUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
createDirectRoomIfNeededWithExpectedRoomNameUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameCalled: Bool {
|
||||
return createDirectRoomIfNeededWithExpectedRoomNameCallsCount > 0
|
||||
}
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameReceivedArguments: (userID: String, expectedRoomName: String?)?
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameReceivedInvocations: [(userID: String, expectedRoomName: String?)] = []
|
||||
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameUnderlyingReturnValue: Result<(roomID: String, isNewRoom: Bool), ClientProxyError>!
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameReturnValue: Result<(roomID: String, isNewRoom: Bool), ClientProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return createDirectRoomIfNeededWithExpectedRoomNameUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<(roomID: String, isNewRoom: Bool), ClientProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = createDirectRoomIfNeededWithExpectedRoomNameUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
createDirectRoomIfNeededWithExpectedRoomNameUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
createDirectRoomIfNeededWithExpectedRoomNameUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var createDirectRoomIfNeededWithExpectedRoomNameClosure: ((String, String?) async -> Result<(roomID: String, isNewRoom: Bool), ClientProxyError>)?
|
||||
|
||||
func createDirectRoomIfNeeded(with userID: String, expectedRoomName: String?) async -> Result<(roomID: String, isNewRoom: Bool), ClientProxyError> {
|
||||
createDirectRoomIfNeededWithExpectedRoomNameCallsCount += 1
|
||||
createDirectRoomIfNeededWithExpectedRoomNameReceivedArguments = (userID: userID, expectedRoomName: expectedRoomName)
|
||||
createDirectRoomIfNeededWithExpectedRoomNameReceivedInvocations.append((userID: userID, expectedRoomName: expectedRoomName))
|
||||
if let createDirectRoomIfNeededWithExpectedRoomNameClosure = createDirectRoomIfNeededWithExpectedRoomNameClosure {
|
||||
return await createDirectRoomIfNeededWithExpectedRoomNameClosure(userID, expectedRoomName)
|
||||
} else {
|
||||
return createDirectRoomIfNeededWithExpectedRoomNameReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - directRoomForUserID
|
||||
|
||||
var directRoomForUserIDUnderlyingCallsCount = 0
|
||||
|
@ -23,11 +23,12 @@ struct RoomMemberDetailsScreenCoordinatorParameters {
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
enum RoomMemberDetailsScreenCoordinatorAction {
|
||||
case openUserProfile
|
||||
case openDirectChat(displayName: String?)
|
||||
case openDirectChat(roomID: String)
|
||||
}
|
||||
|
||||
final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
@ -45,7 +46,8 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
roomProxy: parameters.roomProxy,
|
||||
clientProxy: parameters.clientProxy,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
analytics: parameters.analytics)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@ -55,8 +57,8 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .openUserProfile:
|
||||
actionsSubject.send(.openUserProfile)
|
||||
case .openDirectChat(let displayName):
|
||||
actionsSubject.send(.openDirectChat(displayName: displayName))
|
||||
case .openDirectChat(let roomID):
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
@ -18,7 +18,7 @@ import Foundation
|
||||
|
||||
enum RoomMemberDetailsScreenViewModelAction {
|
||||
case openUserProfile
|
||||
case openDirectChat(displayName: String?)
|
||||
case openDirectChat(roomID: String)
|
||||
}
|
||||
|
||||
struct RoomMemberDetailsScreenViewState: BindableState {
|
||||
@ -86,5 +86,6 @@ enum RoomMemberDetailsScreenViewAction {
|
||||
}
|
||||
|
||||
enum RoomMemberDetailsScreenError: Hashable {
|
||||
case failedOpeningDirectChat
|
||||
case unknown
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
|
||||
private var actionsSubject: PassthroughSubject<RoomMemberDetailsScreenViewModelAction, Never> = .init()
|
||||
|
||||
@ -37,11 +38,13 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
roomProxy: RoomProxyProtocol,
|
||||
clientProxy: ClientProxyProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
analytics: AnalyticsService) {
|
||||
self.roomProxy = roomProxy
|
||||
self.clientProxy = clientProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.analytics = analytics
|
||||
|
||||
let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, bindings: .init())
|
||||
|
||||
@ -85,13 +88,9 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
case .unignoreConfirmed:
|
||||
Task { await unignoreUser() }
|
||||
case .displayAvatar:
|
||||
displayFullScreenAvatar()
|
||||
Task { await displayFullScreenAvatar() }
|
||||
case .openDirectChat:
|
||||
guard let roomMemberProxy else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
actionsSubject.send(.openDirectChat(displayName: roomMemberProxy.displayName))
|
||||
Task { await openDirectChat() }
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +144,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
}
|
||||
}
|
||||
|
||||
private func displayFullScreenAvatar() {
|
||||
private func displayFullScreenAvatar() async {
|
||||
guard let roomMemberProxy else {
|
||||
fatalError()
|
||||
}
|
||||
@ -156,16 +155,32 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
|
||||
let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator"
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
|
||||
Task {
|
||||
defer {
|
||||
userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
||||
}
|
||||
defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) }
|
||||
|
||||
// We don't actually know the mime type here, assume it's an image.
|
||||
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) {
|
||||
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName)
|
||||
// We don't actually know the mime type here, assume it's an image.
|
||||
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) {
|
||||
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
private func openDirectChat() async {
|
||||
guard let roomMemberProxy else { fatalError() }
|
||||
|
||||
let loadingIndicatorIdentifier = "openDirectChatLoadingIndicator"
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier,
|
||||
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false),
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) }
|
||||
|
||||
switch await clientProxy.createDirectRoomIfNeeded(with: roomMemberProxy.userID, expectedRoomName: roomMemberProxy.displayName) {
|
||||
case .success((let roomID, let isNewRoom)):
|
||||
if isNewRoom {
|
||||
analytics.trackCreatedRoom(isDM: true)
|
||||
}
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
case .failure:
|
||||
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,41 +114,9 @@ struct RoomMemberDetailsScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let otherUserViewModel = {
|
||||
let member = RoomMemberProxyMock.mockDan
|
||||
let roomProxyMock = RoomProxyMock(with: .init(name: ""))
|
||||
roomProxyMock.getMemberUserIDReturnValue = .success(member)
|
||||
|
||||
return RoomMemberDetailsScreenViewModel(userID: member.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}()
|
||||
|
||||
static let accountOwnerViewModel = {
|
||||
let member = RoomMemberProxyMock.mockMe
|
||||
let roomProxyMock = RoomProxyMock(with: .init(name: ""))
|
||||
roomProxyMock.getMemberUserIDReturnValue = .success(member)
|
||||
|
||||
return RoomMemberDetailsScreenViewModel(userID: member.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}()
|
||||
|
||||
static let ignoredUserViewModel = {
|
||||
let member = RoomMemberProxyMock.mockIgnored
|
||||
let roomProxyMock = RoomProxyMock(with: .init(name: ""))
|
||||
roomProxyMock.getMemberUserIDReturnValue = .success(member)
|
||||
|
||||
return RoomMemberDetailsScreenViewModel(userID: member.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}()
|
||||
static let otherUserViewModel = makeViewModel(member: .mockDan)
|
||||
static let accountOwnerViewModel = makeViewModel(member: .mockMe)
|
||||
static let ignoredUserViewModel = makeViewModel(member: .mockIgnored)
|
||||
|
||||
static var previews: some View {
|
||||
RoomMemberDetailsScreen(context: otherUserViewModel.context)
|
||||
@ -161,4 +129,16 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
.previewDisplayName("Ignored User")
|
||||
.snapshot(delay: 0.25)
|
||||
}
|
||||
|
||||
static func makeViewModel(member: RoomMemberProxyMock) -> RoomMemberDetailsScreenViewModel {
|
||||
let roomProxyMock = RoomProxyMock(with: .init(name: ""))
|
||||
roomProxyMock.getMemberUserIDReturnValue = .success(member)
|
||||
|
||||
return RoomMemberDetailsScreenViewModel(userID: member.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
}
|
||||
|
@ -19,13 +19,16 @@ import SwiftUI
|
||||
|
||||
struct UserProfileScreenCoordinatorParameters {
|
||||
let userID: String
|
||||
let isPresentedModally: Bool
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
|
||||
enum UserProfileScreenCoordinatorAction {
|
||||
case openDirectChat(displayName: String?)
|
||||
case openDirectChat(roomID: String)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class UserProfileScreenCoordinator: CoordinatorProtocol {
|
||||
@ -40,9 +43,11 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: UserProfileScreenCoordinatorParameters) {
|
||||
viewModel = UserProfileScreenViewModel(userID: parameters.userID,
|
||||
isPresentedModally: parameters.isPresentedModally,
|
||||
clientProxy: parameters.clientProxy,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
analytics: parameters.analytics)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@ -50,8 +55,10 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .openDirectChat(let displayName):
|
||||
actionsSubject.send(.openDirectChat(displayName: displayName))
|
||||
case .openDirectChat(let roomID):
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
@ -17,12 +17,14 @@
|
||||
import Foundation
|
||||
|
||||
enum UserProfileScreenViewModelAction {
|
||||
case openDirectChat(displayName: String?)
|
||||
case openDirectChat(roomID: String)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct UserProfileScreenViewState: BindableState {
|
||||
let userID: String
|
||||
let isOwnUser: Bool
|
||||
let isPresentedModally: Bool
|
||||
|
||||
var userProfile: UserProfileProxy?
|
||||
var permalink: URL?
|
||||
@ -40,8 +42,10 @@ struct UserProfileScreenViewStateBindings {
|
||||
enum UserProfileScreenViewAction {
|
||||
case displayAvatar
|
||||
case openDirectChat
|
||||
case dismiss
|
||||
}
|
||||
|
||||
enum UserProfileScreenError: Hashable {
|
||||
case failedOpeningDirectChat
|
||||
case unknown
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
|
||||
private var actionsSubject: PassthroughSubject<UserProfileScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<UserProfileScreenViewModelAction, Never> {
|
||||
@ -31,23 +32,27 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
}
|
||||
|
||||
init(userID: String,
|
||||
isPresentedModally: Bool,
|
||||
clientProxy: ClientProxyProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
analytics: AnalyticsService) {
|
||||
self.clientProxy = clientProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.analytics = analytics
|
||||
|
||||
let initialViewState = UserProfileScreenViewState(userID: userID,
|
||||
isOwnUser: userID == clientProxy.userID,
|
||||
isPresentedModally: isPresentedModally,
|
||||
bindings: .init())
|
||||
|
||||
super.init(initialViewState: initialViewState, imageProvider: mediaProvider)
|
||||
|
||||
showMemberLoadingIndicator()
|
||||
showLoadingIndicator(allowsInteraction: true)
|
||||
Task {
|
||||
defer {
|
||||
hideMemberLoadingIndicator()
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
switch await clientProxy.profile(for: userID) {
|
||||
@ -67,37 +72,49 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
// Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier.
|
||||
state.bindings.mediaPreviewItem = nil
|
||||
|
||||
hideMemberLoadingIndicator()
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
override func process(viewAction: UserProfileScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .displayAvatar:
|
||||
displayFullScreenAvatar()
|
||||
Task { await displayFullScreenAvatar() }
|
||||
case .openDirectChat:
|
||||
guard let userProfile = state.userProfile else { fatalError() }
|
||||
actionsSubject.send(.openDirectChat(displayName: userProfile.displayName))
|
||||
Task { await openDirectChat() }
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func displayFullScreenAvatar() {
|
||||
private func displayFullScreenAvatar() async {
|
||||
guard let userProfile = state.userProfile else { fatalError() }
|
||||
guard let avatarURL = userProfile.avatarURL else { return }
|
||||
|
||||
let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator"
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
showLoadingIndicator(allowsInteraction: false)
|
||||
defer { hideLoadingIndicator() }
|
||||
|
||||
Task {
|
||||
defer {
|
||||
userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
||||
}
|
||||
// We don't actually know the mime type here, assume it's an image.
|
||||
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) {
|
||||
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
private func openDirectChat() async {
|
||||
guard let userProfile = state.userProfile else { fatalError() }
|
||||
|
||||
showLoadingIndicator(allowsInteraction: false)
|
||||
defer { hideLoadingIndicator() }
|
||||
|
||||
// We don't actually know the mime type here, assume it's an image.
|
||||
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) {
|
||||
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName)
|
||||
switch await clientProxy.createDirectRoomIfNeeded(with: userProfile.userID, expectedRoomName: userProfile.displayName) {
|
||||
case .success((let roomID, let isNewRoom)):
|
||||
if isNewRoom {
|
||||
analytics.trackCreatedRoom(isDM: true)
|
||||
}
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
case .failure:
|
||||
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,15 +122,15 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(UserProfileScreenViewModel.self)-Loading"
|
||||
|
||||
private func showMemberLoadingIndicator() {
|
||||
private func showLoadingIndicator(allowsInteraction: Bool) {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true),
|
||||
type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: allowsInteraction),
|
||||
title: L10n.commonLoading,
|
||||
persistent: true),
|
||||
delay: .milliseconds(100))
|
||||
}
|
||||
|
||||
private func hideMemberLoadingIndicator() {
|
||||
private func hideLoadingIndicator() {
|
||||
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ struct UserProfileScreen: View {
|
||||
}
|
||||
.compoundList()
|
||||
.navigationTitle(L10n.screenRoomMemberDetailsTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.alert(item: $context.alertInfo)
|
||||
.track(screen: .User)
|
||||
.interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true)
|
||||
@ -73,6 +75,17 @@ struct UserProfileScreen: View {
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomMemberDetailsScreen.directChat)
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
if context.viewState.isPresentedModally {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(L10n.actionDone) {
|
||||
context.send(viewAction: .dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
@ -92,8 +105,10 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static func makeViewModel(userID: String) -> UserProfileScreenViewModel {
|
||||
UserProfileScreenViewModel(userID: userID,
|
||||
isPresentedModally: false,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
}
|
||||
|
@ -289,6 +289,23 @@ class ClientProxy: ClientProxyProtocol {
|
||||
try? client.accountUrl(action: action).flatMap(URL.init(string:))
|
||||
}
|
||||
|
||||
func createDirectRoomIfNeeded(with userID: String, expectedRoomName: String?) async -> Result<(roomID: String, isNewRoom: Bool), ClientProxyError> {
|
||||
let currentDirectRoom = await directRoomForUserID(userID)
|
||||
switch currentDirectRoom {
|
||||
case .success(.some(let roomID)):
|
||||
return .success((roomID: roomID, isNewRoom: false))
|
||||
case .success(.none):
|
||||
switch await createDirectRoom(with: userID, expectedRoomName: expectedRoomName) {
|
||||
case .success(let roomID):
|
||||
return .success((roomID: roomID, isNewRoom: true))
|
||||
case .failure(let error):
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
case .failure(let error):
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError> {
|
||||
await Task.dispatch(on: clientQueue) {
|
||||
do {
|
||||
|
@ -109,6 +109,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func accountURL(action: AccountManagementAction) -> URL?
|
||||
|
||||
func createDirectRoomIfNeeded(with userID: String, expectedRoomName: String?) async -> Result<(roomID: String, isNewRoom: Bool), ClientProxyError>
|
||||
|
||||
func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError>
|
||||
|
||||
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
||||
|
@ -118,7 +118,7 @@ class AppRouteURLParserTests: XCTestCase {
|
||||
|
||||
let route = appRouteURLParser.route(from: url)
|
||||
|
||||
XCTAssertEqual(route, .roomMemberDetails(userID: userID))
|
||||
XCTAssertEqual(route, .userProfile(userID: userID))
|
||||
}
|
||||
|
||||
func testMatrixRoomIdentifierURL() {
|
||||
@ -132,4 +132,28 @@ class AppRouteURLParserTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(route, .room(roomID: id))
|
||||
}
|
||||
|
||||
func testWebRoomIDURL() {
|
||||
let id = "!abcdefghijklmnopqrstuvwxyz1234567890:matrix.org"
|
||||
guard let url = URL(string: "https://app.element.io/#/room/\(id)") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
|
||||
let route = appRouteURLParser.route(from: url)
|
||||
|
||||
XCTAssertEqual(route, .room(roomID: id))
|
||||
}
|
||||
|
||||
func testWebUserIDURL() {
|
||||
let id = "@alice:matrix.org"
|
||||
guard let url = URL(string: "https://develop.element.io/#/user/\(id)") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
|
||||
let route = appRouteURLParser.route(from: url)
|
||||
|
||||
XCTAssertEqual(route, .userProfile(userID: id))
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
@ -55,7 +56,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
@ -92,7 +94,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
@ -128,7 +131,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
@ -163,7 +167,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
@ -198,7 +203,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
@ -214,7 +220,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
65
UnitTests/Sources/UserProfileScreenViewModelTests.swift
Normal file
65
UnitTests/Sources/UserProfileScreenViewModelTests.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright 2022 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 XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class UserProfileScreenViewModelTests: XCTestCase {
|
||||
var viewModel: UserProfileScreenViewModel!
|
||||
var context: UserProfileScreenViewModelType.Context { viewModel.context }
|
||||
|
||||
func testInitialState() async throws {
|
||||
let profile = UserProfileProxy(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: .picturesDirectory)
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.profileForReturnValue = .success(profile)
|
||||
|
||||
viewModel = UserProfileScreenViewModel(userID: profile.userID,
|
||||
isPresentedModally: false,
|
||||
clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.userProfile != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnUser)
|
||||
XCTAssertEqual(context.viewState.userProfile, profile)
|
||||
XCTAssertNotNil(context.viewState.permalink)
|
||||
}
|
||||
|
||||
func testInitialStateAccountOwner() async throws {
|
||||
let profile = UserProfileProxy(userID: RoomMemberProxyMock.mockMe.userID, displayName: "Me", avatarURL: .picturesDirectory)
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.profileForReturnValue = .success(profile)
|
||||
|
||||
viewModel = UserProfileScreenViewModel(userID: profile.userID,
|
||||
isPresentedModally: false,
|
||||
clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.userProfile != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
XCTAssertTrue(context.viewState.isOwnUser)
|
||||
XCTAssertEqual(context.viewState.userProfile, profile)
|
||||
XCTAssertNotNil(context.viewState.permalink)
|
||||
}
|
||||
}
|
@ -27,14 +27,9 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var detailCoordinator: CoordinatorProtocol? {
|
||||
let navigationSplitCoordinator = navigationRootCoordinator.rootCoordinator as? NavigationSplitCoordinator
|
||||
return navigationSplitCoordinator?.detailCoordinator
|
||||
}
|
||||
|
||||
var detailNavigationStack: NavigationStackCoordinator? {
|
||||
detailCoordinator as? NavigationStackCoordinator
|
||||
}
|
||||
var splitCoordinator: NavigationSplitCoordinator? { navigationRootCoordinator.rootCoordinator as? NavigationSplitCoordinator }
|
||||
var detailCoordinator: CoordinatorProtocol? { splitCoordinator?.detailCoordinator }
|
||||
var detailNavigationStack: NavigationStackCoordinator? { detailCoordinator as? NavigationStackCoordinator }
|
||||
|
||||
override func setUp() async throws {
|
||||
cancellables.removeAll()
|
||||
@ -157,6 +152,21 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
}
|
||||
|
||||
func testUserProfileClearsStack() async throws {
|
||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
XCTAssertNil(splitCoordinator?.sheetCoordinator)
|
||||
|
||||
try await process(route: .userProfile(userID: "alice"), expectedState: .userProfileScreen)
|
||||
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||
guard let sheetStackCoordinator = splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator else {
|
||||
XCTFail("There should be a navigation stack presented as a sheet.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(sheetStackCoordinator.rootCoordinator is UserProfileScreenCoordinator)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func process(route: AppRoute, expectedState: UserSessionFlowCoordinatorStateMachine.State) async throws {
|
||||
|
Loading…
x
Reference in New Issue
Block a user