mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-25 02:52:45 +02:00
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:
@@ -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";
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
+105
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
+4
-1
@@ -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))
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 }
|
||||
}
|
||||
|
||||
+10
-1
@@ -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.
|
||||
|
||||
|
||||
+3
@@ -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()
|
||||
}
|
||||
|
||||
+50
@@ -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)
|
||||
]
|
||||
}))
|
||||
}
|
||||
}
|
||||
+23
@@ -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] {
|
||||
|
||||
+23
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Spaces: Bring leaving space experience in line with Web
|
||||
Reference in New Issue
Block a user