SP2: Adding Rooms to Spaces element-ios#5230

- Implemented designs with new & existing tabs in a bottom sheet
- Replaced rough edge warnings from space panel overflow with working journeys
This commit is contained in:
Gil Eluard
2021-12-23 14:08:00 +01:00
parent 9bf1c994cc
commit cfd00fea40
55 changed files with 1621 additions and 613 deletions
@@ -1,74 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
/*
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 UIKit
import SwiftUI
final class SpaceCreationMatrixItemChooserCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: SpaceCreationMatrixItemChooserCoordinatorParameters
private let spaceCreationMatrixItemChooserHostingController: UIViewController
private var spaceCreationMatrixItemChooserViewModel: SpaceCreationMatrixItemChooserViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((SpaceCreationMatrixItemChooserCoordinatorAction) -> Void)?
// MARK: - Setup
@available(iOS 14.0, *)
init(parameters: SpaceCreationMatrixItemChooserCoordinatorParameters) {
self.parameters = parameters
let service = SpaceCreationMatrixItemChooserService(session: parameters.session, type: parameters.type, selectedItemIds: [])
let viewModel = SpaceCreationMatrixItemChooserViewModel.makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: service, creationParams: parameters.creationParams)
let view = SpaceCreationMatrixItemChooser(viewModel: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
spaceCreationMatrixItemChooserViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
spaceCreationMatrixItemChooserHostingController = hostingController
}
// MARK: - Public
func start() {
MXLog.debug("[SpaceCreationMatrixItemChooserCoordinator] did start.")
spaceCreationMatrixItemChooserViewModel.callback = { [weak self] result in
MXLog.debug("[SpaceCreationMatrixItemChooserCoordinator] SpaceCreationMatrixItemChooserViewModel did complete with result: \(result).")
guard let self = self else { return }
switch result {
case .cancel:
self.callback?(.cancel)
case .back:
self.callback?(.back)
case .done:
self.callback?(.done)
}
}
}
func toPresentable() -> UIViewController {
return self.spaceCreationMatrixItemChooserHostingController
}
}
@@ -1,25 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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
struct SpaceCreationMatrixItemChooserCoordinatorParameters {
let session: MXSession
let type: SpaceCreationMatrixItemType
let creationParams: SpaceCreationParameters
}
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,10 +14,11 @@
// limitations under the License.
//
import Foundation
import SwiftUI
/// Actions to be performed on the `ViewModel` State
enum SpaceCreationMatrixItemListStateAction {
case updateItems([SpaceCreationMatrixItem])
case updateSelection(Set<String>)
class SpaceCreationMatrixItemChooserViewProvider: MatrixItemChooserCoordinatorViewProvider {
@available(iOS 14, *)
func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView {
return AnyView(SpaceCreationMatrixItemChooser(viewModel: viewModel))
}
}
@@ -1,26 +0,0 @@
//
// 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
struct SpaceCreationMatrixItem {
let id: String
let avatar: AvatarInput
let displayName: String?
let detailText: String?
}
extension SpaceCreationMatrixItem: Identifiable, Equatable {}
@@ -1,24 +0,0 @@
//
// 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
/// Actions returned by the coordinator callback
enum SpaceCreationMatrixItemChooserCoordinatorAction {
case done
case cancel
case back
}
@@ -1,26 +0,0 @@
//
// 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
/// Actions send from the `View` to the `ViewModel`.
enum SpaceCreationMatrixItemListStateActionListViewAction {
case searchTextChanged(String)
case itemTapped(_ itemId: String)
case done
case cancel
case back
}
@@ -1,24 +0,0 @@
//
// 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
/// Actions sent by the`ViewModel` to the `Coordinator`.
enum SpaceCreationMatrixItemListStateActionListViewModelAction {
case done
case cancel
case back
}
@@ -1,27 +0,0 @@
//
// 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
/// State managed by the `ViewModel` delivered to the `View`.
struct SpaceCreationMatrixItemListStateActionListViewState: BindableState {
var navTitle: String
var title: String
var message: String
var emptyListMessage: String
var items: [SpaceCreationMatrixItem]
var selectedItemIds: Set<String>
}
@@ -1,22 +0,0 @@
//
// 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
enum SpaceCreationMatrixItemType {
case room
case people
}
@@ -1,133 +0,0 @@
//
// 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 Combine
@available(iOS 14.0, *)
class SpaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol {
// MARK: - Properties
// MARK: Private
private let processingQueue = DispatchQueue(label: "org.matrix.element.SpaceCreationMatrixItemChooserService.processingQueue")
private let completionQueue = DispatchQueue.main
private let session: MXSession
private let items: [SpaceCreationMatrixItem]
private var filteredItems: [SpaceCreationMatrixItem] {
didSet {
itemsSubject.send(filteredItems)
}
}
private var selectedItemIds: Set<String>
// MARK: Public
private(set) var type: SpaceCreationMatrixItemType
private(set) var itemsSubject: CurrentValueSubject<[SpaceCreationMatrixItem], Never>
private(set) var selectedItemIdsSubject: CurrentValueSubject<Set<String>, Never>
var searchText: String = "" {
didSet {
if searchText.isEmpty {
filteredItems = items
} else {
self.processingQueue.async {
let lowercasedSearchText = self.searchText.lowercased()
let filteredItems = self.items.filter { $0.id.lowercased().contains(lowercasedSearchText) || ($0.displayName ?? "").lowercased().contains(lowercasedSearchText) }
self.completionQueue.async {
self.filteredItems = filteredItems
}
}
}
}
}
// MARK: - Setup
init(session: MXSession, type: SpaceCreationMatrixItemType, selectedItemIds: [String]) {
self.session = session
self.type = type
switch type {
case .people:
self.items = session.users().map { user in
SpaceCreationMatrixItem(mxUser: user)
}
case .room:
self.items = session.rooms.compactMap { room in
if room.summary.roomType == .space || room.isDirect {
return nil
}
return SpaceCreationMatrixItem(mxRoom: room, spaceService: session.spaceService)
}
}
self.itemsSubject = CurrentValueSubject(self.items)
self.filteredItems = self.items
self.selectedItemIds = Set(selectedItemIds)
self.selectedItemIdsSubject = CurrentValueSubject(self.selectedItemIds)
}
// MARK: - Public
func reverseSelectionForItem(withId itemId: String) {
if selectedItemIds.contains(itemId) {
selectedItemIds.remove(itemId)
} else {
selectedItemIds.insert(itemId)
}
selectedItemIdsSubject.send(selectedItemIds)
}
}
fileprivate extension SpaceCreationMatrixItem {
init(mxUser: MXUser) {
self.init(id: mxUser.userId, avatar: mxUser.avatarData, displayName: mxUser.displayname, detailText: mxUser.userId)
}
init(mxRoom: MXRoom, spaceService: MXSpaceService) {
let parentSapceIds = mxRoom.summary.parentSpaceIds ?? Set()
let detailText: String?
if parentSapceIds.isEmpty {
detailText = nil
} else {
if let spaceName = spaceService.getSpace(withId: parentSapceIds.first ?? "")?.summary?.displayname {
let count = parentSapceIds.count - 1
switch count {
case 0:
detailText = VectorL10n.spacesCreationInSpacename(spaceName)
case 1:
detailText = VectorL10n.spacesCreationInSpacenamePlusOne(spaceName)
default:
detailText = VectorL10n.spacesCreationInSpacenamePlusMany(spaceName, "\(count)")
}
} else {
if parentSapceIds.count > 1 {
detailText = VectorL10n.spacesCreationInManySpaces("\(parentSapceIds.count)")
} else {
detailText = VectorL10n.spacesCreationInOneSpace
}
}
}
self.init(id: mxRoom.roomId, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayname, detailText: detailText)
}
}
@@ -1,59 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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 SwiftUI
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
@available(iOS 14.0, *)
enum MockSpaceCreationMatrixItemChooserScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case noItems
case items
case selectedItems
/// The associated screen
var screenType: Any.Type {
SpaceCreationMatrixItem.self
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let service: MockSpaceCreationMatrixItemChooserService
switch self {
case .noItems:
service = MockSpaceCreationMatrixItemChooserService(type: .room, items: [])
case .items:
service = MockSpaceCreationMatrixItemChooserService()
case .selectedItems:
service = MockSpaceCreationMatrixItemChooserService(type: .room, items: MockSpaceCreationMatrixItemChooserService.mockItems, selectedItemIndexes: [0, 2])
}
let viewModel = SpaceCreationMatrixItemChooserViewModel.makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: service, creationParams: SpaceCreationParameters())
// can simulate service and viewModel actions here if needs be.
return (
[service, viewModel],
AnyView(SpaceCreationMatrixItemChooser(viewModel: viewModel.context)
.addDependency(MockAvatarService.example))
)
}
}
@@ -1,66 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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 Combine
@available(iOS 14.0, *)
class MockSpaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol {
static let mockItems = [
SpaceCreationMatrixItem(id: "!aaabaa:matrix.org", avatar: MockAvatarInput.example, displayName: "Matrix Discussion", detailText: "Descripton of this room"),
SpaceCreationMatrixItem(id: "!zzasds:matrix.org", avatar: MockAvatarInput.example, displayName: "Element Mobile", detailText: "Descripton of this room"),
SpaceCreationMatrixItem(id: "!scthve:matrix.org", avatar: MockAvatarInput.example, displayName: "Alice Personal", detailText: "Descripton of this room")
]
var itemsSubject: CurrentValueSubject<[SpaceCreationMatrixItem], Never>
var selectedItemIdsSubject: CurrentValueSubject<Set<String>, Never>
var searchText: String = ""
var type: SpaceCreationMatrixItemType = .room
var selectedItemIds: Set<String> = Set()
init(type: SpaceCreationMatrixItemType = .room, items: [SpaceCreationMatrixItem] = mockItems, selectedItemIndexes: [Int] = []) {
itemsSubject = CurrentValueSubject(items)
var selectedItemIds = Set<String>()
for index in selectedItemIndexes {
if index >= items.count {
continue
}
selectedItemIds.insert(items[index].id)
}
selectedItemIdsSubject = CurrentValueSubject(selectedItemIds)
self.selectedItemIds = selectedItemIds
}
func simulateSelectionForItem(at index: Int) {
guard index < itemsSubject.value.count else {
return
}
reverseSelectionForItem(withId: itemsSubject.value[index].id)
}
func reverseSelectionForItem(withId itemId: String) {
if selectedItemIds.contains(itemId) {
selectedItemIds.remove(itemId)
} else {
selectedItemIds.insert(itemId)
}
selectedItemIdsSubject.send(selectedItemIds)
}
}
@@ -0,0 +1,46 @@
//
// 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
class SpaceCreationAddRoomsItemsProcessor: MatrixItemChooserProcessorProtocol {
// MARK: Private
private let creationParams: SpaceCreationParameters
// MARK: Setup
init(creationParams: SpaceCreationParameters) {
self.creationParams = creationParams
}
// MARK: MatrixItemChooserSelectionProcessorProtocol
var dataType: MatrixItemChooserType {
.room
}
func computeSelection(withIds itemsIds: [String], completion: @escaping (Result<Void, Error>) -> Void) {
creationParams.addedRoomIds = itemsIds
completion(.success(()))
}
func isItemIncluded(_ item: (MatrixListItemData)) -> Bool {
return true
}
}
@@ -0,0 +1,47 @@
//
// 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
class SpaceCreationInviteUsersItemsProcessor: MatrixItemChooserProcessorProtocol {
// MARK: Private
private let creationParams: SpaceCreationParameters
// MARK: Setup
init(creationParams: SpaceCreationParameters) {
self.creationParams = creationParams
}
// MARK: MatrixItemChooserSelectionProcessorProtocol
var dataType: MatrixItemChooserType {
.people
}
func computeSelection(withIds itemsIds: [String], completion: @escaping (Result<Void, Error>) -> Void) {
creationParams.inviteType = .userId
creationParams.userIdInvites = itemsIds
completion(.success(()))
}
func isItemIncluded(_ item: (MatrixListItemData)) -> Bool {
return true
}
}
@@ -1,30 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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 Combine
@available(iOS 14.0, *)
protocol SpaceCreationMatrixItemChooserServiceProtocol {
var type: SpaceCreationMatrixItemType { get }
var itemsSubject: CurrentValueSubject<[SpaceCreationMatrixItem], Never> { get }
var selectedItemIdsSubject: CurrentValueSubject<Set<String>, Never> { get }
var searchText: String { get set }
func reverseSelectionForItem(withId itemId: String)
}
@@ -1,70 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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 XCTest
import RiotSwiftUI
@available(iOS 14.0, *)
class SpaceCreationMatrixItemChooserUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockSpaceCreationMatrixItemChooserScreenState.self
}
override class func createTest() -> MockScreenTest {
return SpaceCreationMatrixItemChooserUITests(selector: #selector(verifySpaceCreationMatrixItemChooserScreen))
}
func verifySpaceCreationMatrixItemChooserScreen() throws {
guard let screenState = screenState as? MockSpaceCreationMatrixItemChooserScreenState else { fatalError("no screen") }
switch screenState {
case .noItems:
verifyEmptyScreen()
case .items:
verifyPopulatedScreen()
case .selectedItems:
verifyPopulatedWithSelectionScreen()
}
}
func verifyEmptyScreen() {
XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle)
XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage)
XCTAssertEqual(app.collectionViews["itemsList"].exists, false)
XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, true)
XCTAssertEqual(app.staticTexts["emptyListMessage"].label, VectorL10n.spacesNoResultFoundTitle)
XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.skip)
}
func verifyPopulatedScreen() {
XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle)
XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage)
XCTAssertEqual(app.collectionViews["itemsList"].exists, true)
XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false)
XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.skip)
}
func verifyPopulatedWithSelectionScreen() {
XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle)
XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage)
XCTAssertEqual(app.collectionViews["itemsList"].exists, true)
XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false)
XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.next)
}
}
@@ -1,53 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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 XCTest
import Combine
@testable import RiotSwiftUI
@available(iOS 14.0, *)
class SpaceCreationMatrixItemChooserViewModelTests: XCTestCase {
var creationParameters = SpaceCreationParameters()
var service: MockSpaceCreationMatrixItemChooserService!
var viewModel: SpaceCreationMatrixItemChooserViewModelProtocol!
var context: SpaceCreationMatrixItemChooserViewModel.Context!
override func setUpWithError() throws {
service = MockSpaceCreationMatrixItemChooserService(type: .room)
viewModel = SpaceCreationMatrixItemChooserViewModel.makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: service, creationParams: creationParameters)
context = viewModel.context
}
func testInitialState() {
XCTAssertEqual(context.viewState.navTitle, creationParameters.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle)
XCTAssertEqual(context.viewState.emptyListMessage, VectorL10n.spacesNoResultFoundTitle)
XCTAssertEqual(context.viewState.title, VectorL10n.spacesCreationAddRoomsTitle)
XCTAssertEqual(context.viewState.message, VectorL10n.spacesCreationAddRoomsMessage)
XCTAssertEqual(context.viewState.items, MockSpaceCreationMatrixItemChooserService.mockItems)
XCTAssertEqual(context.viewState.selectedItemIds.count, 0)
}
func testItemSelection() throws {
XCTAssertEqual(context.viewState.selectedItemIds.count, 0)
service.simulateSelectionForItem(at: 0)
XCTAssertEqual(context.viewState.selectedItemIds.count, 1)
XCTAssertEqual(context.viewState.selectedItemIds.first, MockSpaceCreationMatrixItemChooserService.mockItems[0].id)
}
}
@@ -1,6 +1,4 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,17 +19,16 @@ import SwiftUI
@available(iOS 14.0, *)
struct SpaceCreationMatrixItemChooser: View {
// MARK: - Properties
// MARK: Properties
@ObservedObject var viewModel: SpaceCreationMatrixItemChooserViewModel.Context
@State var searchText: String = ""
@ObservedObject var viewModel: MatrixItemChooserViewModel.Context
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
@ViewBuilder
var body: some View {
VStack {
@@ -46,67 +43,16 @@ struct SpaceCreationMatrixItemChooser: View {
.navigationBarHidden(true)
}
// MARK: Private
@ViewBuilder
private var mainView: some View {
ZStack(alignment: .bottom) {
listContent
MatrixItemChooser(viewModel: viewModel)
footerView
}
}
@ViewBuilder
private var headerView: some View {
VStack {
Text(viewModel.viewState.title)
.font(theme.fonts.bodySB)
.foregroundColor(theme.colors.primaryContent)
.padding(.horizontal)
.padding(.vertical, 8)
.accessibility(identifier: "titleText")
Text(viewModel.viewState.message)
.font(theme.fonts.callout)
.foregroundColor(theme.colors.secondaryContent)
.multilineTextAlignment(.center)
.padding(.horizontal)
.accessibility(identifier: "messageText")
Spacer().frame(height: 24)
SearchBar(placeholder: VectorL10n.searchDefaultPlaceholder, text: $searchText)
.onChange(of: searchText, perform: { value in
viewModel.send(viewAction: .searchTextChanged(searchText))
})
}
}
@ViewBuilder
private var listContent: some View {
ScrollView{
headerView
if viewModel.viewState.items.isEmpty {
Text(viewModel.viewState.emptyListMessage)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
.accessibility(identifier: "emptyListMessage")
Spacer()
} else {
LazyVStack(spacing: 0) {
ForEach(viewModel.viewState.items) { item in
SpaceCreationMatrixItemChooserListRow(
avatar: item.avatar,
displayName: item.displayName,
detailText: item.detailText,
isSelected: viewModel.viewState.selectedItemIds.contains(item.id)
)
.onTapGesture {
viewModel.send(viewAction: .itemTapped(item.id))
}
}
}
.padding(.bottom, 76)
.accessibility(identifier: "itemsList")
.frame(maxHeight: .infinity, alignment: .top)
}
}
}
@ViewBuilder
private var footerView: some View {
@@ -118,17 +64,3 @@ struct SpaceCreationMatrixItemChooser: View {
.padding(.bottom)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct SpaceCreationMatrixItemChooser_Previews: PreviewProvider {
static let stateRenderer = MockSpaceCreationMatrixItemChooserScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup(addNavigation: true)
.theme(.light).preferredColorScheme(.light)
stateRenderer.screenGroup(addNavigation: true)
.theme(.dark).preferredColorScheme(.dark)
}
}
@@ -1,73 +0,0 @@
//
// 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 SpaceCreationMatrixItemChooserListRow: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
let avatar: AvatarInputProtocol
let displayName: String?
let detailText: String?
let isSelected: Bool
@ViewBuilder
var body: some View {
HStack{
AvatarImage(avatarData: avatar, size: .small)
VStack(alignment: .leading) {
Text(displayName ?? "")
.foregroundColor(theme.colors.primaryContent)
.font(theme.fonts.callout)
.accessibility(identifier: "itemNameText")
if let detailText = self.detailText {
Text(detailText)
.foregroundColor(theme.colors.secondaryContent)
.font(theme.fonts.footnote)
.accessibility(identifier: "itemDetailText")
}
}
Spacer()
if isSelected {
Image(systemName: "checkmark.circle.fill").renderingMode(.template).foregroundColor(theme.colors.accent)
} else {
Image(systemName: "circle").renderingMode(.template).foregroundColor(theme.colors.tertiaryContent)
}
}
.contentShape(Rectangle())
.padding(.horizontal)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct SpaceCreationMatrixItemChooserListRow_Previews: PreviewProvider {
static var previews: some View {
TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice")
.addDependency(MockAvatarService.example)
}
}
@@ -1,121 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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
import Combine
@available(iOS 14, *)
typealias SpaceCreationMatrixItemChooserViewModelType = StateStoreViewModel<SpaceCreationMatrixItemListStateActionListViewState,
SpaceCreationMatrixItemListStateAction,
SpaceCreationMatrixItemListStateActionListViewAction>
@available(iOS 14, *)
class SpaceCreationMatrixItemChooserViewModel: SpaceCreationMatrixItemChooserViewModelType, SpaceCreationMatrixItemChooserViewModelProtocol {
// MARK: - Properties
// MARK: Private
private var spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol
private var creationParams: SpaceCreationParameters
// MARK: Public
var callback: ((SpaceCreationMatrixItemListStateActionListViewModelAction) -> Void)?
// MARK: - Setup
static func makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) -> SpaceCreationMatrixItemChooserViewModelProtocol {
return SpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: spaceCreationMatrixItemChooserService, creationParams: creationParams)
}
private init(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) {
self.spaceCreationMatrixItemChooserService = spaceCreationMatrixItemChooserService
self.creationParams = creationParams
super.init(initialViewState: Self.defaultState(spaceCreationMatrixItemChooserService: spaceCreationMatrixItemChooserService, creationParams: creationParams))
startObservingItems()
}
private static func defaultState(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) -> SpaceCreationMatrixItemListStateActionListViewState {
let navTitle = creationParams.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle
let title = spaceCreationMatrixItemChooserService.type == .people ? VectorL10n.spacesCreationInviteByUsernameTitle : VectorL10n.spacesCreationAddRoomsTitle
let message = spaceCreationMatrixItemChooserService.type == .people ? VectorL10n.spacesCreationInviteByUsernameMessage : VectorL10n.spacesCreationAddRoomsMessage
let emptyListMessage = VectorL10n.spacesNoResultFoundTitle
return SpaceCreationMatrixItemListStateActionListViewState(navTitle: navTitle, title: title, message: message, emptyListMessage: emptyListMessage, items: spaceCreationMatrixItemChooserService.itemsSubject.value, selectedItemIds: spaceCreationMatrixItemChooserService.selectedItemIdsSubject.value)
}
private func startObservingItems() {
let itemsUpdatePublisher = spaceCreationMatrixItemChooserService.itemsSubject
.map(SpaceCreationMatrixItemListStateAction.updateItems)
.eraseToAnyPublisher()
dispatch(actionPublisher: itemsUpdatePublisher)
let selectionPublisher = spaceCreationMatrixItemChooserService.selectedItemIdsSubject
.map(SpaceCreationMatrixItemListStateAction.updateSelection)
.eraseToAnyPublisher()
dispatch(actionPublisher: selectionPublisher)
}
// MARK: - Public
override func process(viewAction: SpaceCreationMatrixItemListStateActionListViewAction) {
switch viewAction {
case .cancel:
cancel()
case .back:
back()
case .done:
let selectedItemIds = Array(spaceCreationMatrixItemChooserService.selectedItemIdsSubject.value)
switch spaceCreationMatrixItemChooserService.type {
case .people:
creationParams.inviteType = .userId
creationParams.userIdInvites = selectedItemIds
default:
creationParams.addedRoomIds = selectedItemIds
}
done()
case .searchTextChanged(let searchText):
self.spaceCreationMatrixItemChooserService.searchText = searchText
case .itemTapped(let itemId):
self.spaceCreationMatrixItemChooserService.reverseSelectionForItem(withId: itemId)
}
}
override class func reducer(state: inout SpaceCreationMatrixItemListStateActionListViewState, action: SpaceCreationMatrixItemListStateAction) {
switch action {
case .updateItems(let items):
state.items = items
case .updateSelection(let selectedItemIds):
state.selectedItemIds = selectedItemIds
}
UILog.debug("[SpaceCreationMatrixItemChooserViewModel] reducer with action \(action) produced state: \(state)")
}
private func done() {
callback?(.done)
}
private func cancel() {
callback?(.cancel)
}
private func back() {
callback?(.back)
}
}
@@ -1,28 +0,0 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// 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
protocol SpaceCreationMatrixItemChooserViewModelProtocol {
var callback: ((SpaceCreationMatrixItemListStateActionListViewModelAction) -> Void)? { get set }
@available(iOS 14, *)
static func makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) -> SpaceCreationMatrixItemChooserViewModelProtocol
@available(iOS 14, *)
var context: SpaceCreationMatrixItemChooserViewModelType.Context { get }
}