fractal/src/session/model/notifications/mod.rs

238 lines
7.3 KiB
Rust

use gtk::{
gio,
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use ruma::{
api::client::push::get_notifications::v3::Notification, EventId, OwnedEventId, OwnedRoomId,
RoomId,
};
use tracing::{debug, error, warn};
mod notifications_settings;
pub use self::notifications_settings::{
NotificationsGlobalSetting, NotificationsRoomSetting, NotificationsSettings,
};
use super::{Room, Session};
use crate::{
application::AppShowRoomPayload, prelude::*, spawn, spawn_tokio, utils::matrix::get_event_body,
Application, Window,
};
mod imp {
use std::{cell::RefCell, collections::HashMap};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::Notifications)]
pub struct Notifications {
/// The current session.
#[property(get, set = Self::set_session, explicit_notify, nullable)]
pub session: glib::WeakRef<Session>,
/// A map of room ID to list of event IDs for which a notification was
/// sent to the system.
pub list: RefCell<HashMap<OwnedRoomId, Vec<OwnedEventId>>>,
/// The notifications settings for this session.
#[property(get)]
pub settings: NotificationsSettings,
}
#[glib::object_subclass]
impl ObjectSubclass for Notifications {
const NAME: &'static str = "Notifications";
type Type = super::Notifications;
}
#[glib::derived_properties]
impl ObjectImpl for Notifications {}
impl Notifications {
/// Set the current session.
fn set_session(&self, session: Option<&Session>) {
if self.session.upgrade().as_ref() == session {
return;
}
self.session.set(session);
self.obj().notify_session();
self.settings.set_session(session);
}
}
}
glib::wrapper! {
/// The notifications of a `Session`.
pub struct Notifications(ObjectSubclass<imp::Notifications>);
}
impl Notifications {
pub fn new() -> Self {
glib::Object::new()
}
/// Ask the system to show the given notification, if applicable.
///
/// The notification won't be shown if the application is active and this
/// session is displayed.
pub fn show(&self, matrix_notification: Notification) {
spawn!(clone!(@weak self as obj => async move {
obj.show_inner(matrix_notification).await;
}));
}
async fn show_inner(&self, matrix_notification: Notification) {
let Some(session) = self.session() else {
return;
};
// Don't show notifications if they are disabled.
if !session.settings().notifications_enabled() {
return;
}
let app = Application::default();
let window = app.active_window().and_downcast::<Window>();
let session_id = session.session_id();
// Don't show notifications for the current room in the current session if the
// window is active.
if window.is_some_and(|w| {
w.is_active()
&& w.current_session_id().as_deref() == Some(session_id)
&& w.session_view()
.selected_room()
.is_some_and(|r| r.room_id() == matrix_notification.room_id)
}) {
return;
}
let Some(room) = session.room_list().get(&matrix_notification.room_id) else {
warn!(
"Could not display notification for missing room {}",
matrix_notification.room_id
);
return;
};
let event = match matrix_notification.event.deserialize() {
Ok(event) => event,
Err(error) => {
warn!(
"Could not display notification for unrecognized event in room {}: {error}",
matrix_notification.room_id
);
return;
}
};
let matrix_room = room.matrix_room();
let sender_id = event.sender();
let owned_sender_id = sender_id.to_owned();
let handle =
spawn_tokio!(async move { matrix_room.get_member_no_sync(&owned_sender_id).await });
let sender = match handle.await.unwrap() {
Ok(member) => member,
Err(error) => {
error!("Failed to get member for notification: {error}");
None
}
};
let sender_name = sender
.as_ref()
.and_then(|m| m.display_name())
.unwrap_or_else(|| sender_id.localpart());
let body = match get_event_body(&event, sender_name) {
Some(body) => body,
None => {
debug!("Received notification for event of unexpected type {event:?}",);
return;
}
};
let room_id = room.room_id();
let event_id = event.event_id();
let notification = gio::Notification::new(&room.display_name());
notification.set_priority(gio::NotificationPriority::High);
let payload = AppShowRoomPayload {
session_id: session_id.to_owned(),
room_id: room_id.to_owned(),
};
notification
.set_default_action_and_target_value("app.show-room", Some(&payload.to_variant()));
notification.set_body(Some(&body));
if let Some(icon) = room.avatar_data().as_notification_icon() {
notification.set_icon(&icon);
}
let id = notification_id(session_id, room_id, event_id);
app.send_notification(Some(&id), &notification);
self.imp()
.list
.borrow_mut()
.entry(room_id.to_owned())
.or_default()
.push(event_id.to_owned());
}
/// Ask the system to remove the known notifications for the given room.
///
/// Only the notifications that were shown since the application's startup
/// are known, older ones might still be present.
pub fn withdraw_all_for_room(&self, room: &Room) {
let Some(session) = self.session() else {
return;
};
let room_id = room.room_id();
if let Some(notifications) = self.imp().list.borrow_mut().remove(room_id) {
let app = Application::default();
for event_id in notifications {
let id = notification_id(session.session_id(), room_id, &event_id);
app.withdraw_notification(&id);
}
}
}
/// Ask the system to remove all the known notifications for this session.
///
/// Only the notifications that were shown since the application's startup
/// are known, older ones might still be present.
pub fn clear(&self) {
let Some(session) = self.session() else {
return;
};
let app = Application::default();
for (room_id, notifications) in self.imp().list.take() {
for event_id in notifications {
let id = notification_id(session.session_id(), &room_id, &event_id);
app.withdraw_notification(&id);
}
}
}
}
impl Default for Notifications {
fn default() -> Self {
Self::new()
}
}
fn notification_id(session_id: &str, room_id: &RoomId, event_id: &EventId) -> String {
format!("{session_id}//{room_id}//{event_id}")
}