mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Joining a space from the room flow. (#4563)
* Add the More menu to SpaceScreen. And a simple way to leave a space for testing. * Start a space flow when accepting a space invite from the room flow. * Minimise the tab bar on iOS 26 when scrolling down. * Enable spaces by default!
This commit is contained in:
@@ -367,6 +367,7 @@
|
|||||||
41DFDD212D1BE57CA50D783B /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; };
|
41DFDD212D1BE57CA50D783B /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; };
|
||||||
41F553349AF44567184822D8 /* APNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D670124FC3E84F23A62CCF /* APNSPayload.swift */; };
|
41F553349AF44567184822D8 /* APNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D670124FC3E84F23A62CCF /* APNSPayload.swift */; };
|
||||||
4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */; };
|
4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */; };
|
||||||
|
42239FD59394A086E046C9BF /* Backports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A46ABD27628CB5FC402541 /* Backports.swift */; };
|
||||||
422E8D182CA688D4565CD1E1 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; };
|
422E8D182CA688D4565CD1E1 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; };
|
||||||
4295E5F850897710A51AE114 /* GeoURI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */; };
|
4295E5F850897710A51AE114 /* GeoURI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */; };
|
||||||
42995EA68E194B19DAD6AEEF /* test_rotated_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 723B055A57857BFF0F18D9CB /* test_rotated_image.jpg */; };
|
42995EA68E194B19DAD6AEEF /* test_rotated_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 723B055A57857BFF0F18D9CB /* test_rotated_image.jpg */; };
|
||||||
@@ -2098,6 +2099,7 @@
|
|||||||
753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSendInfoLabel.swift; sourceTree = "<group>"; };
|
753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSendInfoLabel.swift; sourceTree = "<group>"; };
|
||||||
75821CD31A4BD02B99C327A4 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
75821CD31A4BD02B99C327A4 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
||||||
76310030C831D4610A705603 /* URLComponentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsTests.swift; sourceTree = "<group>"; };
|
76310030C831D4610A705603 /* URLComponentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsTests.swift; sourceTree = "<group>"; };
|
||||||
|
76A46ABD27628CB5FC402541 /* Backports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backports.swift; sourceTree = "<group>"; };
|
||||||
7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; };
|
7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; };
|
||||||
780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = "<group>"; };
|
780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
@@ -2995,6 +2997,7 @@
|
|||||||
052CC920F473C10B509F9FC1 /* SwiftUI */ = {
|
052CC920F473C10B509F9FC1 /* SwiftUI */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
76A46ABD27628CB5FC402541 /* Backports.swift */,
|
||||||
693E16574C6F7F9FA1015A8C /* Search.swift */,
|
693E16574C6F7F9FA1015A8C /* Search.swift */,
|
||||||
832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */,
|
832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */,
|
||||||
D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */,
|
D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */,
|
||||||
@@ -7579,6 +7582,7 @@
|
|||||||
0B05A35FF5D1ED9E8A0B41A7 /* AuthenticationStartScreenViewModelProtocol.swift in Sources */,
|
0B05A35FF5D1ED9E8A0B41A7 /* AuthenticationStartScreenViewModelProtocol.swift in Sources */,
|
||||||
4AAA8606FBA290E23D15422E /* AvatarHeaderView.swift in Sources */,
|
4AAA8606FBA290E23D15422E /* AvatarHeaderView.swift in Sources */,
|
||||||
1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */,
|
1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */,
|
||||||
|
42239FD59394A086E046C9BF /* Backports.swift in Sources */,
|
||||||
7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */,
|
7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */,
|
||||||
A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */,
|
A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */,
|
||||||
2B52B2A5A6496C5C0F952776 /* BannedRoomProxy.swift in Sources */,
|
2B52B2A5A6496C5C0F952776 /* BannedRoomProxy.swift in Sources */,
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
|||||||
.toolbar(module.details.barVisibility(in: horizontalSizeClass), for: .tabBar)
|
.toolbar(module.details.barVisibility(in: horizontalSizeClass), for: .tabBar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.backportTabBarMinimizeBehaviorOnScrollDown()
|
||||||
.introspect(.tabView, on: .supportedVersions, customize: configureAppearance)
|
.introspect(.tabView, on: .supportedVersions, customize: configureAppearance)
|
||||||
.sheet(item: $navigationTabCoordinator.sheetModule) { module in
|
.sheet(item: $navigationTabCoordinator.sheetModule) { module in
|
||||||
module.coordinator?.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var threadsEnabled
|
var threadsEnabled
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.spacesEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.spacesEnabled, defaultValue: true, storageType: .userDefaults(store))
|
||||||
var spacesEnabled
|
var spacesEnabled
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.nextGenHTMLParserEnabled, defaultValue: true, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.nextGenHTMLParserEnabled, defaultValue: true, storageType: .userDefaults(store))
|
||||||
|
|||||||
@@ -469,6 +469,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
actionsSubject.send(.showCallScreen(roomProxy: roomProxy))
|
actionsSubject.send(.showCallScreen(roomProxy: roomProxy))
|
||||||
case .verifyUser(let userID):
|
case .verifyUser(let userID):
|
||||||
actionsSubject.send(.sessionVerification(.userInitiator(userID: userID)))
|
actionsSubject.send(.sessionVerification(.userInitiator(userID: userID)))
|
||||||
|
case .continueWithSpaceFlow(let spaceRoomListProxy):
|
||||||
|
stateMachine.processEvent(.startSpaceFlow, userInfo: .init(animated: false, spaceRoomListProxy: spaceRoomListProxy))
|
||||||
case .finished:
|
case .finished:
|
||||||
stateMachine.processEvent(.deselectRoom)
|
stateMachine.processEvent(.deselectRoom)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import UserNotifications
|
|||||||
enum RoomFlowCoordinatorAction: Equatable {
|
enum RoomFlowCoordinatorAction: Equatable {
|
||||||
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
case presentCallScreen(roomProxy: JoinedRoomProxyProtocol)
|
||||||
case verifyUser(userID: String)
|
case verifyUser(userID: String)
|
||||||
|
/// The requested room was actually a space. The room flow has been dismissed
|
||||||
|
/// and a space flow should be started to continue.
|
||||||
|
case continueWithSpaceFlow(SpaceRoomListProxyProtocol)
|
||||||
case finished
|
case finished
|
||||||
|
|
||||||
static func == (lhs: RoomFlowCoordinatorAction, rhs: RoomFlowCoordinatorAction) -> Bool {
|
static func == (lhs: RoomFlowCoordinatorAction, rhs: RoomFlowCoordinatorAction) -> Bool {
|
||||||
@@ -385,6 +388,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
presentJoinRoomScreen(via: via, animated: true)
|
presentJoinRoomScreen(via: via, animated: true)
|
||||||
case (_, .dismissJoinRoomScreen, .complete):
|
case (_, .dismissJoinRoomScreen, .complete):
|
||||||
dismissFlow(animated: animated)
|
dismissFlow(animated: animated)
|
||||||
|
case (_, .joinedSpace, .complete):
|
||||||
|
guard let spaceRoomListProxy = (context.userInfo as? EventUserInfo)?.spaceRoomListProxy else {
|
||||||
|
fatalError("The space room list proxy is required to present a space.")
|
||||||
|
}
|
||||||
|
dismissFlow(animated: animated, continuingWith: spaceRoomListProxy)
|
||||||
|
|
||||||
case (.joinRoomScreen, .presentDeclineAndBlockScreen(let userID), .declineAndBlockScreen):
|
case (.joinRoomScreen, .presentDeclineAndBlockScreen(let userID), .declineAndBlockScreen):
|
||||||
presentDeclineAndBlockScreen(userID: userID)
|
presentDeclineAndBlockScreen(userID: userID)
|
||||||
@@ -720,8 +728,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .joined(.space):
|
case .joined(.space(let spaceRoomListProxy)):
|
||||||
#warning("The space flow should be shown here.")
|
stateMachine.tryEvent(.joinedSpace, userInfo: EventUserInfo(animated: true, spaceRoomListProxy: spaceRoomListProxy))
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
stateMachine.tryEvent(.dismissJoinRoomScreen)
|
stateMachine.tryEvent(.dismissJoinRoomScreen)
|
||||||
case .presentDeclineAndBlock(let userID):
|
case .presentDeclineAndBlock(let userID):
|
||||||
@@ -745,7 +753,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissFlow(animated: Bool) {
|
private func dismissFlow(animated: Bool, continuingWith spaceRoomListProxy: SpaceRoomListProxyProtocol? = nil) {
|
||||||
childRoomFlowCoordinator?.clearRoute(animated: animated)
|
childRoomFlowCoordinator?.clearRoute(animated: animated)
|
||||||
|
|
||||||
if isChildFlow {
|
if isChildFlow {
|
||||||
@@ -757,12 +765,21 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navigationStackCoordinator.popToRoot(animated: false)
|
navigationStackCoordinator.popToRoot(animated: false)
|
||||||
navigationStackCoordinator.setRootCoordinator(nil, animated: false)
|
|
||||||
|
// Leave the root alone when it is about to be replaced by the space flow, otherwise when running on
|
||||||
|
// iPhone the compact module diffs call the dismissal callback and we present a blank space flow 🙈
|
||||||
|
if spaceRoomListProxy == nil {
|
||||||
|
navigationStackCoordinator.setRootCoordinator(nil, animated: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timelineController = nil
|
timelineController = nil
|
||||||
|
|
||||||
actionsSubject.send(.finished)
|
if let spaceRoomListProxy {
|
||||||
|
actionsSubject.send(.continueWithSpaceFlow(spaceRoomListProxy))
|
||||||
|
} else {
|
||||||
|
actionsSubject.send(.finished)
|
||||||
|
}
|
||||||
flowParameters.analytics.signpost.endRoomFlow()
|
flowParameters.analytics.signpost.endRoomFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1457,6 +1474,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||||
case .verifyUser(let userID):
|
case .verifyUser(let userID):
|
||||||
actionsSubject.send(.verifyUser(userID: userID))
|
actionsSubject.send(.verifyUser(userID: userID))
|
||||||
|
case .continueWithSpaceFlow(let spaceRoomListProxy):
|
||||||
|
#warning("Present the space as a child.")
|
||||||
case .finished:
|
case .finished:
|
||||||
stateMachine.tryEvent(.dismissChildFlow)
|
stateMachine.tryEvent(.dismissChildFlow)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,11 +84,13 @@ extension RoomFlowCoordinator {
|
|||||||
struct EventUserInfo {
|
struct EventUserInfo {
|
||||||
let animated: Bool
|
let animated: Bool
|
||||||
var timelineController: TimelineControllerProtocol?
|
var timelineController: TimelineControllerProtocol?
|
||||||
|
var spaceRoomListProxy: SpaceRoomListProxyProtocol?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Event: EventType {
|
enum Event: EventType {
|
||||||
case presentJoinRoomScreen(via: [String])
|
case presentJoinRoomScreen(via: [String])
|
||||||
case dismissJoinRoomScreen
|
case dismissJoinRoomScreen
|
||||||
|
case joinedSpace
|
||||||
|
|
||||||
case presentRoom(presentationAction: PresentationAction?)
|
case presentRoom(presentationAction: PresentationAction?)
|
||||||
case dismissFlow
|
case dismissFlow
|
||||||
@@ -321,6 +323,8 @@ extension RoomFlowCoordinator {
|
|||||||
return .joinRoomScreen
|
return .joinRoomScreen
|
||||||
case (_, .dismissJoinRoomScreen):
|
case (_, .dismissJoinRoomScreen):
|
||||||
return .complete
|
return .complete
|
||||||
|
case (_, .joinedSpace):
|
||||||
|
return .complete
|
||||||
|
|
||||||
case (.joinRoomScreen, .presentDeclineAndBlockScreen):
|
case (.joinRoomScreen, .presentDeclineAndBlockScreen):
|
||||||
return .declineAndBlockScreen
|
return .declineAndBlockScreen
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case presentingChild(childSpaceID: String, previousState: State)
|
case presentingChild(childSpaceID: String, previousState: State)
|
||||||
/// A room flow is in progress
|
/// A room flow is in progress
|
||||||
case roomFlow(previousState: State)
|
case roomFlow(previousState: State)
|
||||||
|
|
||||||
|
case leftSpace
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Event: EventType {
|
enum Event: EventType {
|
||||||
@@ -62,6 +64,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
/// The join space screen joined the space.
|
/// The join space screen joined the space.
|
||||||
case joinedSpace
|
case joinedSpace
|
||||||
|
/// The space screen left the space.
|
||||||
|
case leftSpace
|
||||||
|
|
||||||
/// Request the presentation of a child space flow.
|
/// Request the presentation of a child space flow.
|
||||||
///
|
///
|
||||||
@@ -116,7 +120,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch stateMachine.state {
|
switch stateMachine.state {
|
||||||
case .initial:
|
case .initial:
|
||||||
break
|
break
|
||||||
case .joinSpace, .space:
|
case .joinSpace, .space, .leftSpace:
|
||||||
if isChildFlow {
|
if isChildFlow {
|
||||||
navigationStackCoordinator.pop(animated: animated)
|
navigationStackCoordinator.pop(animated: animated)
|
||||||
} else {
|
} else {
|
||||||
@@ -145,11 +149,18 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
stateMachine.addRoutes(event: .joinedSpace, transitions: [.joinSpace => .space]) { [weak self] _ in
|
stateMachine.addRoutes(event: .joinedSpace, transitions: [.joinSpace => .space]) { [weak self] _ in
|
||||||
self?.presentSpaceAfterJoining()
|
self?.presentSpaceAfterJoining()
|
||||||
}
|
}
|
||||||
|
stateMachine.addRoutes(event: .leftSpace, transitions: [.space => .leftSpace]) { [weak self] _ in
|
||||||
|
self?.clearRoute(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
stateMachine.addRouteMapping { event, fromState, userInfo in
|
stateMachine.addRouteMapping { event, fromState, userInfo in
|
||||||
guard event == .startChildFlow, case .space = fromState else { return nil }
|
guard event == .startChildFlow else { return nil }
|
||||||
guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") }
|
guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") }
|
||||||
return .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: fromState)
|
return switch fromState {
|
||||||
|
case .space: .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: fromState)
|
||||||
|
case .roomFlow(let previousState): .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: previousState)
|
||||||
|
default: nil
|
||||||
|
}
|
||||||
} handler: { [weak self] context in
|
} handler: { [weak self] context in
|
||||||
guard let self, let entryPoint = context.userInfo as? SpaceFlowCoordinatorEntryPoint else { return }
|
guard let self, let entryPoint = context.userInfo as? SpaceFlowCoordinatorEntryPoint else { return }
|
||||||
startChildFlow(with: entryPoint)
|
startChildFlow(with: entryPoint)
|
||||||
@@ -205,6 +216,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
stateMachine.tryEvent(.startChildFlow, userInfo: SpaceFlowCoordinatorEntryPoint.joinSpace(spaceRoomProxy))
|
stateMachine.tryEvent(.startChildFlow, userInfo: SpaceFlowCoordinatorEntryPoint.joinSpace(spaceRoomProxy))
|
||||||
case .selectRoom(let roomID):
|
case .selectRoom(let roomID):
|
||||||
stateMachine.tryEvent(.startRoomFlow(roomID: roomID))
|
stateMachine.tryEvent(.startRoomFlow(roomID: roomID))
|
||||||
|
case .leftSpace:
|
||||||
|
stateMachine.tryEvent(.leftSpace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -314,6 +327,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||||
case .verifyUser(let userID):
|
case .verifyUser(let userID):
|
||||||
actionsSubject.send(.verifyUser(userID: userID))
|
actionsSubject.send(.verifyUser(userID: userID))
|
||||||
|
case .continueWithSpaceFlow(let spaceRoomListProxy):
|
||||||
|
stateMachine.tryEvent(.startChildFlow, userInfo: SpaceFlowCoordinatorEntryPoint.space(spaceRoomListProxy))
|
||||||
case .finished:
|
case .finished:
|
||||||
stateMachine.tryEvent(.stopRoomFlow)
|
stateMachine.tryEvent(.stopRoomFlow)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
@ViewBuilder
|
||||||
|
func backportTabBarMinimizeBehaviorOnScrollDown() -> some View {
|
||||||
|
if #available(iOS 26.0, *) {
|
||||||
|
tabBarMinimizeBehavior(.onScrollDown)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ enum SpaceScreenCoordinatorAction {
|
|||||||
case selectSpace(SpaceRoomListProxyProtocol)
|
case selectSpace(SpaceRoomListProxyProtocol)
|
||||||
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
||||||
case selectRoom(roomID: String)
|
case selectRoom(roomID: String)
|
||||||
|
case leftSpace
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SpaceScreenCoordinator: CoordinatorProtocol {
|
final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||||
@@ -57,6 +58,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
|
|||||||
actionsSubject.send(.selectUnjoinedSpace(spaceRoomProxy))
|
actionsSubject.send(.selectUnjoinedSpace(spaceRoomProxy))
|
||||||
case .selectRoom(let roomID):
|
case .selectRoom(let roomID):
|
||||||
actionsSubject.send(.selectRoom(roomID: roomID))
|
actionsSubject.send(.selectRoom(roomID: roomID))
|
||||||
|
case .leftSpace:
|
||||||
|
actionsSubject.send(.leftSpace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ enum SpaceScreenViewModelAction {
|
|||||||
case selectSpace(SpaceRoomListProxyProtocol)
|
case selectSpace(SpaceRoomListProxyProtocol)
|
||||||
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
case selectUnjoinedSpace(SpaceRoomProxyProtocol)
|
||||||
case selectRoom(roomID: String)
|
case selectRoom(roomID: String)
|
||||||
|
case leftSpace
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SpaceScreenViewState: BindableState {
|
struct SpaceScreenViewState: BindableState {
|
||||||
let space: SpaceRoomProxyProtocol
|
let space: SpaceRoomProxyProtocol
|
||||||
|
|
||||||
|
var permalink: URL?
|
||||||
|
|
||||||
var isPaginating = false
|
var isPaginating = false
|
||||||
var rooms: [SpaceRoomProxyProtocol]
|
var rooms: [SpaceRoomProxyProtocol]
|
||||||
var selectedSpaceRoomID: String?
|
var selectedSpaceRoomID: String?
|
||||||
@@ -30,4 +33,5 @@ struct SpaceScreenViewStateBindings { }
|
|||||||
|
|
||||||
enum SpaceScreenViewAction {
|
enum SpaceScreenViewAction {
|
||||||
case spaceAction(SpaceRoomCell.Action)
|
case spaceAction(SpaceRoomCell.Action)
|
||||||
|
case leaveSpace
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
|||||||
selectedSpaceRoomPublisher
|
selectedSpaceRoomPublisher
|
||||||
.weakAssign(to: \.state.selectedSpaceRoomID, on: self)
|
.weakAssign(to: \.state.selectedSpaceRoomID, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.spaceRoomProxy.id),
|
||||||
|
case let .success(permalinkURL) = await roomProxy.matrixToPermalink() {
|
||||||
|
state.permalink = permalinkURL
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -81,6 +88,15 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
|||||||
}
|
}
|
||||||
case .spaceAction(.join(let spaceRoomProxy)):
|
case .spaceAction(.join(let spaceRoomProxy)):
|
||||||
Task { await join(spaceRoomProxy) }
|
Task { await join(spaceRoomProxy) }
|
||||||
|
case .leaveSpace:
|
||||||
|
#if DEBUG
|
||||||
|
Task { // Temporary implementation to make joining a space easier to test.
|
||||||
|
if case let .joined(roomProxy) = await clientProxy.roomForIdentifier(spaceRoomListProxy.spaceRoomProxy.id),
|
||||||
|
case .success = await roomProxy.leaveRoom() {
|
||||||
|
actionsSubject.send(.leftSpace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ struct SpaceScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ToolbarContentBuilder
|
||||||
var toolbar: some ToolbarContent {
|
var toolbar: some ToolbarContent {
|
||||||
// Use the same trick as the RoomScreen for a leading title view that
|
// Use the same trick as the RoomScreen for a leading title view that
|
||||||
// also hides the navigation title.
|
// also hides the navigation title.
|
||||||
@@ -50,6 +51,22 @@ struct SpaceScreen: View {
|
|||||||
roomAvatar: context.viewState.space.avatar,
|
roomAvatar: context.viewState.space.avatar,
|
||||||
mediaProvider: context.mediaProvider)
|
mediaProvider: context.mediaProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolbarItemGroup(placement: .secondaryAction) {
|
||||||
|
if let permalink = context.viewState.permalink {
|
||||||
|
Section {
|
||||||
|
ShareLink(item: permalink) {
|
||||||
|
Label(L10n.actionShare, icon: \.shareIos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button(role: .destructive) { context.send(viewAction: .leaveSpace) } label: {
|
||||||
|
Label(L10n.actionLeaveSpace, icon: \.leave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// Please see LICENSE files in the repository root for full details.
|
// Please see LICENSE files in the repository root for full details.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import MatrixRustSDK
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
@@ -14,6 +15,10 @@ final class RoomSummaryProviderTests: XCTestCase {
|
|||||||
var roomList: RoomListSDKMock!
|
var roomList: RoomListSDKMock!
|
||||||
var dynamicEntriesController: RoomListDynamicEntriesControllerSDKMock!
|
var dynamicEntriesController: RoomListDynamicEntriesControllerSDKMock!
|
||||||
|
|
||||||
|
let baseFilters: [RoomListEntriesDynamicFilterKind] = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]),
|
||||||
|
.all(filters: [.space, .invite])]),
|
||||||
|
.deduplicateVersions]
|
||||||
|
|
||||||
var roomSummaryProvider: RoomSummaryProvider!
|
var roomSummaryProvider: RoomSummaryProvider!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
@@ -32,9 +37,8 @@ final class RoomSummaryProviderTests: XCTestCase {
|
|||||||
|
|
||||||
// Then it should have the default Rust filters enabled.
|
// Then it should have the default Rust filters enabled.
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1)
|
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1)
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, .all(filters: [.nonLeft,
|
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||||
.nonSpace,
|
.all(filters: baseFilters))
|
||||||
.deduplicateVersions]))
|
|
||||||
|
|
||||||
// When setting one our user filters.
|
// When setting one our user filters.
|
||||||
roomSummaryProvider.setFilter(.all(filters: [.favourites]))
|
roomSummaryProvider.setFilter(.all(filters: [.favourites]))
|
||||||
@@ -42,10 +46,8 @@ final class RoomSummaryProviderTests: XCTestCase {
|
|||||||
|
|
||||||
// Then that filter should be added to the default Rust filters.
|
// Then that filter should be added to the default Rust filters.
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2)
|
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2)
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, .all(filters: [.all(filters: [.favourite, .joined]),
|
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||||
.nonLeft,
|
.all(filters: [.all(filters: [.favourite, .joined])] + baseFilters))
|
||||||
.nonSpace,
|
|
||||||
.deduplicateVersions]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLowPriorityRustFilters() async {
|
func testLowPriorityRustFilters() async {
|
||||||
@@ -56,10 +58,8 @@ final class RoomSummaryProviderTests: XCTestCase {
|
|||||||
// Then the default Rust filters should include the non-low priority filter,
|
// Then the default Rust filters should include the non-low priority filter,
|
||||||
// so that low priority rooms are hidden from the top of the room list.
|
// so that low priority rooms are hidden from the top of the room list.
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1)
|
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1)
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, .all(filters: [.nonLeft,
|
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||||
.nonSpace,
|
.all(filters: baseFilters + [.nonLowPriority]))
|
||||||
.deduplicateVersions,
|
|
||||||
.nonLowPriority]))
|
|
||||||
|
|
||||||
// When setting the low priority filter.
|
// When setting the low priority filter.
|
||||||
roomSummaryProvider.setFilter(.all(filters: [.lowPriority]))
|
roomSummaryProvider.setFilter(.all(filters: [.lowPriority]))
|
||||||
@@ -67,10 +67,8 @@ final class RoomSummaryProviderTests: XCTestCase {
|
|||||||
|
|
||||||
// Then the non-low priority filter should be replaced with the low priority filter.
|
// Then the non-low priority filter should be replaced with the low priority filter.
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2)
|
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2)
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, .all(filters: [.all(filters: [.lowPriority, .joined]),
|
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||||
.nonLeft,
|
.all(filters: [.all(filters: [.lowPriority, .joined])] + baseFilters))
|
||||||
.nonSpace,
|
|
||||||
.deduplicateVersions]))
|
|
||||||
|
|
||||||
// When setting another one of our filters.
|
// When setting another one of our filters.
|
||||||
roomSummaryProvider.setFilter(.all(filters: [.rooms]))
|
roomSummaryProvider.setFilter(.all(filters: [.rooms]))
|
||||||
@@ -78,11 +76,8 @@ final class RoomSummaryProviderTests: XCTestCase {
|
|||||||
|
|
||||||
// Then the filter should be combined with the non-low priority filter.
|
// Then the filter should be combined with the non-low priority filter.
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 3)
|
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 3)
|
||||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, .all(filters: [.all(filters: [.category(expect: .group), .joined]),
|
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||||
.nonLeft,
|
.all(filters: [.all(filters: [.category(expect: .group), .joined])] + baseFilters + [.nonLowPriority]))
|
||||||
.nonSpace,
|
|
||||||
.deduplicateVersions,
|
|
||||||
.nonLowPriority]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|||||||
Reference in New Issue
Block a user