mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Allow joined rooms to be pushed within a space. (#4460)
* Allow joined rooms to be pushed within a space. * Push a room in the space flow tests. Also fixes some snapshots stale snapshots. * Show the selected space/room within a space and set a custom title view.
This commit is contained in:
@@ -9,8 +9,10 @@ import Combine
|
||||
import Foundation
|
||||
import SwiftState
|
||||
|
||||
enum SpaceExplorerFlowCoordinatorAction: Equatable {
|
||||
enum SpaceExplorerFlowCoordinatorAction {
|
||||
case showSettings
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||
case verifyUser(userID: String)
|
||||
}
|
||||
|
||||
class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol {
|
||||
@@ -113,7 +115,7 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func presentSpaceList() {
|
||||
let parameters = SpaceListScreenCoordinatorParameters(userSession: userSession,
|
||||
selectedSpaceSubject: selectedSpaceSubject.asCurrentValuePublisher(),
|
||||
selectedSpacePublisher: selectedSpaceSubject.asCurrentValuePublisher(),
|
||||
userIndicatorController: flowParameters.userIndicatorController)
|
||||
let coordinator = SpaceListScreenCoordinator(parameters: parameters)
|
||||
coordinator.actionsPublisher
|
||||
@@ -143,6 +145,10 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.deselectSpace)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import Combine
|
||||
import Foundation
|
||||
import SwiftState
|
||||
|
||||
enum SpaceFlowCoordinatorAction: Equatable {
|
||||
enum SpaceFlowCoordinatorAction {
|
||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||
case verifyUser(userID: String)
|
||||
case finished
|
||||
}
|
||||
|
||||
@@ -20,17 +22,22 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
private var childSpaceFlowCoordinator: SpaceFlowCoordinator?
|
||||
|
||||
private let flowParameters: CommonFlowParameters
|
||||
|
||||
private let selectedSpaceRoomSubject: CurrentValueSubject<String?, Never> = .init(nil)
|
||||
|
||||
private var childSpaceFlowCoordinator: SpaceFlowCoordinator?
|
||||
private var roomFlowCoordinator: RoomFlowCoordinator?
|
||||
|
||||
indirect enum State: StateType {
|
||||
/// The state machine hasn't started.
|
||||
case initial
|
||||
/// The root screen for this flow.
|
||||
case space
|
||||
/// A child flow is in progress.
|
||||
/// A child (space) flow is in progress.
|
||||
case presentingChild(childSpaceID: String, previousState: State)
|
||||
/// A room flow is in progress
|
||||
case roomFlow(previousState: State)
|
||||
}
|
||||
|
||||
enum Event: EventType {
|
||||
@@ -43,6 +50,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case startChildFlow
|
||||
/// Tidy-up the child flow after it has dismissed itself.
|
||||
case stopChildFlow
|
||||
|
||||
case startRoomFlow(roomID: String)
|
||||
case stopRoomFlow
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
@@ -91,6 +101,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .presentingChild:
|
||||
childSpaceFlowCoordinator?.clearRoute(animated: animated)
|
||||
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
|
||||
case .roomFlow:
|
||||
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +129,24 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
} handler: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
childSpaceFlowCoordinator = nil
|
||||
selectedSpaceRoomSubject.send(nil)
|
||||
}
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
guard case .startRoomFlow = event, case .space = fromState else { return nil }
|
||||
return .roomFlow(previousState: fromState)
|
||||
} handler: { [weak self] context in
|
||||
guard let self, case let .startRoomFlow(roomID) = context.event else { return }
|
||||
startRoomFlow(roomID: roomID)
|
||||
}
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
guard event == .stopRoomFlow, case let .roomFlow(previousState) = fromState else { return nil }
|
||||
return previousState
|
||||
} handler: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
roomFlowCoordinator = nil
|
||||
selectedSpaceRoomSubject.send(nil)
|
||||
}
|
||||
|
||||
stateMachine.addErrorHandler { context in
|
||||
@@ -126,6 +157,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private func presentSpace() {
|
||||
let parameters = SpaceScreenCoordinatorParameters(spaceRoomListProxy: spaceRoomListProxy,
|
||||
spaceServiceProxy: spaceServiceProxy,
|
||||
selectedSpaceRoomPublisher: selectedSpaceRoomSubject.asCurrentValuePublisher(),
|
||||
mediaProvider: flowParameters.userSession.mediaProvider,
|
||||
userIndicatorController: flowParameters.userIndicatorController)
|
||||
let coordinator = SpaceScreenCoordinator(parameters: parameters)
|
||||
@@ -135,6 +167,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .selectSpace(let spaceRoomListProxy):
|
||||
stateMachine.tryEvent(.startChildFlow, userInfo: spaceRoomListProxy)
|
||||
case .selectRoom(let roomID):
|
||||
stateMachine.tryEvent(.startRoomFlow(roomID: roomID))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -164,6 +198,10 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.stopChildFlow)
|
||||
}
|
||||
@@ -172,5 +210,31 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
childSpaceFlowCoordinator = coordinator
|
||||
coordinator.start()
|
||||
selectedSpaceRoomSubject.send(spaceRoomListProxy.spaceRoomProxy.id)
|
||||
}
|
||||
|
||||
private func startRoomFlow(roomID: String) {
|
||||
let coordinator = RoomFlowCoordinator(roomID: roomID,
|
||||
isChildFlow: true,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
flowParameters: flowParameters)
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.stopRoomFlow)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
roomFlowCoordinator = coordinator
|
||||
coordinator.handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
||||
selectedSpaceRoomSubject.send(roomID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,10 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .presentCallScreen(let roomProxy):
|
||||
presentCallScreen(roomProxy: roomProxy)
|
||||
case .verifyUser(let userID):
|
||||
presentSessionVerificationScreen(flow: .userInitiator(userID: userID))
|
||||
case .showSettings:
|
||||
stateMachine.tryEvent(.showSettingsScreen)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ struct ClientProxyMockConfiguration {
|
||||
var userID: String = RoomMemberProxyMock.mockMe.userID
|
||||
var deviceID: String?
|
||||
var roomSummaryProvider: RoomSummaryProviderProtocol = RoomSummaryProviderMock(.init())
|
||||
var joinedSpaceRooms: [SpaceRoomProxyProtocol] = []
|
||||
var roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol?
|
||||
|
||||
var recoveryState: SecureBackupRecoveryState = .enabled
|
||||
@@ -92,11 +93,13 @@ extension ClientProxyMock {
|
||||
spaceService = SpaceServiceProxyMock(.init())
|
||||
|
||||
roomForIdentifierClosure = { [weak self] identifier in
|
||||
guard let room = self?.roomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier }) else {
|
||||
return nil
|
||||
if let room = self?.roomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier }) {
|
||||
await .joined(JoinedRoomProxyMock(.init(id: room.id, name: room.name)))
|
||||
} else if let spaceRoom = configuration.joinedSpaceRooms.first(where: { $0.id == identifier }) {
|
||||
await .joined(JoinedRoomProxyMock(.init(id: spaceRoom.id, name: spaceRoom.name)))
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
return await .joined(JoinedRoomProxyMock(.init(id: room.id, name: room.name)))
|
||||
}
|
||||
|
||||
userIdentityForReturnValue = .success(UserIdentityProxyMock(configuration: .init()))
|
||||
|
||||
@@ -104,7 +104,8 @@ extension [SpaceRoomProxyProtocol] {
|
||||
name: "Management",
|
||||
isSpace: false,
|
||||
joinedMembersCount: 12,
|
||||
topic: "This is where everything gets organised 📋."))]
|
||||
topic: "This is where everything gets organised 📋.",
|
||||
state: .joined))]
|
||||
}
|
||||
|
||||
private static func makeSpaceRooms(isSpace: Bool) -> [SpaceRoomProxyMock] {
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
|
||||
struct SpaceListScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let selectedSpaceSubject: CurrentValuePublisher<String?, Never>
|
||||
let selectedSpacePublisher: CurrentValuePublisher<String?, Never>
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ final class SpaceListScreenCoordinator: CoordinatorProtocol {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = SpaceListScreenViewModel(userSession: parameters.userSession,
|
||||
selectedSpaceSubject: parameters.selectedSpaceSubject,
|
||||
selectedSpacePublisher: parameters.selectedSpacePublisher,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie
|
||||
}
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
selectedSpaceSubject: CurrentValuePublisher<String?, Never>,
|
||||
selectedSpacePublisher: CurrentValuePublisher<String?, Never>,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
spaceServiceProxy = userSession.clientProxy.spaceService
|
||||
self.userIndicatorController = userIndicatorController
|
||||
@@ -35,7 +35,7 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie
|
||||
.weakAssign(to: \.state.joinedSpaces, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
selectedSpaceSubject
|
||||
selectedSpacePublisher
|
||||
.weakAssign(to: \.state.selectedSpaceID, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ struct SpaceListScreen_Previews: PreviewProvider, TestablePreview {
|
||||
clientProxy.spaceService = SpaceServiceProxyMock(.init(joinedSpaces: .mockJoinedSpaces))
|
||||
|
||||
let viewModel = SpaceListScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
selectedSpaceSubject: .init(nil),
|
||||
selectedSpacePublisher: .init(nil),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
return viewModel
|
||||
|
||||
@@ -13,12 +13,14 @@ import SwiftUI
|
||||
struct SpaceScreenCoordinatorParameters {
|
||||
let spaceRoomListProxy: SpaceRoomListProxyProtocol
|
||||
let spaceServiceProxy: SpaceServiceProxyProtocol
|
||||
let selectedSpaceRoomPublisher: CurrentValuePublisher<String?, Never>
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum SpaceScreenCoordinatorAction {
|
||||
case selectSpace(SpaceRoomListProxyProtocol)
|
||||
case selectRoom(roomID: String)
|
||||
}
|
||||
|
||||
final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -37,6 +39,7 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
viewModel = SpaceScreenViewModel(spaceRoomListProxy: parameters.spaceRoomListProxy,
|
||||
spaceServiceProxy: parameters.spaceServiceProxy,
|
||||
selectedSpaceRoomPublisher: parameters.selectedSpaceRoomPublisher,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
@@ -49,6 +52,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .selectSpace(let spaceRoomListProxy):
|
||||
actionsSubject.send(.selectSpace(spaceRoomListProxy))
|
||||
case .selectRoom(let roomID):
|
||||
actionsSubject.send(.selectRoom(roomID: roomID))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
|
||||
enum SpaceScreenViewModelAction {
|
||||
case selectSpace(SpaceRoomListProxyProtocol)
|
||||
case selectRoom(roomID: String)
|
||||
}
|
||||
|
||||
struct SpaceScreenViewState: BindableState {
|
||||
@@ -16,8 +17,11 @@ struct SpaceScreenViewState: BindableState {
|
||||
|
||||
var isPaginating = false
|
||||
var rooms: [SpaceRoomProxyProtocol]
|
||||
var selectedSpaceRoomID: String?
|
||||
|
||||
var bindings = SpaceScreenViewStateBindings()
|
||||
|
||||
var spaceName: String { space.name ?? L10n.commonSpace }
|
||||
}
|
||||
|
||||
struct SpaceScreenViewStateBindings { }
|
||||
|
||||
@@ -21,13 +21,15 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
|
||||
init(spaceRoomListProxy: SpaceRoomListProxyProtocol,
|
||||
spaceServiceProxy: SpaceServiceProxyProtocol,
|
||||
selectedSpaceRoomPublisher: CurrentValuePublisher<String?, Never>,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.spaceServiceProxy = spaceServiceProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: SpaceScreenViewState(space: spaceRoomListProxy.spaceRoomProxy,
|
||||
rooms: spaceRoomListProxy.spaceRoomsPublisher.value),
|
||||
rooms: spaceRoomListProxy.spaceRoomsPublisher.value,
|
||||
selectedSpaceRoomID: selectedSpaceRoomPublisher.value),
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
spaceRoomListProxy.spaceRoomsPublisher
|
||||
@@ -50,6 +52,10 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
selectedSpaceRoomPublisher
|
||||
.weakAssign(to: \.state.selectedSpaceRoomID, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -61,8 +67,10 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
case .spaceAction(.select(let spaceRoomProxy)):
|
||||
if spaceRoomProxy.isSpace {
|
||||
Task { await selectSpace(spaceRoomProxy) }
|
||||
} else {
|
||||
#warning("Implement room flow")
|
||||
} else if spaceRoomProxy.state == .joined {
|
||||
// This probably doesn't need the state condition as the room flow will show a join screen,
|
||||
// but we can allow this later, once we've updated the design to indicate the parent space.
|
||||
actionsSubject.send(.selectRoom(roomID: spaceRoomProxy.id))
|
||||
}
|
||||
case .spaceAction(.join(let spaceID)):
|
||||
#warning("Implement joining.")
|
||||
|
||||
@@ -19,16 +19,17 @@ struct SpaceScreen: View {
|
||||
rooms
|
||||
}
|
||||
}
|
||||
.navigationTitle(context.viewState.space.name ?? L10n.commonSpace)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.navigationTitle(context.viewState.spaceName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var rooms: some View {
|
||||
ForEach(context.viewState.rooms, id: \.id) { spaceRoomProxy in
|
||||
SpaceRoomCell(spaceRoomProxy: spaceRoomProxy,
|
||||
isSelected: false,
|
||||
isSelected: spaceRoomProxy.id == context.viewState.selectedSpaceRoomID,
|
||||
mediaProvider: context.mediaProvider) { action in
|
||||
context.send(viewAction: .spaceAction(action))
|
||||
}
|
||||
@@ -39,6 +40,16 @@ struct SpaceScreen: View {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
var toolbar: some ToolbarContent {
|
||||
// Use the same trick as the RoomScreen for a leading title view that
|
||||
// also hides the navigation title.
|
||||
ToolbarItem(placement: .principal) {
|
||||
RoomHeaderView(roomName: context.viewState.spaceName,
|
||||
roomAvatar: context.viewState.space.avatar,
|
||||
mediaProvider: context.mediaProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
@@ -66,6 +77,7 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
|
||||
spaceServiceProxy: SpaceServiceProxyMock(.init()),
|
||||
selectedSpaceRoomPublisher: .init(nil),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
return viewModel
|
||||
|
||||
@@ -571,7 +571,10 @@ class MockScreen: Identifiable {
|
||||
appSettings.analyticsConsentState = .optedOut
|
||||
appSettings.spacesEnabled = true
|
||||
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", deviceID: "MOCKCLIENT", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com",
|
||||
deviceID: "MOCKCLIENT",
|
||||
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
|
||||
joinedSpaceRooms: .mockSingleRoom))
|
||||
|
||||
// The tab bar remains hidden for the non-spaces tests as we don't supply any mock spaces.
|
||||
let spaceServiceProxy = SpaceServiceProxyMock(id == .userSessionSpacesFlow ? .populated : .init())
|
||||
|
||||
Reference in New Issue
Block a user