matrix_sdk_ffi/
notification.rs

1use std::{collections::HashMap, sync::Arc};
2
3use matrix_sdk_ui::notification_client::{
4    NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
5    NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
6};
7use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
8
9use crate::{
10    client::{Client, JoinRule},
11    error::ClientError,
12    event::TimelineEvent,
13    room::Room,
14};
15
16#[derive(uniffi::Enum)]
17pub enum NotificationEvent {
18    Timeline { event: Arc<TimelineEvent> },
19    Invite { sender: String },
20}
21
22#[derive(uniffi::Record)]
23pub struct NotificationSenderInfo {
24    pub display_name: Option<String>,
25    pub avatar_url: Option<String>,
26    pub is_name_ambiguous: bool,
27}
28
29#[derive(uniffi::Record)]
30pub struct NotificationRoomInfo {
31    pub display_name: String,
32    pub avatar_url: Option<String>,
33    pub canonical_alias: Option<String>,
34    pub topic: Option<String>,
35    pub join_rule: Option<JoinRule>,
36    pub joined_members_count: u64,
37    pub is_encrypted: Option<bool>,
38    pub is_direct: bool,
39    pub is_space: bool,
40}
41
42#[derive(uniffi::Record)]
43pub struct NotificationItem {
44    pub event: NotificationEvent,
45
46    pub sender_info: NotificationSenderInfo,
47    pub room_info: NotificationRoomInfo,
48
49    /// Is the notification supposed to be at the "noisy" level?
50    /// Can be `None` if we couldn't determine this, because we lacked
51    /// information to create a push context.
52    pub is_noisy: Option<bool>,
53    pub has_mention: Option<bool>,
54    pub thread_id: Option<String>,
55
56    /// The push actions for this notification (notify, sound, highlight, etc.).
57    pub actions: Option<Vec<crate::notification_settings::Action>>,
58}
59
60impl NotificationItem {
61    fn from_inner(item: SdkNotificationItem) -> Self {
62        let event = match item.event {
63            SdkNotificationEvent::Timeline(event) => {
64                NotificationEvent::Timeline { event: Arc::new(TimelineEvent(event)) }
65            }
66            SdkNotificationEvent::Invite(event) => {
67                NotificationEvent::Invite { sender: event.sender.to_string() }
68            }
69        };
70        Self {
71            event,
72            sender_info: NotificationSenderInfo {
73                display_name: item.sender_display_name,
74                avatar_url: item.sender_avatar_url,
75                is_name_ambiguous: item.is_sender_name_ambiguous,
76            },
77            room_info: NotificationRoomInfo {
78                display_name: item.room_computed_display_name,
79                avatar_url: item.room_avatar_url,
80                canonical_alias: item.room_canonical_alias,
81                topic: item.room_topic,
82                join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
83                joined_members_count: item.joined_members_count,
84                is_encrypted: item.is_room_encrypted,
85                is_direct: item.is_direct_message_room,
86                is_space: item.is_space,
87            },
88            is_noisy: item.is_noisy,
89            has_mention: item.has_mention,
90            thread_id: item.thread_id.map(|t| t.to_string()),
91            actions: item
92                .actions
93                .map(|a| a.into_iter().filter_map(|action| action.try_into().ok()).collect()),
94        }
95    }
96}
97
98#[allow(clippy::large_enum_variant)]
99#[derive(uniffi::Enum)]
100pub enum NotificationStatus {
101    /// The event has been found and was not filtered out.
102    Event { item: NotificationItem },
103    /// The event couldn't be found in the network queries used to find it.
104    EventNotFound,
105    /// The event has been filtered out, either because of the user's push
106    /// rules, or because the user which triggered it is ignored by the
107    /// current user.
108    EventFilteredOut,
109}
110
111impl From<SdkNotificationStatus> for NotificationStatus {
112    fn from(item: SdkNotificationStatus) -> Self {
113        match item {
114            SdkNotificationStatus::Event(item) => {
115                NotificationStatus::Event { item: NotificationItem::from_inner(*item) }
116            }
117            SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
118            SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
119        }
120    }
121}
122
123#[allow(clippy::large_enum_variant)]
124#[derive(uniffi::Enum)]
125pub enum BatchNotificationResult {
126    /// We have more detailed information about the notification.
127    Ok { status: NotificationStatus },
128    /// An error occurred while trying to fetch the notification.
129    Error {
130        /// The error message observed while handling a specific notification.
131        message: String,
132    },
133}
134
135#[derive(uniffi::Object)]
136pub struct NotificationClient {
137    pub(crate) inner: SdkNotificationClient,
138
139    /// A reference to the FFI client.
140    ///
141    /// Note: we do this to make it so that the FFI `NotificationClient` keeps
142    /// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
143    /// would need to repeat the hack done in the FFI `Client::drop` method.
144    pub(crate) client: Arc<Client>,
145}
146
147#[matrix_sdk_ffi_macros::export]
148impl NotificationClient {
149    /// Fetches a room by its ID using the in-memory state store backed client.
150    ///
151    /// Useful to retrieve room information after running the limited
152    /// notification client sliding sync loop.
153    pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
154        let room_id = RoomId::parse(room_id)?;
155        let sdk_room = self.inner.get_room(&room_id);
156        let room = sdk_room
157            .map(|room| Arc::new(Room::new(room, self.client.utd_hook_manager.get().cloned())));
158        Ok(room)
159    }
160
161    /// Fetches the content of a notification.
162    ///
163    /// This will first try to get the notification using a short-lived sliding
164    /// sync, and if the sliding-sync can't find the event, then it'll use a
165    /// `/context` query to find the event with associated member information.
166    ///
167    /// An error result means that we couldn't resolve the notification; in that
168    /// case, a dummy notification may be displayed instead.
169    pub async fn get_notification(
170        &self,
171        room_id: String,
172        event_id: String,
173    ) -> Result<NotificationStatus, ClientError> {
174        let room_id = RoomId::parse(room_id)?;
175        let event_id = EventId::parse(event_id)?;
176
177        let item =
178            self.inner.get_notification(&room_id, &event_id).await.map_err(ClientError::from)?;
179
180        Ok(item.into())
181    }
182
183    /// Get several notification items in a single batch.
184    ///
185    /// Returns an error if the flow failed when preparing to fetch the
186    /// notifications, and a [`HashMap`] containing either a
187    /// [`BatchNotificationResult`], that indicates if the notification was
188    /// successfully fetched (in which case, it's a [`NotificationStatus`]), or
189    /// an error message if it couldn't be fetched.
190    pub async fn get_notifications(
191        &self,
192        requests: Vec<NotificationItemsRequest>,
193    ) -> Result<HashMap<String, BatchNotificationResult>, ClientError> {
194        let requests =
195            requests.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?;
196
197        let items = self.inner.get_notifications(&requests).await?;
198
199        let mut batch_result = HashMap::new();
200        for (key, value) in items.into_iter() {
201            let result = match value {
202                Ok(status) => BatchNotificationResult::Ok { status: status.into() },
203                Err(error) => BatchNotificationResult::Error { message: error.to_string() },
204            };
205            batch_result.insert(key.to_string(), result);
206        }
207
208        Ok(batch_result)
209    }
210}
211
212/// A request for notification items grouped by their room.
213#[derive(uniffi::Record)]
214pub struct NotificationItemsRequest {
215    room_id: String,
216    event_ids: Vec<String>,
217}
218
219impl NotificationItemsRequest {
220    /// The parsed [`OwnedRoomId`] to use with the SDK crates.
221    pub fn room_id(&self) -> Result<OwnedRoomId, ClientError> {
222        RoomId::parse(&self.room_id).map_err(ClientError::from)
223    }
224
225    /// The parsed [`OwnedEventId`] list to use with the SDK crates.
226    pub fn event_ids(&self) -> Result<Vec<OwnedEventId>, ClientError> {
227        self.event_ids
228            .iter()
229            .map(|id| EventId::parse(id).map_err(ClientError::from))
230            .collect::<Result<Vec<_>, _>>()
231    }
232}
233
234impl TryFrom<NotificationItemsRequest>
235    for matrix_sdk_ui::notification_client::NotificationItemsRequest
236{
237    type Error = ClientError;
238    fn try_from(value: NotificationItemsRequest) -> Result<Self, Self::Error> {
239        Ok(Self { room_id: value.room_id()?, event_ids: value.event_ids()? })
240    }
241}