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,
@@ -15,9 +15,11 @@ enum SearchServiceProxyError: Error {
// sourcery: AutoMockable
protocol SearchServiceProxyProtocol {
var resultsPublisher: CurrentValuePublisher<[SearchServiceResult], Never> { get }
var paginationStatePublisher: CurrentValuePublisher<SearchServicePaginationState, 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
}
@@ -29,3 +31,19 @@ struct SearchServiceResult {
let content: TimelineEventContent
let timestamp: Date
}
enum SearchServicePaginationState: Equatable {
case idle(endReached: Bool)
case loading
}
extension SearchServicePaginationState {
init(sdkState: MatrixRustSDK.SearchServicePaginationState) {
switch sdkState {
case .loading:
self = .loading
case .idle(let endReached):
self = .idle(endReached: endReached)
}
}
}