Public Search View (#2585)

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Mauro
2024-03-21 15:34:35 +01:00
committed by GitHub
parent a62c96f1c5
commit 562e5e739d
44 changed files with 954 additions and 35 deletions
@@ -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
}