matrix_sdk_ffi/
room_list.rs

1#![allow(deprecated)]
2
3use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
4
5use eyeball_im::VectorDiff;
6use futures_util::{pin_mut, StreamExt};
7use matrix_sdk::{
8    ruma::{
9        api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
10        RoomId,
11    },
12    Room as SdkRoom,
13};
14use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
15use matrix_sdk_ui::{
16    room_list_service::filters::{
17        new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
18        new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_invite,
19        new_filter_joined, new_filter_low_priority, new_filter_non_left, new_filter_none,
20        new_filter_normalized_match_room_name, new_filter_not, new_filter_space, new_filter_unread,
21        BoxedFilterFn, RoomCategory,
22    },
23    unable_to_decrypt_hook::UtdHookManager,
24};
25
26use crate::{
27    room::{Membership, Room},
28    runtime::get_runtime_handle,
29    TaskHandle,
30};
31
32#[derive(Debug, thiserror::Error, uniffi::Error)]
33pub enum RoomListError {
34    #[error("sliding sync error: {error}")]
35    SlidingSync { error: String },
36    #[error("unknown list `{list_name}`")]
37    UnknownList { list_name: String },
38    #[error("input cannot be applied")]
39    InputCannotBeApplied,
40    #[error("room `{room_name}` not found")]
41    RoomNotFound { room_name: String },
42    #[error("invalid room ID: {error}")]
43    InvalidRoomId { error: String },
44    #[error("Event cache ran into an error: {error}")]
45    EventCache { error: String },
46    #[error(
47        "The requested room doesn't match the membership requirements {expected:?}, \
48         observed {actual:?}"
49    )]
50    IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
51}
52
53impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
54    fn from(value: matrix_sdk_ui::room_list_service::Error) -> Self {
55        use matrix_sdk_ui::room_list_service::Error::*;
56
57        match value {
58            SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
59            UnknownList(list_name) => Self::UnknownList { list_name },
60            RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
61            EventCache(error) => Self::EventCache { error: error.to_string() },
62        }
63    }
64}
65
66impl From<ruma::IdParseError> for RoomListError {
67    fn from(value: ruma::IdParseError) -> Self {
68        Self::InvalidRoomId { error: value.to_string() }
69    }
70}
71
72#[derive(uniffi::Object)]
73pub struct RoomListService {
74    pub(crate) inner: Arc<matrix_sdk_ui::RoomListService>,
75    pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
76}
77
78#[matrix_sdk_ffi_macros::export]
79impl RoomListService {
80    fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
81        let state_stream = self.inner.state();
82
83        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
84            pin_mut!(state_stream);
85
86            while let Some(state) = state_stream.next().await {
87                listener.on_update(state.into());
88            }
89        })))
90    }
91
92    fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
93        let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
94
95        Ok(Arc::new(Room::new(self.inner.room(room_id)?, self.utd_hook.clone())))
96    }
97
98    async fn all_rooms(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
99        Ok(Arc::new(RoomList {
100            room_list_service: self.clone(),
101            inner: Arc::new(self.inner.all_rooms().await.map_err(RoomListError::from)?),
102        }))
103    }
104
105    fn sync_indicator(
106        &self,
107        delay_before_showing_in_ms: u32,
108        delay_before_hiding_in_ms: u32,
109        listener: Box<dyn RoomListServiceSyncIndicatorListener>,
110    ) -> Arc<TaskHandle> {
111        let sync_indicator_stream = self.inner.sync_indicator(
112            Duration::from_millis(delay_before_showing_in_ms.into()),
113            Duration::from_millis(delay_before_hiding_in_ms.into()),
114        );
115
116        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
117            pin_mut!(sync_indicator_stream);
118
119            while let Some(sync_indicator) = sync_indicator_stream.next().await {
120                listener.on_update(sync_indicator.into());
121            }
122        })))
123    }
124
125    async fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
126        let room_ids = room_ids
127            .into_iter()
128            .map(|room_id| {
129                RoomId::parse(&room_id).map_err(|_| RoomListError::InvalidRoomId { error: room_id })
130            })
131            .collect::<Result<Vec<_>, _>>()?;
132
133        self.inner
134            .subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>())
135            .await;
136
137        Ok(())
138    }
139}
140
141#[derive(uniffi::Object)]
142pub struct RoomList {
143    room_list_service: Arc<RoomListService>,
144    inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
145}
146
147#[matrix_sdk_ffi_macros::export]
148impl RoomList {
149    fn loading_state(
150        &self,
151        listener: Box<dyn RoomListLoadingStateListener>,
152    ) -> Result<RoomListLoadingStateResult, RoomListError> {
153        let loading_state = self.inner.loading_state();
154
155        Ok(RoomListLoadingStateResult {
156            state: loading_state.get().into(),
157            state_stream: Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
158                pin_mut!(loading_state);
159
160                while let Some(loading_state) = loading_state.next().await {
161                    listener.on_update(loading_state.into());
162                }
163            }))),
164        })
165    }
166
167    fn entries_with_dynamic_adapters(
168        self: Arc<Self>,
169        page_size: u32,
170        listener: Box<dyn RoomListEntriesListener>,
171    ) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
172        self.entries_with_dynamic_adapters_with(page_size, false, listener)
173    }
174
175    fn entries_with_dynamic_adapters_with(
176        self: Arc<Self>,
177        page_size: u32,
178        enable_latest_event_sorter: bool,
179        listener: Box<dyn RoomListEntriesListener>,
180    ) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
181        let this = self;
182
183        // The following code deserves a bit of explanation.
184        // `matrix_sdk_ui::room_list_service::RoomList::entries_with_dynamic_adapters`
185        // returns a `Stream` with a lifetime bounds to its `self` (`RoomList`). This is
186        // problematic here as this `Stream` is returned as part of
187        // `RoomListEntriesWithDynamicAdaptersResult` but it is not possible to store
188        // `RoomList` with it inside the `Future` that is run inside the `TaskHandle`
189        // that consumes this `Stream`. We have a lifetime issue: `RoomList` doesn't
190        // live long enough!
191        //
192        // To solve this issue, the trick is to store the `RoomList` inside the
193        // `RoomListEntriesWithDynamicAdaptersResult`. Alright, but then we have another
194        // lifetime issue! `RoomList` cannot move inside this struct because it is
195        // borrowed by `entries_with_dynamic_adapters`. Indeed, the struct is built
196        // after the `Stream` is obtained.
197        //
198        // To solve this issue, we need to build the struct field by field, starting
199        // with `this`, and use a reference to `this` to call
200        // `entries_with_dynamic_adapters`. This is unsafe because a couple of
201        // invariants must hold, but all this is legal and correct if the invariants are
202        // properly fulfilled.
203
204        // Create the struct result with uninitialized fields.
205        let mut result = MaybeUninit::<RoomListEntriesWithDynamicAdaptersResult>::uninit();
206        let ptr = result.as_mut_ptr();
207
208        // Initialize the first field `this`.
209        //
210        // SAFETY: `ptr` is correctly aligned, this is guaranteed by `MaybeUninit`.
211        unsafe {
212            addr_of_mut!((*ptr).this).write(this);
213        }
214
215        // Get a reference to `this`. It is only borrowed, it's not moved.
216        let this =
217            // SAFETY: `ptr` is correct aligned, the `this` field is correctly aligned,
218            // is dereferenceable and points to a correctly initialized value as done
219            // in the previous line.
220            unsafe { addr_of_mut!((*ptr).this).as_ref() }
221                // SAFETY: `this` contains a non null value.
222                .unwrap();
223
224        // Now we can create `entries_stream` and `dynamic_entries_controller` by
225        // borrowing `this`, which is going to live long enough since it will live as
226        // long as `entries_stream` and `dynamic_entries_controller`.
227        let (entries_stream, dynamic_entries_controller) =
228            this.inner.entries_with_dynamic_adapters_with(
229                page_size.try_into().unwrap(),
230                enable_latest_event_sorter,
231            );
232
233        // FFI dance to make those values consumable by foreign language, nothing fancy
234        // here, that's the real code for this method.
235        let dynamic_entries_controller =
236            Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
237
238        let utd_hook = this.room_list_service.utd_hook.clone();
239        let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
240            pin_mut!(entries_stream);
241
242            while let Some(diffs) = entries_stream.next().await {
243                listener.on_update(
244                    diffs
245                        .into_iter()
246                        .map(|diff| {
247                            RoomListEntriesUpdate::from(
248                                utd_hook.clone(),
249                                diff.map(|room| room.into_inner()),
250                            )
251                        })
252                        .collect(),
253                );
254            }
255        })));
256
257        // Initialize the second field `controller`.
258        //
259        // SAFETY: `ptr` is correctly aligned.
260        unsafe {
261            addr_of_mut!((*ptr).controller).write(dynamic_entries_controller);
262        }
263
264        // Initialize the third and last field `entries_stream`.
265        //
266        // SAFETY: `ptr` is correctly aligned.
267        unsafe {
268            addr_of_mut!((*ptr).entries_stream).write(entries_stream);
269        }
270
271        // The result is complete, let's return it!
272        //
273        // SAFETY: `result` is fully initialized, all its fields have received a valid
274        // value.
275        Arc::new(unsafe { result.assume_init() })
276    }
277
278    fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
279        self.room_list_service.room(room_id)
280    }
281}
282
283#[derive(uniffi::Object)]
284pub struct RoomListEntriesWithDynamicAdaptersResult {
285    this: Arc<RoomList>,
286    controller: Arc<RoomListDynamicEntriesController>,
287    entries_stream: Arc<TaskHandle>,
288}
289
290#[matrix_sdk_ffi_macros::export]
291impl RoomListEntriesWithDynamicAdaptersResult {
292    fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
293        self.controller.clone()
294    }
295
296    fn entries_stream(&self) -> Arc<TaskHandle> {
297        self.entries_stream.clone()
298    }
299}
300
301#[derive(uniffi::Record)]
302pub struct RoomListLoadingStateResult {
303    pub state: RoomListLoadingState,
304    pub state_stream: Arc<TaskHandle>,
305}
306
307#[derive(uniffi::Enum)]
308pub enum RoomListServiceState {
309    // Name it `Initial` instead of `Init`, otherwise it creates a keyword conflict in Swift
310    // as of 2023-08-21.
311    Initial,
312    SettingUp,
313    Recovering,
314    Running,
315    Error,
316    Terminated,
317}
318
319impl From<matrix_sdk_ui::room_list_service::State> for RoomListServiceState {
320    fn from(value: matrix_sdk_ui::room_list_service::State) -> Self {
321        use matrix_sdk_ui::room_list_service::State as S;
322
323        match value {
324            S::Init => Self::Initial,
325            S::SettingUp => Self::SettingUp,
326            S::Recovering => Self::Recovering,
327            S::Running => Self::Running,
328            S::Error { .. } => Self::Error,
329            S::Terminated { .. } => Self::Terminated,
330        }
331    }
332}
333
334#[derive(uniffi::Enum)]
335pub enum RoomListServiceSyncIndicator {
336    Show,
337    Hide,
338}
339
340impl From<matrix_sdk_ui::room_list_service::SyncIndicator> for RoomListServiceSyncIndicator {
341    fn from(value: matrix_sdk_ui::room_list_service::SyncIndicator) -> Self {
342        use matrix_sdk_ui::room_list_service::SyncIndicator as SI;
343
344        match value {
345            SI::Show => Self::Show,
346            SI::Hide => Self::Hide,
347        }
348    }
349}
350
351#[derive(uniffi::Enum)]
352pub enum RoomListLoadingState {
353    NotLoaded,
354    Loaded { maximum_number_of_rooms: Option<u32> },
355}
356
357impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLoadingState {
358    fn from(value: matrix_sdk_ui::room_list_service::RoomListLoadingState) -> Self {
359        use matrix_sdk_ui::room_list_service::RoomListLoadingState as LS;
360
361        match value {
362            LS::NotLoaded => Self::NotLoaded,
363            LS::Loaded { maximum_number_of_rooms } => Self::Loaded { maximum_number_of_rooms },
364        }
365    }
366}
367
368#[matrix_sdk_ffi_macros::export(callback_interface)]
369pub trait RoomListServiceStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
370    fn on_update(&self, state: RoomListServiceState);
371}
372
373#[matrix_sdk_ffi_macros::export(callback_interface)]
374pub trait RoomListLoadingStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
375    fn on_update(&self, state: RoomListLoadingState);
376}
377
378#[matrix_sdk_ffi_macros::export(callback_interface)]
379pub trait RoomListServiceSyncIndicatorListener: SendOutsideWasm + SyncOutsideWasm + Debug {
380    fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
381}
382
383#[derive(uniffi::Enum)]
384pub enum RoomListEntriesUpdate {
385    Append { values: Vec<Arc<Room>> },
386    Clear,
387    PushFront { value: Arc<Room> },
388    PushBack { value: Arc<Room> },
389    PopFront,
390    PopBack,
391    Insert { index: u32, value: Arc<Room> },
392    Set { index: u32, value: Arc<Room> },
393    Remove { index: u32 },
394    Truncate { length: u32 },
395    Reset { values: Vec<Arc<Room>> },
396}
397
398impl RoomListEntriesUpdate {
399    fn from(utd_hook: Option<Arc<UtdHookManager>>, vector_diff: VectorDiff<SdkRoom>) -> Self {
400        match vector_diff {
401            VectorDiff::Append { values } => Self::Append {
402                values: values
403                    .into_iter()
404                    .map(|value| Arc::new(Room::new(value, utd_hook.clone())))
405                    .collect(),
406            },
407            VectorDiff::Clear => Self::Clear,
408            VectorDiff::PushFront { value } => {
409                Self::PushFront { value: Arc::new(Room::new(value, utd_hook)) }
410            }
411            VectorDiff::PushBack { value } => {
412                Self::PushBack { value: Arc::new(Room::new(value, utd_hook)) }
413            }
414            VectorDiff::PopFront => Self::PopFront,
415            VectorDiff::PopBack => Self::PopBack,
416            VectorDiff::Insert { index, value } => Self::Insert {
417                index: u32::try_from(index).unwrap(),
418                value: Arc::new(Room::new(value, utd_hook)),
419            },
420            VectorDiff::Set { index, value } => Self::Set {
421                index: u32::try_from(index).unwrap(),
422                value: Arc::new(Room::new(value, utd_hook)),
423            },
424            VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
425            VectorDiff::Truncate { length } => {
426                Self::Truncate { length: u32::try_from(length).unwrap() }
427            }
428            VectorDiff::Reset { values } => Self::Reset {
429                values: values
430                    .into_iter()
431                    .map(|value| Arc::new(Room::new(value, utd_hook.clone())))
432                    .collect(),
433            },
434        }
435    }
436}
437
438#[matrix_sdk_ffi_macros::export(callback_interface)]
439pub trait RoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
440    fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
441}
442
443#[derive(uniffi::Object)]
444pub struct RoomListDynamicEntriesController {
445    inner: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
446}
447
448impl RoomListDynamicEntriesController {
449    fn new(
450        dynamic_entries_controller: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
451    ) -> Self {
452        Self { inner: dynamic_entries_controller }
453    }
454}
455
456#[matrix_sdk_ffi_macros::export]
457impl RoomListDynamicEntriesController {
458    fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
459        self.inner.set_filter(kind.into())
460    }
461
462    fn add_one_page(&self) {
463        self.inner.add_one_page();
464    }
465
466    fn reset_to_one_page(&self) {
467        self.inner.reset_to_one_page();
468    }
469}
470
471#[derive(uniffi::Enum)]
472pub enum RoomListEntriesDynamicFilterKind {
473    All { filters: Vec<RoomListEntriesDynamicFilterKind> },
474    Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
475    NonSpace,
476    Space,
477    NonLeft,
478    // Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
479    // support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
480    Joined,
481    Unread,
482    Favourite,
483    LowPriority,
484    NonLowPriority,
485    Invite,
486    Category { expect: RoomListFilterCategory },
487    None,
488    NormalizedMatchRoomName { pattern: String },
489    FuzzyMatchRoomName { pattern: String },
490    DeduplicateVersions,
491}
492
493#[derive(uniffi::Enum)]
494pub enum RoomListFilterCategory {
495    Group,
496    People,
497}
498
499impl From<RoomListFilterCategory> for RoomCategory {
500    fn from(value: RoomListFilterCategory) -> Self {
501        match value {
502            RoomListFilterCategory::Group => Self::Group,
503            RoomListFilterCategory::People => Self::People,
504        }
505    }
506}
507
508impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
509    fn from(value: RoomListEntriesDynamicFilterKind) -> Self {
510        use RoomListEntriesDynamicFilterKind as Kind;
511
512        match value {
513            Kind::All { filters } => Box::new(new_filter_all(
514                filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
515            )),
516            Kind::Any { filters } => Box::new(new_filter_any(
517                filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
518            )),
519            Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
520            Kind::Space => Box::new(new_filter_space()),
521            Kind::NonLeft => Box::new(new_filter_non_left()),
522            Kind::Joined => Box::new(new_filter_joined()),
523            Kind::Unread => Box::new(new_filter_unread()),
524            Kind::Favourite => Box::new(new_filter_favourite()),
525            Kind::LowPriority => Box::new(new_filter_low_priority()),
526            Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
527            Kind::Invite => Box::new(new_filter_invite()),
528            Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
529            Kind::None => Box::new(new_filter_none()),
530            Kind::NormalizedMatchRoomName { pattern } => {
531                Box::new(new_filter_normalized_match_room_name(&pattern))
532            }
533            Kind::FuzzyMatchRoomName { pattern } => {
534                Box::new(new_filter_fuzzy_match_room_name(&pattern))
535            }
536            Kind::DeduplicateVersions => Box::new(new_filter_deduplicate_versions()),
537        }
538    }
539}
540
541#[derive(uniffi::Object)]
542pub struct UnreadNotificationsCount {
543    highlight_count: u32,
544    notification_count: u32,
545}
546
547#[matrix_sdk_ffi_macros::export]
548impl UnreadNotificationsCount {
549    fn highlight_count(&self) -> u32 {
550        self.highlight_count
551    }
552
553    fn notification_count(&self) -> u32 {
554        self.notification_count
555    }
556
557    fn has_notifications(&self) -> bool {
558        self.notification_count != 0 || self.highlight_count != 0
559    }
560}
561
562impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
563    fn from(inner: RumaUnreadNotificationsCount) -> Self {
564        UnreadNotificationsCount {
565            highlight_count: inner
566                .highlight_count
567                .and_then(|x| x.try_into().ok())
568                .unwrap_or_default(),
569            notification_count: inner
570                .notification_count
571                .and_then(|x| x.try_into().ok())
572                .unwrap_or_default(),
573        }
574    }
575}