implemented the logic to transfer ownership from a space

This commit is contained in:
Mauro Romito
2026-01-27 17:52:28 +01:00
committed by Mauro
parent b903658975
commit 9de49bad9e
11 changed files with 72 additions and 22 deletions
@@ -349,6 +349,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.addRooms) stateMachine.tryEvent(.addRooms)
case .displayCreateChildRoomFlow(let space): case .displayCreateChildRoomFlow(let space):
stateMachine.tryEvent(.startCreateChildRoomFlow, userInfo: space) stateMachine.tryEvent(.startCreateChildRoomFlow, userInfo: space)
case .displayTransferOwnership(roomProxy: let roomProxy):
// TODO: Implement
break
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -23,11 +23,11 @@ extension LeaveSpaceHandleSDKMock {
} }
extension [LeaveSpaceRoom] { extension [LeaveSpaceRoom] {
static func mockRoomsWithSpace(spaceServiceRoom: SpaceServiceRoom, isLastOwner: Bool) -> [LeaveSpaceRoom] { static func mockRoomsWithSpace(spaceServiceRoom: SpaceServiceRoom, isLastOwner: Bool, areCreatorsPrivileged: Bool) -> [LeaveSpaceRoom] {
mockRooms + mockSingleSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: isLastOwner) mockRooms + mockSingleSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: isLastOwner, areCreatorsPrivileged: areCreatorsPrivileged)
} }
static func mockSingleSpace(spaceServiceRoom: SpaceServiceRoom, isLastOwner: Bool) -> [LeaveSpaceRoom] { static func mockSingleSpace(spaceServiceRoom: SpaceServiceRoom, isLastOwner: Bool, areCreatorsPrivileged: Bool) -> [LeaveSpaceRoom] {
[LeaveSpaceRoom(spaceRoom: SpaceRoom(id: spaceServiceRoom.id, [LeaveSpaceRoom(spaceRoom: SpaceRoom(id: spaceServiceRoom.id,
name: spaceServiceRoom.name, name: spaceServiceRoom.name,
avatarURL: spaceServiceRoom.avatarURL, avatarURL: spaceServiceRoom.avatarURL,
@@ -35,7 +35,7 @@ extension [LeaveSpaceRoom] {
memberCount: UInt64(spaceServiceRoom.joinedMembersCount), memberCount: UInt64(spaceServiceRoom.joinedMembersCount),
joinRule: spaceServiceRoom.joinRule), joinRule: spaceServiceRoom.joinRule),
isLastOwner: isLastOwner, isLastOwner: isLastOwner,
areCreatorsPrivileged: false)] areCreatorsPrivileged: areCreatorsPrivileged)]
} }
static var mockNeedNewOwnerRooms: [LeaveSpaceRoom] { static var mockNeedNewOwnerRooms: [LeaveSpaceRoom] {
@@ -93,15 +93,15 @@ struct RoomChangeRolesScreen: View {
@ToolbarContentBuilder @ToolbarContentBuilder
private var toolbar: some ToolbarContent { private var toolbar: some ToolbarContent {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button(L10n.actionSave) { ToolbarButton(role: .save) {
context.send(viewAction: .save) context.send(viewAction: .save)
} }
.disabled(!context.viewState.hasChanges) .disabled(!context.viewState.hasChanges)
} }
if context.viewState.hasChanges { if context.viewState.mode == .owner {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) { ToolbarButton(role: .cancel) {
context.send(viewAction: .cancel) context.send(viewAction: .cancel)
} }
} }
@@ -226,6 +226,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
case .didLeaveSpace: case .didLeaveSpace:
state.bindings.leaveSpaceViewModel = nil state.bindings.leaveSpaceViewModel = nil
actionsSubject.send(.leftRoom) actionsSubject.send(.leftRoom)
case .presentTransferOwnership:
state.bindings.leaveSpaceViewModel = nil
actionsSubject.send(.transferOwnership)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -11,6 +11,7 @@ enum LeaveSpaceViewAction {
case selectAll case selectAll
case toggleRoom(roomID: String) case toggleRoom(roomID: String)
case rolesAndPermissions case rolesAndPermissions
case transferOwnership
case cancel case cancel
} }
@@ -44,5 +45,6 @@ struct LeaveSpaceViewState: BindableState {
enum LeaveSpaceViewModelAction { enum LeaveSpaceViewModelAction {
case didLeaveSpace case didLeaveSpace
case presentRolesAndPermissions case presentRolesAndPermissions
case presentTransferOwnership
case didCancel case didCancel
} }
@@ -41,6 +41,8 @@ class LeaveSpaceViewModel: LeaveSpaceViewModelType {
withTransaction(\.disablesAnimations, true) { // The button is adding an unwanted animation. withTransaction(\.disablesAnimations, true) { // The button is adding an unwanted animation.
state.leaveHandle.toggleRoom(roomID: roomID) state.leaveHandle.toggleRoom(roomID: roomID)
} }
case .transferOwnership:
actionsSubject.send(.presentTransferOwnership)
} }
} }
@@ -66,7 +66,7 @@ struct LeaveSpaceView: View {
Section { Section {
ForEach(context.viewState.leaveHandle.rooms, id: \.spaceServiceRoom.id) { room in ForEach(context.viewState.leaveHandle.rooms, id: \.spaceServiceRoom.id) { room in
LeaveSpaceRoomDetailsCell(room: room, LeaveSpaceRoomDetailsCell(room: room,
hideSelection: context.viewState.leaveHandle.mode == .roomsNeedNewOwner, hideSelection: !context.viewState.leaveHandle.canLeave,
mediaProvider: context.mediaProvider) { mediaProvider: context.mediaProvider) {
context.send(viewAction: .toggleRoom(roomID: room.spaceServiceRoom.id)) context.send(viewAction: .toggleRoom(roomID: room.spaceServiceRoom.id))
} }
@@ -90,20 +90,32 @@ struct LeaveSpaceView: View {
var buttons: some View { var buttons: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
if context.viewState.leaveHandle.canLeave { switch context.viewState.leaveHandle.mode {
case .spaceNeedsNewOwner(let useTransferOwnershipFlow):
if context.viewState.canEditRolesAndPermissions {
if useTransferOwnershipFlow {
Button(role: .destructive) {
context.send(viewAction: .transferOwnership)
} label: {
Label(L10n.actionGoToRolesAndPermissions, icon: \.settings)
}
.buttonStyle(.compound(.primary))
} else {
Button {
context.send(viewAction: .rolesAndPermissions)
} label: {
Label(L10n.actionGoToRolesAndPermissions, icon: \.settings)
}
.buttonStyle(.compound(.primary))
}
}
default:
Button(role: .destructive) { Button(role: .destructive) {
context.send(viewAction: .confirmLeaveSpace) context.send(viewAction: .confirmLeaveSpace)
} label: { } label: {
Label(context.viewState.confirmationTitle, icon: \.leave) Label(context.viewState.confirmationTitle, icon: \.leave)
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
} else if context.viewState.canEditRolesAndPermissions {
Button {
context.send(viewAction: .rolesAndPermissions)
} label: {
Label(L10n.actionGoToRolesAndPermissions, icon: \.settings)
}
.buttonStyle(.compound(.primary))
} }
Button(L10n.actionCancel, action: dismiss.callAsFunction) Button(L10n.actionCancel, action: dismiss.callAsFunction)
@@ -123,7 +135,8 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview {
static let manyViewModel = makeViewModel(mode: .manyRooms) static let manyViewModel = makeViewModel(mode: .manyRooms)
static let onlyAdminViewModel = makeViewModel(mode: .roomsNeedNewOwner) static let onlyAdminViewModel = makeViewModel(mode: .roomsNeedNewOwner)
static let noRoomsViewModel = makeViewModel(mode: .noRooms) static let noRoomsViewModel = makeViewModel(mode: .noRooms)
static let lastAdminViewModel = makeViewModel(mode: .spaceNeedsNewOwner) static let lastAdminViewModel = makeViewModel(mode: .spaceNeedsNewOwner(useTransferOwnershipFlow: false))
static let lastOwnerViewModel = makeViewModel(mode: .spaceNeedsNewOwner(useTransferOwnershipFlow: true))
static var previews: some View { static var previews: some View {
LeaveSpaceView(context: manyViewModel.context) LeaveSpaceView(context: manyViewModel.context)
@@ -134,6 +147,8 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview {
.previewDisplayName("No Rooms") .previewDisplayName("No Rooms")
LeaveSpaceView(context: lastAdminViewModel.context) LeaveSpaceView(context: lastAdminViewModel.context)
.previewDisplayName("Last Space Admin") .previewDisplayName("Last Space Admin")
LeaveSpaceView(context: lastOwnerViewModel.context)
.previewDisplayName("Last Space Owner")
} }
static let spaceServiceRoom = SpaceServiceRoom.mock(id: "!eng-space:matrix.org", static let spaceServiceRoom = SpaceServiceRoom.mock(id: "!eng-space:matrix.org",
@@ -149,8 +164,12 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview {
let rooms: [LeaveSpaceRoom] = switch mode { let rooms: [LeaveSpaceRoom] = switch mode {
case .manyRooms: .mockRooms case .manyRooms: .mockRooms
case .roomsNeedNewOwner: .mockNeedNewOwnerRooms case .roomsNeedNewOwner: .mockNeedNewOwnerRooms
case .noRooms: .mockSingleSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: false) case .noRooms: .mockSingleSpace(spaceServiceRoom: spaceServiceRoom,
case .spaceNeedsNewOwner: .mockRoomsWithSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: true) isLastOwner: false,
areCreatorsPrivileged: false)
case .spaceNeedsNewOwner(let useTransferOwnershipFlow): .mockRoomsWithSpace(spaceServiceRoom: spaceServiceRoom,
isLastOwner: true,
areCreatorsPrivileged: useTransferOwnershipFlow)
} }
let leaveHandle = LeaveSpaceHandleProxy(spaceID: spaceServiceRoom.id, let leaveHandle = LeaveSpaceHandleProxy(spaceID: spaceServiceRoom.id,
@@ -28,6 +28,7 @@ enum SpaceScreenCoordinatorAction {
case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displayMembers(roomProxy: JoinedRoomProxyProtocol)
case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol)
case displayRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) case displayRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol)
case displayTransferOwnership(roomProxy: JoinedRoomProxyProtocol)
case addExistingChildren case addExistingChildren
case displayCreateChildRoomFlow(space: SpaceServiceRoom) case displayCreateChildRoomFlow(space: SpaceServiceRoom)
} }
@@ -78,6 +79,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.addExistingChildren) actionsSubject.send(.addExistingChildren)
case .displayCreateChildRoomFlow(let space): case .displayCreateChildRoomFlow(let space):
actionsSubject.send(.displayCreateChildRoomFlow(space: space)) actionsSubject.send(.displayCreateChildRoomFlow(space: space))
case .presentTransferOwnership(roomProxy: let roomProxy):
actionsSubject.send(.displayTransferOwnership(roomProxy: roomProxy))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -14,6 +14,7 @@ enum SpaceScreenViewModelAction {
case selectRoom(roomID: String) case selectRoom(roomID: String)
case leftSpace case leftSpace
case presentRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) case presentRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol)
case presentTransferOwnership(roomProxy: JoinedRoomProxyProtocol)
case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displayMembers(roomProxy: JoinedRoomProxyProtocol)
case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol)
case addExistingChildren case addExistingChildren
@@ -261,6 +261,12 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
case .didLeaveSpace: case .didLeaveSpace:
state.bindings.leaveSpaceViewModel = nil state.bindings.leaveSpaceViewModel = nil
actionsSubject.send(.leftSpace) actionsSubject.send(.leftSpace)
case .presentTransferOwnership:
guard let roomProxy = state.roomProxy else {
fatalError("The space screen should always have a room proxy")
}
state.bindings.leaveSpaceViewModel = nil
actionsSubject.send(.presentTransferOwnership(roomProxy: roomProxy))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -13,13 +13,24 @@ final class LeaveSpaceHandleProxy {
let id: String let id: String
var rooms: [LeaveSpaceRoomDetails] var rooms: [LeaveSpaceRoomDetails]
enum Mode { case manyRooms, roomsNeedNewOwner, noRooms, spaceNeedsNewOwner } enum Mode: Equatable {
case manyRooms
case roomsNeedNewOwner
case noRooms
case spaceNeedsNewOwner(useTransferOwnershipFlow: Bool)
}
let mode: Mode let mode: Mode
private let leaveHandle: LeaveSpaceHandleProtocol private let leaveHandle: LeaveSpaceHandleProtocol
var canLeave: Bool { var canLeave: Bool {
mode != .spaceNeedsNewOwner switch mode {
case .spaceNeedsNewOwner:
false
default:
true
}
} }
var selectedCount: Int { var selectedCount: Int {
@@ -47,7 +58,7 @@ final class LeaveSpaceHandleProxy {
} }
mode = if let space, space.isLastOwner, space.spaceRoom.numJoinedMembers > 1 { mode = if let space, space.isLastOwner, space.spaceRoom.numJoinedMembers > 1 {
.spaceNeedsNewOwner .spaceNeedsNewOwner(useTransferOwnershipFlow: space.areCreatorsPrivileged)
} else if self.rooms.isEmpty { } else if self.rooms.isEmpty {
.noRooms .noRooms
} else if self.rooms.count(where: { $0.canLeave }) == 0 { } else if self.rooms.count(where: { $0.canLeave }) == 0 {