Add a Messages tab to global search

Split global search into Rooms and Messages tabs. The Messages tab lists
matching events with the room's name and avatar, a sender-prefixed preview,
rich media (image/video/audio/file) previews, and a loading indicator, then
paginates as you scroll. Queries are debounced and the previous one is
superseded so late results can't clobber a newer search.
This commit is contained in:
Stefan Ceriu
2026-07-02 13:25:56 +03:00
parent 5851100f07
commit 4fbfc87551
13 changed files with 727 additions and 81 deletions
@@ -11,27 +11,41 @@ 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()
}
// periphery:ignore - required for instance retention in the rust codebase
private var paginationStateHandle: TaskHandle?
private let paginationStateSubject: CurrentValueSubject<SearchServicePaginationState, Never>
var paginationStatePublisher: CurrentValuePublisher<SearchServicePaginationState, Never> {
paginationStateSubject.asCurrentValuePublisher()
}
init(searchService: SearchServiceProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) {
self.searchService = searchService
self.timelineItemFactory = timelineItemFactory
paginationStateSubject = CurrentValueSubject(.init(sdkState: searchService.paginationState()))
paginationStateHandle = searchService.subscribeToPaginationStateUpdates(listener: SDKListener.onMainActor { [weak self] state in
self?.paginationStateSubject.send(.init(sdkState: state))
})
}
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(())
@@ -40,7 +54,7 @@ class SearchServiceProxy: SearchServiceProxyProtocol {
return .failure(.sdkError(error))
}
}
func paginate() async {
do {
try await searchService.paginate()
@@ -48,12 +62,12 @@ class SearchServiceProxy: SearchServiceProxyProtocol {
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):
@@ -88,15 +102,15 @@ class SearchServiceProxy: SearchServiceProxyProtocol {
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):
@@ -111,7 +125,7 @@ class SearchServiceProxy: SearchServiceProxyProtocol {
default:
content = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
}
return SearchServiceResult(roomID: roomID,
eventID: result.eventId,
sender: sender,