use crate::schema::{NotificationImportance, NotificationPermission, NotificationPresentation};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Notification {
pub(crate) key: String,
pub(crate) title: String,
pub(crate) body: String,
pub(crate) channel: String,
pub(crate) actions: [String; 2],
pub(crate) importance: NotificationImportance,
pub(crate) presentation: NotificationPresentation,
}
impl Notification {
pub fn new(title: impl Into<String>) -> Self {
Self {
key: String::new(),
title: title.into(),
body: String::new(),
channel: String::new(),
actions: [String::new(), String::new()],
importance: NotificationImportance::Normal,
presentation: NotificationPresentation::Always,
}
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = body.into();
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = key.into();
self
}
pub fn channel(mut self, channel: impl Into<String>) -> Self {
self.channel = channel.into();
self
}
pub fn action_1(mut self, label: impl Into<String>) -> Self {
self.actions[0] = label.into();
self
}
pub fn action_2(mut self, label: impl Into<String>) -> Self {
self.actions[1] = label.into();
self
}
pub fn importance(mut self, importance: NotificationImportance) -> Self {
self.importance = importance;
self
}
pub fn presentation(mut self, presentation: NotificationPresentation) -> Self {
self.presentation = presentation;
self
}
pub fn post(self) {
submit(NotificationCommand::Post(self));
}
}
pub fn withdraw_notification(key: impl Into<String>) {
submit(NotificationCommand::Withdraw(key.into()));
}
pub fn request_notification_permission() {
submit(NotificationCommand::RequestPermission);
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct NotificationSender {
_private: (),
}
impl NotificationSender {
pub const fn new() -> Self {
Self { _private: () }
}
pub fn post(&self, notification: Notification) {
submit(NotificationCommand::Post(notification));
}
pub fn withdraw(&self, key: impl Into<String>) {
submit(NotificationCommand::Withdraw(key.into()));
}
pub fn request_permission(&self) {
submit(NotificationCommand::RequestPermission);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum NotificationCommand {
Post(Notification),
Withdraw(String),
RequestPermission,
}
static QUEUE: Mutex<Vec<NotificationCommand>> = Mutex::new(Vec::new());
static PENDING: AtomicBool = AtomicBool::new(false);
fn queue() -> MutexGuard<'static, Vec<NotificationCommand>> {
QUEUE.lock().unwrap_or_else(|poison| poison.into_inner())
}
fn submit(command: NotificationCommand) {
queue().push(command);
PENDING.store(true, Ordering::Release);
if !crate::boundary::in_host_call() {
crate::boundary::request_frame_for_notifications();
}
}
pub(crate) fn drain(mut emit: impl FnMut(&NotificationCommand)) -> usize {
if !PENDING.swap(false, Ordering::AcqRel) {
return 0;
}
let mut taken = std::mem::take(&mut *queue());
let mut posts = 0;
for command in &taken {
if matches!(command, NotificationCommand::Post(_)) {
posts += 1;
}
emit(command);
}
taken.clear();
let mut guard = queue();
if guard.is_empty() {
*guard = taken;
} else {
PENDING.store(true, Ordering::Release);
}
posts
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NotificationActivation {
pub key: String,
pub action: u32,
}
impl NotificationActivation {
pub fn is_body(&self) -> bool {
self.action == 0
}
pub fn action_button(&self) -> Option<u32> {
(self.action != 0).then_some(self.action)
}
}
struct Subscriber {
id: u64,
notify: Arc<dyn Fn() + Send + Sync>,
}
type ActivationHandler = Rc<dyn Fn(NotificationActivation)>;
thread_local! {
static PERMISSION: Cell<NotificationPermission> =
const { Cell::new(NotificationPermission::NotDetermined) };
static PERMISSION_SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
static ACTIVATION_HANDLERS: RefCell<Vec<(u64, ActivationHandler)>> =
const { RefCell::new(Vec::new()) };
static NEXT_ID: Cell<u64> = const { Cell::new(1) };
}
static EXPLAINED: AtomicBool = AtomicBool::new(false);
static POSTED: AtomicBool = AtomicBool::new(false);
fn next_id() -> u64 {
NEXT_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
})
}
pub fn notification_permission() -> NotificationPermission {
PERMISSION.with(Cell::get)
}
pub(crate) fn publish_permission(state: NotificationPermission) -> bool {
if state == NotificationPermission::Unsupported && POSTED.load(Ordering::Acquire) {
explain_unsupported();
}
if PERMISSION.with(Cell::get) == state {
return false;
}
PERMISSION.with(|current| current.set(state));
PERMISSION_SUBSCRIBERS.with_borrow(|subscribers| {
for subscriber in subscribers {
(subscriber.notify)();
}
!subscribers.is_empty()
})
}
pub(crate) fn note_posted(posts: usize) {
if posts == 0 {
return;
}
POSTED.store(true, Ordering::Release);
if notification_permission() == NotificationPermission::Unsupported {
explain_unsupported();
}
}
fn explain_unsupported() {
if cfg!(debug_assertions) && !EXPLAINED.swap(true, Ordering::AcqRel) {
eprintln!("compose-rust: {}", unsupported_reason());
}
}
pub fn unsupported_reason() -> &'static str {
if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
"notifications are not shown in this run: it has no bundle identifier, so the \
notification centre will not take them. A signed application bundle has one; a \
development run of a bare executable does not."
} else if cfg!(target_os = "windows") {
"notifications are not shown in this run: no Start menu shortcut carries an \
AppUserModelID for this executable, and toasts need one. An installed package \
(MSIX, or an installer that makes the shortcut) has it; an unpacked executable \
does not."
} else if cfg!(target_os = "android") {
"notifications are not shown in this run: the system has no notification service \
for this application."
} else if cfg!(target_family = "wasm") {
"notifications are not shown in this run: the page is not a secure context (HTTPS \
or localhost), or this browser has no Notifications API."
} else {
"notifications are not shown in this run: nothing on the session bus owns \
org.freedesktop.Notifications, so there is no notification daemon to show them."
}
}
pub(crate) fn activate(activation: NotificationActivation) -> bool {
let handlers: Vec<ActivationHandler> = ACTIVATION_HANDLERS
.with_borrow(|handlers| handlers.iter().map(|(_, h)| h.clone()).collect());
for handler in &handlers {
handler(activation.clone());
}
!handlers.is_empty()
}
#[doc(hidden)]
pub fn reset_notifications() {
queue().clear();
PENDING.store(false, Ordering::Release);
PERMISSION.with(|current| current.set(NotificationPermission::NotDetermined));
PERMISSION_SUBSCRIBERS.with_borrow_mut(Vec::clear);
ACTIVATION_HANDLERS.with_borrow_mut(Vec::clear);
POSTED.store(false, Ordering::Release);
EXPLAINED.store(false, Ordering::Release);
}
pub struct PermissionSubscription {
id: u64,
}
pub fn subscribe_permission(notify: Arc<dyn Fn() + Send + Sync>) -> PermissionSubscription {
let id = next_id();
PERMISSION_SUBSCRIBERS
.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
PermissionSubscription { id }
}
impl Drop for PermissionSubscription {
fn drop(&mut self) {
PERMISSION_SUBSCRIBERS.with_borrow_mut(|subscribers| {
subscribers.retain(|subscriber| subscriber.id != self.id);
});
}
}
pub struct ActivationSubscription {
id: u64,
}
impl Drop for ActivationSubscription {
fn drop(&mut self) {
ACTIVATION_HANDLERS.with_borrow_mut(|handlers| handlers.retain(|(id, _)| *id != self.id));
}
}
pub fn on_activation(handler: Rc<dyn Fn(NotificationActivation)>) -> ActivationSubscription {
let id = next_id();
ACTIVATION_HANDLERS.with_borrow_mut(|handlers| handlers.push((id, handler)));
ActivationSubscription { id }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fr36_a_notification_says_nothing_it_was_not_told() {
let notification = Notification::new("title");
assert_eq!(notification.key, "");
assert_eq!(notification.body, "");
assert_eq!(notification.channel, "");
assert_eq!(notification.actions, [String::new(), String::new()]);
assert_eq!(notification.importance, NotificationImportance::Normal);
assert_eq!(notification.presentation, NotificationPresentation::Always);
}
#[test]
fn fr36_an_activation_knows_whether_a_button_was_pressed() {
let body = NotificationActivation {
key: "k".to_owned(),
action: 0,
};
let button = NotificationActivation {
key: "k".to_owned(),
action: 2,
};
assert!(body.is_body());
assert_eq!(body.action_button(), None);
assert!(!button.is_body());
assert_eq!(button.action_button(), Some(2));
}
}