mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-23 01:52:44 +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
-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
|
||||
|
||||
@@ -118,6 +125,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
|
||||
|
||||
|
||||
+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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user