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 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 //MARK: - addChild
var addChildToUnderlyingCallsCount = 0 var addChildToUnderlyingCallsCount = 0
@@ -111,6 +111,7 @@ extension [SpaceServiceRoomProtocol] {
childrenCount: 1, childrenCount: 1,
joinedMembersCount: 500, joinedMembersCount: 500,
canonicalAlias: "#the-foundation:matrix.org", canonicalAlias: "#the-foundation:matrix.org",
joinRule: .private,
state: .joined)), state: .joined)),
SpaceServiceRoomMock(.init(id: "space2", SpaceServiceRoomMock(.init(id: "space2",
name: "The Second Foundation", name: "The Second Foundation",
@@ -16,6 +16,7 @@ extension SpaceServiceProxyMock {
var topLevelSpaces: [SpaceServiceRoomProtocol] = [] var topLevelSpaces: [SpaceServiceRoomProtocol] = []
var spaceFilters: [SpaceServiceFilter] = [] var spaceFilters: [SpaceServiceFilter] = []
var joinedParentSpaces: [SpaceServiceRoomProtocol] = [] var joinedParentSpaces: [SpaceServiceRoomProtocol] = []
var editableSpaces: [SpaceServiceRoomProtocol] = []
var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:] var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:]
var leaveSpaceRooms: [LeaveSpaceRoom] = [] var leaveSpaceRooms: [LeaveSpaceRoom] = []
} }
@@ -27,6 +28,7 @@ extension SpaceServiceProxyMock {
spaceFilterPublisher = .init(configuration.spaceFilters) spaceFilterPublisher = .init(configuration.spaceFilters)
joinedParentsChildIDReturnValue = .success(configuration.joinedParentSpaces) joinedParentsChildIDReturnValue = .success(configuration.joinedParentSpaces)
editableSpacesReturnValue = configuration.editableSpaces
spaceRoomListSpaceIDClosure = { spaceID in spaceRoomListSpaceIDClosure = { spaceID in
if let spaceRoomList = configuration.spaceRoomLists[spaceID] { if let spaceRoomList = configuration.spaceRoomLists[spaceID] {
.success(spaceRoomList) .success(spaceRoomList)
@@ -31,7 +31,7 @@ struct CreateRoomScreenViewState: BindableState {
let isKnockingFeatureEnabled: Bool let isKnockingFeatureEnabled: Bool
let canSelectSpace: Bool let canSelectSpace: Bool
var aliasLocalPart: String var aliasLocalPart: String
var topLevelSpaces: [SpaceServiceRoomProtocol] var editableSpaces: [SpaceServiceRoomProtocol] = []
var bindings: CreateRoomScreenViewStateBindings var bindings: CreateRoomScreenViewStateBindings
var avatarMediaInfo: MediaInfo? { var avatarMediaInfo: MediaInfo? {
didSet { didSet {
@@ -61,11 +61,16 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
isKnockingFeatureEnabled: appSettings.knockingEnabled, isKnockingFeatureEnabled: appSettings.knockingEnabled,
canSelectSpace: canSelectSpace, canSelectSpace: canSelectSpace,
aliasLocalPart: roomAliasNameFromRoomDisplayName(roomName: ""), aliasLocalPart: roomAliasNameFromRoomDisplayName(roomName: ""),
topLevelSpaces: canSelectSpace ? userSession.clientProxy.spaceService.topLevelSpacesPublisher.value : [],
bindings: bindings), bindings: bindings),
mediaProvider: userSession.mediaProvider) mediaProvider: userSession.mediaProvider)
setupBindings() setupBindings()
if canSelectSpace {
Task {
state.editableSpaces = await userSession.clientProxy.spaceService.editableSpaces()
}
}
} }
// MARK: - Public // MARK: - Public
@@ -195,15 +200,6 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
if state.canSelectSpace {
userSession
.clientProxy
.spaceService
.topLevelSpacesPublisher
.weakAssign(to: \.state.topLevelSpaces, on: self)
.store(in: &cancellables)
}
} }
private func createRoom() async { private func createRoom() async {
@@ -277,15 +273,21 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
var spaceRoomListProxy: SpaceRoomListProxyProtocol? var spaceRoomListProxy: SpaceRoomListProxyProtocol?
if state.isSpace { if state.isSpace {
switch await userSession.clientProxy.spaceService.spaceRoomList(spaceID: roomProxy.id) { switch await userSession.clientProxy.spaceService.spaceRoomList(spaceID: roomID) {
case .success(let value): case .success(let value):
spaceRoomListProxy = value spaceRoomListProxy = value
case .failure: 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)) 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)) actionsSubject.send(.createdRoom(roomProxy, spaceRoomListProxy))
case .failure: case .failure:
state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom, state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom,
@@ -236,8 +236,11 @@ struct CreateRoomScreen: View {
} else { } else {
ListRow(label: .default(title: L10n.screenCreateRoomSpaceSelectionNoSpaceTitle, ListRow(label: .default(title: L10n.screenCreateRoomSpaceSelectionNoSpaceTitle,
description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription, description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription,
icon: \.home, icon: CompoundIcon(\.homeSolid, size: .small, relativeTo: .body)
iconAlignment: .top), .foregroundColor(.compound.iconPrimary)
.background(.compound.bgSubtleSecondary)
.scaledFrame(size: 32)
.clipAvatar(isSpace: true, size: 32)),
kind: .navigationLink { kind: .navigationLink {
context.showSpaceSelectionSheet = true context.showSpaceSelectionSheet = true
}) })
@@ -446,6 +449,56 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
viewModel.context.send(viewAction: .updateAliasLocalPart("existing")) viewModel.context.send(viewAction: .updateAliasLocalPart("existing"))
return viewModel 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 { static var previews: some View {
NavigationStack { NavigationStack {
@@ -491,5 +544,20 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
} }
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { !$0.aliasErrors.isEmpty }) .snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { !$0.aliasErrors.isEmpty })
.previewDisplayName("Create Public Room, existing alias") .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 { Section {
ListRow(label: .default(title: L10n.screenCreateRoomSpaceSelectionNoSpaceTitle, ListRow(label: .default(title: L10n.screenCreateRoomSpaceSelectionNoSpaceTitle,
description: L10n.screenCreateRoomSpaceSelectionNoSpaceDescription, 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) { kind: .selection(isSelected: context.selectedSpace == nil) {
context.selectedSpace = nil context.selectedSpace = nil
dismiss() dismiss()
}) })
ForEach(context.viewState.topLevelSpaces, id: \.id) { space in ForEach(context.viewState.editableSpaces, id: \.id) { space in
ListRow(label: .avatar(title: space.name, ListRow(label: .avatar(title: space.name,
description: space.canonicalAlias, description: space.canonicalAlias,
icon: RoomAvatarImage(avatar: space.avatar, icon: RoomAvatarImage(avatar: space.avatar,
@@ -66,7 +70,7 @@ struct CreateRoomSpaceSelectionSheet: View {
struct CreateRoomSpaceSelectionSheet_Previews: PreviewProvider, TestablePreview { struct CreateRoomSpaceSelectionSheet_Previews: PreviewProvider, TestablePreview {
static let viewModel = { static let viewModel = {
let clientProxy = ClientProxyMock(.init(userID: "@userid:example.com")) 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)) let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return CreateRoomScreenViewModel(isSpace: false, return CreateRoomScreenViewModel(isSpace: false,
@@ -80,5 +84,6 @@ struct CreateRoomSpaceSelectionSheet_Previews: PreviewProvider, TestablePreview
static var previews: some View { static var previews: some View {
CreateRoomSpaceSelectionSheet(context: viewModel.context) 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> { 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))
@@ -94,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,6 +52,8 @@ 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
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.
func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError> func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError>
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:aa327c6149879d4aa96fa4c5574bac7486fc6abfaa003ae74596c8b88adf6b0b oid sha256:cd25013fbbabd76733d42f38aeabefbe579800c005892e1201b65d894dba3a71
size 156228 size 165592
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:704b115de7bad6d64ea253ea261a8812e536622dc32280a11e91a3bdc9473340 oid sha256:2a4261ad92874c8d26ff1030dd9dc0b40e4f49c75f535cc4116b56b72a12764f
size 186268 size 200632
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:66f4d91f70ce033d8c6553479ecf7471d53ffdb181986cea4c9032701a595c25 oid sha256:9c848aa3b95b5a7d2bcd21ff7cb84fadd32cfcb7fad9d02a267d7eb20b5cd975
size 106657 size 114307
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0c355007d6ac8f669a701f0ef534e6e381895722d3c495be9e4ef49d20a7de79 oid sha256:b6f0109d908c6f92b2066be1ec631366d6fe170304eee39716c60da61b9a50e8
size 143489 size 134090
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:023c513c47df068c3bc47b4b06f028e630048d7289636dc9b9dac1433abcf442
size 139154
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e35b0f0e56364235d60bd8688b7272664ce6c294eb0642d51967d9138289715c
size 165021
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2482a63567a5a2ccb6f415e3dca4c05f697affecb9fc05e160c69c17c6020b9e
size 89586
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:409ca12c1ad636585be6fef0e1a25cdc17a57a1af00358131ff74995c65e0a26
size 120101
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:31efa5cc91a3b0a623056e85e8d5e0c6544a464d959b29ef771e2c9fa2843d7e oid sha256:783ef584086fb642ea70add67521b336e4bcf48e87d67bbc321bc05cb8801341
size 156446 size 165850
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e638ad86f90e438978096a9fc928f85b6ca0d883f02221978307fe3068e20e35 oid sha256:353482a407588083863c271a0da9fa000e0b2f28db930a1ac355c995377d9cda
size 188066 size 202463
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9b51dfc640af59dd5b3564148d48a1267e9423b1c77fb02edc4607d91d10910b oid sha256:d6709be30e596cf5ab8a7a36be93ca3710b801e290c0aad8756dd5002fae6cba
size 108742 size 116153
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8b8b277cc422ac3f6fba5b7b555e42c18d2b6ecde65a36eba6c4b63279fb80b5 oid sha256:d478eac4ccdc9e70cb83450b771ec600c1aa944ee246cf2de35a76f34fdba02e
size 145650 size 148446
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b71055f825775861294542ae166fbe7013800fc3672ce4d39ab4419716eedf45 oid sha256:857dac4f0f576d660c86a344a754ae948f1c87344ea02c983ab90f81fc67f2bb
size 141129 size 150715
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d2b85ce0989c5e068e489de22275aeaf723e51fe6b18cb304d16b831e8e8f368 oid sha256:a8ba3dcce7b0b2b1f55a795a0f701882a787b8f2ef8b7cf61aef21fa19b2aff7
size 164236 size 178610
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a5adc15a61b998447a66a4696b59f89413381583c4455be02b1f28594701b986 oid sha256:41b047f803117538ea8bd7cdc15464d825d3e86226ceab49fd7925fa8ca53506
size 89299 size 98304
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5823312ac25afe1b349f8eda8807299d6c30656e961d387c01e544eda40e6c4e oid sha256:704cc86d9b1ee34f09b9327d935efa3e40654381e4bbbe510167f54ec44afb7f
size 116112 size 127362
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:400c0c0ed48ced1122851b21a6d3e9e48f0985635711fec7e4abe8660994a6b1 oid sha256:2629190bbf8a4101ac0a3cc07ce246d10727c043f6b691c15bdb727ae380f04e
size 159870 size 169245
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:575f32a8b119f2ffbaf6e234b9ea23366850ea245f3f63403a9ba0a2691a2a3b oid sha256:5f93aab61df326b141ce7ceebde1ff99f0b7fb82610c1c45d7ed7c6a1d636439
size 195889 size 210144
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9002a2ad841596a8e206326fa282b131b750cc556e57a8b8fbf355318106e6ac oid sha256:b75b9be2f855101eb35fd48c8219b4dcfc4cd3bcc850683297b5ef6aec2f0a6b
size 110759 size 118048
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fab82b9e91cb4b3bad0a3bd13e1b2077a992473889ddb62297d0c902d724e104 oid sha256:cdfac096c01f4cb5ea50c834ecd573d3761a77e20fd71e14ce22368beef01180
size 150819 size 147952
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9db61c6bde46ef08cfd7d1aceb50f379544053024ac4633d1234c863c98c066a oid sha256:6d19bc6c512bc48a5493b1e3eb8785c33b165daa51a529449c674fed1287ee25
size 123222 size 132860
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e287da07572fd2bd3424894537844637e57629235b372108a8d99ae1b0994b1b oid sha256:81c651fb4c443c1442fc6b8fccd168e901fb16f994428c44951f57f187a1a38e
size 140788 size 155183
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8b820693bf00b97aaa0a797808106267ce741e495bba57936de8082dc4745bbf oid sha256:a0ae349c1658b8ff32004e524ff2f16e31c013dae9e6ea9e4a6e3bd0e3756e64
size 73369 size 81447
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ff2841d40e79e09cc472837c7ff350082d6d60894539302056c0d20f645264b1 oid sha256:064a83888029fc47666e0b110fc4260e436499aba4a527c02bfa9493b889a6fa
size 90112 size 100906
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11421d65287250704b17af8ff783882e61e524f1ad7dd7e332bb308a9fa2b8a0
size 148258
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e45145df3773cfb4f0852576406e6fb9c1ae168539dedaccb000df45681b5af
size 168502
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d90ac6bdafa18434682cf09e1434fe73358acea095b388902bbc06a43eb4355d
size 95080
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b46eb0c44ad26ec12798e0e6a55295e11374141766b856f4eca29b67b519578
size 115710
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6cd61bb621776260dbb0d91621b0bfaa1aa6d5c19511771fa55b3237130b10ad
size 126389
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1406d5b6cff0b0b73f1b012a58ce8bb306de648d1bfff626fbf68ec14cc86301
size 145450
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9aff42b3a7cf336f604a40d5f9c5f87aedac8e7656cb9429efb2dbec6ef0738e
size 75993
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7830c4f7a205182d1fc62ddedaa79098b869af45366fbf49de08d42bad646a24
size 95487
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dcbee0919af7296c78578076842b77cea64add28137e6d6bf27e1837563c9eab oid sha256:d4c40aa821fba074dc2c45d68dad1ea5c4133209b940321a8126ae8da4af8b87
size 156762 size 166423
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dd2792d7127bb9e498a814189fb5c5dfa2b270dd333cea7623ea2b9905ee981f oid sha256:7ef469565f325404cbbe1ccd024681bf7779a74055b34402666f31df309163c0
size 174527 size 188779
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d9d45b7c4d6aebb9c72558ed440ee099c756eb6b0196a5657d7da85557853d39 oid sha256:a9cb09216e7c5a198c106c2654a34f449e74d536129256fde1ad9a06bb9063c3
size 105940 size 114073
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3be54af6b52da9441294266c11a08bcaf85abfd2005e6ecc795e6e2d1b9b6cf6 oid sha256:f06e5655fe4409ddc5eed23ffec370d3e9bb6e95d5cf60dfcaa8af1a86ef6135
size 122952 size 133808
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:30039d5e15ac969e75c966de760a6e3d28659c29a06555c335f6ba8ab4bd0055
size 108672
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47977fb6d9fb4357cf81cf6724a1a54102ee949a5dd4c4e0fc1ef53160f87328
size 111545
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c17b4ed900e67eab1d9ab41496c4dd228c1ddd5aa6ac80bb16d5d99e1c02c884
size 65065
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:14ab4d7d5890e6838282885e34d232e520a1ce631eae54d88c09b4cc0fc34700
size 67699
+143 -16
View File
@@ -15,6 +15,7 @@ import XCTest
class CreateRoomScreenViewModelTests: XCTestCase { class CreateRoomScreenViewModelTests: XCTestCase {
var viewModel: CreateRoomScreenViewModelProtocol! var viewModel: CreateRoomScreenViewModelProtocol!
var clientProxy: ClientProxyMock! var clientProxy: ClientProxyMock!
var spaceService: SpaceServiceProxyMock!
var userSession: UserSessionMock! var userSession: UserSessionMock!
private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([]) private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
@@ -23,39 +24,31 @@ class CreateRoomScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
override func setUpWithError() throws {
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", userID: "@a:b.com"))
clientProxy.roomForIdentifierClosure = { roomID in .joined(JoinedRoomProxyMock(.init(id: roomID))) }
userSession = UserSessionMock(.init(clientProxy: clientProxy))
ServiceLocator.shared.settings.knockingEnabled = true
let viewModel = CreateRoomScreenViewModel(isSpace: false,
spaceSelectionMode: .list,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
self.viewModel = viewModel
}
override func tearDown() { override func tearDown() {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
viewModel = nil viewModel = nil
clientProxy = nil clientProxy = nil
spaceService = nil
userSession = nil userSession = nil
} }
func testDefaultSecurity() { func testDefaultState() {
setup()
XCTAssertEqual(context.viewState.bindings.selectedAccessType, .private) XCTAssertEqual(context.viewState.bindings.selectedAccessType, .private)
XCTAssertNil(context.selectedSpace)
XCTAssertEqual(context.viewState.availableAccessTypes, [.public, .askToJoin, .private])
XCTAssertTrue(context.viewState.canSelectSpace)
} }
func testCreateRoomRequirements() { func testCreateRoomRequirements() {
setup()
XCTAssertFalse(context.viewState.canCreateRoom) XCTAssertFalse(context.viewState.canCreateRoom)
context.send(viewAction: .updateRoomName("A")) context.send(viewAction: .updateRoomName("A"))
XCTAssertTrue(context.viewState.canCreateRoom) XCTAssertTrue(context.viewState.canCreateRoom)
} }
func testCreateRoom() async throws { func testCreateRoom() async throws {
setup()
// Given a form with a blank topic. // Given a form with a blank topic.
context.send(viewAction: .updateRoomName("A")) context.send(viewAction: .updateRoomName("A"))
context.roomTopic = "" context.roomTopic = ""
@@ -76,9 +69,11 @@ class CreateRoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A") XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic, XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
"The topic should be sent as nil when it is empty.") "The topic should be sent as nil when it is empty.")
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private)
} }
func testCreateSpace() async throws { func testCreateSpace() async throws {
setup(isSpace: true)
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org",
userID: "@a:b.com", userID: "@a:b.com",
spaceServiceConfiguration: .init(spaceRoomLists: ["1": .init()]))) spaceServiceConfiguration: .init(spaceRoomLists: ["1": .init()])))
@@ -116,9 +111,11 @@ class CreateRoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A") XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic, XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
"The topic should be sent as nil when it is empty.") "The topic should be sent as nil when it is empty.")
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private)
} }
func testCreateKnockingRoom() async { func testCreateKnockingRoom() async {
setup()
context.send(viewAction: .updateRoomName("A")) context.send(viewAction: .updateRoomName("A"))
context.roomTopic = "B" context.roomTopic = "B"
context.selectedAccessType = .askToJoin context.selectedAccessType = .askToJoin
@@ -139,6 +136,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
} }
func testCreatePublicRoomFailsForInvalidAlias() async throws { func testCreatePublicRoomFailsForInvalidAlias() async throws {
setup()
context.send(viewAction: .updateRoomName("A")) context.send(viewAction: .updateRoomName("A"))
context.roomTopic = "B" context.roomTopic = "B"
context.selectedAccessType = .public context.selectedAccessType = .public
@@ -161,6 +159,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
} }
func testCreatePublicRoomFailsForExistingAlias() async throws { func testCreatePublicRoomFailsForExistingAlias() async throws {
setup()
clientProxy.isAliasAvailableReturnValue = .success(false) clientProxy.isAliasAvailableReturnValue = .success(false)
context.send(viewAction: .updateRoomName("A")) context.send(viewAction: .updateRoomName("A"))
context.roomTopic = "B" context.roomTopic = "B"
@@ -192,6 +191,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
} }
func testNameAndAddressSync() async { func testNameAndAddressSync() async {
setup()
context.selectedAccessType = .private context.selectedAccessType = .private
await Task.yield() await Task.yield()
context.send(viewAction: .updateRoomName("abc")) context.send(viewAction: .updateRoomName("abc"))
@@ -221,4 +221,131 @@ class CreateRoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!") XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!")
XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!") XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!")
} }
func testCreateRoomInASelectedSpaceFromTheList() async throws {
let spaces = [SpaceServiceRoomProtocol].mockJoinedSpaces2
setup()
// Given a form with a blank topic.
context.send(viewAction: .updateRoomName("A"))
context.roomTopic = ""
context.selectedAccessType = .public
XCTAssertTrue(context.viewState.canCreateRoom)
XCTAssertNil(context.selectedSpace)
XCTAssertEqual(context.viewState.availableAccessTypes, [.public, .askToJoin, .private])
XCTAssertTrue(context.viewState.canSelectSpace)
var deferred = deferFulfillment(context.$viewState) { viewState in
viewState.editableSpaces.map(\.id) == spaces.map(\.id)
}
try await deferred.fulfill()
context.selectedSpace = spaces[0]
deferred = deferFulfillment(context.$viewState) { viewState in
viewState.bindings.selectedSpace?.id == spaces[0].id &&
viewState.availableAccessTypes == [.spaceMembers, .askToJoinWithSpaceMembers, .private] &&
// The value should reset since the original one is not available anymore
viewState.bindings.selectedAccessType == .private
}
try await deferred.fulfill()
// When creating the room.
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
let expectation = expectation(description: "Wait for the addChild function to be called")
spaceService.addChildToClosure = { roomID, spaceID in
defer { expectation.fulfill() }
XCTAssertEqual(roomID, "1")
XCTAssertEqual(spaceID, spaces[0].id)
return .success(())
}
let deferredAction = deferFulfillment(viewModel.actions) { action in
guard case .createdRoom(let roomProxy, nil) = action, roomProxy.id == "1" else { return false }
return true
}
context.send(viewAction: .createRoom)
await fulfillment(of: [expectation])
try await deferredAction.fulfill()
// Then the room should be created and a topic should not be set.
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private)
XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
"The topic should be sent as nil when it is empty.")
}
func testCreateRoomInAnAlreadySelectedSpace() async throws {
let space = SpaceServiceRoomMock(.init(isSpace: true, joinRule: .private))
setup(spacesSelectionMode: .selected(space))
// Given a form with a blank topic.
context.send(viewAction: .updateRoomName("A"))
context.roomTopic = ""
context.selectedAccessType = .spaceMembers
XCTAssertTrue(context.viewState.canCreateRoom)
XCTAssertEqual(context.selectedSpace?.id, space.id)
XCTAssertEqual(context.viewState.availableAccessTypes, [.spaceMembers, .askToJoinWithSpaceMembers, .private])
XCTAssertFalse(context.viewState.canSelectSpace)
// When creating the room.
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
let expectation = expectation(description: "Wait for the addChild function to be called")
spaceService.addChildToClosure = { roomID, spaceID in
defer { expectation.fulfill() }
XCTAssertEqual(roomID, "1")
XCTAssertEqual(spaceID, space.id)
return .success(())
}
let deferredAction = deferFulfillment(viewModel.actions) { action in
guard case .createdRoom(let roomProxy, nil) = action, roomProxy.id == "1" else { return false }
return true
}
context.send(viewAction: .createRoom)
await fulfillment(of: [expectation])
try await deferredAction.fulfill()
// Then the room should be created and a topic should not be set.
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
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 {
let space = SpaceServiceRoomMock(.init(isSpace: true, joinRule: .public))
setup(spacesSelectionMode: .selected(space))
// Given a form with a blank topic.
context.send(viewAction: .updateRoomName("A"))
context.roomTopic = ""
context.selectedAccessType = .spaceMembers
XCTAssertTrue(context.viewState.canCreateRoom)
XCTAssertEqual(context.selectedSpace?.id, space.id)
XCTAssertEqual(context.viewState.availableAccessTypes, [.public, .askToJoin, .private])
XCTAssertFalse(context.viewState.canSelectSpace)
}
private func setup(isSpace: Bool = false, spacesSelectionMode: CreateRoomScreenSpaceSelectionMode = .list) {
spaceService = SpaceServiceProxyMock(.init(editableSpaces: .mockJoinedSpaces2,
spaceRoomLists: ["1": .init()]))
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org",
userID: "@a:b.com"))
clientProxy.spaceService = spaceService
clientProxy.roomForIdentifierClosure = { roomID in .joined(JoinedRoomProxyMock(.init(id: roomID))) }
userSession = UserSessionMock(.init(clientProxy: clientProxy))
ServiceLocator.shared.settings.knockingEnabled = true
let viewModel = CreateRoomScreenViewModel(isSpace: isSpace,
spaceSelectionMode: spacesSelectionMode,
shouldShowCancelButton: false,
userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
self.viewModel = viewModel
}
} }