mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Membership aware room routes (#2709)
* Remove the invites button when room list invites are enabled * element-hq/element-x-ios/issues/2632 - Handle room permalinks based on their membership state and show the according room joining screens: - modify the room flow coordinator to only take a roomID and be able to decide its own room flows (especially for unjoined rooms) - add support for the new room preview API - have RoomDirectorySearch go through the same paths
This commit is contained in:
@@ -40,7 +40,7 @@ enum RoomFlowCoordinatorAction: Equatable {
|
||||
|
||||
// swiftlint:disable file_length
|
||||
class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let roomID: String
|
||||
private let userSession: UserSessionProtocol
|
||||
private let isChildFlow: Bool
|
||||
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
|
||||
@@ -52,6 +52,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let orientationManager: OrientationManagerProtocol
|
||||
|
||||
private var roomProxy: RoomProxyProtocol!
|
||||
|
||||
private weak var joinRoomScreenCoordinator: JoinRoomScreenCoordinator?
|
||||
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
@@ -68,7 +72,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private var timelineController: RoomTimelineControllerProtocol?
|
||||
|
||||
init(roomProxy: RoomProxyProtocol,
|
||||
init(roomID: String,
|
||||
userSession: UserSessionProtocol,
|
||||
isChildFlow: Bool,
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
|
||||
@@ -79,7 +83,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
orientationManager: OrientationManagerProtocol) async {
|
||||
self.roomProxy = roomProxy
|
||||
self.roomID = roomID
|
||||
self.userSession = userSession
|
||||
self.isChildFlow = isChildFlow
|
||||
self.roomTimelineControllerFactory = roomTimelineControllerFactory
|
||||
@@ -91,14 +95,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.orientationManager = orientationManager
|
||||
|
||||
// The SDK needs to handle multiple subscription calls before we can start adding child room flows.
|
||||
let subscriptionTask = Task { await roomProxy.subscribeForUpdates() }
|
||||
|
||||
setupStateMachine()
|
||||
|
||||
analytics.signpost.beginRoomFlow(roomProxy.id)
|
||||
|
||||
_ = await subscriptionTask.result
|
||||
analytics.signpost.beginRoomFlow(roomID)
|
||||
}
|
||||
|
||||
// MARK: - FlowCoordinatorProtocol
|
||||
@@ -110,21 +109,31 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
switch appRoute {
|
||||
case .room(let roomID):
|
||||
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
||||
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
|
||||
Task {
|
||||
await handleRoomRoute(roomID: roomID, animated: animated)
|
||||
}
|
||||
case .childRoom(let roomID):
|
||||
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
} else if roomID != roomProxy.id {
|
||||
stateMachine.tryEvent(.presentChildRoom(roomID: roomID), userInfo: EventUserInfo(animated: animated))
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID), userInfo: EventUserInfo(animated: animated))
|
||||
} else {
|
||||
MXLog.info("Ignoring presentation of the same room as a child of this one.")
|
||||
}
|
||||
case .roomDetails(let roomID):
|
||||
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
||||
stateMachine.tryEvent(.presentRoomDetails, userInfo: EventUserInfo(animated: animated))
|
||||
case .roomList:
|
||||
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
||||
guard roomID == self.roomID else { fatalError("Navigation route doesn't belong to this room flow.") }
|
||||
|
||||
Task {
|
||||
if roomProxy == nil {
|
||||
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||
return
|
||||
}
|
||||
|
||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||
}
|
||||
|
||||
stateMachine.tryEvent(.presentRoomDetails, userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
case .roomMemberDetails(let userID):
|
||||
// Always assume this will be presented on the child, external permalinks to a user aren't for a room member.
|
||||
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||
@@ -132,27 +141,68 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
} else {
|
||||
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
case .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
||||
case .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRoomRoute(roomID: String, animated: Bool) async {
|
||||
guard roomID == self.roomID else { fatalError("Navigation route doesn't belong to this room flow.") }
|
||||
|
||||
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||
stateMachine.tryEvent(.presentJoinRoomScreen, userInfo: EventUserInfo(animated: animated))
|
||||
return
|
||||
}
|
||||
|
||||
switch roomProxy.membership {
|
||||
case .joined:
|
||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
|
||||
default:
|
||||
stateMachine.tryEvent(.presentJoinRoomScreen, userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
guard stateMachine.state != .initial else {
|
||||
return
|
||||
}
|
||||
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
||||
|
||||
stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func storeAndSubscribeToRoomProxy(_ roomProxy: RoomProxyProtocol) async {
|
||||
if let oldRoomProxy = self.roomProxy {
|
||||
if oldRoomProxy.id != roomProxy.id {
|
||||
fatalError("Trying to create different room proxies for the same flow coordinator")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
guard roomProxy.membership == .joined else {
|
||||
fatalError("Requesting room details for an unjoined room")
|
||||
}
|
||||
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
await roomProxy.subscribeForUpdates()
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
private func setupStateMachine() {
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
switch (fromState, event) {
|
||||
case (_, .presentJoinRoomScreen):
|
||||
return .joinRoomScreen
|
||||
case (_, .dismissJoinRoomScreen):
|
||||
return .complete
|
||||
|
||||
case (_, .presentRoom):
|
||||
return .room
|
||||
case (_, .dismissRoom):
|
||||
case (_, .dismissFlow):
|
||||
return .complete
|
||||
|
||||
case (.initial, .presentRoomDetails):
|
||||
@@ -255,9 +305,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// Child flow
|
||||
|
||||
case (_, .presentChildRoom(let roomID)):
|
||||
case (_, .startChildFlow(let roomID)):
|
||||
return .presentingChild(childRoomID: roomID, previousState: fromState)
|
||||
case (.presentingChild(_, let previousState), .dismissChildRoom):
|
||||
case (.presentingChild(_, let previousState), .dismissChildFlow):
|
||||
return previousState
|
||||
|
||||
default:
|
||||
@@ -271,9 +321,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
let animated = (context.userInfo as? EventUserInfo)?.animated ?? true
|
||||
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (_, .presentJoinRoomScreen, .joinRoomScreen):
|
||||
presentJoinRoomScreen(animated: true)
|
||||
case (_, .dismissJoinRoomScreen, .complete):
|
||||
dismissFlow(animated: animated)
|
||||
|
||||
case (_, .presentRoom, .room):
|
||||
Task { await self.presentRoom(animated: animated) }
|
||||
case (_, .dismissRoom, .complete):
|
||||
case (_, .dismissFlow, .complete):
|
||||
dismissFlow(animated: animated)
|
||||
|
||||
case (.initial, .presentRoomDetails, .roomDetails(let isRoot)),
|
||||
@@ -381,9 +436,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
rolesAndPermissionsFlowCoordinator = nil
|
||||
|
||||
// Child flow
|
||||
case (_, .presentChildRoom(let roomID), .presentingChild):
|
||||
Task { await self.presentChildFlow(for: roomID) }
|
||||
case (.presentingChild, .dismissChildRoom, _):
|
||||
case (_, .startChildFlow(let roomID), .presentingChild):
|
||||
Task { await self.startChildFlow(for: roomID) }
|
||||
case (.presentingChild, .dismissChildFlow, _):
|
||||
childRoomFlowCoordinator = nil
|
||||
|
||||
default:
|
||||
@@ -479,27 +534,84 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
if !isChildFlow {
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissRoom)
|
||||
self?.stateMachine.tryEvent(.dismissFlow)
|
||||
}
|
||||
} else {
|
||||
if joinRoomScreenCoordinator != nil {
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissFlow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentJoinRoomScreen(animated: Bool) {
|
||||
let coordinator = JoinRoomScreenCoordinator(parameters: .init(roomID: roomID,
|
||||
clientProxy: userSession.clientProxy,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
userIndicatorController: userIndicatorController))
|
||||
|
||||
joinRoomScreenCoordinator = coordinator
|
||||
|
||||
coordinator.actionsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .joined:
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
if let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) {
|
||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
|
||||
|
||||
analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount))
|
||||
} else {
|
||||
stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
}
|
||||
case .cancelled:
|
||||
stateMachine.tryEvent(.dismissJoinRoomScreen)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if !isChildFlow {
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
|
||||
if self?.stateMachine.state == .joinRoomScreen {
|
||||
self?.stateMachine.tryEvent(.dismissJoinRoomScreen)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissRoom)
|
||||
if self?.stateMachine.state == .joinRoomScreen {
|
||||
self?.stateMachine.tryEvent(.dismissJoinRoomScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissFlow(animated: Bool) {
|
||||
childRoomFlowCoordinator?.handleAppRoute(.roomList, animated: animated)
|
||||
childRoomFlowCoordinator?.clearRoute(animated: animated)
|
||||
|
||||
if isChildFlow {
|
||||
// We don't support dismissing a child flow by itself, only the entire chain.
|
||||
MXLog.info("Leaving navigation clean-up to the parent flow.")
|
||||
MXLog.info("Leaving the rest of the navigation clean-up to the parent flow.")
|
||||
|
||||
if joinRoomScreenCoordinator != nil {
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
} else {
|
||||
navigationStackCoordinator.popToRoot(animated: false)
|
||||
navigationStackCoordinator.setRootCoordinator(nil, animated: false)
|
||||
}
|
||||
|
||||
roomProxy.unsubscribeFromUpdates()
|
||||
roomProxy?.unsubscribeFromUpdates()
|
||||
|
||||
timelineController = nil
|
||||
|
||||
actionsSubject.send(.finished)
|
||||
@@ -520,7 +632,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .leftRoom:
|
||||
stateMachine.tryEvent(.dismissRoom)
|
||||
stateMachine.tryEvent(.dismissFlow)
|
||||
case .presentRoomMembersList:
|
||||
stateMachine.tryEvent(.presentRoomMembersList)
|
||||
case .presentRoomDetailsEditScreen:
|
||||
@@ -539,7 +651,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
if isRoot {
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissRoom) // This seems wrong but it isn't, Maybe the event should be called dismissFlow?
|
||||
self?.stateMachine.tryEvent(.dismissFlow)
|
||||
}
|
||||
} else {
|
||||
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||
@@ -890,7 +1002,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .openUserProfile:
|
||||
stateMachine.tryEvent(.presentUserProfile(userID: userID))
|
||||
case .openDirectChat(let roomID):
|
||||
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -913,7 +1025,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .openDirectChat(let roomID):
|
||||
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
|
||||
case .dismiss:
|
||||
break // Not supported when pushed.
|
||||
}
|
||||
@@ -983,7 +1095,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// We don't need to worry about passing in the room proxy as timelines are
|
||||
// cached. The local echo will be visible when fetching the room by its ID.
|
||||
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
|
||||
}
|
||||
|
||||
private func presentNotificationSettingsScreen() {
|
||||
@@ -1117,15 +1229,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// MARK: - Child Flow
|
||||
|
||||
private func presentChildFlow(for roomID: String) async {
|
||||
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||
MXLog.error("Child flow requested for missing room.")
|
||||
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
||||
stateMachine.tryEvent(.dismissChildRoom)
|
||||
return
|
||||
}
|
||||
|
||||
let coordinator = await RoomFlowCoordinator(roomProxy: roomProxy,
|
||||
private func startChildFlow(for roomID: String) async {
|
||||
let coordinator = await RoomFlowCoordinator(roomID: roomID,
|
||||
userSession: userSession,
|
||||
isChildFlow: true,
|
||||
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
||||
@@ -1145,7 +1250,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.dismissChildRoom)
|
||||
stateMachine.tryEvent(.dismissChildFlow)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -1170,6 +1275,7 @@ private extension RoomFlowCoordinator {
|
||||
|
||||
indirect enum State: StateType {
|
||||
case initial
|
||||
case joinRoomScreen
|
||||
case room
|
||||
case roomDetails(isRoot: Bool)
|
||||
case roomDetailsEditScreen
|
||||
@@ -1201,8 +1307,11 @@ private extension RoomFlowCoordinator {
|
||||
}
|
||||
|
||||
enum Event: EventType {
|
||||
case presentJoinRoomScreen
|
||||
case dismissJoinRoomScreen
|
||||
|
||||
case presentRoom
|
||||
case dismissRoom
|
||||
case dismissFlow
|
||||
|
||||
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||
case dismissReportContent
|
||||
@@ -1256,8 +1365,8 @@ private extension RoomFlowCoordinator {
|
||||
case dismissRolesAndPermissionsScreen
|
||||
|
||||
// Child room flow events
|
||||
case presentChildRoom(roomID: String)
|
||||
case dismissChildRoom
|
||||
case startChildFlow(roomID: String)
|
||||
case dismissChildFlow
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,12 +183,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch appRoute {
|
||||
case .room(let roomID):
|
||||
Task { await self.handleRoomRoute(roomID: roomID, animated: animated) }
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||
case .childRoom(let roomID):
|
||||
if let roomFlowCoordinator {
|
||||
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
} else {
|
||||
Task { await self.handleRoomRoute(roomID: roomID, animated: animated) }
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||
}
|
||||
case .roomDetails(let roomID):
|
||||
if stateMachine.state.selectedRoomID == roomID {
|
||||
@@ -196,12 +196,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
} else {
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated))
|
||||
}
|
||||
case .roomList, .roomMemberDetails:
|
||||
self.roomFlowCoordinator?.handleAppRoute(appRoute, 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):
|
||||
self.navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
|
||||
navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
|
||||
case .oidcCallback:
|
||||
break
|
||||
case .settings, .chatBackupSettings:
|
||||
@@ -210,23 +212,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRoomRoute(roomID: String, animated: Bool) async {
|
||||
switch await userSession.clientProxy.roomForIdentifier(roomID)?.membership {
|
||||
case .invited:
|
||||
if UIDevice.current.isPhone {
|
||||
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||
}
|
||||
stateMachine.processEvent(.showInvitesScreen, userInfo: .init(animated: animated))
|
||||
case .joined:
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||
case .left, .none:
|
||||
// Do nothing but maybe we should ask design to have some kind of error state
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
roomFlowCoordinator?.handleAppRoute(.roomList, animated: animated)
|
||||
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -262,14 +249,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
attemptStartingOnboarding()
|
||||
|
||||
case(.roomList, .selectRoom(let roomID, let showingRoomDetails), .roomList):
|
||||
Task { await self.presentRoomFlow(roomID: roomID, showingRoomDetails: showingRoomDetails, animated: animated) }
|
||||
Task { await self.startRoomFlow(roomID: roomID, showingRoomDetails: showingRoomDetails, animated: animated) }
|
||||
case(.roomList, .deselectRoom, .roomList):
|
||||
tearDownRoomFlow(animated: animated)
|
||||
dismissRoomFlow(animated: animated)
|
||||
|
||||
case (.invitesScreen, .selectRoom, .invitesScreen):
|
||||
break
|
||||
case (.invitesScreen, .deselectRoom, .invitesScreen):
|
||||
tearDownRoomFlow(animated: animated)
|
||||
dismissRoomFlow(animated: animated)
|
||||
|
||||
case (.roomList, .showSettingsScreen, .settingsScreen):
|
||||
break
|
||||
@@ -350,9 +337,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .presentRoom(let roomID):
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
|
||||
handleAppRoute(.room(roomID: roomID), animated: true)
|
||||
case .presentRoomDetails(let roomID):
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true))
|
||||
handleAppRoute(.roomDetails(roomID: roomID), animated: true)
|
||||
case .roomLeft(let roomID):
|
||||
if case .roomList(selectedRoomID: let selectedRoomID) = stateMachine.state,
|
||||
selectedRoomID == roomID {
|
||||
@@ -430,13 +417,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// MARK: Room Flow
|
||||
|
||||
private func presentRoomFlow(roomID: String, showingRoomDetails: Bool, animated: Bool) async {
|
||||
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
||||
MXLog.error("Invalid room ID: \(roomID)")
|
||||
return
|
||||
}
|
||||
|
||||
let coordinator = await RoomFlowCoordinator(roomProxy: roomProxy,
|
||||
private func startRoomFlow(roomID: String, showingRoomDetails: Bool, animated: Bool) async {
|
||||
let coordinator = await RoomFlowCoordinator(roomID: roomID,
|
||||
userSession: userSession,
|
||||
isChildFlow: false,
|
||||
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
||||
@@ -486,7 +468,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownRoomFlow(animated: Bool) {
|
||||
private func dismissRoomFlow(animated: Bool) {
|
||||
// THIS MUST BE CALLED *AFTER* THE FLOW HAS TIDIED UP THE STACK OR IT CAN CAUSE A CRASH.
|
||||
navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated)
|
||||
roomFlowCoordinator = nil
|
||||
@@ -650,7 +632,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .joined(let roomID):
|
||||
case .select(let roomID):
|
||||
stateMachine.processEvent(.dismissedRoomDirectorySearchScreen)
|
||||
handleAppRoute(.room(roomID: roomID), animated: true)
|
||||
case .dismiss:
|
||||
|
||||
@@ -2553,6 +2553,74 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
return roomForIdentifierReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - roomPreviewForIdentifier
|
||||
|
||||
var roomPreviewForIdentifierUnderlyingCallsCount = 0
|
||||
var roomPreviewForIdentifierCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomPreviewForIdentifierUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomPreviewForIdentifierUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomPreviewForIdentifierUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomPreviewForIdentifierUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var roomPreviewForIdentifierCalled: Bool {
|
||||
return roomPreviewForIdentifierCallsCount > 0
|
||||
}
|
||||
var roomPreviewForIdentifierReceivedIdentifier: String?
|
||||
var roomPreviewForIdentifierReceivedInvocations: [String] = []
|
||||
|
||||
var roomPreviewForIdentifierUnderlyingReturnValue: Result<RoomPreviewDetails, ClientProxyError>!
|
||||
var roomPreviewForIdentifierReturnValue: Result<RoomPreviewDetails, ClientProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomPreviewForIdentifierUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<RoomPreviewDetails, ClientProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomPreviewForIdentifierUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomPreviewForIdentifierUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomPreviewForIdentifierUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var roomPreviewForIdentifierClosure: ((String) async -> Result<RoomPreviewDetails, ClientProxyError>)?
|
||||
|
||||
func roomPreviewForIdentifier(_ identifier: String) async -> Result<RoomPreviewDetails, ClientProxyError> {
|
||||
roomPreviewForIdentifierCallsCount += 1
|
||||
roomPreviewForIdentifierReceivedIdentifier = identifier
|
||||
roomPreviewForIdentifierReceivedInvocations.append(identifier)
|
||||
if let roomPreviewForIdentifierClosure = roomPreviewForIdentifierClosure {
|
||||
return await roomPreviewForIdentifierClosure(identifier)
|
||||
} else {
|
||||
return roomPreviewForIdentifierReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - loadUserDisplayName
|
||||
|
||||
var loadUserDisplayNameUnderlyingCallsCount = 0
|
||||
|
||||
@@ -113,7 +113,7 @@ struct HomeScreenContent: View {
|
||||
HomeScreenRecoveryKeyConfirmationBanner(context: context)
|
||||
}
|
||||
|
||||
if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused {
|
||||
if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused, !ServiceLocator.shared.settings.roomListInvitesEnabled {
|
||||
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
||||
context.send(viewAction: .selectInvites)
|
||||
}
|
||||
|
||||
@@ -40,12 +40,18 @@ struct HomeScreenInviteCell: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.trailing, 16)
|
||||
.multilineTextAlignment(.leading)
|
||||
.overlay(alignment: .bottom) {
|
||||
separator
|
||||
}
|
||||
}
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 16)
|
||||
.onTapGesture {
|
||||
if let roomId = room.roomId {
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: roomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@@ -19,10 +19,9 @@ import SwiftUI
|
||||
|
||||
struct JoinRoomScreenCoordinatorParameters {
|
||||
let roomID: String
|
||||
let roomName: String
|
||||
let avatarURL: URL?
|
||||
let interaction: JoinRoomScreenInteraction
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum JoinRoomScreenCoordinatorAction {
|
||||
@@ -45,10 +44,9 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = JoinRoomScreenViewModel(roomID: parameters.roomID,
|
||||
roomName: parameters.roomName,
|
||||
avatarURL: parameters.avatarURL,
|
||||
interaction: parameters.interaction,
|
||||
clientProxy: parameters.clientProxy)
|
||||
clientProxy: parameters.clientProxy,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@@ -65,6 +63,10 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
viewModel.stop()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(JoinRoomScreen(context: viewModel.context))
|
||||
|
||||
@@ -21,34 +21,44 @@ enum JoinRoomScreenViewModelAction {
|
||||
case cancelled
|
||||
}
|
||||
|
||||
enum JoinRoomScreenInteractionMode {
|
||||
case unknown
|
||||
case invited
|
||||
case join
|
||||
case knock
|
||||
}
|
||||
|
||||
struct JoinRoomScreenViewState: BindableState {
|
||||
// Maybe use room summary details or similar here??
|
||||
let roomID: String
|
||||
let roomName: String
|
||||
let avatarURL: URL?
|
||||
|
||||
let interaction: JoinRoomScreenInteraction
|
||||
var roomDetails: RoomPreviewDetails?
|
||||
|
||||
var isJoining = false
|
||||
var mode: JoinRoomScreenInteractionMode {
|
||||
guard let roomDetails else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
if roomDetails.isInvited {
|
||||
return .invited
|
||||
}
|
||||
|
||||
if roomDetails.isPublic {
|
||||
return .join
|
||||
}
|
||||
|
||||
// Knocking is not supported yet, treat it as .unknown
|
||||
// if roomDetails.canKnock {
|
||||
// return .knock
|
||||
// }
|
||||
|
||||
return .unknown
|
||||
}
|
||||
|
||||
var bindings = JoinRoomScreenViewStateBindings()
|
||||
|
||||
var title: String {
|
||||
switch interaction {
|
||||
case .knock:
|
||||
L10n.screenJoinRoomTitleKnock
|
||||
case .join, .invited:
|
||||
L10n.screenJoinRoomTitleNoPreview
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String {
|
||||
switch interaction {
|
||||
case .knock:
|
||||
L10n.screenJoinRoomSubtitleKnock
|
||||
case .join, .invited:
|
||||
L10n.screenJoinRoomSubtitleNoPreview
|
||||
}
|
||||
roomDetails?.name ?? L10n.screenJoinRoomTitleNoPreview
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +67,7 @@ struct JoinRoomScreenViewStateBindings {
|
||||
}
|
||||
|
||||
enum JoinRoomScreenAlertType {
|
||||
case joinFailed
|
||||
}
|
||||
|
||||
enum JoinRoomScreenInteraction {
|
||||
case knock
|
||||
case join
|
||||
case invited
|
||||
case declineInvite
|
||||
}
|
||||
|
||||
enum JoinRoomScreenViewAction {
|
||||
|
||||
@@ -20,20 +20,28 @@ import SwiftUI
|
||||
typealias JoinRoomScreenViewModelType = StateStoreViewModel<JoinRoomScreenViewState, JoinRoomScreenViewAction>
|
||||
|
||||
class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewModelProtocol {
|
||||
private let roomID: String
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<JoinRoomScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<JoinRoomScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(roomID: String, roomName: String, avatarURL: URL?, interaction: JoinRoomScreenInteraction, clientProxy: ClientProxyProtocol) {
|
||||
init(roomID: String,
|
||||
clientProxy: ClientProxyProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.roomID = roomID
|
||||
self.clientProxy = clientProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID,
|
||||
roomName: roomName,
|
||||
avatarURL: avatarURL,
|
||||
interaction: interaction))
|
||||
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID), imageProvider: mediaProvider)
|
||||
|
||||
Task {
|
||||
await loadRoomDetails()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -47,21 +55,93 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
case .join:
|
||||
Task { await joinRoom() }
|
||||
case .acceptInvite:
|
||||
break
|
||||
Task { await joinRoom() }
|
||||
case .declineInvite:
|
||||
break
|
||||
showDeclineInviteConfirmationAlert()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func joinRoom() async {
|
||||
state.isJoining = true
|
||||
switch await clientProxy.joinRoom(state.roomID) {
|
||||
case .success:
|
||||
actionsSubject.send(.joined)
|
||||
private func loadRoomDetails() async {
|
||||
showLoadingIndicator()
|
||||
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
switch await clientProxy.roomPreviewForIdentifier(roomID) {
|
||||
case .success(let roomDetails):
|
||||
state.roomDetails = roomDetails
|
||||
case .failure:
|
||||
state.bindings.alertInfo = .init(id: .joinFailed)
|
||||
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
|
||||
private func joinRoom() async {
|
||||
showLoadingIndicator()
|
||||
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
switch await clientProxy.joinRoom(roomID) {
|
||||
case .success:
|
||||
actionsSubject.send(.joined)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed joining room with error: \(error)")
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
|
||||
private func showDeclineInviteConfirmationAlert() {
|
||||
guard let roomDetails = state.roomDetails else {
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
return
|
||||
}
|
||||
|
||||
let roomName = roomDetails.name ?? roomID
|
||||
state.bindings.alertInfo = .init(id: .declineInvite,
|
||||
title: L10n.screenInvitesDeclineChatTitle,
|
||||
message: L10n.screenInvitesDeclineChatMessage(roomName),
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
|
||||
secondaryButton: .init(title: L10n.actionDecline, role: .destructive, action: { Task { await self.declineInvite() } }))
|
||||
}
|
||||
|
||||
private func declineInvite() async {
|
||||
defer {
|
||||
userIndicatorController.retractIndicatorWithId(roomID)
|
||||
}
|
||||
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: roomID, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
|
||||
guard let roomProxy = await clientProxy.roomForIdentifier(roomID) else {
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
return
|
||||
}
|
||||
|
||||
let result = await roomProxy.rejectInvitation()
|
||||
|
||||
if case .failure = result {
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(JoinRoomScreenViewModel.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true),
|
||||
title: L10n.commonLoading,
|
||||
persistent: true),
|
||||
delay: .seconds(0.25))
|
||||
}
|
||||
|
||||
private func hideLoadingIndicator() {
|
||||
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ import Combine
|
||||
protocol JoinRoomScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<JoinRoomScreenViewModelAction, Never> { get }
|
||||
var context: JoinRoomScreenViewModelType.Context { get }
|
||||
|
||||
func stop()
|
||||
}
|
||||
|
||||
@@ -28,16 +28,16 @@ struct JoinRoomScreen: View {
|
||||
} bottomContent: {
|
||||
buttons
|
||||
}
|
||||
.alert(item: $context.alertInfo)
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgCanvasDefault)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
}
|
||||
|
||||
var mainContent: some View {
|
||||
VStack(spacing: 16) {
|
||||
LoadableAvatarImage(url: context.viewState.avatarURL,
|
||||
name: context.viewState.roomName,
|
||||
LoadableAvatarImage(url: context.viewState.roomDetails?.avatarURL,
|
||||
name: context.viewState.title,
|
||||
contentID: context.viewState.roomID,
|
||||
avatarSize: .room(on: .joinRoom),
|
||||
imageProvider: context.imageProvider)
|
||||
@@ -49,10 +49,28 @@ struct JoinRoomScreen: View {
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(context.viewState.subtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
if let alias = context.viewState.roomDetails?.canonicalAlias {
|
||||
Text(alias)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
|
||||
if let memberCount = context.viewState.roomDetails?.memberCount {
|
||||
Label("\(memberCount)", icon: \.userProfile, iconSize: .small, relativeTo: .compound.bodySMSemibold)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.padding(4)
|
||||
.background(Capsule().fill(.compound.bgSubtleSecondary))
|
||||
.labelStyle(.custom(spacing: 4))
|
||||
}
|
||||
|
||||
if let topic = context.viewState.roomDetails?.topic {
|
||||
Text(topic)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(3)
|
||||
}
|
||||
}
|
||||
|
||||
Image(asset: Asset.Images.joinRoomGraphic)
|
||||
@@ -62,7 +80,9 @@ struct JoinRoomScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
var buttons: some View {
|
||||
switch context.viewState.interaction {
|
||||
switch context.viewState.mode {
|
||||
case .unknown:
|
||||
EmptyView()
|
||||
case .knock:
|
||||
Button(L10n.screenJoinRoomKnockAction) { context.send(viewAction: .knock) }
|
||||
.buttonStyle(.compound(.primary))
|
||||
@@ -79,52 +99,82 @@ struct JoinRoomScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
var inviteButtons: some View {
|
||||
Button(L10n.actionDecline) { context.send(viewAction: .acceptInvite) }
|
||||
Button(L10n.actionDecline) { context.send(viewAction: .declineInvite) }
|
||||
.buttonStyle(.compound(.secondary))
|
||||
Button(L10n.actionAccept) { context.send(viewAction: .declineInvite) }
|
||||
Button(L10n.actionAccept) { context.send(viewAction: .acceptInvite) }
|
||||
.buttonStyle(.compound(.primary))
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .principal) {
|
||||
RoomHeaderView(roomID: context.viewState.roomID,
|
||||
roomName: context.viewState.roomName,
|
||||
avatarURL: context.viewState.avatarURL,
|
||||
imageProvider: context.imageProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let knockViewModel = makeViewModel(interaction: .knock)
|
||||
static let joinViewModel = makeViewModel(interaction: .join)
|
||||
static let inviteViewModel = makeViewModel(interaction: .invited)
|
||||
static let unknownViewModel = makeViewModel(mode: .unknown)
|
||||
static let knockViewModel = makeViewModel(mode: .knock)
|
||||
static let joinViewModel = makeViewModel(mode: .join)
|
||||
static let inviteViewModel = makeViewModel(mode: .invited)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
JoinRoomScreen(context: knockViewModel.context)
|
||||
JoinRoomScreen(context: unknownViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Knock")
|
||||
.previewDisplayName("Unknown")
|
||||
.snapshot(delay: 0.25)
|
||||
|
||||
// NavigationStack {
|
||||
// JoinRoomScreen(context: knockViewModel.context)
|
||||
// }
|
||||
// .previewDisplayName("Knock")
|
||||
// .snapshot(delay: 0.25)
|
||||
|
||||
NavigationStack {
|
||||
JoinRoomScreen(context: joinViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Join")
|
||||
.snapshot(delay: 0.25)
|
||||
|
||||
NavigationStack {
|
||||
JoinRoomScreen(context: inviteViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Invite")
|
||||
.snapshot(delay: 0.25)
|
||||
}
|
||||
|
||||
static func makeViewModel(interaction: JoinRoomScreenInteraction) -> JoinRoomScreenViewModel {
|
||||
JoinRoomScreenViewModel(roomID: "6",
|
||||
roomName: "Room Name",
|
||||
avatarURL: nil,
|
||||
interaction: interaction,
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
static func makeViewModel(mode: JoinRoomScreenInteractionMode) -> JoinRoomScreenViewModel {
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
|
||||
// swiftlint:disable:next large_tuple
|
||||
let membership: (isJoined: Bool, isInvited: Bool, isPublic: Bool, canKnock: Bool) = switch mode {
|
||||
case .unknown:
|
||||
(false, false, false, false)
|
||||
case .invited:
|
||||
(false, true, false, false)
|
||||
case .join:
|
||||
(false, false, true, false)
|
||||
case .knock:
|
||||
(false, false, false, true)
|
||||
}
|
||||
|
||||
if mode == .unknown {
|
||||
clientProxy.roomPreviewForIdentifierReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
} else {
|
||||
clientProxy.roomPreviewForIdentifierReturnValue = .success(.init(roomID: "1",
|
||||
name: "The Three-Body Problem - 三体",
|
||||
canonicalAlias: "#3🌞problem:matrix.org",
|
||||
// swiftlint:disable:next line_length
|
||||
topic: "“Science and technology were the only keys to opening the door to the future, and people approached science with the faith and sincerity of elementary school students.”",
|
||||
avatarURL: URL.homeDirectory,
|
||||
memberCount: UInt(100),
|
||||
isHistoryWorldReadable: false,
|
||||
isJoined: membership.isJoined,
|
||||
isInvited: membership.isInvited,
|
||||
isPublic: membership.isPublic,
|
||||
canKnock: membership.canKnock))
|
||||
}
|
||||
|
||||
return JoinRoomScreenViewModel(roomID: "1",
|
||||
clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ struct RoomDirectorySearchScreenCoordinatorParameters {
|
||||
}
|
||||
|
||||
enum RoomDirectorySearchScreenCoordinatorAction {
|
||||
case joined(roomID: String)
|
||||
case select(roomID: String)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ final class RoomDirectorySearchScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .joined(let roomID):
|
||||
actionsSubject.send(.joined(roomID: roomID))
|
||||
case .select(let roomID):
|
||||
actionsSubject.send(.select(roomID: roomID))
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
enum RoomDirectorySearchScreenViewModelAction {
|
||||
case joined(roomID: String)
|
||||
case select(roomID: String)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
@@ -35,6 +35,6 @@ struct RoomDirectorySearchScreenViewStateBindings {
|
||||
|
||||
enum RoomDirectorySearchScreenViewAction {
|
||||
case dismiss
|
||||
case join(roomID: String)
|
||||
case select(roomID: String)
|
||||
case reachedBottom
|
||||
}
|
||||
|
||||
+2
-33
@@ -65,8 +65,8 @@ class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType
|
||||
switch viewAction {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .join(roomID: let roomID):
|
||||
joinRoom(roomID: roomID)
|
||||
case .select(roomID: let roomID):
|
||||
actionsSubject.send(.select(roomID: roomID))
|
||||
case .reachedBottom:
|
||||
loadNextPage()
|
||||
}
|
||||
@@ -74,24 +74,6 @@ class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func joinRoom(roomID: String) {
|
||||
showLoadingIndicator()
|
||||
|
||||
Task {
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
switch await clientProxy.joinRoom(roomID) {
|
||||
case .success:
|
||||
actionsSubject.send(.joined(roomID: roomID))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed joining room with error: \(error)")
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let errorID = "roomDirectorySearchViewModelLoadingError"
|
||||
|
||||
private func search(query: String?) {
|
||||
@@ -122,17 +104,4 @@ class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(RoomDirectorySearchScreenViewModel.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController.submitIndicator(.init(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func hideLoadingIndicator() {
|
||||
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ struct RoomDirectorySearchScreen: View {
|
||||
Section {
|
||||
ForEach(context.viewState.rooms) { room in
|
||||
RoomDirectorySearchCell(result: room, imageProvider: context.imageProvider) {
|
||||
context.send(viewAction: .join(roomID: room.id))
|
||||
context.send(viewAction: .select(roomID: room.id))
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
|
||||
@@ -456,6 +456,16 @@ class ClientProxy: ClientProxyProtocol {
|
||||
room: room,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
}
|
||||
|
||||
func roomPreviewForIdentifier(_ identifier: String) async -> Result<RoomPreviewDetails, ClientProxyError> {
|
||||
do {
|
||||
let roomPreview = try await client.getRoomPreview(roomIdOrAlias: identifier)
|
||||
return .success(.init(roomPreview))
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving preview for room: \(identifier) with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<Void, ClientProxyError> {
|
||||
await Task.dispatch(on: clientQueue) {
|
||||
@@ -930,3 +940,19 @@ private class IgnoredUsersListenerProxy: IgnoredUsersListener {
|
||||
onUpdateClosure(ignoredUserIds)
|
||||
}
|
||||
}
|
||||
|
||||
private extension RoomPreviewDetails {
|
||||
init(_ roomPreview: RoomPreview) {
|
||||
self = RoomPreviewDetails(roomID: roomPreview.roomId,
|
||||
name: roomPreview.name,
|
||||
canonicalAlias: roomPreview.canonicalAlias,
|
||||
topic: roomPreview.topic,
|
||||
avatarURL: roomPreview.avatarUrl.flatMap(URL.init(string:)),
|
||||
memberCount: UInt(roomPreview.numJoinedMembers),
|
||||
isHistoryWorldReadable: roomPreview.isHistoryWorldReadable,
|
||||
isJoined: roomPreview.isJoined,
|
||||
isInvited: roomPreview.isInvited,
|
||||
isPublic: roomPreview.isPublic,
|
||||
canKnock: roomPreview.canKnock)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,20 @@ enum SessionVerificationState {
|
||||
case unverified
|
||||
}
|
||||
|
||||
struct RoomPreviewDetails {
|
||||
let roomID: String
|
||||
let name: String?
|
||||
let canonicalAlias: String?
|
||||
let topic: String?
|
||||
let avatarURL: URL?
|
||||
let memberCount: UInt
|
||||
let isHistoryWorldReadable: Bool
|
||||
let isJoined: Bool
|
||||
let isInvited: Bool
|
||||
let isPublic: Bool
|
||||
let canKnock: Bool
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
||||
@@ -123,6 +137,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol?
|
||||
|
||||
func roomPreviewForIdentifier(_ identifier: String) async -> Result<RoomPreviewDetails, ClientProxyError>
|
||||
|
||||
@discardableResult func loadUserDisplayName() async -> Result<Void, ClientProxyError>
|
||||
|
||||
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:47d269397c823468762d9c7fd7075a6f998b4180f4ef5c126d13623348ba02b2
|
||||
size 2089420
|
||||
oid sha256:95b4eb933dfcab0a5da9c608a0defc26a62206ab665e3a9af0dd6ac4a29bba72
|
||||
size 2099891
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1df7a389ffbf315811e1605cb542a01ae560e977872c49e15092dc0f7432b1eb
|
||||
size 2053936
|
||||
oid sha256:0b1b5884816d8998bc519d62fb538d961d878e79c62c386c152aa7d6910f3acf
|
||||
size 2064569
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf9c852e2d1618056391d08faacfbe402be84ced71793364e25e4e1fd0a84481
|
||||
size 2057420
|
||||
oid sha256:1e227ebf40207f27f1e9ad19881d274a6e6b87bdc2ea40051b6e7fa1e59c84c4
|
||||
size 2054048
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:991acd0c4f868421686daa929e1c52bc7c1d47afcc39e23da19d17f0f19c8a0a
|
||||
size 2048512
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5df7c700584320710a80633c4b06b1045886a88a32ba112a692560b58ce5eaf0
|
||||
size 2123755
|
||||
oid sha256:da004ba89cda86c27b44ee6acf78c69d845d887ede2776e90e4194f2819a4e1a
|
||||
size 2104970
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8945225fb83489edf46bb1454770b0c55d2265b8b330ecd16079534c1fc2ef76
|
||||
size 2087732
|
||||
oid sha256:3a663cf0f68bc9ed2ac0df7e88a9b144a4cc25205fc71d9a3f0e25f892c86788
|
||||
size 2069238
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e60ad9a4485f37882412ec46821127ea0f605f9abe435598f43d8bc2d5279c83
|
||||
size 2104307
|
||||
oid sha256:09dca383db2dfc978c9f0d78bda8331783aca72a1650c60cba1262fb73637af3
|
||||
size 2059391
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ac085ce2465a37d69978c118fba9b35b301449f1a8c5acd97fb5632cb0a8fe47
|
||||
size 2062324
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e17277315e799bbac0caf145de342a624bfc80357aa63f297334fe8d6c81367
|
||||
size 826589
|
||||
oid sha256:2806fc99ef156ed9b0d9e97bc7329a00905eabe5bac5200a17e59124de5b94c1
|
||||
size 857415
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4e45734de2938d98904d8b7c19a44b997fb1f6447839f60909e79c1e620096fd
|
||||
size 806983
|
||||
oid sha256:5691851137a7bd9c6fd1e6b18597ed4f5b6aa6d89cba714224f54e620e24fb96
|
||||
size 837040
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a0f4de3f95f6242680a8b8b52939fd2a3a79a299d8dea6d5609244038c95bc1a
|
||||
size 811524
|
||||
oid sha256:c28a125af66dac1dbdfe12cb8dc588b51a1c4611693275113caf522ffe1c68c8
|
||||
size 829240
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:32985fbf4c2f829638965222ef31392e490fd260ff0d07bc777571eed90d25de
|
||||
size 810769
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d0bca263cdc71f987ef6b0f4f3d70f4779ea3865a603f752fc5e3ae773c9f394
|
||||
size 857464
|
||||
oid sha256:52d999afeabc4fd6dded6ad2ece35968c59c955b90c53961ee1fd42179aeb706
|
||||
size 861376
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:171a0753cc99267b93c83f3bc2caf9546d2d72c685251f9fe70796556db5fc3e
|
||||
size 837228
|
||||
oid sha256:0bae4ecdbf0b1a8d06e4115af0add88ea5dadd6dc43f61e6a6c0a4aa55dbd34e
|
||||
size 840927
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a88bb18fd0311c7c380f76a7c1364a1eb844f708835425b8a22abcc12997c25c
|
||||
size 851763
|
||||
oid sha256:32d350137393e9ca3ab38f481c57fa7eacf9c0a89be63de6cdacc2274d0f26cb
|
||||
size 834432
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d1a65eab7cb10c6918f1a4efa9ae210b5b55959d90128a3a21536d5235948daf
|
||||
size 825025
|
||||
@@ -34,13 +34,44 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testAcceptInviteInteraction() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined }
|
||||
context.send(viewAction: .acceptInvite)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testDeclineInviteInteraction() async throws {
|
||||
setupViewModel()
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.roomDetails != nil }.fulfill()
|
||||
|
||||
context.send(viewAction: .declineInvite)
|
||||
|
||||
XCTAssertEqual(viewModel.context.alertInfo?.id, .declineInvite)
|
||||
}
|
||||
|
||||
private func setupViewModel(throwing: Bool = false) {
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
|
||||
clientProxy.joinRoomReturnValue = throwing ? .failure(.sdkError(ClientProxyMockError.generic)) : .success(())
|
||||
|
||||
clientProxy.roomPreviewForIdentifierReturnValue = .success(.init(roomID: "",
|
||||
name: nil,
|
||||
canonicalAlias: nil,
|
||||
topic: nil,
|
||||
avatarURL: nil,
|
||||
memberCount: 0,
|
||||
isHistoryWorldReadable: false,
|
||||
isJoined: false,
|
||||
isInvited: false,
|
||||
isPublic: false,
|
||||
canKnock: false))
|
||||
|
||||
viewModel = JoinRoomScreenViewModel(roomID: "1",
|
||||
roomName: "Room Name",
|
||||
avatarURL: nil,
|
||||
interaction: .join,
|
||||
clientProxy: clientProxy)
|
||||
clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,32 +21,33 @@ import Combine
|
||||
|
||||
@MainActor
|
||||
class RoomFlowCoordinatorTests: XCTestCase {
|
||||
var clientProxy: ClientProxyMock!
|
||||
var roomFlowCoordinator: RoomFlowCoordinator!
|
||||
var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func testRoomPresentation() async throws {
|
||||
await setupViewModel()
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
|
||||
try await process(route: .roomList, expectedAction: .finished)
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
}
|
||||
|
||||
func testRoomDetailsPresentation() async throws {
|
||||
await setupViewModel()
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .roomDetails(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
|
||||
try await process(route: .roomList, expectedAction: .finished)
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
}
|
||||
|
||||
func testNoOp() async throws {
|
||||
await setupViewModel()
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .roomDetails(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
@@ -60,7 +61,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testPushDetails() async throws {
|
||||
await setupViewModel()
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
@@ -73,7 +74,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testChildRoomFlow() async throws {
|
||||
await setupViewModel()
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
@@ -88,14 +89,14 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
|
||||
try await process(route: .roomList, expectedAction: .finished)
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
}
|
||||
|
||||
/// Tests the child flow teardown in isolation of it's parent.
|
||||
func testChildFlowTearDown() async throws {
|
||||
await setupViewModel(asChildFlow: true)
|
||||
await setupRoomFlowCoordinator(asChildFlow: true)
|
||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||
|
||||
try await process(route: .room(roomID: "1"))
|
||||
@@ -105,7 +106,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator)
|
||||
|
||||
try await process(route: .roomList, expectedAction: .finished)
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2, "A child room flow should leave its parent to clean up the stack.")
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator, "A child room flow should leave its parent to clean up the stack.")
|
||||
@@ -113,7 +114,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testChildRoomMemberDetails() async throws {
|
||||
await setupViewModel()
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
@@ -130,7 +131,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testChildRoomIgnoresDirectDuplicate() async throws {
|
||||
await setupViewModel()
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
@@ -151,6 +152,60 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
func testRoomMembershipInvite() async throws {
|
||||
await setupRoomFlowCoordinator(roomType: .invited(roomID: "InvitedRoomID"))
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is JoinRoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is JoinRoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
|
||||
// "Join" the room
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
RoomProxyMock(with: .init())
|
||||
}
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
}
|
||||
|
||||
func testChildRoomMembershipInvite() async throws {
|
||||
await setupRoomFlowCoordinator(asChildFlow: true, roomType: .invited(roomID: "InvitedRoomID"))
|
||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID"))
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is JoinRoomScreenCoordinator)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.stackCoordinators.last, "A child room flow should remove the join room scren on dismissal")
|
||||
|
||||
navigationStackCoordinator.popToRoot()
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID"))
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is JoinRoomScreenCoordinator)
|
||||
|
||||
// "Join" the room
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
RoomProxyMock(with: .init())
|
||||
}
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID"))
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func process(route: AppRoute) async throws {
|
||||
@@ -159,11 +214,15 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
}
|
||||
|
||||
private func process(route: AppRoute, expectedAction: RoomFlowCoordinatorAction) async throws {
|
||||
try await process(route: route, expectedActions: [expectedAction])
|
||||
private func clearRoute(expectedActions: [RoomFlowCoordinatorAction]) async throws {
|
||||
try await processRouteOrClear(route: nil, expectedActions: expectedActions)
|
||||
}
|
||||
|
||||
private func process(route: AppRoute, expectedActions: [RoomFlowCoordinatorAction]) async throws {
|
||||
try await processRouteOrClear(route: route, expectedActions: expectedActions)
|
||||
}
|
||||
|
||||
private func processRouteOrClear(route: AppRoute?, expectedActions: [RoomFlowCoordinatorAction]) async throws {
|
||||
guard !expectedActions.isEmpty else {
|
||||
return
|
||||
}
|
||||
@@ -176,16 +235,40 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
})
|
||||
}
|
||||
|
||||
roomFlowCoordinator.handleAppRoute(route, animated: true)
|
||||
if let route {
|
||||
roomFlowCoordinator.handleAppRoute(route, animated: true)
|
||||
} else {
|
||||
roomFlowCoordinator.clearRoute(animated: true)
|
||||
}
|
||||
|
||||
for fulfillment in fulfillments {
|
||||
try await fulfillment.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupViewModel(asChildFlow: Bool = false) async {
|
||||
private func setupRoomFlowCoordinator(asChildFlow: Bool = false, roomType: RoomType? = nil) async {
|
||||
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)))))
|
||||
|
||||
clientProxy.roomPreviewForIdentifierClosure = { [roomType] roomID in
|
||||
switch roomType {
|
||||
case .invited:
|
||||
return .success(.init(roomID: roomID,
|
||||
name: "Invite",
|
||||
canonicalAlias: nil,
|
||||
topic: nil,
|
||||
avatarURL: nil,
|
||||
memberCount: 0,
|
||||
isHistoryWorldReadable: false,
|
||||
isJoined: false,
|
||||
isInvited: true,
|
||||
isPublic: false,
|
||||
canKnock: false))
|
||||
default:
|
||||
fatalError("Something isn't set up right")
|
||||
}
|
||||
}
|
||||
|
||||
let mediaProvider = MockMediaProvider()
|
||||
let voiceMessageMediaManager = VoiceMessageMediaManagerMock()
|
||||
let userSession = MockUserSession(clientProxy: clientProxy,
|
||||
@@ -196,7 +279,14 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator)
|
||||
|
||||
roomFlowCoordinator = await RoomFlowCoordinator(roomProxy: RoomProxyMock(with: .init(id: "1")),
|
||||
let roomID = switch roomType {
|
||||
case .invited(let roomID):
|
||||
roomID
|
||||
default:
|
||||
"1"
|
||||
}
|
||||
|
||||
roomFlowCoordinator = await RoomFlowCoordinator(roomID: roomID,
|
||||
userSession: userSession,
|
||||
isChildFlow: asChildFlow,
|
||||
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
|
||||
@@ -209,3 +299,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
orientationManager: OrientationManagerMock())
|
||||
}
|
||||
}
|
||||
|
||||
private enum RoomType {
|
||||
case invited(roomID: String)
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
let items = [TextRoomTimelineItem(eventID: "t1"),
|
||||
TextRoomTimelineItem(eventID: "t2"),
|
||||
TextRoomTimelineItem(eventID: "t3")]
|
||||
let (viewModel, roomProxy, timelineProxy, _, notificationCenter) = readReceiptsConfiguration(with: items)
|
||||
let (viewModel, _, timelineProxy, _, _) = readReceiptsConfiguration(with: items)
|
||||
|
||||
// When sending a read receipt for the last item.
|
||||
viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
|
||||
|
||||
Reference in New Issue
Block a user