mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Public Search View (#2585)
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDirectorySearchScreenCoordinatorParameters {
|
||||
let roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol
|
||||
let imageProvider: ImageProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum RoomDirectorySearchScreenCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class RoomDirectorySearchScreenCoordinator: CoordinatorProtocol {
|
||||
private let viewModel: RoomDirectorySearchScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<RoomDirectorySearchScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<RoomDirectorySearchScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: RoomDirectorySearchScreenCoordinatorParameters) {
|
||||
viewModel = RoomDirectorySearchScreenViewModel(roomDirectorySearch: parameters.roomDirectorySearchProxy, userIndicatorController: parameters.userIndicatorController, imageProvider: parameters.imageProvider)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(RoomDirectorySearchScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum RoomDirectorySearchScreenViewModelAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct RoomDirectorySearchScreenViewState: BindableState {
|
||||
var searchResults: [RoomDirectorySearchResult] = []
|
||||
var isLoading = false
|
||||
|
||||
var bindings = RoomDirectorySearchScreenViewStateBindings()
|
||||
}
|
||||
|
||||
struct RoomDirectorySearchScreenViewStateBindings {
|
||||
var isSearching = false
|
||||
var searchString = ""
|
||||
}
|
||||
|
||||
enum RoomDirectorySearchScreenViewAction {
|
||||
case dismiss
|
||||
case join(roomID: String)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// 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 Combine
|
||||
|
||||
@MainActor
|
||||
protocol RoomDirectorySearchScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<RoomDirectorySearchScreenViewModelAction, Never> { get }
|
||||
var context: RoomDirectorySearchScreenViewModelType.Context { get }
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias RoomDirectorySearchScreenViewModelType = StateStoreViewModel<RoomDirectorySearchScreenViewState, RoomDirectorySearchScreenViewAction>
|
||||
|
||||
class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType, RoomDirectorySearchScreenViewModelProtocol {
|
||||
private let roomDirectorySearch: RoomDirectorySearchProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<RoomDirectorySearchScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<RoomDirectorySearchScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(roomDirectorySearch: RoomDirectorySearchProxyProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
imageProvider: ImageProviderProtocol) {
|
||||
self.roomDirectorySearch = roomDirectorySearch
|
||||
self.userIndicatorController = userIndicatorController
|
||||
super.init(initialViewState: RoomDirectorySearchScreenViewState(), imageProvider: imageProvider)
|
||||
|
||||
roomDirectorySearch.resultsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.weakAssign(to: \.state.searchResults, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
context.$viewState.map(\.bindings.searchString)
|
||||
// only listen to the search string when is not loading
|
||||
.combineLatest(context.$viewState.map(\.isLoading)
|
||||
.filter { $0 == false })
|
||||
.map(\.0)
|
||||
.debounceTextQueriesAndRemoveDuplicates()
|
||||
.sink { [weak self] query in
|
||||
self?.search(query: query)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: RoomDirectorySearchScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .join(roomID: let roomID):
|
||||
joinRoom(roomID: roomID)
|
||||
}
|
||||
}
|
||||
|
||||
private func joinRoom(roomID: String) { }
|
||||
|
||||
private static let errorID = "roomDirectorySearchViewModelLoadingError"
|
||||
|
||||
private func search(query: String?) {
|
||||
guard !state.isLoading else {
|
||||
return
|
||||
}
|
||||
|
||||
state.isLoading = true
|
||||
Task {
|
||||
switch await roomDirectorySearch.search(query: query) {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.errorID,
|
||||
type: .toast,
|
||||
title: L10n.screenRoomDirectorySearchLoadingError,
|
||||
iconName: "xmark"))
|
||||
}
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright 2024 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 Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDirectorySearchCell: View {
|
||||
let result: RoomDirectorySearchResult
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
|
||||
private var description: String? {
|
||||
if let topic = result.topic {
|
||||
return topic
|
||||
}
|
||||
|
||||
if result.name != nil, let alias = result.alias {
|
||||
return alias
|
||||
} else if result.name == nil, result.alias == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.id
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ListRow(label: .avatar(title: result.name ?? result.alias ?? result.id,
|
||||
description: description,
|
||||
icon: avatar), kind: .label)
|
||||
}
|
||||
|
||||
private var avatar: some View {
|
||||
LoadableAvatarImage(url: result.avatarURL,
|
||||
name: result.name,
|
||||
contentID: result.id,
|
||||
avatarSize: .room(on: .roomDirectorySearch),
|
||||
imageProvider: imageProvider)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
List {
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_1:matrix.org", alias: "#test:example.com", name: "Test title", topic: "test description", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_2:matrix.org", alias: "#test:example.com", name: nil, topic: "test description", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_3:example.com", alias: "#test_no_topic:example.com", name: "Test title no topic", topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_4:example.com", alias: "#test_no_topic:example.com", name: nil, topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_5:example.com", alias: nil, name: "Test title no alias", topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_6:example.com", alias: nil, name: "Test title no alias", topic: "Topic", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_7:example.com", alias: nil, name: nil, topic: "Topic", avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
RoomDirectorySearchCell(result: .init(id: "!test_id_8:example.com", alias: nil, name: nil, topic: nil, avatarURL: nil, canBeJoined: false), imageProvider: MockMediaProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// 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 Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDirectorySearchScreen: View {
|
||||
@ObservedObject var context: RoomDirectorySearchScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
ForEach(context.viewState.searchResults) {
|
||||
RoomDirectorySearchCell(result: $0, imageProvider: context.imageProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.environment(\.defaultMinListRowHeight, 48)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.isSearching($context.isSearching)
|
||||
.searchable(text: $context.searchString, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.navigationTitle(L10n.screenRoomDirectorySearchTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomDirectorySearchScreenScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomDirectorySearchScreenViewModel(roomDirectorySearch: RoomDirectorySearchProxyMock(configuration: .init(results: [.init(id: "test_1",
|
||||
alias: "#test_1:example.com",
|
||||
name: "Test 1",
|
||||
topic: "Test description 1",
|
||||
avatarURL: nil,
|
||||
canBeJoined: true),
|
||||
.init(id: "test_2",
|
||||
alias: "#test_2:example.com",
|
||||
name: "Test 2",
|
||||
topic: "Test description 2",
|
||||
avatarURL: URL.documentsDirectory,
|
||||
canBeJoined: false)])),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
imageProvider: MockMediaProvider())
|
||||
|
||||
static var previews: some View {
|
||||
RoomDirectorySearchScreen(context: viewModel.context)
|
||||
.snapshot(delay: 1.0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user