mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +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
|
// swiftlint:disable file_length
|
||||||
class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||||
private let roomProxy: RoomProxyProtocol
|
private let roomID: String
|
||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
private let isChildFlow: Bool
|
private let isChildFlow: Bool
|
||||||
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
|
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
|
||||||
@@ -52,6 +52,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let orientationManager: OrientationManagerProtocol
|
private let orientationManager: OrientationManagerProtocol
|
||||||
|
|
||||||
|
private var roomProxy: RoomProxyProtocol!
|
||||||
|
|
||||||
|
private weak var joinRoomScreenCoordinator: JoinRoomScreenCoordinator?
|
||||||
|
|
||||||
// periphery:ignore - used to avoid deallocation
|
// periphery:ignore - used to avoid deallocation
|
||||||
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
|
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
|
||||||
// periphery:ignore - used to avoid deallocation
|
// periphery:ignore - used to avoid deallocation
|
||||||
@@ -68,7 +72,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
private var timelineController: RoomTimelineControllerProtocol?
|
private var timelineController: RoomTimelineControllerProtocol?
|
||||||
|
|
||||||
init(roomProxy: RoomProxyProtocol,
|
init(roomID: String,
|
||||||
userSession: UserSessionProtocol,
|
userSession: UserSessionProtocol,
|
||||||
isChildFlow: Bool,
|
isChildFlow: Bool,
|
||||||
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
|
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
|
||||||
@@ -79,7 +83,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
analytics: AnalyticsService,
|
analytics: AnalyticsService,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
orientationManager: OrientationManagerProtocol) async {
|
orientationManager: OrientationManagerProtocol) async {
|
||||||
self.roomProxy = roomProxy
|
self.roomID = roomID
|
||||||
self.userSession = userSession
|
self.userSession = userSession
|
||||||
self.isChildFlow = isChildFlow
|
self.isChildFlow = isChildFlow
|
||||||
self.roomTimelineControllerFactory = roomTimelineControllerFactory
|
self.roomTimelineControllerFactory = roomTimelineControllerFactory
|
||||||
@@ -91,14 +95,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.orientationManager = orientationManager
|
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()
|
setupStateMachine()
|
||||||
|
|
||||||
analytics.signpost.beginRoomFlow(roomProxy.id)
|
analytics.signpost.beginRoomFlow(roomID)
|
||||||
|
|
||||||
_ = await subscriptionTask.result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - FlowCoordinatorProtocol
|
// MARK: - FlowCoordinatorProtocol
|
||||||
@@ -110,21 +109,31 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||||
switch appRoute {
|
switch appRoute {
|
||||||
case .room(let roomID):
|
case .room(let roomID):
|
||||||
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
Task {
|
||||||
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
|
await handleRoomRoute(roomID: roomID, animated: animated)
|
||||||
|
}
|
||||||
case .childRoom(let roomID):
|
case .childRoom(let roomID):
|
||||||
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||||
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
} else if roomID != roomProxy.id {
|
} else if roomID != roomProxy.id {
|
||||||
stateMachine.tryEvent(.presentChildRoom(roomID: roomID), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID), userInfo: EventUserInfo(animated: animated))
|
||||||
} else {
|
} else {
|
||||||
MXLog.info("Ignoring presentation of the same room as a child of this one.")
|
MXLog.info("Ignoring presentation of the same room as a child of this one.")
|
||||||
}
|
}
|
||||||
case .roomDetails(let roomID):
|
case .roomDetails(let roomID):
|
||||||
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
|
guard roomID == self.roomID else { fatalError("Navigation route doesn't belong to this room flow.") }
|
||||||
stateMachine.tryEvent(.presentRoomDetails, userInfo: EventUserInfo(animated: animated))
|
|
||||||
case .roomList:
|
Task {
|
||||||
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
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):
|
case .roomMemberDetails(let userID):
|
||||||
// Always assume this will be presented on the child, external permalinks to a user aren't for a room member.
|
// 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 {
|
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||||
@@ -132,27 +141,68 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
} else {
|
} else {
|
||||||
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
||||||
}
|
}
|
||||||
case .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
case .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
|
||||||
break
|
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) {
|
func clearRoute(animated: Bool) {
|
||||||
guard stateMachine.state != .initial else {
|
guard stateMachine.state != .initial else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
|
||||||
|
stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// 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
|
// swiftlint:disable:next function_body_length
|
||||||
private func setupStateMachine() {
|
private func setupStateMachine() {
|
||||||
stateMachine.addRouteMapping { event, fromState, _ in
|
stateMachine.addRouteMapping { event, fromState, _ in
|
||||||
switch (fromState, event) {
|
switch (fromState, event) {
|
||||||
|
case (_, .presentJoinRoomScreen):
|
||||||
|
return .joinRoomScreen
|
||||||
|
case (_, .dismissJoinRoomScreen):
|
||||||
|
return .complete
|
||||||
|
|
||||||
case (_, .presentRoom):
|
case (_, .presentRoom):
|
||||||
return .room
|
return .room
|
||||||
case (_, .dismissRoom):
|
case (_, .dismissFlow):
|
||||||
return .complete
|
return .complete
|
||||||
|
|
||||||
case (.initial, .presentRoomDetails):
|
case (.initial, .presentRoomDetails):
|
||||||
@@ -255,9 +305,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// Child flow
|
// Child flow
|
||||||
|
|
||||||
case (_, .presentChildRoom(let roomID)):
|
case (_, .startChildFlow(let roomID)):
|
||||||
return .presentingChild(childRoomID: roomID, previousState: fromState)
|
return .presentingChild(childRoomID: roomID, previousState: fromState)
|
||||||
case (.presentingChild(_, let previousState), .dismissChildRoom):
|
case (.presentingChild(_, let previousState), .dismissChildFlow):
|
||||||
return previousState
|
return previousState
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -271,9 +321,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
let animated = (context.userInfo as? EventUserInfo)?.animated ?? true
|
let animated = (context.userInfo as? EventUserInfo)?.animated ?? true
|
||||||
|
|
||||||
switch (context.fromState, context.event, context.toState) {
|
switch (context.fromState, context.event, context.toState) {
|
||||||
|
case (_, .presentJoinRoomScreen, .joinRoomScreen):
|
||||||
|
presentJoinRoomScreen(animated: true)
|
||||||
|
case (_, .dismissJoinRoomScreen, .complete):
|
||||||
|
dismissFlow(animated: animated)
|
||||||
|
|
||||||
case (_, .presentRoom, .room):
|
case (_, .presentRoom, .room):
|
||||||
Task { await self.presentRoom(animated: animated) }
|
Task { await self.presentRoom(animated: animated) }
|
||||||
case (_, .dismissRoom, .complete):
|
case (_, .dismissFlow, .complete):
|
||||||
dismissFlow(animated: animated)
|
dismissFlow(animated: animated)
|
||||||
|
|
||||||
case (.initial, .presentRoomDetails, .roomDetails(let isRoot)),
|
case (.initial, .presentRoomDetails, .roomDetails(let isRoot)),
|
||||||
@@ -381,9 +436,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
rolesAndPermissionsFlowCoordinator = nil
|
rolesAndPermissionsFlowCoordinator = nil
|
||||||
|
|
||||||
// Child flow
|
// Child flow
|
||||||
case (_, .presentChildRoom(let roomID), .presentingChild):
|
case (_, .startChildFlow(let roomID), .presentingChild):
|
||||||
Task { await self.presentChildFlow(for: roomID) }
|
Task { await self.startChildFlow(for: roomID) }
|
||||||
case (.presentingChild, .dismissChildRoom, _):
|
case (.presentingChild, .dismissChildFlow, _):
|
||||||
childRoomFlowCoordinator = nil
|
childRoomFlowCoordinator = nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -479,27 +534,84 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
if !isChildFlow {
|
if !isChildFlow {
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
|
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 {
|
} else {
|
||||||
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
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) {
|
private func dismissFlow(animated: Bool) {
|
||||||
childRoomFlowCoordinator?.handleAppRoute(.roomList, animated: animated)
|
childRoomFlowCoordinator?.clearRoute(animated: animated)
|
||||||
|
|
||||||
if isChildFlow {
|
if isChildFlow {
|
||||||
// We don't support dismissing a child flow by itself, only the entire chain.
|
// 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 {
|
} else {
|
||||||
navigationStackCoordinator.popToRoot(animated: false)
|
navigationStackCoordinator.popToRoot(animated: false)
|
||||||
navigationStackCoordinator.setRootCoordinator(nil, animated: false)
|
navigationStackCoordinator.setRootCoordinator(nil, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
roomProxy.unsubscribeFromUpdates()
|
roomProxy?.unsubscribeFromUpdates()
|
||||||
|
|
||||||
timelineController = nil
|
timelineController = nil
|
||||||
|
|
||||||
actionsSubject.send(.finished)
|
actionsSubject.send(.finished)
|
||||||
@@ -520,7 +632,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .leftRoom:
|
case .leftRoom:
|
||||||
stateMachine.tryEvent(.dismissRoom)
|
stateMachine.tryEvent(.dismissFlow)
|
||||||
case .presentRoomMembersList:
|
case .presentRoomMembersList:
|
||||||
stateMachine.tryEvent(.presentRoomMembersList)
|
stateMachine.tryEvent(.presentRoomMembersList)
|
||||||
case .presentRoomDetailsEditScreen:
|
case .presentRoomDetailsEditScreen:
|
||||||
@@ -539,7 +651,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
if isRoot {
|
if isRoot {
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
|
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 {
|
} else {
|
||||||
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||||
@@ -890,7 +1002,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .openUserProfile:
|
case .openUserProfile:
|
||||||
stateMachine.tryEvent(.presentUserProfile(userID: userID))
|
stateMachine.tryEvent(.presentUserProfile(userID: userID))
|
||||||
case .openDirectChat(let roomID):
|
case .openDirectChat(let roomID):
|
||||||
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -913,7 +1025,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .openDirectChat(let roomID):
|
case .openDirectChat(let roomID):
|
||||||
stateMachine.tryEvent(.presentChildRoom(roomID: roomID))
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
break // Not supported when pushed.
|
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
|
// 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.
|
// 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() {
|
private func presentNotificationSettingsScreen() {
|
||||||
@@ -1117,15 +1229,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Child Flow
|
// MARK: - Child Flow
|
||||||
|
|
||||||
private func presentChildFlow(for roomID: String) async {
|
private func startChildFlow(for roomID: String) async {
|
||||||
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
let coordinator = await RoomFlowCoordinator(roomID: roomID,
|
||||||
MXLog.error("Child flow requested for missing room.")
|
|
||||||
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
|
||||||
stateMachine.tryEvent(.dismissChildRoom)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let coordinator = await RoomFlowCoordinator(roomProxy: roomProxy,
|
|
||||||
userSession: userSession,
|
userSession: userSession,
|
||||||
isChildFlow: true,
|
isChildFlow: true,
|
||||||
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
||||||
@@ -1145,7 +1250,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .presentCallScreen(let roomProxy):
|
case .presentCallScreen(let roomProxy):
|
||||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||||
case .finished:
|
case .finished:
|
||||||
stateMachine.tryEvent(.dismissChildRoom)
|
stateMachine.tryEvent(.dismissChildFlow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -1170,6 +1275,7 @@ private extension RoomFlowCoordinator {
|
|||||||
|
|
||||||
indirect enum State: StateType {
|
indirect enum State: StateType {
|
||||||
case initial
|
case initial
|
||||||
|
case joinRoomScreen
|
||||||
case room
|
case room
|
||||||
case roomDetails(isRoot: Bool)
|
case roomDetails(isRoot: Bool)
|
||||||
case roomDetailsEditScreen
|
case roomDetailsEditScreen
|
||||||
@@ -1201,8 +1307,11 @@ private extension RoomFlowCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Event: EventType {
|
enum Event: EventType {
|
||||||
|
case presentJoinRoomScreen
|
||||||
|
case dismissJoinRoomScreen
|
||||||
|
|
||||||
case presentRoom
|
case presentRoom
|
||||||
case dismissRoom
|
case dismissFlow
|
||||||
|
|
||||||
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
case dismissReportContent
|
case dismissReportContent
|
||||||
@@ -1256,8 +1365,8 @@ private extension RoomFlowCoordinator {
|
|||||||
case dismissRolesAndPermissionsScreen
|
case dismissRolesAndPermissionsScreen
|
||||||
|
|
||||||
// Child room flow events
|
// Child room flow events
|
||||||
case presentChildRoom(roomID: String)
|
case startChildFlow(roomID: String)
|
||||||
case dismissChildRoom
|
case dismissChildFlow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,12 +183,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
switch appRoute {
|
switch appRoute {
|
||||||
case .room(let roomID):
|
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):
|
case .childRoom(let roomID):
|
||||||
if let roomFlowCoordinator {
|
if let roomFlowCoordinator {
|
||||||
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
} else {
|
} else {
|
||||||
Task { await self.handleRoomRoute(roomID: roomID, animated: animated) }
|
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
|
||||||
}
|
}
|
||||||
case .roomDetails(let roomID):
|
case .roomDetails(let roomID):
|
||||||
if stateMachine.state.selectedRoomID == roomID {
|
if stateMachine.state.selectedRoomID == roomID {
|
||||||
@@ -196,12 +196,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
} else {
|
} else {
|
||||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated))
|
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated))
|
||||||
}
|
}
|
||||||
case .roomList, .roomMemberDetails:
|
case .roomList:
|
||||||
self.roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||||
|
case .roomMemberDetails:
|
||||||
|
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
||||||
case .userProfile(let userID):
|
case .userProfile(let userID):
|
||||||
stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated))
|
stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated))
|
||||||
case .genericCallLink(let url):
|
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:
|
case .oidcCallback:
|
||||||
break
|
break
|
||||||
case .settings, .chatBackupSettings:
|
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) {
|
func clearRoute(animated: Bool) {
|
||||||
roomFlowCoordinator?.handleAppRoute(.roomList, animated: animated)
|
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@@ -262,14 +249,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
attemptStartingOnboarding()
|
attemptStartingOnboarding()
|
||||||
|
|
||||||
case(.roomList, .selectRoom(let roomID, let showingRoomDetails), .roomList):
|
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):
|
case(.roomList, .deselectRoom, .roomList):
|
||||||
tearDownRoomFlow(animated: animated)
|
dismissRoomFlow(animated: animated)
|
||||||
|
|
||||||
case (.invitesScreen, .selectRoom, .invitesScreen):
|
case (.invitesScreen, .selectRoom, .invitesScreen):
|
||||||
break
|
break
|
||||||
case (.invitesScreen, .deselectRoom, .invitesScreen):
|
case (.invitesScreen, .deselectRoom, .invitesScreen):
|
||||||
tearDownRoomFlow(animated: animated)
|
dismissRoomFlow(animated: animated)
|
||||||
|
|
||||||
case (.roomList, .showSettingsScreen, .settingsScreen):
|
case (.roomList, .showSettingsScreen, .settingsScreen):
|
||||||
break
|
break
|
||||||
@@ -350,9 +337,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .presentRoom(let roomID):
|
case .presentRoom(let roomID):
|
||||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
|
handleAppRoute(.room(roomID: roomID), animated: true)
|
||||||
case .presentRoomDetails(let roomID):
|
case .presentRoomDetails(let roomID):
|
||||||
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true))
|
handleAppRoute(.roomDetails(roomID: roomID), animated: true)
|
||||||
case .roomLeft(let roomID):
|
case .roomLeft(let roomID):
|
||||||
if case .roomList(selectedRoomID: let selectedRoomID) = stateMachine.state,
|
if case .roomList(selectedRoomID: let selectedRoomID) = stateMachine.state,
|
||||||
selectedRoomID == roomID {
|
selectedRoomID == roomID {
|
||||||
@@ -430,13 +417,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: Room Flow
|
// MARK: Room Flow
|
||||||
|
|
||||||
private func presentRoomFlow(roomID: String, showingRoomDetails: Bool, animated: Bool) async {
|
private func startRoomFlow(roomID: String, showingRoomDetails: Bool, animated: Bool) async {
|
||||||
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
|
let coordinator = await RoomFlowCoordinator(roomID: roomID,
|
||||||
MXLog.error("Invalid room ID: \(roomID)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let coordinator = await RoomFlowCoordinator(roomProxy: roomProxy,
|
|
||||||
userSession: userSession,
|
userSession: userSession,
|
||||||
isChildFlow: false,
|
isChildFlow: false,
|
||||||
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
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.
|
// THIS MUST BE CALLED *AFTER* THE FLOW HAS TIDIED UP THE STACK OR IT CAN CAUSE A CRASH.
|
||||||
navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated)
|
navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated)
|
||||||
roomFlowCoordinator = nil
|
roomFlowCoordinator = nil
|
||||||
@@ -650,7 +632,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .joined(let roomID):
|
case .select(let roomID):
|
||||||
stateMachine.processEvent(.dismissedRoomDirectorySearchScreen)
|
stateMachine.processEvent(.dismissedRoomDirectorySearchScreen)
|
||||||
handleAppRoute(.room(roomID: roomID), animated: true)
|
handleAppRoute(.room(roomID: roomID), animated: true)
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
|
|||||||
@@ -2553,6 +2553,74 @@ class ClientProxyMock: ClientProxyProtocol {
|
|||||||
return roomForIdentifierReturnValue
|
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
|
//MARK: - loadUserDisplayName
|
||||||
|
|
||||||
var loadUserDisplayNameUnderlyingCallsCount = 0
|
var loadUserDisplayNameUnderlyingCallsCount = 0
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ struct HomeScreenContent: View {
|
|||||||
HomeScreenRecoveryKeyConfirmationBanner(context: context)
|
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) {
|
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
||||||
context.send(viewAction: .selectInvites)
|
context.send(viewAction: .selectInvites)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,18 @@ struct HomeScreenInviteCell: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
.padding(.trailing, 16)
|
.padding(.trailing, 16)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
.overlay(alignment: .bottom) {
|
.overlay(alignment: .bottom) {
|
||||||
separator
|
separator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
|
.onTapGesture {
|
||||||
|
if let roomId = room.roomId {
|
||||||
|
context.send(viewAction: .selectRoom(roomIdentifier: roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ import SwiftUI
|
|||||||
|
|
||||||
struct JoinRoomScreenCoordinatorParameters {
|
struct JoinRoomScreenCoordinatorParameters {
|
||||||
let roomID: String
|
let roomID: String
|
||||||
let roomName: String
|
|
||||||
let avatarURL: URL?
|
|
||||||
let interaction: JoinRoomScreenInteraction
|
|
||||||
let clientProxy: ClientProxyProtocol
|
let clientProxy: ClientProxyProtocol
|
||||||
|
let mediaProvider: MediaProviderProtocol
|
||||||
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum JoinRoomScreenCoordinatorAction {
|
enum JoinRoomScreenCoordinatorAction {
|
||||||
@@ -45,10 +44,9 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = JoinRoomScreenViewModel(roomID: parameters.roomID,
|
viewModel = JoinRoomScreenViewModel(roomID: parameters.roomID,
|
||||||
roomName: parameters.roomName,
|
clientProxy: parameters.clientProxy,
|
||||||
avatarURL: parameters.avatarURL,
|
mediaProvider: parameters.mediaProvider,
|
||||||
interaction: parameters.interaction,
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
clientProxy: parameters.clientProxy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
@@ -65,6 +63,10 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
viewModel.stop()
|
||||||
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
AnyView(JoinRoomScreen(context: viewModel.context))
|
AnyView(JoinRoomScreen(context: viewModel.context))
|
||||||
|
|||||||
@@ -21,34 +21,44 @@ enum JoinRoomScreenViewModelAction {
|
|||||||
case cancelled
|
case cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum JoinRoomScreenInteractionMode {
|
||||||
|
case unknown
|
||||||
|
case invited
|
||||||
|
case join
|
||||||
|
case knock
|
||||||
|
}
|
||||||
|
|
||||||
struct JoinRoomScreenViewState: BindableState {
|
struct JoinRoomScreenViewState: BindableState {
|
||||||
// Maybe use room summary details or similar here??
|
// Maybe use room summary details or similar here??
|
||||||
let roomID: String
|
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 bindings = JoinRoomScreenViewStateBindings()
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch interaction {
|
roomDetails?.name ?? L10n.screenJoinRoomTitleNoPreview
|
||||||
case .knock:
|
|
||||||
L10n.screenJoinRoomTitleKnock
|
|
||||||
case .join, .invited:
|
|
||||||
L10n.screenJoinRoomTitleNoPreview
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var subtitle: String {
|
|
||||||
switch interaction {
|
|
||||||
case .knock:
|
|
||||||
L10n.screenJoinRoomSubtitleKnock
|
|
||||||
case .join, .invited:
|
|
||||||
L10n.screenJoinRoomSubtitleNoPreview
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,13 +67,7 @@ struct JoinRoomScreenViewStateBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum JoinRoomScreenAlertType {
|
enum JoinRoomScreenAlertType {
|
||||||
case joinFailed
|
case declineInvite
|
||||||
}
|
|
||||||
|
|
||||||
enum JoinRoomScreenInteraction {
|
|
||||||
case knock
|
|
||||||
case join
|
|
||||||
case invited
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum JoinRoomScreenViewAction {
|
enum JoinRoomScreenViewAction {
|
||||||
|
|||||||
@@ -20,20 +20,28 @@ import SwiftUI
|
|||||||
typealias JoinRoomScreenViewModelType = StateStoreViewModel<JoinRoomScreenViewState, JoinRoomScreenViewAction>
|
typealias JoinRoomScreenViewModelType = StateStoreViewModel<JoinRoomScreenViewState, JoinRoomScreenViewAction>
|
||||||
|
|
||||||
class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewModelProtocol {
|
class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewModelProtocol {
|
||||||
|
private let roomID: String
|
||||||
private let clientProxy: ClientProxyProtocol
|
private let clientProxy: ClientProxyProtocol
|
||||||
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
private let actionsSubject: PassthroughSubject<JoinRoomScreenViewModelAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<JoinRoomScreenViewModelAction, Never> = .init()
|
||||||
var actionsPublisher: AnyPublisher<JoinRoomScreenViewModelAction, Never> {
|
var actionsPublisher: AnyPublisher<JoinRoomScreenViewModelAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
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.clientProxy = clientProxy
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID,
|
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID), imageProvider: mediaProvider)
|
||||||
roomName: roomName,
|
|
||||||
avatarURL: avatarURL,
|
Task {
|
||||||
interaction: interaction))
|
await loadRoomDetails()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -47,21 +55,93 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
|||||||
case .join:
|
case .join:
|
||||||
Task { await joinRoom() }
|
Task { await joinRoom() }
|
||||||
case .acceptInvite:
|
case .acceptInvite:
|
||||||
break
|
Task { await joinRoom() }
|
||||||
case .declineInvite:
|
case .declineInvite:
|
||||||
break
|
showDeclineInviteConfirmationAlert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
hideLoadingIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func joinRoom() async {
|
private func loadRoomDetails() async {
|
||||||
state.isJoining = true
|
showLoadingIndicator()
|
||||||
switch await clientProxy.joinRoom(state.roomID) {
|
|
||||||
case .success:
|
defer {
|
||||||
actionsSubject.send(.joined)
|
hideLoadingIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch await clientProxy.roomPreviewForIdentifier(roomID) {
|
||||||
|
case .success(let roomDetails):
|
||||||
|
state.roomDetails = roomDetails
|
||||||
case .failure:
|
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 {
|
protocol JoinRoomScreenViewModelProtocol {
|
||||||
var actionsPublisher: AnyPublisher<JoinRoomScreenViewModelAction, Never> { get }
|
var actionsPublisher: AnyPublisher<JoinRoomScreenViewModelAction, Never> { get }
|
||||||
var context: JoinRoomScreenViewModelType.Context { get }
|
var context: JoinRoomScreenViewModelType.Context { get }
|
||||||
|
|
||||||
|
func stop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,16 +28,16 @@ struct JoinRoomScreen: View {
|
|||||||
} bottomContent: {
|
} bottomContent: {
|
||||||
buttons
|
buttons
|
||||||
}
|
}
|
||||||
|
.alert(item: $context.alertInfo)
|
||||||
.background()
|
.background()
|
||||||
.backgroundStyle(.compound.bgCanvasDefault)
|
.backgroundStyle(.compound.bgCanvasDefault)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar { toolbar }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainContent: some View {
|
var mainContent: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
LoadableAvatarImage(url: context.viewState.avatarURL,
|
LoadableAvatarImage(url: context.viewState.roomDetails?.avatarURL,
|
||||||
name: context.viewState.roomName,
|
name: context.viewState.title,
|
||||||
contentID: context.viewState.roomID,
|
contentID: context.viewState.roomID,
|
||||||
avatarSize: .room(on: .joinRoom),
|
avatarSize: .room(on: .joinRoom),
|
||||||
imageProvider: context.imageProvider)
|
imageProvider: context.imageProvider)
|
||||||
@@ -49,10 +49,28 @@ struct JoinRoomScreen: View {
|
|||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Text(context.viewState.subtitle)
|
if let alias = context.viewState.roomDetails?.canonicalAlias {
|
||||||
.font(.compound.bodyMD)
|
Text(alias)
|
||||||
.foregroundStyle(.compound.textSecondary)
|
.font(.compound.bodyMD)
|
||||||
.multilineTextAlignment(.center)
|
.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)
|
Image(asset: Asset.Images.joinRoomGraphic)
|
||||||
@@ -62,7 +80,9 @@ struct JoinRoomScreen: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var buttons: some View {
|
var buttons: some View {
|
||||||
switch context.viewState.interaction {
|
switch context.viewState.mode {
|
||||||
|
case .unknown:
|
||||||
|
EmptyView()
|
||||||
case .knock:
|
case .knock:
|
||||||
Button(L10n.screenJoinRoomKnockAction) { context.send(viewAction: .knock) }
|
Button(L10n.screenJoinRoomKnockAction) { context.send(viewAction: .knock) }
|
||||||
.buttonStyle(.compound(.primary))
|
.buttonStyle(.compound(.primary))
|
||||||
@@ -79,52 +99,82 @@ struct JoinRoomScreen: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var inviteButtons: some View {
|
var inviteButtons: some View {
|
||||||
Button(L10n.actionDecline) { context.send(viewAction: .acceptInvite) }
|
Button(L10n.actionDecline) { context.send(viewAction: .declineInvite) }
|
||||||
.buttonStyle(.compound(.secondary))
|
.buttonStyle(.compound(.secondary))
|
||||||
Button(L10n.actionAccept) { context.send(viewAction: .declineInvite) }
|
Button(L10n.actionAccept) { context.send(viewAction: .acceptInvite) }
|
||||||
.buttonStyle(.compound(.primary))
|
.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
|
// MARK: - Previews
|
||||||
|
|
||||||
struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview {
|
struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||||
static let knockViewModel = makeViewModel(interaction: .knock)
|
static let unknownViewModel = makeViewModel(mode: .unknown)
|
||||||
static let joinViewModel = makeViewModel(interaction: .join)
|
static let knockViewModel = makeViewModel(mode: .knock)
|
||||||
static let inviteViewModel = makeViewModel(interaction: .invited)
|
static let joinViewModel = makeViewModel(mode: .join)
|
||||||
|
static let inviteViewModel = makeViewModel(mode: .invited)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
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 {
|
NavigationStack {
|
||||||
JoinRoomScreen(context: joinViewModel.context)
|
JoinRoomScreen(context: joinViewModel.context)
|
||||||
}
|
}
|
||||||
.previewDisplayName("Join")
|
.previewDisplayName("Join")
|
||||||
|
.snapshot(delay: 0.25)
|
||||||
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
JoinRoomScreen(context: inviteViewModel.context)
|
JoinRoomScreen(context: inviteViewModel.context)
|
||||||
}
|
}
|
||||||
.previewDisplayName("Invite")
|
.previewDisplayName("Invite")
|
||||||
|
.snapshot(delay: 0.25)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel(interaction: JoinRoomScreenInteraction) -> JoinRoomScreenViewModel {
|
static func makeViewModel(mode: JoinRoomScreenInteractionMode) -> JoinRoomScreenViewModel {
|
||||||
JoinRoomScreenViewModel(roomID: "6",
|
let clientProxy = ClientProxyMock(.init())
|
||||||
roomName: "Room Name",
|
|
||||||
avatarURL: nil,
|
// swiftlint:disable:next large_tuple
|
||||||
interaction: interaction,
|
let membership: (isJoined: Bool, isInvited: Bool, isPublic: Bool, canKnock: Bool) = switch mode {
|
||||||
clientProxy: ClientProxyMock(.init()))
|
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 {
|
enum RoomDirectorySearchScreenCoordinatorAction {
|
||||||
case joined(roomID: String)
|
case select(roomID: String)
|
||||||
case dismiss
|
case dismiss
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +48,8 @@ final class RoomDirectorySearchScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel.actionsPublisher.sink { [weak self] action in
|
viewModel.actionsPublisher.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .joined(let roomID):
|
case .select(let roomID):
|
||||||
actionsSubject.send(.joined(roomID: roomID))
|
actionsSubject.send(.select(roomID: roomID))
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum RoomDirectorySearchScreenViewModelAction {
|
enum RoomDirectorySearchScreenViewModelAction {
|
||||||
case joined(roomID: String)
|
case select(roomID: String)
|
||||||
case dismiss
|
case dismiss
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +35,6 @@ struct RoomDirectorySearchScreenViewStateBindings {
|
|||||||
|
|
||||||
enum RoomDirectorySearchScreenViewAction {
|
enum RoomDirectorySearchScreenViewAction {
|
||||||
case dismiss
|
case dismiss
|
||||||
case join(roomID: String)
|
case select(roomID: String)
|
||||||
case reachedBottom
|
case reachedBottom
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-33
@@ -65,8 +65,8 @@ class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType
|
|||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
case .join(roomID: let roomID):
|
case .select(roomID: let roomID):
|
||||||
joinRoom(roomID: roomID)
|
actionsSubject.send(.select(roomID: roomID))
|
||||||
case .reachedBottom:
|
case .reachedBottom:
|
||||||
loadNextPage()
|
loadNextPage()
|
||||||
}
|
}
|
||||||
@@ -74,24 +74,6 @@ class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType
|
|||||||
|
|
||||||
// MARK: - Private
|
// 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 static let errorID = "roomDirectorySearchViewModelLoadingError"
|
||||||
|
|
||||||
private func search(query: String?) {
|
private func search(query: String?) {
|
||||||
@@ -122,17 +104,4 @@ class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType
|
|||||||
state.isLoading = false
|
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 {
|
Section {
|
||||||
ForEach(context.viewState.rooms) { room in
|
ForEach(context.viewState.rooms) { room in
|
||||||
RoomDirectorySearchCell(result: room, imageProvider: context.imageProvider) {
|
RoomDirectorySearchCell(result: room, imageProvider: context.imageProvider) {
|
||||||
context.send(viewAction: .join(roomID: room.id))
|
context.send(viewAction: .select(roomID: room.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
|
|||||||
@@ -456,6 +456,16 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
room: room,
|
room: room,
|
||||||
backgroundTaskService: backgroundTaskService)
|
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> {
|
func loadUserDisplayName() async -> Result<Void, ClientProxyError> {
|
||||||
await Task.dispatch(on: clientQueue) {
|
await Task.dispatch(on: clientQueue) {
|
||||||
@@ -930,3 +940,19 @@ private class IgnoredUsersListenerProxy: IgnoredUsersListener {
|
|||||||
onUpdateClosure(ignoredUserIds)
|
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
|
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
|
// sourcery: AutoMockable
|
||||||
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||||
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
||||||
@@ -123,6 +137,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
|
|
||||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol?
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol?
|
||||||
|
|
||||||
|
func roomPreviewForIdentifier(_ identifier: String) async -> Result<RoomPreviewDetails, ClientProxyError>
|
||||||
|
|
||||||
@discardableResult func loadUserDisplayName() async -> Result<Void, ClientProxyError>
|
@discardableResult func loadUserDisplayName() async -> Result<Void, ClientProxyError>
|
||||||
|
|
||||||
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:47d269397c823468762d9c7fd7075a6f998b4180f4ef5c126d13623348ba02b2
|
oid sha256:95b4eb933dfcab0a5da9c608a0defc26a62206ab665e3a9af0dd6ac4a29bba72
|
||||||
size 2089420
|
size 2099891
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1df7a389ffbf315811e1605cb542a01ae560e977872c49e15092dc0f7432b1eb
|
oid sha256:0b1b5884816d8998bc519d62fb538d961d878e79c62c386c152aa7d6910f3acf
|
||||||
size 2053936
|
size 2064569
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cf9c852e2d1618056391d08faacfbe402be84ced71793364e25e4e1fd0a84481
|
oid sha256:1e227ebf40207f27f1e9ad19881d274a6e6b87bdc2ea40051b6e7fa1e59c84c4
|
||||||
size 2057420
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5df7c700584320710a80633c4b06b1045886a88a32ba112a692560b58ce5eaf0
|
oid sha256:da004ba89cda86c27b44ee6acf78c69d845d887ede2776e90e4194f2819a4e1a
|
||||||
size 2123755
|
size 2104970
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8945225fb83489edf46bb1454770b0c55d2265b8b330ecd16079534c1fc2ef76
|
oid sha256:3a663cf0f68bc9ed2ac0df7e88a9b144a4cc25205fc71d9a3f0e25f892c86788
|
||||||
size 2087732
|
size 2069238
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e60ad9a4485f37882412ec46821127ea0f605f9abe435598f43d8bc2d5279c83
|
oid sha256:09dca383db2dfc978c9f0d78bda8331783aca72a1650c60cba1262fb73637af3
|
||||||
size 2104307
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5e17277315e799bbac0caf145de342a624bfc80357aa63f297334fe8d6c81367
|
oid sha256:2806fc99ef156ed9b0d9e97bc7329a00905eabe5bac5200a17e59124de5b94c1
|
||||||
size 826589
|
size 857415
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4e45734de2938d98904d8b7c19a44b997fb1f6447839f60909e79c1e620096fd
|
oid sha256:5691851137a7bd9c6fd1e6b18597ed4f5b6aa6d89cba714224f54e620e24fb96
|
||||||
size 806983
|
size 837040
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a0f4de3f95f6242680a8b8b52939fd2a3a79a299d8dea6d5609244038c95bc1a
|
oid sha256:c28a125af66dac1dbdfe12cb8dc588b51a1c4611693275113caf522ffe1c68c8
|
||||||
size 811524
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d0bca263cdc71f987ef6b0f4f3d70f4779ea3865a603f752fc5e3ae773c9f394
|
oid sha256:52d999afeabc4fd6dded6ad2ece35968c59c955b90c53961ee1fd42179aeb706
|
||||||
size 857464
|
size 861376
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:171a0753cc99267b93c83f3bc2caf9546d2d72c685251f9fe70796556db5fc3e
|
oid sha256:0bae4ecdbf0b1a8d06e4115af0add88ea5dadd6dc43f61e6a6c0a4aa55dbd34e
|
||||||
size 837228
|
size 840927
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a88bb18fd0311c7c380f76a7c1364a1eb844f708835425b8a22abcc12997c25c
|
oid sha256:32d350137393e9ca3ab38f481c57fa7eacf9c0a89be63de6cdacc2274d0f26cb
|
||||||
size 851763
|
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()
|
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) {
|
private func setupViewModel(throwing: Bool = false) {
|
||||||
let clientProxy = ClientProxyMock(.init())
|
let clientProxy = ClientProxyMock(.init())
|
||||||
|
|
||||||
clientProxy.joinRoomReturnValue = throwing ? .failure(.sdkError(ClientProxyMockError.generic)) : .success(())
|
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",
|
viewModel = JoinRoomScreenViewModel(roomID: "1",
|
||||||
roomName: "Room Name",
|
clientProxy: clientProxy,
|
||||||
avatarURL: nil,
|
mediaProvider: MockMediaProvider(),
|
||||||
interaction: .join,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
clientProxy: clientProxy)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,32 +21,33 @@ import Combine
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class RoomFlowCoordinatorTests: XCTestCase {
|
class RoomFlowCoordinatorTests: XCTestCase {
|
||||||
|
var clientProxy: ClientProxyMock!
|
||||||
var roomFlowCoordinator: RoomFlowCoordinator!
|
var roomFlowCoordinator: RoomFlowCoordinator!
|
||||||
var navigationStackCoordinator: NavigationStackCoordinator!
|
var navigationStackCoordinator: NavigationStackCoordinator!
|
||||||
var cancellables = Set<AnyCancellable>()
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
func testRoomPresentation() async throws {
|
func testRoomPresentation() async throws {
|
||||||
await setupViewModel()
|
await setupRoomFlowCoordinator()
|
||||||
|
|
||||||
try await process(route: .room(roomID: "1"))
|
try await process(route: .room(roomID: "1"))
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
|
|
||||||
try await process(route: .roomList, expectedAction: .finished)
|
try await clearRoute(expectedActions: [.finished])
|
||||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRoomDetailsPresentation() async throws {
|
func testRoomDetailsPresentation() async throws {
|
||||||
await setupViewModel()
|
await setupRoomFlowCoordinator()
|
||||||
|
|
||||||
try await process(route: .roomDetails(roomID: "1"))
|
try await process(route: .roomDetails(roomID: "1"))
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||||
|
|
||||||
try await process(route: .roomList, expectedAction: .finished)
|
try await clearRoute(expectedActions: [.finished])
|
||||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testNoOp() async throws {
|
func testNoOp() async throws {
|
||||||
await setupViewModel()
|
await setupRoomFlowCoordinator()
|
||||||
|
|
||||||
try await process(route: .roomDetails(roomID: "1"))
|
try await process(route: .roomDetails(roomID: "1"))
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||||
@@ -60,7 +61,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testPushDetails() async throws {
|
func testPushDetails() async throws {
|
||||||
await setupViewModel()
|
await setupRoomFlowCoordinator()
|
||||||
|
|
||||||
try await process(route: .room(roomID: "1"))
|
try await process(route: .room(roomID: "1"))
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
@@ -73,7 +74,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testChildRoomFlow() async throws {
|
func testChildRoomFlow() async throws {
|
||||||
await setupViewModel()
|
await setupRoomFlowCoordinator()
|
||||||
|
|
||||||
try await process(route: .room(roomID: "1"))
|
try await process(route: .room(roomID: "1"))
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
@@ -88,14 +89,14 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||||
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||||
|
|
||||||
try await process(route: .roomList, expectedAction: .finished)
|
try await clearRoute(expectedActions: [.finished])
|
||||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tests the child flow teardown in isolation of it's parent.
|
/// Tests the child flow teardown in isolation of it's parent.
|
||||||
func testChildFlowTearDown() async throws {
|
func testChildFlowTearDown() async throws {
|
||||||
await setupViewModel(asChildFlow: true)
|
await setupRoomFlowCoordinator(asChildFlow: true)
|
||||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||||
|
|
||||||
try await process(route: .room(roomID: "1"))
|
try await process(route: .room(roomID: "1"))
|
||||||
@@ -105,7 +106,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator)
|
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator)
|
||||||
|
|
||||||
try await process(route: .roomList, expectedAction: .finished)
|
try await clearRoute(expectedActions: [.finished])
|
||||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator)
|
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator)
|
||||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2, "A child room flow should leave its parent to clean up the stack.")
|
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.")
|
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 {
|
func testChildRoomMemberDetails() async throws {
|
||||||
await setupViewModel()
|
await setupRoomFlowCoordinator()
|
||||||
|
|
||||||
try await process(route: .room(roomID: "1"))
|
try await process(route: .room(roomID: "1"))
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
@@ -130,7 +131,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testChildRoomIgnoresDirectDuplicate() async throws {
|
func testChildRoomIgnoresDirectDuplicate() async throws {
|
||||||
await setupViewModel()
|
await setupRoomFlowCoordinator()
|
||||||
|
|
||||||
try await process(route: .room(roomID: "1"))
|
try await process(route: .room(roomID: "1"))
|
||||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||||
@@ -151,6 +152,60 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
private func process(route: AppRoute) async throws {
|
private func process(route: AppRoute) async throws {
|
||||||
@@ -159,11 +214,15 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
try await Task.sleep(for: .milliseconds(100))
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func process(route: AppRoute, expectedAction: RoomFlowCoordinatorAction) async throws {
|
private func clearRoute(expectedActions: [RoomFlowCoordinatorAction]) async throws {
|
||||||
try await process(route: route, expectedActions: [expectedAction])
|
try await processRouteOrClear(route: nil, expectedActions: expectedActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func process(route: AppRoute, expectedActions: [RoomFlowCoordinatorAction]) async throws {
|
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 {
|
guard !expectedActions.isEmpty else {
|
||||||
return
|
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 {
|
for fulfillment in fulfillments {
|
||||||
try await fulfillment.fulfill()
|
try await fulfillment.fulfill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupViewModel(asChildFlow: Bool = false) async {
|
private func setupRoomFlowCoordinator(asChildFlow: Bool = false, roomType: RoomType? = nil) async {
|
||||||
cancellables.removeAll()
|
cancellables.removeAll()
|
||||||
let clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
||||||
|
|
||||||
|
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 mediaProvider = MockMediaProvider()
|
||||||
let voiceMessageMediaManager = VoiceMessageMediaManagerMock()
|
let voiceMessageMediaManager = VoiceMessageMediaManagerMock()
|
||||||
let userSession = MockUserSession(clientProxy: clientProxy,
|
let userSession = MockUserSession(clientProxy: clientProxy,
|
||||||
@@ -196,7 +279,14 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
navigationStackCoordinator = NavigationStackCoordinator()
|
navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
navigationSplitCoordinator.setDetailCoordinator(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,
|
userSession: userSession,
|
||||||
isChildFlow: asChildFlow,
|
isChildFlow: asChildFlow,
|
||||||
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
|
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
|
||||||
@@ -209,3 +299,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
|||||||
orientationManager: OrientationManagerMock())
|
orientationManager: OrientationManagerMock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum RoomType {
|
||||||
|
case invited(roomID: String)
|
||||||
|
}
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
let items = [TextRoomTimelineItem(eventID: "t1"),
|
let items = [TextRoomTimelineItem(eventID: "t1"),
|
||||||
TextRoomTimelineItem(eventID: "t2"),
|
TextRoomTimelineItem(eventID: "t2"),
|
||||||
TextRoomTimelineItem(eventID: "t3")]
|
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.
|
// When sending a read receipt for the last item.
|
||||||
viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
|
viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
|
||||||
|
|||||||
Reference in New Issue
Block a user