7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 1 | // Copyright 2026 The Matrix.org Foundation C.I.C. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 2 | // |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 4 | // you may not use this file except in compliance with the License. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 5 | // You may obtain a copy of the License at |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 6 | // |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 8 | // |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 9 | // Unless required by applicable law or agreed to in writing, software |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 12 | // See the License for that specific language governing permissions and |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 13 | // limitations under the License. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 14 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 15 | use matrix_sdk::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 16 | deserialized_responses::TimelineEvent, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 17 | message_search::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 18 | GlobalSearchIterator as SdkGlobalSearchIterator, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 19 | RoomSearchIterator as SdkRoomSearchIterator, SearchError as SdkSearchError, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 20 | }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 21 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 22 | use matrix_sdk_ui::timeline::TimelineDetails; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 23 | use tokio::sync::Mutex; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 24 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 25 | use crate::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 26 | client::Client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 27 | error::ClientError, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 28 | room::Room, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 29 | timeline::{ProfileDetails, TimelineItemContent}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 30 | utils::Timestamp, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 31 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 32 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 33 | #[derive(uniffi::Error, thiserror::Error, Debug)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 34 | pub enum SearchError { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 35 | #[error("Failed to search through the index: {0}")] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 36 | IndexError(String), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 37 | #[error("Failed to load event content for search result: {0}")] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 38 | EventLoadError(String), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 39 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 40 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 41 | impl From<SdkSearchError> for SearchError { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 42 | fn from(err: SdkSearchError) -> Self { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 43 | match err { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 44 | SdkSearchError::IndexError(err) => SearchError::IndexError(err.to_string()), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 45 | SdkSearchError::EventLoadError(err) => SearchError::EventLoadError(err.to_string()), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 46 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 47 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 48 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 49 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 50 | #[matrix_sdk_ffi_macros::export] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 51 | impl Room { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 52 | /// Search for messages in this room matching the given query, returning an |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 53 | /// iterator over the results that yields `num_results_per_batch` results at |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 54 | /// a time. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 55 | pub fn search_messages(&self, query: String, num_results_per_batch: u32) -> RoomSearchIterator { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 56 | RoomSearchIterator { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 57 | sdk_room: (*self.inner).clone(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 58 | inner: Mutex::new(self.inner.search_messages(query, num_results_per_batch as usize)), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 59 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 60 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 61 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 62 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 63 | #[derive(uniffi::Object)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 64 | pub struct RoomSearchIterator { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 65 | sdk_room: matrix_sdk::room::Room, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 66 | inner: Mutex<SdkRoomSearchIterator>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 67 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 68 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 69 | #[matrix_sdk_ffi_macros::export] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 70 | impl RoomSearchIterator { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 71 | /// Return a list of events for the next batch of search results, or `None` |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 72 | /// if there are no more results. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 73 | pub async fn next_events(&self) -> Result<Option<Vec<RoomSearchResult>>, SearchError> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 74 | let Some(events) = self.inner.lock().await.next_events().await? else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 75 | return Ok(None); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 76 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 77 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 78 | let mut results = Vec::with_capacity(events.len()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 79 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 80 | for event in events { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 81 | if let Some(result) = RoomSearchResult::from(&self.sdk_room, event).await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 82 | results.push(result); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 83 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 84 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 85 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 86 | results.shrink_to_fit(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 87 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 88 | Ok(Some(results)) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 89 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 90 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 91 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 92 | #[derive(Clone, uniffi::Record)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 93 | pub struct RoomSearchResult { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 94 | event_id: String, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 95 | sender: String, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 96 | sender_profile: ProfileDetails, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 97 | content: TimelineItemContent, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 98 | timestamp: Timestamp, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 99 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 100 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 101 | impl RoomSearchResult { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 102 | async fn from(room: &matrix_sdk::room::Room, event: TimelineEvent) -> Option<Self> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 103 | let sender = event.sender()?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 104 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 105 | let event_id = event.event_id().unwrap().to_string(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 106 | let timestamp = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 107 | event.timestamp().unwrap_or_else(ruma::MilliSecondsSinceUnixEpoch::now).into(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 108 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 109 | let content = matrix_sdk_ui::timeline::TimelineItemContent::from_event(room, event).await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 110 | let profile = TimelineDetails::from_initial_value( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 111 | matrix_sdk_ui::timeline::Profile::load(room, &sender).await, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 112 | ); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 113 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 114 | Some(Self { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 115 | event_id, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 116 | sender: sender.to_string(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 117 | sender_profile: ProfileDetails::from(profile), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 118 | content: TimelineItemContent::from(content), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 119 | timestamp, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 120 | }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 121 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 122 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 123 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 124 | #[derive(Clone, uniffi::Enum)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 125 | pub enum SearchRoomFilter { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 126 | /// All the joined rooms (= DMs + non-DMs). |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 127 | Rooms, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 128 | /// Only joined DM rooms. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 129 | Dms, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 130 | /// Only joined non-DM (group) rooms. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 131 | NonDms, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 132 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 133 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 134 | #[matrix_sdk_ffi_macros::export] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 135 | impl Client { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 136 | /// Search across all all rooms for the given query, returning an iterator |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 137 | /// over the results. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 138 | pub async fn search_messages( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 139 | &self, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 140 | query: String, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 141 | filter: SearchRoomFilter, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 142 | num_results_per_batch: u32, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 143 | ) -> Result<GlobalSearchIterator, ClientError> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 144 | let sdk_client = (*self.inner).clone(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 145 | let mut search = sdk_client.search_messages(query, num_results_per_batch as usize); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 146 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 147 | match filter { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 148 | SearchRoomFilter::Rooms => {} |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 149 | SearchRoomFilter::Dms => search = search.only_dm_rooms().await?, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 150 | SearchRoomFilter::NonDms => search = search.no_dms().await?, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 151 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 152 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 153 | Ok(GlobalSearchIterator { sdk_client, inner: Mutex::new(search.build()) }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 154 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 155 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 156 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 157 | #[derive(uniffi::Record)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 158 | pub struct GlobalSearchResult { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 159 | room_id: String, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 160 | result: RoomSearchResult, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 161 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 162 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 163 | #[derive(uniffi::Object)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 164 | pub struct GlobalSearchIterator { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 165 | sdk_client: matrix_sdk::Client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 166 | inner: Mutex<SdkGlobalSearchIterator>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 167 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 168 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 169 | #[matrix_sdk_ffi_macros::export] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 170 | impl GlobalSearchIterator { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 171 | /// Return a list of events for the next batch of search results, or `None` |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 172 | /// if there are no more results. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 173 | pub async fn next_events(&self) -> Result<Option<Vec<GlobalSearchResult>>, SearchError> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 174 | let Some(events) = self.inner.lock().await.next_events().await? else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 175 | return Ok(None); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 176 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 177 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 178 | let mut results = Vec::with_capacity(events.len()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 179 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 180 | for (room_id, event) in events { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 181 | let Some(room) = self.sdk_client.get_room(&room_id) else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 182 | continue; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 183 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 184 | if let Some(result) = RoomSearchResult::from(&room, event).await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 185 | results.push(GlobalSearchResult { room_id: room_id.to_string(), result }); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 186 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 187 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 188 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 189 | results.shrink_to_fit(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 190 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 191 | Ok(Some(results)) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 192 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 193 | } |