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:
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// 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 Combine
|
||||
import Foundation
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
|
||||
private let roomDirectorySearch: RoomDirectorySearchProtocol
|
||||
private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomdirectorysearch", qos: .default)
|
||||
|
||||
private let resultsSubject = CurrentValueSubject<[RoomDirectorySearchResult], Never>([])
|
||||
|
||||
var resultsPublisher: CurrentValuePublisher<[RoomDirectorySearchResult], Never> {
|
||||
resultsSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private var results: [RoomDirectorySearchResult] {
|
||||
get { resultsSubject.value }
|
||||
set { resultsSubject.send(newValue) }
|
||||
}
|
||||
|
||||
private let diffsPublisher = PassthroughSubject<[RoomDirectorySearchEntryUpdate], Never>()
|
||||
|
||||
private var searchEntriesSubscription: TaskHandle?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(roomDirectorySearch: RoomDirectorySearchProtocol) {
|
||||
self.roomDirectorySearch = roomDirectorySearch
|
||||
diffsPublisher
|
||||
.receive(on: serialDispatchQueue)
|
||||
.sink { [weak self] in self?.updateResultsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
searchEntriesSubscription = await roomDirectorySearch.results(listener: RoomDirectorySearchEntriesListenerProxy { [weak self] updates in
|
||||
self?.diffsPublisher.send(updates)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func search(query: String?) async -> Result<Void, RoomDirectorySearchError> {
|
||||
do {
|
||||
try await roomDirectorySearch.search(filter: query, batchSize: 50)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func nextPage() async -> Result<Void, RoomDirectorySearchError> {
|
||||
do {
|
||||
try await roomDirectorySearch.nextPage()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.nextPageQueryFailed)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateResultsWithDiffs(_ updates: [RoomDirectorySearchEntryUpdate]) {
|
||||
results = updates.reduce(results) { currentItems, diff in
|
||||
processDiff(diff, on: currentItems)
|
||||
}
|
||||
}
|
||||
|
||||
private func processDiff(_ diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> [RoomDirectorySearchResult] {
|
||||
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
|
||||
return currentItems
|
||||
}
|
||||
|
||||
guard let updatedItems = currentItems.applying(collectionDiff) else {
|
||||
return currentItems
|
||||
}
|
||||
|
||||
return updatedItems
|
||||
}
|
||||
|
||||
private func buildDiff(from diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> CollectionDifference<RoomDirectorySearchResult>? {
|
||||
var changes = [CollectionDifference<RoomDirectorySearchResult>.Change]()
|
||||
|
||||
switch diff {
|
||||
case .append(let values):
|
||||
for (index, value) in values.enumerated() {
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: results.count + index, element: result, associatedWith: nil))
|
||||
}
|
||||
case .clear:
|
||||
for (index, value) in results.enumerated() {
|
||||
changes.append(.remove(offset: index, element: value, associatedWith: nil))
|
||||
}
|
||||
case .insert(let index, let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: Int(index), element: result, associatedWith: nil))
|
||||
case .popBack:
|
||||
guard let value = results.last else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
changes.append(.remove(offset: results.count - 1, element: value, associatedWith: nil))
|
||||
case .popFront:
|
||||
let result = results[0]
|
||||
changes.append(.remove(offset: 0, element: result, associatedWith: nil))
|
||||
case .pushBack(let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: results.count, element: result, associatedWith: nil))
|
||||
case .pushFront(let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.insert(offset: 0, element: result, associatedWith: nil))
|
||||
case .remove(let index):
|
||||
let result = results[Int(index)]
|
||||
changes.append(.remove(offset: Int(index), element: result, associatedWith: nil))
|
||||
case .reset(let values):
|
||||
for (index, result) in results.enumerated() {
|
||||
changes.append(.remove(offset: index, element: result, associatedWith: nil))
|
||||
}
|
||||
|
||||
for (index, value) in values.enumerated() {
|
||||
changes.append(.insert(offset: index, element: buildResultForRoomDescription(value), associatedWith: nil))
|
||||
}
|
||||
case .set(let index, let value):
|
||||
let result = buildResultForRoomDescription(value)
|
||||
changes.append(.remove(offset: Int(index), element: result, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(index), element: result, associatedWith: nil))
|
||||
case .truncate(let length):
|
||||
for (index, value) in results.enumerated() {
|
||||
if index < length {
|
||||
continue
|
||||
}
|
||||
|
||||
changes.append(.remove(offset: index, element: value, associatedWith: nil))
|
||||
}
|
||||
}
|
||||
|
||||
return CollectionDifference(changes)
|
||||
}
|
||||
|
||||
private func buildResultForRoomDescription(_ value: RoomDescription) -> RoomDirectorySearchResult {
|
||||
RoomDirectorySearchResult(id: value.roomId,
|
||||
alias: value.alias,
|
||||
name: value.name,
|
||||
topic: value.topic,
|
||||
avatarURL: value.avatarUrl.flatMap(URL.init(string:)),
|
||||
canBeJoined: value.joinRule == .public)
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoomDirectorySearchEntriesListenerProxy: RoomDirectorySearchEntriesListener {
|
||||
private let onUpdateClosure: ([RoomDirectorySearchEntryUpdate]) -> Void
|
||||
|
||||
func onUpdate(roomEntriesUpdate: [RoomDirectorySearchEntryUpdate]) {
|
||||
onUpdateClosure(roomEntriesUpdate)
|
||||
}
|
||||
|
||||
init(_ onUpdateClosure: @escaping (([RoomDirectorySearchEntryUpdate]) -> Void)) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// 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 Foundation
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol RoomDirectorySearchProxyProtocol {
|
||||
var resultsPublisher: CurrentValuePublisher<[RoomDirectorySearchResult], Never> { get }
|
||||
|
||||
func search(query: String?) async -> Result<Void, RoomDirectorySearchError>
|
||||
func nextPage() async -> Result<Void, RoomDirectorySearchError>
|
||||
}
|
||||
|
||||
enum RoomDirectorySearchError: Error {
|
||||
case searchFailed
|
||||
case nextPageQueryFailed
|
||||
}
|
||||
|
||||
struct RoomDirectorySearchResult: Identifiable {
|
||||
let id: String
|
||||
let alias: String?
|
||||
let name: String?
|
||||
let topic: String?
|
||||
let avatarURL: URL?
|
||||
let canBeJoined: Bool
|
||||
}
|
||||
Reference in New Issue
Block a user