mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Expose a global search service from the client
Wrap the SDK's SearchService in a SearchServiceProxy that publishes typed message results and paginates on demand, expose it on the ClientProxy, and enable the search index store on the session. Also opens up the timeline item factory's message content builder so the proxy can reuse it.
This commit is contained in:
@@ -293,3 +293,11 @@ nonisolated extension SDKListener: RoomDirectorySearchEntriesListener where T ==
|
||||
onUpdateClosure(roomEntriesUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SearchServiceProxy
|
||||
|
||||
nonisolated extension SDKListener: SearchServiceResultsListener where T == [SearchServiceResultsUpdate] {
|
||||
func onUpdate(updates: [SearchServiceResultsUpdate]) {
|
||||
onUpdateClosure(updates)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
|
||||
|
||||
let spaceService: SpaceServiceProxyProtocol
|
||||
let searchService: SearchServiceProxyProtocol
|
||||
|
||||
let capabilities: HomeserverCapabilitiesProxyProtocol
|
||||
|
||||
@@ -236,6 +237,13 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
spaceService = await SpaceServiceProxy(spaceService: client.spaceService())
|
||||
|
||||
let searchUserID = try client.userId()
|
||||
searchService = SearchServiceProxy(searchService: client.searchService(),
|
||||
timelineItemFactory: RoomTimelineItemFactory(userID: searchUserID,
|
||||
attributedStringBuilder: AttributedStringBuilder(cacheKey: "search",
|
||||
mentionBuilder: PlainMentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: searchUserID)))
|
||||
|
||||
capabilities = HomeserverCapabilitiesProxy(underlyingCapabilities: client.homeserverCapabilities())
|
||||
|
||||
let configuredAppService = try await ClientProxyServices(client: client,
|
||||
|
||||
@@ -155,6 +155,8 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
|
||||
var spaceService: SpaceServiceProxyProtocol { get }
|
||||
|
||||
var searchService: SearchServiceProxyProtocol { get }
|
||||
|
||||
var capabilities: HomeserverCapabilitiesProxyProtocol { get }
|
||||
|
||||
var isReportRoomSupported: Bool { get async }
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class SearchServiceProxy: SearchServiceProxyProtocol {
|
||||
private let searchService: SearchServiceProtocol
|
||||
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
|
||||
|
||||
// periphery:ignore - required for instance retention in the rust codebase
|
||||
private var resultsHandle: TaskHandle?
|
||||
|
||||
private let resultsSubject = CurrentValueSubject<[SearchServiceResult], Never>([])
|
||||
var resultsPublisher: CurrentValuePublisher<[SearchServiceResult], Never> {
|
||||
resultsSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
init(searchService: SearchServiceProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) {
|
||||
self.searchService = searchService
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
}
|
||||
|
||||
func setQuery(_ query: String) async -> Result<Void, SearchServiceProxyError> {
|
||||
if resultsHandle == nil {
|
||||
resultsHandle = await searchService.subscribeToResults(listener: SDKListener.onMainActor { [weak self] updates in
|
||||
self?.handleResultUpdates(updates)
|
||||
})
|
||||
}
|
||||
|
||||
do {
|
||||
try await searchService.setQuery(query: query)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed to set search query: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func paginate() async {
|
||||
do {
|
||||
try await searchService.paginate()
|
||||
} catch {
|
||||
MXLog.error("Failed to paginate search results: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func handleResultUpdates(_ updates: [SearchServiceResultsUpdate]) {
|
||||
var results = resultsSubject.value
|
||||
|
||||
for update in updates {
|
||||
switch update {
|
||||
case .append(let values):
|
||||
results.append(contentsOf: values.compactMap { self.makeResult($0) })
|
||||
case .clear:
|
||||
results.removeAll()
|
||||
case .pushFront(let value):
|
||||
if let result = makeResult(value) {
|
||||
results.insert(result, at: 0)
|
||||
}
|
||||
case .pushBack(let value):
|
||||
if let result = makeResult(value) {
|
||||
results.append(result)
|
||||
}
|
||||
case .popFront:
|
||||
results.removeFirst()
|
||||
case .popBack:
|
||||
results.removeLast()
|
||||
case .insert(let index, let value):
|
||||
if let result = makeResult(value) {
|
||||
results.insert(result, at: Int(index))
|
||||
}
|
||||
case .set(let index, let value):
|
||||
if let result = makeResult(value) {
|
||||
results[Int(index)] = result
|
||||
}
|
||||
case .remove(let index):
|
||||
results.remove(at: Int(index))
|
||||
case .truncate(let length):
|
||||
results.removeSubrange(Int(length)..<results.count)
|
||||
case .reset(let values):
|
||||
results = values.compactMap { self.makeResult($0) }
|
||||
}
|
||||
}
|
||||
|
||||
resultsSubject.send(results)
|
||||
}
|
||||
|
||||
private func makeResult(_ searchResult: MatrixRustSDK.SearchServiceResult) -> SearchServiceResult? {
|
||||
switch searchResult {
|
||||
case .message(let roomID, let result):
|
||||
let sender = TimelineItemSender(senderID: result.sender, senderProfile: result.senderProfile)
|
||||
|
||||
let content: TimelineEventContent
|
||||
switch result.content {
|
||||
case .msgLike(let msgLike):
|
||||
switch msgLike.kind {
|
||||
case .message(let msg):
|
||||
content = .message(timelineItemFactory.buildMessageTimelineItemContent(messageType: msg.msgType,
|
||||
senderID: result.sender,
|
||||
senderDisplayName: sender.displayName))
|
||||
default:
|
||||
content = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||
}
|
||||
default:
|
||||
content = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||
}
|
||||
|
||||
return SearchServiceResult(roomID: roomID,
|
||||
eventID: result.eventId,
|
||||
sender: sender,
|
||||
content: content,
|
||||
timestamp: Date(timeIntervalSince1970: TimeInterval(result.timestamp) / 1000))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum SearchServiceProxyError: Error {
|
||||
case sdkError(Error)
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol SearchServiceProxyProtocol {
|
||||
var resultsPublisher: CurrentValuePublisher<[SearchServiceResult], Never> { get }
|
||||
|
||||
func setQuery(_ query: String) async -> Result<Void, SearchServiceProxyError>
|
||||
|
||||
/// Loads the next page of results. No-ops if a page is already loading or the end has been reached.
|
||||
func paginate() async
|
||||
}
|
||||
|
||||
struct SearchServiceResult {
|
||||
let roomID: String
|
||||
let eventID: String
|
||||
let sender: TimelineItemSender
|
||||
let content: TimelineEventContent
|
||||
let timestamp: Date
|
||||
}
|
||||
@@ -895,7 +895,7 @@ nonisolated struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func buildMessageTimelineItemContent(messageType: MessageType?, senderID: String, senderDisplayName: String?) -> EventBasedMessageTimelineItemContentType {
|
||||
func buildMessageTimelineItemContent(messageType: MessageType?, senderID: String, senderDisplayName: String?) -> EventBasedMessageTimelineItemContentType {
|
||||
switch messageType {
|
||||
case .audio(let content):
|
||||
if content.voice != nil {
|
||||
|
||||
@@ -12,4 +12,5 @@ import MatrixRustSDK
|
||||
nonisolated protocol RoomTimelineItemFactoryProtocol: Sendable {
|
||||
func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol?
|
||||
func buildTimelineItemReply(_ details: MatrixRustSDK.InReplyToDetails) -> TimelineItemReply
|
||||
func buildMessageTimelineItemContent(messageType: MessageType?, senderID: String, senderDisplayName: String?) -> EventBasedMessageTimelineItemContentType
|
||||
}
|
||||
|
||||
@@ -140,6 +140,8 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
.sqliteStore(config: .init(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
|
||||
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: credentials.restorationToken.passphrase))
|
||||
.withSearchIndexStore(path: credentials.restorationToken.sessionDirectories.dataPath,
|
||||
password: credentials.restorationToken.passphrase)
|
||||
.username(username: credentials.userID)
|
||||
.homeserverUrl(url: homeserverURL)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user