mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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>
|
||||
|
||||
Reference in New Issue
Block a user