1use std::pin::Pin;
29
30use eyeball::{ObservableWriteGuard, SharedObservable, Subscriber};
31use eyeball_im::{ObservableVector, Vector, VectorSubscriberBatchedStream};
32use futures_util::{Stream, StreamExt as _};
33use matrix_sdk::{
34 Client, deserialized_responses::TimelineEvent, message_search::SearchError, room::Room,
35};
36use ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId};
37use tokio::sync::Mutex as AsyncMutex;
38
39use crate::timeline::{Profile, TimelineDetails, TimelineItemContent};
40
41type ResultsStream =
44 Pin<Box<dyn Stream<Item = Result<Vec<(OwnedRoomId, TimelineEvent)>, SearchError>> + Send>>;
45
46#[cfg_attr(feature = "uniffi", derive(uniffi::Enum), uniffi(name = "SearchServicePaginationState"))]
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub enum PaginationState {
50 Idle { end_reached: bool },
53 Loading,
55}
56
57#[derive(Debug, Clone)]
61pub enum ResultType {
62 Message(MessageResult),
64}
65
66#[derive(Debug, Clone)]
68pub struct MessageResult {
69 pub room_id: OwnedRoomId,
71 pub event_id: OwnedEventId,
73 pub sender: OwnedUserId,
75 pub sender_profile: TimelineDetails<Profile>,
77 pub content: TimelineItemContent,
79 pub timestamp: MilliSecondsSinceUnixEpoch,
81}
82
83impl MessageResult {
84 async fn from_event(room: &Room, event: TimelineEvent) -> Option<Self> {
87 let sender = event.sender()?;
88 let event_id = event.event_id()?.to_owned();
89 let timestamp = event.timestamp().unwrap_or_else(MilliSecondsSinceUnixEpoch::now);
90
91 let content = TimelineItemContent::from_event(room, event).await?;
92 let sender_profile =
93 TimelineDetails::from_initial_value(Profile::load(room, &sender).await);
94
95 Some(Self {
96 room_id: room.room_id().to_owned(),
97 event_id,
98 sender,
99 sender_profile,
100 content,
101 timestamp,
102 })
103 }
104}
105
106pub struct SearchService {
108 client: Client,
110
111 stream: AsyncMutex<Option<ResultsStream>>,
114
115 pagination_state: SharedObservable<PaginationState>,
118
119 results: AsyncMutex<ObservableVector<ResultType>>,
122}
123
124impl SearchService {
125 pub fn new(client: Client) -> Self {
127 Self {
128 client,
129 stream: AsyncMutex::new(None),
130 pagination_state: SharedObservable::new(PaginationState::Idle { end_reached: false }),
131 results: AsyncMutex::new(ObservableVector::new()),
132 }
133 }
134
135 pub async fn set_query(&self, query: String) -> Result<(), SearchError> {
139 let stream = self.client.search_messages(query).build_events();
140 *self.stream.lock().await = Some(Box::pin(stream));
141 self.results.lock().await.clear();
142 self.pagination_state.set(PaginationState::Idle { end_reached: false });
143
144 self.paginate().await
145 }
146
147 pub fn pagination_state(&self) -> PaginationState {
149 self.pagination_state.get()
150 }
151
152 pub fn subscribe_to_pagination_state_updates(&self) -> Subscriber<PaginationState> {
154 self.pagination_state.subscribe()
155 }
156
157 pub async fn results(&self) -> Vec<ResultType> {
159 self.results.lock().await.iter().cloned().collect()
160 }
161
162 pub async fn subscribe_to_results(
164 &self,
165 ) -> (Vector<ResultType>, VectorSubscriberBatchedStream<ResultType>) {
166 self.results.lock().await.subscribe().into_values_and_batched_stream()
167 }
168
169 pub async fn paginate(&self) -> Result<(), SearchError> {
171 {
172 let mut pagination_state = self.pagination_state.write();
173
174 match *pagination_state {
175 PaginationState::Idle { end_reached } if end_reached => return Ok(()),
176 PaginationState::Loading => return Ok(()),
177 _ => {}
178 }
179
180 ObservableWriteGuard::set(&mut pagination_state, PaginationState::Loading);
181 }
182
183 let mut stream = self.stream.lock().await;
184 let Some(stream) = stream.as_mut() else {
185 self.pagination_state.set(PaginationState::Idle { end_reached: true });
186 return Ok(());
187 };
188
189 match stream.next().await {
190 None => {
191 self.pagination_state.set(PaginationState::Idle { end_reached: true });
192 }
193 Some(Err(err)) => {
194 self.pagination_state.set(PaginationState::Idle { end_reached: false });
195 return Err(err);
196 }
197 Some(Ok(page)) => {
198 let mut resolved = Vector::new();
199 for (room_id, event) in page {
200 let Some(room) = self.client.get_room(&room_id) else {
201 continue;
202 };
203 let Some(result) = MessageResult::from_event(&room, event).await else {
204 continue;
205 };
206 resolved.push_back(ResultType::Message(result));
207 }
208
209 if !resolved.is_empty() {
210 self.results.lock().await.append(resolved);
211 }
212
213 self.pagination_state.set(PaginationState::Idle { end_reached: false });
214 }
215 }
216
217 Ok(())
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use std::time::Duration;
224
225 use assert_matches2::assert_let;
226 use eyeball_im::VectorDiff;
227 use futures_util::pin_mut;
228 use matrix_sdk::test_utils::mocks::MatrixMockServer;
229 use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
230 use ruma::{event_id, room_id, user_id};
231 use stream_assert::{assert_next_matches, assert_pending};
232 use tokio::time::sleep;
233
234 use super::{PaginationState, ResultType, SearchService};
235
236 #[async_test]
237 async fn test_search_pagination() {
238 let server = MatrixMockServer::new().await;
239 let client = server.client_builder().build().await;
240
241 let event_cache = client.event_cache();
242 event_cache.subscribe().unwrap();
243
244 let room_id = room_id!("!room:localhost");
245 let event_id = event_id!("$event:localhost");
246 let f = EventFactory::new().sender(user_id!("@user:localhost"));
247
248 server
249 .mock_sync()
250 .ok_and_run(&client, |builder| {
251 builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(
252 f.text_msg("hello world").room(room_id).event_id(event_id),
253 ));
254 })
255 .await;
256
257 sleep(Duration::from_millis(300)).await;
259
260 let search = SearchService::new(client);
261
262 assert_eq!(search.pagination_state(), PaginationState::Idle { end_reached: false });
264 assert!(search.results().await.is_empty());
265
266 search.set_query("world".to_owned()).await.unwrap();
268
269 assert_eq!(search.pagination_state(), PaginationState::Idle { end_reached: false });
270 let results = search.results().await;
271 assert_eq!(results.len(), 1);
272 assert_let!(ResultType::Message(message) = &results[0]);
273 assert_eq!(message.event_id, event_id);
274
275 let (initial, results_stream) = search.subscribe_to_results().await;
277 assert_eq!(initial.len(), 1);
278 pin_mut!(results_stream);
279 assert_pending!(results_stream);
280
281 search.paginate().await.unwrap();
283
284 assert_pending!(results_stream);
285 assert_eq!(search.pagination_state(), PaginationState::Idle { end_reached: true });
286 assert_eq!(search.results().await.len(), 1);
287 }
288
289 #[async_test]
290 async fn test_search_resets_on_query_change() {
291 let server = MatrixMockServer::new().await;
292 let client = server.client_builder().build().await;
293
294 let event_cache = client.event_cache();
295 event_cache.subscribe().unwrap();
296
297 let room_id = room_id!("!room:localhost");
298 let apple_event = event_id!("$apple:localhost");
299 let banana_event = event_id!("$banana:localhost");
300 let f = EventFactory::new().sender(user_id!("@user:localhost"));
301
302 server
303 .mock_sync()
304 .ok_and_run(&client, |builder| {
305 builder.add_joined_room(
306 JoinedRoomBuilder::new(room_id)
307 .add_timeline_event(
308 f.text_msg("apple pie").room(room_id).event_id(apple_event),
309 )
310 .add_timeline_event(
311 f.text_msg("banana split").room(room_id).event_id(banana_event),
312 ),
313 );
314 })
315 .await;
316
317 sleep(Duration::from_millis(300)).await;
318
319 let search = SearchService::new(client);
320
321 search.set_query("apple".to_owned()).await.unwrap();
323
324 let (initial, results_stream) = search.subscribe_to_results().await;
325 assert_eq!(initial.len(), 1);
326 assert_let!(ResultType::Message(message) = &initial[0]);
327 assert_eq!(message.event_id, apple_event);
328 pin_mut!(results_stream);
329 assert_pending!(results_stream);
330
331 search.set_query("banana".to_owned()).await.unwrap();
333
334 assert_next_matches!(results_stream, diffs => {
336 assert_let!([VectorDiff::Clear, VectorDiff::Append { values }] = diffs.as_slice());
337 assert_eq!(values.len(), 1);
338 assert_let!(ResultType::Message(message) = &values[0]);
339 assert_eq!(message.event_id, banana_event);
340 });
341
342 let results = search.results().await;
343 assert_eq!(results.len(), 1);
344 assert_let!(ResultType::Message(message) = &results[0]);
345 assert_eq!(message.event_id, banana_event);
346 }
347}