Implemented adding the room to the space after selection, and the editableSpace proxied API. Also updated and added tests.

This commit is contained in:
Mauro Romito
2026-01-21 14:58:03 +01:00
committed by Mauro
parent 07243be949
commit 9dead7ed55
50 changed files with 406 additions and 83 deletions
@@ -17100,6 +17100,70 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable {
return joinedParentsChildIDReturnValue
}
}
//MARK: - editableSpaces
var editableSpacesUnderlyingCallsCount = 0
var editableSpacesCallsCount: Int {
get {
if Thread.isMainThread {
return editableSpacesUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = editableSpacesUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
editableSpacesUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
editableSpacesUnderlyingCallsCount = newValue
}
}
}
}
var editableSpacesCalled: Bool {
return editableSpacesCallsCount > 0
}
var editableSpacesUnderlyingReturnValue: [SpaceServiceRoomProtocol]!
var editableSpacesReturnValue: [SpaceServiceRoomProtocol]! {
get {
if Thread.isMainThread {
return editableSpacesUnderlyingReturnValue
} else {
var returnValue: [SpaceServiceRoomProtocol]? = nil
DispatchQueue.main.sync {
returnValue = editableSpacesUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
editableSpacesUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
editableSpacesUnderlyingReturnValue = newValue
}
}
}
}
var editableSpacesClosure: (() async -> [SpaceServiceRoomProtocol])?
func editableSpaces() async -> [SpaceServiceRoomProtocol] {
editableSpacesCallsCount += 1
if let editableSpacesClosure = editableSpacesClosure {
return await editableSpacesClosure()
} else {
return editableSpacesReturnValue
}
}
//MARK: - addChild
var addChildToUnderlyingCallsCount = 0
@@ -111,6 +111,7 @@ extension [SpaceServiceRoomProtocol] {
childrenCount: 1,
joinedMembersCount: 500,
canonicalAlias: "#the-foundation:matrix.org",
joinRule: .private,
state: .joined)),
SpaceServiceRoomMock(.init(id: "space2",
name: "The Second Foundation",
@@ -16,6 +16,7 @@ extension SpaceServiceProxyMock {
var topLevelSpaces: [SpaceServiceRoomProtocol] = []
var spaceFilters: [SpaceServiceFilter] = []
var joinedParentSpaces: [SpaceServiceRoomProtocol] = []
var editableSpaces: [SpaceServiceRoomProtocol] = []
var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:]
var leaveSpaceRooms: [LeaveSpaceRoom] = []
}
@@ -27,6 +28,7 @@ extension SpaceServiceProxyMock {
spaceFilterPublisher = .init(configuration.spaceFilters)
joinedParentsChildIDReturnValue = .success(configuration.joinedParentSpaces)
editableSpacesReturnValue = configuration.editableSpaces
spaceRoomListSpaceIDClosure = { spaceID in
if let spaceRoomList = configuration.spaceRoomLists[spaceID] {
.success(spaceRoomList)
@@ -31,7 +31,7 @@ struct CreateRoomScreenViewState: BindableState {
let isKnockingFeatureEnabled: Bool
let canSelectSpace: Bool
var aliasLocalPart: String
var topLevelSpaces: [SpaceServiceRoomProtocol]
var editableSpaces: [SpaceServiceRoomProtocol] = []
var bindings: CreateRoomScreenViewStateBindings
var avatarMediaInfo: MediaInfo? {
didSet {
@@ -61,11 +61,16 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
isKnockingFeatureEnabled: appSettings.knockingEnabled,
canSelectSpace: canSelectSpace,
aliasLocalPart: roomAliasNameFromRoomDisplayName(roomName: ""),
topLevelSpaces: canSelectSpace ? userSession.clientProxy.spaceService.topLevelSpacesPublisher.value : [],
bindings: bindings),
mediaProvider: userSession.mediaProvider)
setupBindings()
if canSelectSpace {
Task {
state.editableSpaces = await userSession.clientProxy.spaceService.editableSpaces()
}
}
}
// MARK: - Public
@@ -195,15 +200,6 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
}
}
.store(in: &cancellables)
if state.canSelectSpace {
userSession
.clientProxy
.spaceService
.topLevelSpacesPublisher
.weakAssign(to: \.state.topLevelSpaces, on: self)
.store(in: &cancellables)
}
}
private func createRoom() async {
@@ -277,15 +273,21 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
var spaceRoomListProxy: SpaceRoomListProxyProtocol?
if state.isSpace {
switch await userSession.clientProxy.spaceService.spaceRoomList(spaceID: roomProxy.id) {
switch await userSession.clientProxy.spaceService.spaceRoomList(spaceID: roomID) {
case .success(let value):
spaceRoomListProxy = value
case .failure:
MXLog.error("Failed to get space room list for newly created space with id: \(roomProxy.id)")
MXLog.error("Failed to get space room list for newly created space with id: \(roomID)")
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
if let selectedSpace = state.bindings.selectedSpace,
case .failure = await userSession.clientProxy.spaceService.addChild(roomID, to: selectedSpace.id) {
MXLog.error("Failed to add the created room with id: \(roomID) to the space with id: \(selectedSpace.id)")
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
actionsSubject.send(.createdRoom(roomProxy, spaceRoomListProxy))
case .failure:
state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom,
@@ -236,8 +236,11 @@ struct CreateRoomScreen: View {
} else {
ListRow(label: .default(title: L10n.screenCreateRoomSpaceSelectionNoSpaceTitle,
description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription,
icon: \.home,
iconAlignment: .top),
icon: CompoundIcon(\.homeSolid, size: .small, relativeTo: .body)
.foregroundColor(.compound.iconPrimary)
.background(.compound.bgSubtleSecondary)
.scaledFrame(size: 32)
.clipAvatar(isSpace: true, size: 32)),
kind: .navigationLink {
context.showSpaceSelectionSheet = true
})
@@ -446,6 +449,56 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
viewModel.context.send(viewAction: .updateAliasLocalPart("existing"))
return viewModel
}()
static let selectedSpaceViewModel = {
AppSettings.resetAllSettings()
let mockedSpace = SpaceServiceRoomMock(.init(name: "Awesome Space", isSpace: true, joinRule: .private))
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
return CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .selected(mockedSpace),
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let selectedSpaceWithListViewModel = {
let clientProxy = ClientProxyMock(.init(userID: "@userid:example.com"))
let spaces = [SpaceServiceRoomProtocol].mockJoinedSpaces2
clientProxy.spaceService = SpaceServiceProxyMock(.init(editableSpaces: spaces))
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
let viewModel = CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .list,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
viewModel.context.selectedSpace = spaces[0]
return viewModel
}()
static let selectedSpaceWithAskToJoinViewModel = {
AppSettings.resetAllSettings()
let appSettings = AppSettings()
appSettings.knockingEnabled = true
let mockedSpace = SpaceServiceRoomMock(.init(name: "Awesome Space", isSpace: true, joinRule: .private))
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
let viewModel = CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .selected(mockedSpace),
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: appSettings)
viewModel.context.selectedAccessType = .askToJoinWithSpaceMembers
return viewModel
}()
static var previews: some View {
NavigationStack {
@@ -491,5 +544,20 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
}
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { !$0.aliasErrors.isEmpty })
.previewDisplayName("Create Public Room, existing alias")
NavigationStack {
CreateRoomScreen(context: selectedSpaceViewModel.context)
}
.previewDisplayName("Create Room with already selected Space")
NavigationStack {
CreateRoomScreen(context: selectedSpaceWithListViewModel.context)
}
.previewDisplayName("Create Room with a selected Space from the list")
NavigationStack {
CreateRoomScreen(context: selectedSpaceWithAskToJoinViewModel.context)
}
.previewDisplayName("Create Knockable Room with already selected Space")
}
}
@@ -26,12 +26,16 @@ struct CreateRoomSpaceSelectionSheet: View {
Section {
ListRow(label: .default(title: L10n.screenCreateRoomSpaceSelectionNoSpaceTitle,
description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription,
icon: \.home),
icon: CompoundIcon(\.homeSolid, size: .small, relativeTo: .body)
.foregroundColor(.compound.iconPrimary)
.background(.compound.bgSubtleSecondary)
.scaledFrame(size: 32)
.clipAvatar(isSpace: true, size: 32)),
kind: .selection(isSelected: context.selectedSpace == nil) {
context.selectedSpace = nil
dismiss()
})
ForEach(context.viewState.topLevelSpaces, id: \.id) { space in
ForEach(context.viewState.editableSpaces, id: \.id) { space in
ListRow(label: .avatar(title: space.name,
description: space.canonicalAlias,
icon: RoomAvatarImage(avatar: space.avatar,
@@ -66,7 +70,7 @@ struct CreateRoomSpaceSelectionSheet: View {
struct CreateRoomSpaceSelectionSheet_Previews: PreviewProvider, TestablePreview {
static let viewModel = {
let clientProxy = ClientProxyMock(.init(userID: "@userid:example.com"))
clientProxy.spaceService = SpaceServiceProxyMock(.init(topLevelSpaces: .mockJoinedSpaces2))
clientProxy.spaceService = SpaceServiceProxyMock(.init(editableSpaces: .mockJoinedSpaces2))
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return CreateRoomScreenViewModel(isSpace: false,
@@ -80,5 +84,6 @@ struct CreateRoomSpaceSelectionSheet_Previews: PreviewProvider, TestablePreview
static var previews: some View {
CreateRoomSpaceSelectionSheet(context: viewModel.context)
.snapshotPreferences(expect: viewModel.context.$viewState.map { $0.editableSpaces.count > 0 })
}
}
@@ -77,6 +77,10 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
}
}
func editableSpaces() async -> [SpaceServiceRoomProtocol] {
await spaceService.editableSpaces().map(SpaceServiceRoom.init)
}
func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError> {
do {
return try await .success(spaceService.addChildToSpace(childId: childID, spaceId: spaceID))
@@ -94,7 +98,7 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
return .failure(.sdkError(error))
}
}
// MARK: - Private
private func handleSpaceListUpdates(_ updates: [SpaceListUpdate]) {
@@ -52,6 +52,8 @@ protocol SpaceServiceProxyProtocol {
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>
/// Returns all the parent spaces of a child that user has joined.
func joinedParents(childID: String) async -> Result<[SpaceServiceRoomProtocol], SpaceServiceProxyError>
/// Returns all the parent spaces that can be edited by the user
func editableSpaces() async -> [SpaceServiceRoomProtocol]
/// Adds a room (or space) as a child of another space.
func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError>