pr suggestions + await PLs of the room to be ready before adding it to the space

This commit is contained in:
Mauro Romito
2026-01-22 12:48:55 +01:00
committed by Mauro
parent d4ec756fe3
commit 5fd32ec67e
6 changed files with 67 additions and 138 deletions
@@ -282,10 +282,8 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
} }
} }
if let selectedSpace = state.bindings.selectedSpace, if let spaceID = state.bindings.selectedSpace?.id {
case .failure = await userSession.clientProxy.spaceService.addChild(roomID, to: selectedSpace.id) { await addRoomToSpace(roomProxy: roomProxy, spaceID: spaceID)
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)) actionsSubject.send(.createdRoom(roomProxy, spaceRoomListProxy))
@@ -296,6 +294,25 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
} }
} }
private func addRoomToSpace(roomProxy: JoinedRoomProxyProtocol, spaceID: String) async {
roomProxy.subscribeToRoomInfoUpdates()
let runner = ExpiringTaskRunner {
// Necessary to build the room cache so that the space can be added as a parent.
_ = await roomProxy.infoPublisher.values.first { $0.powerLevels != nil }
}
do {
try await runner.run(timeout: .seconds(30))
if case .failure = await userSession.clientProxy.spaceService.addChild(roomProxy.id, to: spaceID) {
MXLog.error("Failed to add the created room with id: \(roomProxy.id) to the space with id: \(spaceID)")
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
} catch {
MXLog.error("Timed out waiting for power levels to load for room with id: \(roomProxy.id)")
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
// MARK: Loading indicator // MARK: Loading indicator
private static let loadingIndicatorIdentifier = "\(CreateRoomScreenViewModel.self)-Loading" private static let loadingIndicatorIdentifier = "\(CreateRoomScreenViewModel.self)-Loading"
@@ -238,8 +238,8 @@ struct CreateRoomScreen: View {
description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription, description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription,
icon: CompoundIcon(\.homeSolid, size: .small, relativeTo: .body) icon: CompoundIcon(\.homeSolid, size: .small, relativeTo: .body)
.foregroundColor(.compound.iconPrimary) .foregroundColor(.compound.iconPrimary)
.background(.compound.bgSubtleSecondary)
.scaledFrame(size: 32) .scaledFrame(size: 32)
.background(.compound.bgSubtleSecondary)
.clipAvatar(isSpace: true, size: 32)), .clipAvatar(isSpace: true, size: 32)),
kind: .navigationLink { kind: .navigationLink {
context.showSpaceSelectionSheet = true context.showSpaceSelectionSheet = true
@@ -336,166 +336,63 @@ private struct CreateRoomAccessRow: View {
// MARK: - Previews // MARK: - Previews
struct CreateRoom_Previews: PreviewProvider, TestablePreview { struct CreateRoom_Previews: PreviewProvider, TestablePreview {
static let viewModel = { static let viewModel = makeViewModel()
AppSettings.resetAllSettings()
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
return CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .list,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let avatarViewModel = { static let avatarViewModel = {
AppSettings.resetAllSettings() let viewModel = makeViewModel()
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
let viewModel = CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .list,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
viewModel.updateAvatar(fileURL: Bundle.main.url(forResource: "preview_avatar_room", withExtension: "jpg") ?? .picturesDirectory) viewModel.updateAvatar(fileURL: Bundle.main.url(forResource: "preview_avatar_room", withExtension: "jpg") ?? .picturesDirectory)
return viewModel return viewModel
}() }()
static let spaceViewModel = { static let spaceViewModel = makeViewModel(isSpace: true, selectionMode: nil)
AppSettings.resetAllSettings()
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
return CreateRoomScreenViewModel(isSpace: true,
spaceSelectionMode: nil,
shouldShowCancelButton: true,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let spaceWithAvatarViewModel = { static let spaceWithAvatarViewModel = {
AppSettings.resetAllSettings() let viewModel = makeViewModel(isSpace: true, selectionMode: nil)
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
let viewModel = CreateRoomScreenViewModel(isSpace: true,
spaceSelectionMode: nil,
shouldShowCancelButton: true,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
viewModel.updateAvatar(fileURL: Bundle.main.url(forResource: "preview_avatar_room", withExtension: "jpg") ?? .picturesDirectory) viewModel.updateAvatar(fileURL: Bundle.main.url(forResource: "preview_avatar_room", withExtension: "jpg") ?? .picturesDirectory)
return viewModel return viewModel
}() }()
static let publicRoomViewModel = { static let publicRoomViewModel = {
AppSettings.resetAllSettings() let viewModel = makeViewModel()
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
let viewModel = CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .list,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
viewModel.context.selectedAccessType = .public viewModel.context.selectedAccessType = .public
return viewModel return viewModel
}() }()
static let askToJoinViewModel = { static let askToJoinViewModel = {
AppSettings.resetAllSettings() let viewModel = makeViewModel(isKnockingEnabled: true)
let appSettings = AppSettings()
appSettings.knockingEnabled = true
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
let viewModel = CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .list,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: appSettings)
viewModel.context.selectedAccessType = .askToJoin viewModel.context.selectedAccessType = .askToJoin
return viewModel return viewModel
}() }()
static let publicRoomInvalidAliasViewModel = { static let publicRoomInvalidAliasViewModel = {
AppSettings.resetAllSettings() let viewModel = makeViewModel()
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
let viewModel = CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .list,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
viewModel.context.selectedAccessType = .public viewModel.context.selectedAccessType = .public
viewModel.context.send(viewAction: .updateAliasLocalPart("#:")) viewModel.context.send(viewAction: .updateAliasLocalPart("#:"))
return viewModel return viewModel
}() }()
static let publicRoomExistingAliasViewModel = { static let publicRoomExistingAliasViewModel = {
AppSettings.resetAllSettings() let viewModel = makeViewModel(isAliasAvailable: false)
let clientProxy = ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))
clientProxy.isAliasAvailableReturnValue = .success(false)
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.selectedAccessType = .public viewModel.context.selectedAccessType = .public
viewModel.context.send(viewAction: .updateAliasLocalPart("existing")) viewModel.context.send(viewAction: .updateAliasLocalPart("existing"))
return viewModel return viewModel
}() }()
static let selectedSpaceViewModel = { static let selectedSpaceViewModel = makeViewModel(selectionMode: .selected(SpaceServiceRoomMock(.init(name: "Awesome Space",
AppSettings.resetAllSettings() isSpace: true,
let mockedSpace = SpaceServiceRoomMock(.init(name: "Awesome Space", isSpace: true, joinRule: .private)) 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 = { static let selectedSpaceWithListViewModel = {
let clientProxy = ClientProxyMock(.init(userID: "@userid:example.com")) let viewModel = makeViewModel()
let spaces = [SpaceServiceRoomProtocol].mockJoinedSpaces2 viewModel.context.selectedSpace = [SpaceServiceRoomProtocol].mockJoinedSpaces2.first
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 return viewModel
}() }()
static let selectedSpaceWithAskToJoinViewModel = { static let selectedSpaceWithAskToJoinViewModel = {
AppSettings.resetAllSettings() let viewModel = makeViewModel(isKnockingEnabled: true,
let appSettings = AppSettings() selectionMode: .selected(SpaceServiceRoomMock(.init(name: "Awesome Space",
appSettings.knockingEnabled = true isSpace: true,
joinRule: .private))))
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 viewModel.context.selectedAccessType = .askToJoinWithSpaceMembers
return viewModel return viewModel
}() }()
@@ -560,4 +457,28 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
} }
.previewDisplayName("Create Knockable Room with already selected Space") .previewDisplayName("Create Knockable Room with already selected Space")
} }
private static func makeViewModel(isKnockingEnabled: Bool = false,
isSpace: Bool = false,
selectionMode: CreateRoomScreenSpaceSelectionMode? = .list,
isAliasAvailable: Bool = true) -> CreateRoomScreenViewModel {
AppSettings.resetAllSettings()
let appSettings = AppSettings()
appSettings.knockingEnabled = isKnockingEnabled
let clientProxy = ClientProxyMock(.init(userIDServerName: "example.org",
userID: "@userid:example.com"))
clientProxy.isAliasAvailableReturnValue = .success(isAliasAvailable)
let spaces = [SpaceServiceRoomProtocol].mockJoinedSpaces2
clientProxy.spaceService = SpaceServiceProxyMock(.init(editableSpaces: spaces))
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return CreateRoomScreenViewModel(isSpace: isSpace,
spaceSelectionMode: selectionMode,
shouldShowCancelButton: isSpace,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: appSettings)
}
} }
@@ -28,6 +28,7 @@ struct CreateRoomSpaceSelectionSheet: View {
description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription, description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription,
icon: CompoundIcon(\.homeSolid, size: .small, relativeTo: .body) icon: CompoundIcon(\.homeSolid, size: .small, relativeTo: .body)
.foregroundColor(.compound.iconPrimary) .foregroundColor(.compound.iconPrimary)
.scaledFrame(size: 32)
.background(.compound.bgSubtleSecondary) .background(.compound.bgSubtleSecondary)
.scaledFrame(size: 32) .scaledFrame(size: 32)
.clipAvatar(isSpace: true, size: 32)), .clipAvatar(isSpace: true, size: 32)),
@@ -80,7 +80,7 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
func editableSpaces() async -> [SpaceServiceRoomProtocol] { func editableSpaces() async -> [SpaceServiceRoomProtocol] {
await spaceService.editableSpaces().map(SpaceServiceRoom.init) await spaceService.editableSpaces().map(SpaceServiceRoom.init)
} }
func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError> { func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError> {
do { do {
return try await .success(spaceService.addChildToSpace(childId: childID, spaceId: spaceID)) return try await .success(spaceService.addChildToSpace(childId: childID, spaceId: spaceID))
@@ -98,7 +98,7 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
return .failure(.sdkError(error)) return .failure(.sdkError(error))
} }
} }
// MARK: - Private // MARK: - Private
private func handleSpaceListUpdates(_ updates: [SpaceListUpdate]) { private func handleSpaceListUpdates(_ updates: [SpaceListUpdate]) {
@@ -52,7 +52,7 @@ protocol SpaceServiceProxyProtocol {
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError> func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>
/// Returns all the parent spaces of a child that user has joined. /// Returns all the parent spaces of a child that user has joined.
func joinedParents(childID: String) async -> Result<[SpaceServiceRoomProtocol], SpaceServiceProxyError> func joinedParents(childID: String) async -> Result<[SpaceServiceRoomProtocol], SpaceServiceProxyError>
/// Returns all the parent spaces that can be edited by the user /// Returns all the joined spaces that can be edited by the user
func editableSpaces() async -> [SpaceServiceRoomProtocol] func editableSpaces() async -> [SpaceServiceRoomProtocol]
/// Adds a room (or space) as a child of another space. /// Adds a room (or space) as a child of another space.
@@ -226,9 +226,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
let spaces = [SpaceServiceRoomProtocol].mockJoinedSpaces2 let spaces = [SpaceServiceRoomProtocol].mockJoinedSpaces2
setup() setup()
// Given a form with a blank topic.
context.send(viewAction: .updateRoomName("A")) context.send(viewAction: .updateRoomName("A"))
context.roomTopic = ""
context.selectedAccessType = .public context.selectedAccessType = .public
XCTAssertTrue(context.viewState.canCreateRoom) XCTAssertTrue(context.viewState.canCreateRoom)
XCTAssertNil(context.selectedSpace) XCTAssertNil(context.selectedSpace)
@@ -268,21 +266,16 @@ class CreateRoomScreenViewModelTests: XCTestCase {
await fulfillment(of: [expectation]) await fulfillment(of: [expectation])
try await deferredAction.fulfill() try await deferredAction.fulfill()
// Then the room should be created and a topic should not be set.
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled) XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A") XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private) XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private)
XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
"The topic should be sent as nil when it is empty.")
} }
func testCreateRoomInAnAlreadySelectedSpace() async throws { func testCreateRoomInAnAlreadySelectedSpace() async throws {
let space = SpaceServiceRoomMock(.init(isSpace: true, joinRule: .private)) let space = SpaceServiceRoomMock(.init(isSpace: true, joinRule: .private))
setup(spacesSelectionMode: .selected(space)) setup(spacesSelectionMode: .selected(space))
// Given a form with a blank topic.
context.send(viewAction: .updateRoomName("A")) context.send(viewAction: .updateRoomName("A"))
context.roomTopic = ""
context.selectedAccessType = .spaceMembers context.selectedAccessType = .spaceMembers
XCTAssertTrue(context.viewState.canCreateRoom) XCTAssertTrue(context.viewState.canCreateRoom)
XCTAssertEqual(context.selectedSpace?.id, space.id) XCTAssertEqual(context.selectedSpace?.id, space.id)
@@ -308,12 +301,9 @@ class CreateRoomScreenViewModelTests: XCTestCase {
await fulfillment(of: [expectation]) await fulfillment(of: [expectation])
try await deferredAction.fulfill() try await deferredAction.fulfill()
// Then the room should be created and a topic should not be set.
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled) XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A") XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .spaceMembers(spaceID: space.id)) XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .spaceMembers(spaceID: space.id))
XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
"The topic should be sent as nil when it is empty.")
} }
func testCreateRoomInAnPublicSpaceAvailableTypes() async throws { func testCreateRoomInAnPublicSpaceAvailableTypes() async throws {