1#![allow(deprecated)]
2
3use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
4
5use async_compat::get_runtime_handle;
6use eyeball_im::VectorDiff;
7use futures_util::{pin_mut, StreamExt, TryFutureExt};
8use matrix_sdk::ruma::{
9 api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
10 RoomId,
11};
12use matrix_sdk_ui::{
13 room_list_service::filters::{
14 new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
15 new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
16 new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
17 new_filter_unread, BoxedFilterFn, RoomCategory,
18 },
19 timeline::default_event_filter,
20 unable_to_decrypt_hook::UtdHookManager,
21};
22use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
23use tokio::sync::RwLock;
24
25use crate::{
26 error::ClientError,
27 room::{Membership, Room},
28 room_info::RoomInfo,
29 room_preview::RoomPreview,
30 timeline::{configuration::TimelineEventTypeFilter, EventTimelineItem, Timeline},
31 utils::AsyncRuntimeDropped,
32 TaskHandle,
33};
34
35#[derive(Debug, thiserror::Error, uniffi::Error)]
36pub enum RoomListError {
37 #[error("sliding sync error: {error}")]
38 SlidingSync { error: String },
39 #[error("unknown list `{list_name}`")]
40 UnknownList { list_name: String },
41 #[error("input cannot be applied")]
42 InputCannotBeApplied,
43 #[error("room `{room_name}` not found")]
44 RoomNotFound { room_name: String },
45 #[error("invalid room ID: {error}")]
46 InvalidRoomId { error: String },
47 #[error("A timeline instance already exists for room {room_name}")]
48 TimelineAlreadyExists { room_name: String },
49 #[error("A timeline instance hasn't been initialized for room {room_name}")]
50 TimelineNotInitialized { room_name: String },
51 #[error("Timeline couldn't be initialized: {error}")]
52 InitializingTimeline { error: String },
53 #[error("Event cache ran into an error: {error}")]
54 EventCache { error: String },
55 #[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
56 IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
57}
58
59impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
60 fn from(value: matrix_sdk_ui::room_list_service::Error) -> Self {
61 use matrix_sdk_ui::room_list_service::Error::*;
62
63 match value {
64 SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
65 UnknownList(list_name) => Self::UnknownList { list_name },
66 RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
67 TimelineAlreadyExists(room_id) => {
68 Self::TimelineAlreadyExists { room_name: room_id.to_string() }
69 }
70 InitializingTimeline(source) => {
71 Self::InitializingTimeline { error: source.to_string() }
72 }
73 EventCache(error) => Self::EventCache { error: error.to_string() },
74 }
75 }
76}
77
78impl From<ruma::IdParseError> for RoomListError {
79 fn from(value: ruma::IdParseError) -> Self {
80 Self::InvalidRoomId { error: value.to_string() }
81 }
82}
83
84#[derive(uniffi::Object)]
85pub struct RoomListService {
86 pub(crate) inner: Arc<matrix_sdk_ui::RoomListService>,
87 pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
88}
89
90#[matrix_sdk_ffi_macros::export]
91impl RoomListService {
92 fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
93 let state_stream = self.inner.state();
94
95 Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
96 pin_mut!(state_stream);
97
98 while let Some(state) = state_stream.next().await {
99 listener.on_update(state.into());
100 }
101 })))
102 }
103
104 fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
105 let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
106
107 Ok(Arc::new(RoomListItem {
108 inner: Arc::new(self.inner.room(room_id)?),
109 utd_hook: self.utd_hook.clone(),
110 }))
111 }
112
113 async fn all_rooms(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
114 Ok(Arc::new(RoomList {
115 room_list_service: self.clone(),
116 inner: Arc::new(self.inner.all_rooms().await.map_err(RoomListError::from)?),
117 }))
118 }
119
120 fn sync_indicator(
121 &self,
122 delay_before_showing_in_ms: u32,
123 delay_before_hiding_in_ms: u32,
124 listener: Box<dyn RoomListServiceSyncIndicatorListener>,
125 ) -> Arc<TaskHandle> {
126 let sync_indicator_stream = self.inner.sync_indicator(
127 Duration::from_millis(delay_before_showing_in_ms.into()),
128 Duration::from_millis(delay_before_hiding_in_ms.into()),
129 );
130
131 Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
132 pin_mut!(sync_indicator_stream);
133
134 while let Some(sync_indicator) = sync_indicator_stream.next().await {
135 listener.on_update(sync_indicator.into());
136 }
137 })))
138 }
139
140 fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
141 let room_ids = room_ids
142 .into_iter()
143 .map(|room_id| {
144 RoomId::parse(&room_id).map_err(|_| RoomListError::InvalidRoomId { error: room_id })
145 })
146 .collect::<Result<Vec<_>, _>>()?;
147
148 self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
149
150 Ok(())
151 }
152}
153
154#[derive(uniffi::Object)]
155pub struct RoomList {
156 room_list_service: Arc<RoomListService>,
157 inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
158}
159
160#[matrix_sdk_ffi_macros::export]
161impl RoomList {
162 fn loading_state(
163 &self,
164 listener: Box<dyn RoomListLoadingStateListener>,
165 ) -> Result<RoomListLoadingStateResult, RoomListError> {
166 let loading_state = self.inner.loading_state();
167
168 Ok(RoomListLoadingStateResult {
169 state: loading_state.get().into(),
170 state_stream: Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
171 pin_mut!(loading_state);
172
173 while let Some(loading_state) = loading_state.next().await {
174 listener.on_update(loading_state.into());
175 }
176 }))),
177 })
178 }
179
180 fn entries_with_dynamic_adapters(
181 self: Arc<Self>,
182 page_size: u32,
183 listener: Box<dyn RoomListEntriesListener>,
184 ) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
185 let this = self.clone();
186 let utd_hook = self.room_list_service.utd_hook.clone();
187
188 let mut result = MaybeUninit::<RoomListEntriesWithDynamicAdaptersResult>::uninit();
211 let ptr = result.as_mut_ptr();
212
213 unsafe {
217 addr_of_mut!((*ptr).this).write(this);
218 }
219
220 let this =
222 unsafe { addr_of_mut!((*ptr).this).as_ref() }
226 .unwrap();
228
229 let (entries_stream, dynamic_entries_controller) =
233 this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
234
235 let dynamic_entries_controller =
238 Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
239
240 let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
241 pin_mut!(entries_stream);
242
243 while let Some(diffs) = entries_stream.next().await {
244 listener.on_update(
245 diffs
246 .into_iter()
247 .map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
248 .collect(),
249 );
250 }
251 })));
252
253 unsafe {
257 addr_of_mut!((*ptr).controller).write(dynamic_entries_controller);
258 }
259
260 unsafe {
264 addr_of_mut!((*ptr).entries_stream).write(entries_stream);
265 }
266
267 Arc::new(unsafe { result.assume_init() })
272 }
273
274 fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
275 self.room_list_service.room(room_id)
276 }
277}
278
279#[derive(uniffi::Object)]
280pub struct RoomListEntriesWithDynamicAdaptersResult {
281 this: Arc<RoomList>,
282 controller: Arc<RoomListDynamicEntriesController>,
283 entries_stream: Arc<TaskHandle>,
284}
285
286#[matrix_sdk_ffi_macros::export]
287impl RoomListEntriesWithDynamicAdaptersResult {
288 fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
289 self.controller.clone()
290 }
291
292 fn entries_stream(&self) -> Arc<TaskHandle> {
293 self.entries_stream.clone()
294 }
295}
296
297#[derive(uniffi::Record)]
298pub struct RoomListLoadingStateResult {
299 pub state: RoomListLoadingState,
300 pub state_stream: Arc<TaskHandle>,
301}
302
303#[derive(uniffi::Enum)]
304pub enum RoomListServiceState {
305 Initial,
308 SettingUp,
309 Recovering,
310 Running,
311 Error,
312 Terminated,
313}
314
315impl From<matrix_sdk_ui::room_list_service::State> for RoomListServiceState {
316 fn from(value: matrix_sdk_ui::room_list_service::State) -> Self {
317 use matrix_sdk_ui::room_list_service::State as S;
318
319 match value {
320 S::Init => Self::Initial,
321 S::SettingUp => Self::SettingUp,
322 S::Recovering => Self::Recovering,
323 S::Running => Self::Running,
324 S::Error { .. } => Self::Error,
325 S::Terminated { .. } => Self::Terminated,
326 }
327 }
328}
329
330#[derive(uniffi::Enum)]
331pub enum RoomListServiceSyncIndicator {
332 Show,
333 Hide,
334}
335
336impl From<matrix_sdk_ui::room_list_service::SyncIndicator> for RoomListServiceSyncIndicator {
337 fn from(value: matrix_sdk_ui::room_list_service::SyncIndicator) -> Self {
338 use matrix_sdk_ui::room_list_service::SyncIndicator as SI;
339
340 match value {
341 SI::Show => Self::Show,
342 SI::Hide => Self::Hide,
343 }
344 }
345}
346
347#[derive(uniffi::Enum)]
348pub enum RoomListLoadingState {
349 NotLoaded,
350 Loaded { maximum_number_of_rooms: Option<u32> },
351}
352
353impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLoadingState {
354 fn from(value: matrix_sdk_ui::room_list_service::RoomListLoadingState) -> Self {
355 use matrix_sdk_ui::room_list_service::RoomListLoadingState as LS;
356
357 match value {
358 LS::NotLoaded => Self::NotLoaded,
359 LS::Loaded { maximum_number_of_rooms } => Self::Loaded { maximum_number_of_rooms },
360 }
361 }
362}
363
364#[matrix_sdk_ffi_macros::export(callback_interface)]
365pub trait RoomListServiceStateListener: Send + Sync + Debug {
366 fn on_update(&self, state: RoomListServiceState);
367}
368
369#[matrix_sdk_ffi_macros::export(callback_interface)]
370pub trait RoomListLoadingStateListener: Send + Sync + Debug {
371 fn on_update(&self, state: RoomListLoadingState);
372}
373
374#[matrix_sdk_ffi_macros::export(callback_interface)]
375pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
376 fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
377}
378
379#[derive(uniffi::Enum)]
380pub enum RoomListEntriesUpdate {
381 Append { values: Vec<Arc<RoomListItem>> },
382 Clear,
383 PushFront { value: Arc<RoomListItem> },
384 PushBack { value: Arc<RoomListItem> },
385 PopFront,
386 PopBack,
387 Insert { index: u32, value: Arc<RoomListItem> },
388 Set { index: u32, value: Arc<RoomListItem> },
389 Remove { index: u32 },
390 Truncate { length: u32 },
391 Reset { values: Vec<Arc<RoomListItem>> },
392}
393
394impl RoomListEntriesUpdate {
395 fn from(
396 vector_diff: VectorDiff<matrix_sdk_ui::room_list_service::Room>,
397 utd_hook: Option<Arc<UtdHookManager>>,
398 ) -> Self {
399 match vector_diff {
400 VectorDiff::Append { values } => Self::Append {
401 values: values
402 .into_iter()
403 .map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
404 .collect(),
405 },
406 VectorDiff::Clear => Self::Clear,
407 VectorDiff::PushFront { value } => {
408 Self::PushFront { value: Arc::new(RoomListItem::from(value, utd_hook)) }
409 }
410 VectorDiff::PushBack { value } => {
411 Self::PushBack { value: Arc::new(RoomListItem::from(value, utd_hook)) }
412 }
413 VectorDiff::PopFront => Self::PopFront,
414 VectorDiff::PopBack => Self::PopBack,
415 VectorDiff::Insert { index, value } => Self::Insert {
416 index: u32::try_from(index).unwrap(),
417 value: Arc::new(RoomListItem::from(value, utd_hook)),
418 },
419 VectorDiff::Set { index, value } => Self::Set {
420 index: u32::try_from(index).unwrap(),
421 value: Arc::new(RoomListItem::from(value, utd_hook)),
422 },
423 VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
424 VectorDiff::Truncate { length } => {
425 Self::Truncate { length: u32::try_from(length).unwrap() }
426 }
427 VectorDiff::Reset { values } => Self::Reset {
428 values: values
429 .into_iter()
430 .map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
431 .collect(),
432 },
433 }
434 }
435}
436
437#[matrix_sdk_ffi_macros::export(callback_interface)]
438pub trait RoomListEntriesListener: Send + Sync + Debug {
439 fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
440}
441
442#[derive(uniffi::Object)]
443pub struct RoomListDynamicEntriesController {
444 inner: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
445}
446
447impl RoomListDynamicEntriesController {
448 fn new(
449 dynamic_entries_controller: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
450 ) -> Self {
451 Self { inner: dynamic_entries_controller }
452 }
453}
454
455#[matrix_sdk_ffi_macros::export]
456impl RoomListDynamicEntriesController {
457 fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
458 self.inner.set_filter(kind.into())
459 }
460
461 fn add_one_page(&self) {
462 self.inner.add_one_page();
463 }
464
465 fn reset_to_one_page(&self) {
466 self.inner.reset_to_one_page();
467 }
468}
469
470#[derive(uniffi::Enum)]
471pub enum RoomListEntriesDynamicFilterKind {
472 All { filters: Vec<RoomListEntriesDynamicFilterKind> },
473 Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
474 NonLeft,
475 Joined,
476 Unread,
477 Favourite,
478 Invite,
479 Category { expect: RoomListFilterCategory },
480 None,
481 NormalizedMatchRoomName { pattern: String },
482 FuzzyMatchRoomName { pattern: String },
483}
484
485#[derive(uniffi::Enum)]
486pub enum RoomListFilterCategory {
487 Group,
488 People,
489}
490
491impl From<RoomListFilterCategory> for RoomCategory {
492 fn from(value: RoomListFilterCategory) -> Self {
493 match value {
494 RoomListFilterCategory::Group => Self::Group,
495 RoomListFilterCategory::People => Self::People,
496 }
497 }
498}
499
500impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
501 fn from(value: RoomListEntriesDynamicFilterKind) -> Self {
502 use RoomListEntriesDynamicFilterKind as Kind;
503
504 match value {
505 Kind::All { filters } => Box::new(new_filter_all(
506 filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
507 )),
508 Kind::Any { filters } => Box::new(new_filter_any(
509 filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
510 )),
511 Kind::NonLeft => Box::new(new_filter_non_left()),
512 Kind::Joined => Box::new(new_filter_joined()),
513 Kind::Unread => Box::new(new_filter_unread()),
514 Kind::Favourite => Box::new(new_filter_favourite()),
515 Kind::Invite => Box::new(new_filter_invite()),
516 Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
517 Kind::None => Box::new(new_filter_none()),
518 Kind::NormalizedMatchRoomName { pattern } => {
519 Box::new(new_filter_normalized_match_room_name(&pattern))
520 }
521 Kind::FuzzyMatchRoomName { pattern } => {
522 Box::new(new_filter_fuzzy_match_room_name(&pattern))
523 }
524 }
525 }
526}
527
528#[derive(uniffi::Object)]
529pub struct RoomListItem {
530 inner: Arc<matrix_sdk_ui::room_list_service::Room>,
531 utd_hook: Option<Arc<UtdHookManager>>,
532}
533
534impl RoomListItem {
535 fn from(
536 value: matrix_sdk_ui::room_list_service::Room,
537 utd_hook: Option<Arc<UtdHookManager>>,
538 ) -> Self {
539 Self { inner: Arc::new(value), utd_hook }
540 }
541}
542
543#[matrix_sdk_ffi_macros::export]
544impl RoomListItem {
545 fn id(&self) -> String {
546 self.inner.id().to_string()
547 }
548
549 fn display_name(&self) -> Option<String> {
553 self.inner.cached_display_name()
554 }
555
556 fn avatar_url(&self) -> Option<String> {
557 self.inner.avatar_url().map(|uri| uri.to_string())
558 }
559
560 async fn is_direct(&self) -> bool {
561 self.inner.inner_room().is_direct().await.unwrap_or(false)
562 }
563
564 fn canonical_alias(&self) -> Option<String> {
565 self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
566 }
567
568 async fn room_info(&self) -> Result<RoomInfo, ClientError> {
569 RoomInfo::new(self.inner.inner_room()).await
570 }
571
572 fn membership(&self) -> Membership {
574 self.inner.inner_room().state().into()
575 }
576
577 async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
580 let server_names: Vec<OwnedServerName> = via
582 .into_iter()
583 .map(|server| ServerName::parse(server).map_err(ClientError::from))
584 .collect::<Result<_, ClientError>>()?;
585
586 let client = self.inner.client();
588 let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
589 {
590 let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
591 (room_or_alias_id, Vec::new())
592 } else {
593 let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
594 (room_or_alias_id, server_names)
595 };
596
597 if server_names.is_empty() {
600 if let Ok(invite_details) = self.inner.invite_details().await {
601 if let Some(inviter) = invite_details.inviter {
602 server_names.push(inviter.user_id().server_name().to_owned());
603 }
604 }
605 }
606
607 let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
608
609 Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
610 }
611
612 fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
617 if !matches!(self.membership(), Membership::Joined) {
618 return Err(RoomListError::IncorrectRoomMembership {
619 expected: vec![Membership::Joined],
620 actual: self.membership(),
621 });
622 }
623
624 if let Some(timeline) = self.inner.timeline() {
625 Ok(Arc::new(Room::with_timeline(
626 self.inner.inner_room().clone(),
627 Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
628 )))
629 } else {
630 Err(RoomListError::TimelineNotInitialized {
631 room_name: self.inner.inner_room().room_id().to_string(),
632 })
633 }
634 }
635
636 fn is_timeline_initialized(&self) -> bool {
638 self.inner.is_timeline_initialized()
639 }
640
641 async fn init_timeline(
650 &self,
651 event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
652 internal_id_prefix: Option<String>,
653 ) -> Result<(), RoomListError> {
654 let mut timeline_builder = self
655 .inner
656 .default_room_timeline_builder()
657 .await
658 .map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
659
660 if let Some(event_type_filter) = event_type_filter {
661 timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
662 default_event_filter(event, room_version_id) && event_type_filter.filter(event)
664 });
665 }
666
667 if let Some(internal_id_prefix) = internal_id_prefix {
668 timeline_builder = timeline_builder.with_internal_id_prefix(internal_id_prefix);
669 }
670
671 if let Some(utd_hook) = self.utd_hook.clone() {
672 timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
673 }
674
675 self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
676 }
677
678 async fn is_encrypted(&self) -> bool {
683 self.inner
684 .latest_encryption_state()
685 .await
686 .map(|state| state.is_encrypted())
687 .unwrap_or(false)
688 }
689
690 async fn latest_event(&self) -> Option<EventTimelineItem> {
691 self.inner.latest_event().await.map(Into::into)
692 }
693}
694
695#[derive(uniffi::Object)]
696pub struct UnreadNotificationsCount {
697 highlight_count: u32,
698 notification_count: u32,
699}
700
701#[matrix_sdk_ffi_macros::export]
702impl UnreadNotificationsCount {
703 fn highlight_count(&self) -> u32 {
704 self.highlight_count
705 }
706
707 fn notification_count(&self) -> u32 {
708 self.notification_count
709 }
710
711 fn has_notifications(&self) -> bool {
712 self.notification_count != 0 || self.highlight_count != 0
713 }
714}
715
716impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
717 fn from(inner: RumaUnreadNotificationsCount) -> Self {
718 UnreadNotificationsCount {
719 highlight_count: inner
720 .highlight_count
721 .and_then(|x| x.try_into().ok())
722 .unwrap_or_default(),
723 notification_count: inner
724 .notification_count
725 .and_then(|x| x.try_into().ok())
726 .unwrap_or_default(),
727 }
728 }
729}