matrix_ui_serializable/room/
notifications.rs1use 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
24static TOAST_NOTIFICATION: SegQueue<ToastNotificationRequest> = SegQueue::new();
29
30pub 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
49pub 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
65pub 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 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}