import GLib from "gi://GLib" import { App, Astal, Gdk, Gtk } from "astal/gtk3" import Notifd from "gi://AstalNotifd" import { Variable, bind } from "astal" import { MAX_TIMEOUT } from "../notif-config" const notifd = Notifd.get_default() export const dndIcon = Variable.derive([bind(notifd, "dontDisturb")], (dnd) => dnd ? "notifications-disabled-symbolic" : "preferences-system-notifications-symbolic", ) // every notification received this session is kept until it is explicitly // cleared, so popups that time out remain in the dropdown's list. only // notifications dismissed directly from their popup skip the list. export const history = Variable([]) export const notifCount = bind(history).as((ns) => ns.length) export function DndToggle() { return ( ) } // popups are timed out entirely client-side: the daemon never resolves them // on its own, so a popup under the pointer can be kept alive indefinitely notifd.ignore_timeout = true notifd.default_timeout = -1 // notifications restored from the daemon's cache on startup are stale for (const n of notifd.notifications) { n.dismiss() } notifd.connect("notified", (_, id: number) => { const n = notifd.get_notification(id) if (!n) return // re-notified ids update the existing entry instead of adding a duplicate if (!history.get().some((m) => m.id === id)) { history.set([...history.get(), n]) } }) // ids of notifications whose popup is currently under the pointer const hovered = new Set() // enforce our own cap for popups, honoring shorter app-requested timeouts. // critical notifications are exempt and stay until dismissed, as are popups // being hovered. GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { const now = GLib.DateTime.new_now_local().to_unix() for (const n of [...notifd.notifications]) { if (n.urgency === Notifd.Urgency.CRITICAL || hovered.has(n.id)) continue const timeout = n.expire_timeout > 0 ? Math.min(n.expire_timeout, MAX_TIMEOUT) : MAX_TIMEOUT if (now - n.time >= timeout / 1000) n.dismiss() } return GLib.SOURCE_CONTINUE }) // drop a notification from the list and withdraw its popup; used when a // notification is dismissed directly instead of timing out export function dismissNotification(n: Notifd.Notification) { hovered.delete(n.id) history.set(history.get().filter((m) => m.id !== n.id)) notifd.get_notification(n.id)?.dismiss() } export function clearNotifications() { for (const n of [...notifd.notifications]) { dismissNotification(n) } history.set([]) } function formatTime(n: Notifd.Notification) { return GLib.DateTime.new_from_unix_local(n.time).format("%H:%M")! } export function NotificationCard(n: Notifd.Notification, toast = false) { const critical = n.urgency === Notifd.Urgency.CRITICAL return ( {n.image !== "" && ( )} {n.actions.filter((a) => a.label).length > 0 && ( {n.actions .filter((a) => a.label) .map((a) => ( ))} )} ) } export default function Notifications(gdkmonitor: Gdk.Monitor) { const { TOP, RIGHT } = Astal.WindowAnchor return ( ns.length > 0)} onNotifyVisible={(self) => { if (!self.visible) hovered.clear() }} > {bind(notifd, "notifications").as((ns) => [...ns] .sort((a, b) => b.time - a.time) .map((n) => NotificationCard(n, true)), )} ) }