diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index bd88a4836..5c70c1aff 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -349,6 +349,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.addRooms) case .displayCreateChildRoomFlow(let space): stateMachine.tryEvent(.startCreateChildRoomFlow, userInfo: space) + case .displayTransferOwnership(roomProxy: let roomProxy): + // TODO: Implement + break } } .store(in: &cancellables) diff --git a/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift b/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift index 9e4c32d52..baae94987 100644 --- a/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift +++ b/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift @@ -23,11 +23,11 @@ extension LeaveSpaceHandleSDKMock { } extension [LeaveSpaceRoom] { - static func mockRoomsWithSpace(spaceServiceRoom: SpaceServiceRoom, isLastOwner: Bool) -> [LeaveSpaceRoom] { - mockRooms + mockSingleSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: isLastOwner) + static func mockRoomsWithSpace(spaceServiceRoom: SpaceServiceRoom, isLastOwner: Bool, areCreatorsPrivileged: Bool) -> [LeaveSpaceRoom] { + 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, name: spaceServiceRoom.name, avatarURL: spaceServiceRoom.avatarURL, @@ -35,7 +35,7 @@ extension [LeaveSpaceRoom] { memberCount: UInt64(spaceServiceRoom.joinedMembersCount), joinRule: spaceServiceRoom.joinRule), isLastOwner: isLastOwner, - areCreatorsPrivileged: false)] + areCreatorsPrivileged: areCreatorsPrivileged)] } static var mockNeedNewOwnerRooms: [LeaveSpaceRoom] { diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift index 81152839d..d4bee1853 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift @@ -93,15 +93,15 @@ struct RoomChangeRolesScreen: View { @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .confirmationAction) { - Button(L10n.actionSave) { + ToolbarButton(role: .save) { context.send(viewAction: .save) } .disabled(!context.viewState.hasChanges) } - if context.viewState.hasChanges { + if context.viewState.mode == .owner { ToolbarItem(placement: .cancellationAction) { - Button(L10n.actionCancel) { + ToolbarButton(role: .cancel) { context.send(viewAction: .cancel) } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index afc97636c..1f982adf9 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -226,6 +226,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr case .didLeaveSpace: state.bindings.leaveSpaceViewModel = nil actionsSubject.send(.leftRoom) + case .presentTransferOwnership: + state.bindings.leaveSpaceViewModel = nil + actionsSubject.send(.transferOwnership) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift index 33cc32f5f..c1bde70d5 100644 --- a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift @@ -11,6 +11,7 @@ enum LeaveSpaceViewAction { case selectAll case toggleRoom(roomID: String) case rolesAndPermissions + case transferOwnership case cancel } @@ -44,5 +45,6 @@ struct LeaveSpaceViewState: BindableState { enum LeaveSpaceViewModelAction { case didLeaveSpace case presentRolesAndPermissions + case presentTransferOwnership case didCancel } diff --git a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift index 34791d76b..6c779c5fb 100644 --- a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift @@ -41,6 +41,8 @@ class LeaveSpaceViewModel: LeaveSpaceViewModelType { withTransaction(\.disablesAnimations, true) { // The button is adding an unwanted animation. state.leaveHandle.toggleRoom(roomID: roomID) } + case .transferOwnership: + actionsSubject.send(.presentTransferOwnership) } } diff --git a/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift index 5f2ba93e4..7ac464b5a 100644 --- a/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift @@ -66,7 +66,7 @@ struct LeaveSpaceView: View { Section { ForEach(context.viewState.leaveHandle.rooms, id: \.spaceServiceRoom.id) { room in LeaveSpaceRoomDetailsCell(room: room, - hideSelection: context.viewState.leaveHandle.mode == .roomsNeedNewOwner, + hideSelection: !context.viewState.leaveHandle.canLeave, mediaProvider: context.mediaProvider) { context.send(viewAction: .toggleRoom(roomID: room.spaceServiceRoom.id)) } @@ -90,20 +90,32 @@ struct LeaveSpaceView: View { var buttons: some View { 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) { context.send(viewAction: .confirmLeaveSpace) } label: { Label(context.viewState.confirmationTitle, icon: \.leave) } .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) @@ -123,7 +135,8 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { static let manyViewModel = makeViewModel(mode: .manyRooms) static let onlyAdminViewModel = makeViewModel(mode: .roomsNeedNewOwner) 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 { LeaveSpaceView(context: manyViewModel.context) @@ -134,6 +147,8 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { .previewDisplayName("No Rooms") LeaveSpaceView(context: lastAdminViewModel.context) .previewDisplayName("Last Space Admin") + LeaveSpaceView(context: lastOwnerViewModel.context) + .previewDisplayName("Last Space Owner") } static let spaceServiceRoom = SpaceServiceRoom.mock(id: "!eng-space:matrix.org", @@ -149,8 +164,12 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { let rooms: [LeaveSpaceRoom] = switch mode { case .manyRooms: .mockRooms case .roomsNeedNewOwner: .mockNeedNewOwnerRooms - case .noRooms: .mockSingleSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: false) - case .spaceNeedsNewOwner: .mockRoomsWithSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: true) + case .noRooms: .mockSingleSpace(spaceServiceRoom: spaceServiceRoom, + isLastOwner: false, + areCreatorsPrivileged: false) + case .spaceNeedsNewOwner(let useTransferOwnershipFlow): .mockRoomsWithSpace(spaceServiceRoom: spaceServiceRoom, + isLastOwner: true, + areCreatorsPrivileged: useTransferOwnershipFlow) } let leaveHandle = LeaveSpaceHandleProxy(spaceID: spaceServiceRoom.id, diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index eb84e0089..20da565e0 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -28,6 +28,7 @@ enum SpaceScreenCoordinatorAction { case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case displayRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) + case displayTransferOwnership(roomProxy: JoinedRoomProxyProtocol) case addExistingChildren case displayCreateChildRoomFlow(space: SpaceServiceRoom) } @@ -78,6 +79,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.addExistingChildren) case .displayCreateChildRoomFlow(let space): actionsSubject.send(.displayCreateChildRoomFlow(space: space)) + case .presentTransferOwnership(roomProxy: let roomProxy): + actionsSubject.send(.displayTransferOwnership(roomProxy: roomProxy)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index c1f381c73..5ee985237 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -14,6 +14,7 @@ enum SpaceScreenViewModelAction { case selectRoom(roomID: String) case leftSpace case presentRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) + case presentTransferOwnership(roomProxy: JoinedRoomProxyProtocol) case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) case addExistingChildren diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index d269fceeb..5185327e7 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -261,6 +261,12 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc case .didLeaveSpace: state.bindings.leaveSpaceViewModel = nil 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) diff --git a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift index 540bda479..1553253c1 100644 --- a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift +++ b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift @@ -13,13 +13,24 @@ final class LeaveSpaceHandleProxy { let id: String 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 private let leaveHandle: LeaveSpaceHandleProtocol var canLeave: Bool { - mode != .spaceNeedsNewOwner + switch mode { + case .spaceNeedsNewOwner: + false + default: + true + } } var selectedCount: Int { @@ -47,7 +58,7 @@ final class LeaveSpaceHandleProxy { } mode = if let space, space.isLastOwner, space.spaceRoom.numJoinedMembers > 1 { - .spaceNeedsNewOwner + .spaceNeedsNewOwner(useTransferOwnershipFlow: space.areCreatorsPrivileged) } else if self.rooms.isEmpty { .noRooms } else if self.rooms.count(where: { $0.canLeave }) == 0 {