4850 bring leaving space experience in line with web (#6062)

* Bring leaving space experience in line with Web #4850

- Done
This commit is contained in:
Gil Eluard
2022-04-27 13:31:14 +02:00
committed by GitHub
parent 2b5989bc9b
commit 987eb7d5c0
23 changed files with 537 additions and 60 deletions
@@ -4,7 +4,8 @@
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -21,3 +21,10 @@
"image_picker_action_files" = "Choose from files";
"spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer.";
"leave_space_action" = "Leave space";
"leave_space_and_one_room" = "Leave space and 1 room";
"leave_space_and_more_rooms" = "Leave space and %@ rooms";
"leave_space_selection_title" = "SELECT ROOMS";
"leave_space_selection_all_rooms" = "Select all rooms";
"leave_space_selection_no_rooms" = "Select no rooms";
+24
View File
@@ -14,6 +14,30 @@ public extension VectorL10n {
static var imagePickerActionFiles: String {
return VectorL10n.tr("Untranslated", "image_picker_action_files")
}
/// Leave space
static var leaveSpaceAction: String {
return VectorL10n.tr("Untranslated", "leave_space_action")
}
/// Leave space and %@ rooms
static func leaveSpaceAndMoreRooms(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "leave_space_and_more_rooms", p1)
}
/// Leave space and 1 room
static var leaveSpaceAndOneRoom: String {
return VectorL10n.tr("Untranslated", "leave_space_and_one_room")
}
/// Select all rooms
static var leaveSpaceSelectionAllRooms: String {
return VectorL10n.tr("Untranslated", "leave_space_selection_all_rooms")
}
/// Select no rooms
static var leaveSpaceSelectionNoRooms: String {
return VectorL10n.tr("Untranslated", "leave_space_selection_no_rooms")
}
/// SELECT ROOMS
static var leaveSpaceSelectionTitle: String {
return VectorL10n.tr("Untranslated", "leave_space_selection_title")
}
/// This feature isn't available here. For now, you can do this with %@ on your computer.
static func spacesFeatureNotAvailable(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "spaces_feature_not_available", p1)
@@ -25,6 +25,7 @@ enum SpaceMenuListItemAction {
case addSpace
case settings
case leaveSpace
case leaveSpaceAndChooseRooms
case invite
}
@@ -45,6 +45,8 @@ class SpaceMenuPresenter: NSObject {
private weak var selectedSpace: MXSpace?
private var session: MXSession!
private var spaceId: String!
private weak var spaceMenuViewController: SpaceMenuViewController?
private var leaveSpaceCoordinator: Coordinator?
// MARK: - Public
@@ -74,6 +76,7 @@ class SpaceMenuPresenter: NSObject {
private func showMenu(for spaceId: String, session: MXSession) {
let menuViewController = SpaceMenuViewController.instantiate(forSpaceWithId: spaceId, matrixSession: session, viewModel: self.viewModel)
self.present(menuViewController, animated: true)
self.spaceMenuViewController = menuViewController
}
private func present(_ viewController: SpaceMenuViewController, animated: Bool) {
@@ -96,6 +99,29 @@ class SpaceMenuPresenter: NSObject {
self.presentingViewController?.present(viewController, animated: animated, completion: nil)
}
}
@available(iOS 14.0, *)
private func showLeaveSpace() {
let name = session.spaceService.getSpace(withId: spaceId)?.summary?.displayname ?? VectorL10n.spaceTag
let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle,
selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms,
selectNoneTitle: VectorL10n.leaveSpaceSelectionNoRooms)
let paramaters = MatrixItemChooserCoordinatorParameters(session: session,
title: VectorL10n.leaveSpaceTitle(name),
detail: VectorL10n.leaveSpaceMessage(name),
selectionHeader: selectionHeader,
viewProvider: LeaveSpaceViewProvider(navTitle: nil),
itemsProcessor: LeaveSpaceItemsProcessor(spaceId: spaceId, session: session))
let coordinator = MatrixItemChooserCoordinator(parameters: paramaters)
coordinator.start()
self.leaveSpaceCoordinator = coordinator
coordinator.completion = { [weak self] result in
self?.spaceMenuViewController?.dismiss(animated: true)
self?.leaveSpaceCoordinator = nil
}
self.spaceMenuViewController?.present(coordinator.toPresentable(), animated: true)
}
}
// MARK: - SpaceMenuModelViewModelCoordinatorDelegate
@@ -120,6 +146,10 @@ extension SpaceMenuPresenter: SpaceMenuModelViewModelCoordinatorDelegate {
self.delegate?.spaceMenuPresenter(self, didCompleteWith: .settings, forSpaceWithId: self.spaceId, with: self.session)
case .invite:
self.delegate?.spaceMenuPresenter(self, didCompleteWith: .invite, forSpaceWithId: self.spaceId, with: self.session)
case .leaveSpaceAndChooseRooms:
if #available(iOS 14.0, *) {
self.showLeaveSpace()
}
default:
MXLog.error("[SpaceMenuPresenter] spaceListViewModel didSelectItem: invalid action \(action)")
}
@@ -85,6 +85,7 @@ class SpaceMenuViewModel: SpaceMenuViewModelType {
}
private func leaveSpace() {
guard #available(iOS 14, *) else {
guard let room = self.session.room(withRoomId: self.spaceId), let displayName = room.summary?.displayname else {
return
}
@@ -97,6 +98,11 @@ class SpaceMenuViewModel: SpaceMenuViewModelType {
}
self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .leaveOptions(displayName, isAdmin))
return
}
self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .deselect)
self.coordinatorDelegate?.spaceMenuViewModel(self, didSelectItemWith: .leaveSpaceAndChooseRooms)
}
private func leaveSpaceAndKeepRooms() {
@@ -0,0 +1,70 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct RadioButton: View {
// MARK: - Properties
var title: String
var selected: Bool
let action: () -> Void
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
var body: some View {
Button(action: action, label: {
HStack {
Image(systemName: selected ? "largecircle.fill.circle" : "circle")
.renderingMode(.template)
.resizable().frame(width: 20, height: 20)
.foregroundColor(selected ? theme.colors.accent : theme.colors.tertiaryContent)
Text(title)
.font(theme.fonts.callout)
.foregroundColor(theme.colors.primaryContent)
Spacer()
}
.padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3))
.background(Color.clear)
})
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct RadioButton_Previews: PreviewProvider {
static var previews: some View {
Group {
buttonGroup.theme(.light)
buttonGroup.theme(.dark).preferredColorScheme(.dark)
}
.padding()
}
static var buttonGroup: some View {
VStack {
RadioButton(title: "A title", selected: false, action: {})
RadioButton(title: "A title", selected: true, action: {})
}
}
}
@@ -0,0 +1,31 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
class LeaveSpaceViewProvider: MatrixItemChooserCoordinatorViewProvider {
private let navTitle: String?
init(navTitle: String?) {
self.navTitle = navTitle
}
@available(iOS 14, *)
func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView {
return AnyView(LeaveSpace(viewModel: viewModel, navTitle: navTitle))
}
}
@@ -0,0 +1,105 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import MatrixSDK
class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol {
// MARK: Private
private let spaceId: String
private let session: MXSession
// MARK: Setup
init(spaceId: String, session: MXSession) {
self.spaceId = spaceId
self.session = session
self.dataSource = MatrixItemChooserDirectChildrenDataSource(parentId: spaceId)
}
// MARK: MatrixItemChooserSelectionProcessorProtocol
private(set) var dataSource: MatrixItemChooserDataSource
var loadingText: String? {
VectorL10n.roomAccessSettingsScreenSettingRoomAccess
}
func computeSelection(withIds itemsIds: [String], completion: @escaping (Result<Void, Error>) -> Void) {
guard let space = self.session.spaceService.getSpace(withId: self.spaceId) else {
return
}
self.leaveAllRooms(from: itemsIds, at: 0) { [weak self] result in
guard let self = self else { return }
switch result {
case .success:
self.leaveSpace(space, completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
}
func isItemIncluded(_ item: (MatrixListItemData)) -> Bool {
return true
}
// MARK: Private
/// Leave room with room ID from `roomIds` at `index`.
/// Recurse to the next index once done.
private func leaveAllRooms(from roomIds: [String], at index: Int, completion: @escaping (Result<Void, Error>) -> Void) {
guard index < roomIds.count else {
completion(.success(()))
return
}
guard let room = self.session.room(withRoomId: roomIds[index]), !room.isDirect else {
self.leaveAllRooms(from: roomIds, at: index+1, completion: completion)
return
}
MXLog.debug("[LeaveSpaceItemsProcessor] leaving room \(room.displayName ?? room.roomId)")
room.leave { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.leaveAllRooms(from: roomIds, at: index+1, completion: completion)
case .failure(let error):
MXLog.error("[LeaveSpaceItemsProcessor] failed to leave room with error: \(error)")
completion(.failure(error))
}
}
}
private func leaveSpace(_ space: MXSpace, completion: @escaping (Result<Void, Error>) -> Void) {
MXLog.debug("[LeaveSpaceItemsProcessor] leaving space")
space.room?.leave(completion: { response in
switch response {
case .success:
completion(.success(()))
case .failure(let error):
MXLog.error("[LeaveSpaceItemsProcessor] failed to leave space with error: \(error)")
completion(.failure(error))
}
})
}
}
@@ -0,0 +1,58 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct LeaveSpace: View {
// MARK: Properties
@ObservedObject var viewModel: MatrixItemChooserViewModel.Context
let navTitle: String?
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
@ViewBuilder
var body: some View {
mainView
.background(theme.colors.background)
}
// MARK: - Private
@ViewBuilder
private var mainView: some View {
ZStack(alignment: .bottom) {
MatrixItemChooser(viewModel: viewModel, listBottomPadding: 72)
footerView
}
}
@ViewBuilder
private var footerView: some View {
Button {
viewModel.send(viewAction: .done)
} label: {
Text(viewModel.viewState.selectedItemIds.isEmpty ? VectorL10n.leaveSpaceAction : (viewModel.viewState.selectedItemIds.count == 1 ? VectorL10n.leaveSpaceAndOneRoom : VectorL10n.leaveSpaceAndMoreRooms("\(viewModel.viewState.selectedItemIds.count)")))
}
.buttonStyle(PrimaryActionButtonStyle(customColor: theme.colors.alert))
.padding()
}
}
@@ -31,11 +31,13 @@ struct MatrixItemChooserCoordinatorParameters {
let selectedItemsIds: [String]
let viewProvider: MatrixItemChooserCoordinatorViewProvider?
let itemsProcessor: MatrixItemChooserProcessorProtocol
let selectionHeader: MatrixItemChooserSelectionHeader?
init(session: MXSession,
title: String? = nil,
detail: String? = nil,
selectedItemsIds: [String] = [],
selectionHeader: MatrixItemChooserSelectionHeader? = nil,
viewProvider: MatrixItemChooserCoordinatorViewProvider? = nil,
itemsProcessor: MatrixItemChooserProcessorProtocol) {
self.session = session
@@ -44,6 +46,7 @@ struct MatrixItemChooserCoordinatorParameters {
self.selectedItemsIds = selectedItemsIds
self.viewProvider = viewProvider
self.itemsProcessor = itemsProcessor
self.selectionHeader = selectionHeader
}
}
@@ -69,7 +72,7 @@ final class MatrixItemChooserCoordinator: Coordinator, Presentable {
@available(iOS 14.0, *)
init(parameters: MatrixItemChooserCoordinatorParameters) {
self.parameters = parameters
let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail)
let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail, selectionHeader: parameters.selectionHeader)
matrixItemChooserViewModel = viewModel
if let viewProvider = parameters.viewProvider {
let view = viewProvider.view(with: viewModel.context).addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
@@ -27,13 +27,6 @@ enum MatrixItemChooserType {
// MARK: View model
enum MatrixItemChooserStateAction {
case loadingState(Bool)
case updateError(Error?)
case updateSections([MatrixListItemSectionData])
case updateSelection(Set<String>)
}
enum MatrixItemChooserViewModelResult {
case cancel
case done([String])
@@ -77,15 +70,23 @@ struct MatrixListItemData {
extension MatrixListItemData: Identifiable, Equatable {}
struct MatrixItemChooserSelectionHeader {
var title: String
var selectAllTitle: String
var selectNoneTitle: String
}
struct MatrixItemChooserViewState: BindableState {
var title: String?
var message: String?
var emptyListMessage: String
var sections: [MatrixListItemSectionData]
var itemCount: Int
var selectedItemIds: Set<String>
var loadingText: String?
var loading: Bool
var error: String?
var selectionHeader: MatrixItemChooserSelectionHeader?
}
enum MatrixItemChooserViewAction {
@@ -94,4 +95,6 @@ enum MatrixItemChooserViewAction {
case done
case cancel
case back
case selectAll
case selectNone
}
@@ -19,7 +19,7 @@ import Combine
@available(iOS 14, *)
typealias MatrixItemChooserViewModelType = StateStoreViewModel<MatrixItemChooserViewState,
MatrixItemChooserStateAction,
Never,
MatrixItemChooserViewAction>
@available(iOS 14, *)
class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChooserViewModelProtocol {
@@ -30,40 +30,49 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo
private var matrixItemChooserService: MatrixItemChooserServiceProtocol
private var isLoading: Bool = false {
didSet {
state.loading = isLoading
if isLoading {
state.error = nil
}
}
}
// MARK: Public
var completion: ((MatrixItemChooserViewModelResult) -> Void)?
// MARK: - Setup
static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewModelProtocol {
return MatrixItemChooserViewModel(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail)
static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewModelProtocol {
return MatrixItemChooserViewModel(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail, selectionHeader: selectionHeader)
}
private init(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) {
private init(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) {
self.matrixItemChooserService = matrixItemChooserService
super.init(initialViewState: Self.defaultState(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail))
super.init(initialViewState: Self.defaultState(service: matrixItemChooserService, title: title, detail: detail, selectionHeader: selectionHeader))
startObservingItems()
}
private static func defaultState(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewState {
private static func defaultState(service: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewState {
let title = title
let message = detail
let emptyListMessage = VectorL10n.spacesNoResultFoundTitle
return MatrixItemChooserViewState(title: title, message: message, emptyListMessage: emptyListMessage, sections: matrixItemChooserService.sectionsSubject.value, selectedItemIds: matrixItemChooserService.selectedItemIdsSubject.value, loadingText: matrixItemChooserService.loadingText, loading: false)
return MatrixItemChooserViewState(title: title, message: message, emptyListMessage: emptyListMessage, sections: service.sectionsSubject.value, itemCount: service.itemCount, selectedItemIds: service.selectedItemIdsSubject.value, loadingText: service.loadingText, loading: false, selectionHeader: selectionHeader)
}
private func startObservingItems() {
let sectionsUpdatePublisher = matrixItemChooserService.sectionsSubject
.map(MatrixItemChooserStateAction.updateSections)
.eraseToAnyPublisher()
dispatch(actionPublisher: sectionsUpdatePublisher)
let selectionPublisher = matrixItemChooserService.selectedItemIdsSubject
.map(MatrixItemChooserStateAction.updateSelection)
.eraseToAnyPublisher()
dispatch(actionPublisher: selectionPublisher)
matrixItemChooserService.sectionsSubject.sink { [weak self] sections in
self?.state.sections = sections
self?.state.itemCount = self?.matrixItemChooserService.itemCount ?? 0
}
.store(in: &cancellables)
matrixItemChooserService.selectedItemIdsSubject.sink { [weak self] selectedItemIds in
self?.state.selectedItemIds = selectedItemIds
}
.store(in: &cancellables)
}
// MARK: - Public
@@ -75,11 +84,11 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo
case .back:
back()
case .done:
dispatch(action: .loadingState(true))
isLoading = true
matrixItemChooserService.processSelection { [weak self] result in
guard let self = self else { return }
self.dispatch(action: .loadingState(false))
self.isLoading = false
switch result {
case .success:
@@ -87,31 +96,20 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo
self.done(selectedItemsId: selectedItemsId)
case .failure(let error):
self.matrixItemChooserService.refresh()
self.dispatch(action: .updateError(error))
self.state.error = error.localizedDescription
}
}
case .searchTextChanged(let searchText):
self.matrixItemChooserService.searchText = searchText
case .itemTapped(let itemId):
self.matrixItemChooserService.reverseSelectionForItem(withId: itemId)
case .selectAll:
self.matrixItemChooserService.selectAllItems()
case .selectNone:
self.matrixItemChooserService.deselectAllItems()
}
}
override class func reducer(state: inout MatrixItemChooserViewState, action: MatrixItemChooserStateAction) {
switch action {
case .updateSections(let sections):
state.sections = sections
case .updateSelection(let selectedItemIds):
state.selectedItemIds = selectedItemIds
case .loadingState(let loading):
state.loading = loading
state.error = nil
case .updateError(let error):
state.error = error?.localizedDescription
}
UILog.debug("[MatrixItemChooserViewModel] reducer with action \(action) produced state: \(state)")
}
private func done(selectedItemsId: [String]) {
completion?(.done(selectedItemsId))
}
@@ -20,7 +20,7 @@ protocol MatrixItemChooserViewModelProtocol {
var completion: ((MatrixItemChooserViewModelResult) -> Void)? { get set }
@available(iOS 14, *)
static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewModelProtocol
static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewModelProtocol
@available(iOS 14, *)
var context: MatrixItemChooserViewModelType.Context { get }
}
@@ -27,6 +27,7 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable {
case noItems
case items
case selectedItems
case selectionHeader
/// The associated screen
var screenType: Any.Type {
@@ -36,17 +37,25 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable {
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let service: MockMatrixItemChooserService
let selectionHeader: MatrixItemChooserSelectionHeader?
switch self {
case .noItems:
selectionHeader = nil
service = MockMatrixItemChooserService(type: .room, sections: [MatrixListItemSectionData()])
case .items:
selectionHeader = nil
service = MockMatrixItemChooserService()
case .selectedItems:
selectionHeader = nil
service = MockMatrixItemChooserService(type: .room, sections: MockMatrixItemChooserService.mockSections, selectedItemIndexPaths: [IndexPath(row: 0, section: 0), IndexPath(row: 2, section: 0), IndexPath(row: 1, section: 1)])
case .selectionHeader:
selectionHeader = MatrixItemChooserSelectionHeader(title: "Selection Title", selectAllTitle: "Select all items", selectNoneTitle: "Select no items")
service = MockMatrixItemChooserService(type: .room, sections: MockMatrixItemChooserService.mockSections, selectedItemIndexPaths: [IndexPath(row: 0, section: 0), IndexPath(row: 2, section: 0), IndexPath(row: 1, section: 1)])
}
let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service,
title: VectorL10n.spacesCreationAddRoomsTitle,
detail: VectorL10n.spacesCreationAddRoomsMessage)
detail: VectorL10n.spacesCreationAddRoomsMessage,
selectionHeader: selectionHeader)
// can simulate service and viewModel actions here if needs be.
@@ -23,8 +23,11 @@ protocol MatrixItemChooserServiceProtocol {
var selectedItemIdsSubject: CurrentValueSubject<Set<String>, Never> { get }
var searchText: String { get set }
var loadingText: String? { get }
var itemCount: Int { get }
func reverseSelectionForItem(withId itemId: String)
func processSelection(completion: @escaping (Result<Void, Error>) -> Void)
func refresh()
func selectAllItems()
func deselectAllItems()
}
@@ -0,0 +1,50 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
class MatrixItemChooserDirectChildrenDataSource: MatrixItemChooserDataSource {
// MARK: - Private
private let parentId: String
// MARK: - Setup
init(parentId: String) {
self.parentId = parentId
}
// MARK: - MatrixItemChooserDataSource
var preselectedItemIds: Set<String>? { nil }
func sections(with session: MXSession, completion: @escaping (Result<[MatrixListItemSectionData], Error>) -> Void) {
let space = session.spaceService.getSpace(withId: parentId)
let children: [MatrixListItemData] = space?.childRoomIds.compactMap({ roomId in
guard let room = session.room(withRoomId: roomId), !room.isDirect else {
return nil
}
return MatrixListItemData(mxRoom: room, spaceService: session.spaceService)
}) ?? []
completion(Result(catching: {
[
MatrixListItemSectionData(items: children)
]
}))
}
}
@@ -61,6 +61,13 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol {
var loadingText: String? {
itemsProcessor.loadingText
}
var itemCount: Int {
var itemCount = 0
for section in sections {
itemCount += section.items.count
}
return itemCount
}
// MARK: - Setup
@@ -119,6 +126,22 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol {
}
}
func selectAllItems() {
var newSelection: Set<String> = Set()
for section in sections {
for item in section.items {
newSelection.insert(item.id)
}
}
self.selectedItemIds = newSelection
selectedItemIdsSubject.send(selectedItemIds)
}
func deselectAllItems() {
self.selectedItemIds = Set()
selectedItemIdsSubject.send(selectedItemIds)
}
// MARK: - Private
private func filter(sections: [MatrixListItemSectionData]) -> [MatrixListItemSectionData] {
@@ -39,6 +39,13 @@ class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol {
var loadingText: String? {
nil
}
var itemCount: Int {
var itemCount = 0
for section in sectionsSubject.value {
itemCount += section.items.count
}
return itemCount
}
init(type: MatrixItemChooserType = .room, sections: [MatrixListItemSectionData] = mockSections, selectedItemIndexPaths: [IndexPath] = []) {
sectionsSubject = CurrentValueSubject(sections)
@@ -79,4 +86,20 @@ class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol {
func refresh() {
}
func selectAllItems() {
var newSelection: Set<String> = Set()
for section in sectionsSubject.value {
for item in section.items {
newSelection.insert(item.id)
}
}
self.selectedItemIds = newSelection
selectedItemIdsSubject.send(selectedItemIds)
}
func deselectAllItems() {
self.selectedItemIds = Set()
selectedItemIdsSubject.send(selectedItemIds)
}
}
@@ -37,6 +37,8 @@ class MatrixItemChooserUITests: MockScreenTest {
verifyPopulatedScreen()
case .selectedItems:
verifyPopulatedWithSelectionScreen()
case .selectionHeader:
break
}
}
@@ -28,7 +28,7 @@ class MatrixItemChooserViewModelTests: XCTestCase {
override func setUpWithError() throws {
service = MockMatrixItemChooserService(type: .room)
viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: VectorL10n.spacesCreationAddRoomsTitle, detail: VectorL10n.spacesCreationAddRoomsMessage)
viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: VectorL10n.spacesCreationAddRoomsTitle, detail: VectorL10n.spacesCreationAddRoomsMessage, selectionHeader: nil)
context = viewModel.context
}
@@ -90,6 +90,7 @@ struct MatrixItemChooser: View {
.frame(maxHeight: .infinity, alignment: .top)
.animation(nil)
}
.animation(nil)
}
@ViewBuilder
@@ -116,8 +117,36 @@ struct MatrixItemChooser: View {
.onChange(of: searchText) { value in
viewModel.send(viewAction: .searchTextChanged(searchText))
}
if let selectionHeader = viewModel.viewState.selectionHeader, searchText.isEmpty {
Spacer().frame(height: spacerHeight)
itemSelectionHeader(with: selectionHeader)
}
}
}
private func itemSelectionHeader(with selectionHeader: MatrixItemChooserSelectionHeader) -> some View {
VStack(alignment:.leading) {
HStack {
Text(selectionHeader.title)
.font(theme.fonts.calloutSB)
.foregroundColor(theme.colors.primaryContent)
Text("\(viewModel.viewState.itemCount)")
.font(theme.fonts.calloutSB)
.foregroundColor(theme.colors.tertiaryContent)
}
HStack {
RadioButton(title: selectionHeader.selectAllTitle, selected: viewModel.viewState.itemCount > 0 && viewModel.viewState.selectedItemIds.count == viewModel.viewState.itemCount) {
viewModel.send(viewAction: .selectAll)
}
RadioButton(title: selectionHeader.selectNoneTitle, selected: viewModel.viewState.selectedItemIds.isEmpty) {
viewModel.send(viewAction: .selectNone)
}
}
}
.padding(.vertical, 4)
.padding(.horizontal)
.background(theme.colors.tile)
}
}
// MARK: - Previews
@@ -127,9 +156,9 @@ struct MatrixItemChooser_Previews: PreviewProvider {
static let stateRenderer = MockMatrixItemChooserScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup(addNavigation: true)
stateRenderer.screenGroup(addNavigation: false)
.theme(.light).preferredColorScheme(.light)
stateRenderer.screenGroup(addNavigation: true)
stateRenderer.screenGroup(addNavigation: false)
.theme(.dark).preferredColorScheme(.dark)
}
}
+1
View File
@@ -0,0 +1 @@
Spaces: Bring leaving space experience in line with Web