matrix_ui_serializable/room/
notifications.rs

1use std::time::SystemTime;
2
3use crossbeam_queue::SegQueue;
4use matrix_sdk::{
5    Client, Room,
6    deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
7    notification_settings::{IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode},
8    ruma::{
9        MilliSecondsSinceUnixEpoch,
10        api::client::push::{Pusher, PusherIds, PusherInit, PusherKind},
11        events::{AnyMessageLikeEventContent, AnySyncTimelineEvent, room::message::MessageType},
12        push::HttpPusherData,
13        serde::Raw,
14    },
15    sync::Notification,
16};
17use unicode_segmentation::UnicodeSegmentation;
18
19use crate::{
20    init::singletons::{UIUpdateMessage, broadcast_event, get_event_bridge},
21    models::events::{EmitEvent, OsNotificationRequest, ToastNotificationRequest},
22};
23
24//
25// TOAST Notifications (in app)
26//
27
28static TOAST_NOTIFICATION: SegQueue<ToastNotificationRequest> = SegQueue::new();
29
30/// Displays a new toast notification with the given message.
31///
32/// Toast notifications will be shown in the order they were enqueued.
33pub fn enqueue_toast_notification(notification: ToastNotificationRequest) {
34    TOAST_NOTIFICATION.push(notification);
35    broadcast_event(UIUpdateMessage::RefreshUI).expect("Couldn't broadcast event to UI");
36}
37
38pub async fn process_toast_notifications() -> anyhow::Result<()> {
39    if TOAST_NOTIFICATION.is_empty() {
40        return Ok(());
41    };
42    let event_bridge = get_event_bridge()?;
43    while let Some(notif) = TOAST_NOTIFICATION.pop() {
44        event_bridge.emit(EmitEvent::ToastNotification(notif));
45    }
46    Ok(())
47}
48
49//
50// OS Notifications (push notifications for mobiles)
51//
52
53pub async fn register_notifications(
54    client: &Client,
55    _mobile_push_config: Option<MobilePushNotificationConfig>,
56) {
57    #[cfg(any(target_os = "android", target_os = "ios"))]
58    if _mobile_push_config.is_some() {
59        _register_mobile_push_notifications(&client, _mobile_push_config.unwrap()).await;
60    }
61    #[cfg(not(any(target_os = "android", target_os = "ios")))]
62    register_os_desktop_notifications(&client).await;
63}
64
65/// The required parameters to register Push Notifications for a mobile app.
66pub struct MobilePushNotificationConfig {
67    token: String,
68    sygnal_gateway_url: String,
69    app_id: String,
70}
71
72impl MobilePushNotificationConfig {
73    pub fn new(token: String, sygnal_gateway_url: String, app_id: String) -> Self {
74        Self {
75            token,
76            sygnal_gateway_url,
77            app_id,
78        }
79    }
80
81    pub fn token(&self) -> &str {
82        &self.token
83    }
84
85    pub fn sygnal_gateway_url(&self) -> &str {
86        &self.sygnal_gateway_url
87    }
88
89    pub fn app_id(&self) -> &str {
90        &self.app_id
91    }
92}
93
94pub async fn _register_mobile_push_notifications(
95    client: &Client,
96    config: MobilePushNotificationConfig,
97) {
98    let MobilePushNotificationConfig {
99        token,
100        sygnal_gateway_url,
101        app_id,
102    } = config;
103
104    let http_pusher = HttpPusherData::new(sygnal_gateway_url);
105
106    let pusher_ids = PusherIds::new(token, app_id);
107
108    let pusher = PusherInit {
109        ids: pusher_ids,
110        app_display_name: "Matrix Svelte Client".to_string(),
111        device_display_name: "My device".to_string(),
112        profile_tag: None,
113        kind: PusherKind::Http(http_pusher),
114        lang: "en".to_string(),
115    };
116
117    let pusher: Pusher = pusher.into();
118
119    let _ = client
120        .pusher()
121        .set(pusher)
122        .await
123        .expect("Couldn't set the notification pusher correcly");
124}
125
126pub async fn register_os_desktop_notifications(client: &Client) {
127    let server_settings = client.notification_settings().await;
128    let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
129        return;
130    };
131
132    client
133        .register_notification_handler(
134            move |notification: Notification, room: Room, client: Client| {
135                let server_settings = server_settings.clone();
136                async move {
137                    let mode = global_or_room_mode(&server_settings, &room).await;
138                    if mode == RoomNotificationMode::Mute {
139                        return;
140                    }
141
142                    match notification.event {
143                        RawAnySyncOrStrippedTimelineEvent::Sync(e) => {
144                            match parse_full_notification(e, room, true).await {
145                                Ok((summary, body, server_ts)) => {
146                                    if server_ts < startup_ts {
147                                        return;
148                                    }
149
150                                    if is_missing_mention(&body, mode, &client) {
151                                        return;
152                                    }
153
154                                    let event_bridge =
155                                        get_event_bridge().expect("Event bridge is not init");
156
157                                    event_bridge.emit(EmitEvent::OsNotification(
158                                        OsNotificationRequest::new(summary, body),
159                                    ));
160                                }
161                                Err(err) => {
162                                    eprintln!("Failed to extract notification data: {err}")
163                                }
164                            }
165                        }
166                        // Stripped events may be dropped silently because they're
167                        // only relevant if we're not in a room, and we presumably
168                        // don't want notifications for rooms we're not in.
169                        RawAnySyncOrStrippedTimelineEvent::Stripped(_) => (),
170                    }
171                }
172            },
173        )
174        .await;
175}
176
177pub async fn global_or_room_mode(
178    settings: &NotificationSettings,
179    room: &Room,
180) -> RoomNotificationMode {
181    let room_mode = settings
182        .get_user_defined_room_notification_mode(room.room_id())
183        .await;
184    if let Some(mode) = room_mode {
185        return mode;
186    }
187    let is_one_to_one = match room.is_direct().await {
188        Ok(true) => IsOneToOne::Yes,
189        _ => IsOneToOne::No,
190    };
191    let is_encrypted = match room.encryption_state().is_encrypted() {
192        true => IsEncrypted::Yes,
193        false => IsEncrypted::No,
194    };
195    settings
196        .get_default_room_notification_mode(is_encrypted, is_one_to_one)
197        .await
198}
199
200fn is_missing_mention(body: &Option<String>, mode: RoomNotificationMode, client: &Client) -> bool {
201    if let Some(body) = body {
202        if mode == RoomNotificationMode::MentionsAndKeywordsOnly {
203            let mentioned = match client.user_id() {
204                Some(user_id) => body.contains(user_id.localpart()),
205                _ => false,
206            };
207            return !mentioned;
208        }
209    }
210    false
211}
212
213pub async fn parse_full_notification(
214    event: Raw<AnySyncTimelineEvent>,
215    room: Room,
216    show_body: bool,
217) -> anyhow::Result<(String, Option<String>, MilliSecondsSinceUnixEpoch)> {
218    let event = event.deserialize().map_err(anyhow::Error::from)?;
219
220    let server_ts = event.origin_server_ts();
221
222    let sender_id = event.sender();
223    let sender = room
224        .get_member_no_sync(sender_id)
225        .await
226        .map_err(anyhow::Error::from)?;
227
228    let sender_name = sender
229        .as_ref()
230        .and_then(|m| m.display_name())
231        .unwrap_or_else(|| sender_id.localpart());
232
233    let summary = if let Some(room_name) = room.cached_display_name() {
234        if room.is_direct().await.map_err(anyhow::Error::from)?
235            && sender_name == room_name.to_string()
236        {
237            sender_name.to_string()
238        } else {
239            format!("{sender_name} in {room_name}")
240        }
241    } else {
242        sender_name.to_string()
243    };
244
245    let body = if show_body {
246        event_notification_body(&event, sender_name).map(truncate)
247    } else {
248        None
249    };
250
251    return Ok((summary, body, server_ts));
252}
253
254pub fn event_notification_body(event: &AnySyncTimelineEvent, sender_name: &str) -> Option<String> {
255    let AnySyncTimelineEvent::MessageLike(event) = event else {
256        return None;
257    };
258
259    match event.original_content()? {
260        AnyMessageLikeEventContent::RoomMessage(message) => {
261            let body = match message.msgtype {
262                MessageType::Audio(_) => {
263                    format!("{sender_name} sent an audio file.")
264                }
265                MessageType::Emote(content) => content.body,
266                MessageType::File(_) => {
267                    format!("{sender_name} sent a file.")
268                }
269                MessageType::Image(_) => {
270                    format!("{sender_name} sent an image.")
271                }
272                MessageType::Location(_) => {
273                    format!("{sender_name} sent their location.")
274                }
275                MessageType::Notice(content) => content.body,
276                MessageType::ServerNotice(content) => content.body,
277                MessageType::Text(content) => content.body,
278                MessageType::Video(_) => {
279                    format!("{sender_name} sent a video.")
280                }
281                MessageType::VerificationRequest(_) => {
282                    format!("{sender_name} sent a verification request.")
283                }
284                _ => {
285                    format!("[Unknown message type: {:?}]", &message.msgtype)
286                }
287            };
288            Some(body)
289        }
290        AnyMessageLikeEventContent::Sticker(_) => Some(format!("{sender_name} sent a sticker.")),
291        _ => None,
292    }
293}
294
295fn truncate(s: String) -> String {
296    static MAX_LENGTH: usize = 5000;
297    if s.graphemes(true).count() > MAX_LENGTH {
298        let truncated: String = s.graphemes(true).take(MAX_LENGTH).collect();
299        truncated + "..."
300    } else {
301        s
302    }
303}