fractal/session/model/notifications/
mod.rsuse gettextrs::gettext;
use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*};
use matrix_sdk::{sync::Notification, Room as MatrixRoom};
use ruma::{api::client::device::get_device, OwnedRoomId, RoomId};
use tracing::{debug, warn};
mod notifications_settings;
pub use self::notifications_settings::{
NotificationsGlobalSetting, NotificationsRoomSetting, NotificationsSettings,
};
use super::{IdentityVerification, Session, VerificationKey};
use crate::{
gettext_f, intent,
prelude::*,
spawn_tokio,
utils::matrix::{get_event_body, AnySyncOrStrippedTimelineEvent},
Application, Window,
};
mod imp {
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::Notifications)]
pub struct Notifications {
#[property(get, set = Self::set_session, explicit_notify, nullable)]
pub session: glib::WeakRef<Session>,
pub push: RefCell<HashMap<OwnedRoomId, HashSet<String>>>,
pub identity_verifications: RefCell<HashMap<VerificationKey, String>>,
#[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 {
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! {
pub struct Notifications(ObjectSubclass<imp::Notifications>);
}
impl Notifications {
pub fn new() -> Self {
glib::Object::new()
}
pub fn enabled(&self) -> bool {
let settings = self.settings();
settings.account_enabled() && settings.session_enabled()
}
fn send_notification(
id: &str,
title: &str,
body: &str,
default_action: (&str, glib::Variant),
icon: Option<&gdk::Texture>,
) {
let notification = gio::Notification::new(title);
notification.set_category(Some("im.received"));
notification.set_priority(gio::NotificationPriority::High);
notification.set_body(Some(body));
let (action, target_value) = default_action;
notification.set_default_action_and_target_value(action, Some(&target_value));
if let Some(notification_icon) = icon {
notification.set_icon(notification_icon);
}
Application::default().send_notification(Some(id), ¬ification);
}
pub async fn show_push(&self, matrix_notification: Notification, matrix_room: MatrixRoom) {
if !self.enabled() {
return;
}
let Some(session) = self.session() else {
return;
};
let app = Application::default();
let window = app.active_window().and_downcast::<Window>();
let session_id = session.session_id();
let room_id = matrix_room.room_id();
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() == room_id)
}) {
return;
}
let Some(room) = session.room_list().get(room_id) else {
warn!("Could not display notification for missing room {room_id}",);
return;
};
let event = match AnySyncOrStrippedTimelineEvent::from_raw(&matrix_notification.event) {
Ok(event) => event,
Err(error) => {
warn!(
"Could not display notification for unrecognized event in room {room_id}: {error}",
);
return;
}
};
let is_direct = room.direct_member().is_some();
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) => {
warn!("Could not get member for notification: {error}");
None
}
};
let sender_name = sender.as_ref().map_or_else(
|| sender_id.localpart().to_owned(),
|member| {
let name = member.name();
if member.name_ambiguous() {
format!("{name} ({})", member.user_id())
} else {
name.to_owned()
}
},
);
let Some(body) = get_event_body(&event, &sender_name, session.user_id(), !is_direct) else {
debug!("Received notification for event of unexpected type {event:?}",);
return;
};
let room_id = room.room_id().to_owned();
let event_id = event.event_id();
let payload = intent::ShowRoomPayload {
session_id: session_id.to_owned(),
room_id: room_id.clone(),
};
let icon = room.avatar_data().as_notification_icon().await;
let id = if let Some(event_id) = event_id {
format!("{session_id}//{room_id}//{event_id}")
} else {
let random_id = glib::uuid_string_random();
format!("{session_id}//{room_id}//{random_id}")
};
Self::send_notification(
&id,
&room.display_name(),
&body,
("app.show-room", payload.to_variant()),
icon.as_ref(),
);
self.imp()
.push
.borrow_mut()
.entry(room_id)
.or_default()
.insert(id);
}
pub async fn show_in_room_identity_verification(&self, verification: &IdentityVerification) {
if !self.enabled() {
return;
}
let Some(session) = self.session() else {
return;
};
let Some(room) = verification.room() else {
return;
};
let room_id = room.room_id().to_owned();
let session_id = session.session_id();
let flow_id = verification.flow_id();
let user = verification.user();
let user_id = user.user_id();
let title = gettext("Verification Request");
let body = gettext_f(
"{user} sent a verification request",
&[("user", &user.display_name())],
);
let payload = intent::ShowIdentityVerificationPayload {
session_id: session_id.to_owned(),
key: verification.key(),
};
let icon = user.avatar_data().as_notification_icon().await;
let id = format!("{session_id}//{room_id}//{user_id}//{flow_id}");
Self::send_notification(
&id,
&title,
&body,
("app.show-identity-verification", payload.to_variant()),
icon.as_ref(),
);
self.imp()
.identity_verifications
.borrow_mut()
.insert(verification.key(), id);
}
pub async fn show_to_device_identity_verification(&self, verification: &IdentityVerification) {
if !self.enabled() {
return;
}
let Some(session) = self.session() else {
return;
};
let Some(other_device_id) = verification.other_device_id() else {
return;
};
let session_id = session.session_id();
let flow_id = verification.flow_id();
let client = session.client();
let request = get_device::v3::Request::new(other_device_id.clone());
let handle = spawn_tokio!(async move { client.send(request).await });
let display_name = match handle.await.unwrap() {
Ok(res) => res.device.display_name,
Err(error) => {
warn!("Could not get device for notification: {error}");
None
}
};
let display_name = display_name
.as_deref()
.unwrap_or_else(|| other_device_id.as_str());
let title = gettext("Login Request From Another Session");
let body = gettext_f(
"Verify your new session “{name}”",
&[("name", display_name)],
);
let payload = intent::ShowIdentityVerificationPayload {
session_id: session_id.to_owned(),
key: verification.key(),
};
let id = format!("{session_id}//{other_device_id}//{flow_id}");
Self::send_notification(
&id,
&title,
&body,
("app.show-identity-verification", payload.to_variant()),
None,
);
self.imp()
.identity_verifications
.borrow_mut()
.insert(verification.key(), id);
}
pub fn withdraw_all_for_room(&self, room_id: &RoomId) {
if let Some(notifications) = self.imp().push.borrow_mut().remove(room_id) {
let app = Application::default();
for id in notifications {
app.withdraw_notification(&id);
}
}
}
pub fn withdraw_identity_verification(&self, key: &VerificationKey) {
if let Some(id) = self.imp().identity_verifications.borrow_mut().remove(key) {
let app = Application::default();
app.withdraw_notification(&id);
}
}
pub fn clear(&self) {
let app = Application::default();
for id in self.imp().push.take().values().flatten() {
app.withdraw_notification(id);
}
for id in self.imp().identity_verifications.take().values() {
app.withdraw_notification(id);
}
}
}
impl Default for Notifications {
fn default() -> Self {
Self::new()
}
}