diff options
Diffstat (limited to 'widget/notifications/Notifications.tsx')
-rw-r--r-- | widget/notifications/Notifications.tsx | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/widget/notifications/Notifications.tsx b/widget/notifications/Notifications.tsx new file mode 100644 index 0000000..6708b96 --- /dev/null +++ b/widget/notifications/Notifications.tsx @@ -0,0 +1,108 @@ +import { GLib, Variable } from "astal"; +import { bind, Subscribable } from "astal/binding"; +import { Astal, Gdk, Gtk } from "astal/gtk4"; +import AstalNotifd from "gi://AstalNotifd?version=0.1"; + +const notificationTimeout: number = 5000; + +class NotificationHandler implements Subscribable<Gtk.Widget[]> { + private notifications: Variable<Gtk.Widget[]> = Variable([]); + private notificationsMap: Map<number, Gtk.Widget> = new Map(); + + constructor() { + const notifd = AstalNotifd.get_default(); + + notifd.connect("notified", (_source, id, _replaced) => { + const n = notifd.get_notification(id); + this.create(n); + setTimeout(() => this.remove(id), notificationTimeout); + }); + + notifd.connect("resolved", (_source, id, _reason) => { + this.remove(id); + }); + } + + private rerender() { + this.notifications.set([...this.notificationsMap.values()].reverse()); + } + + private create(n: AstalNotifd.Notification) { + const notification = Notification(n); + this.notificationsMap.get(n.id)?.emit("destroy"); + this.notificationsMap.set(n.id, notification); + this.rerender(); + } + + private remove(id: number) { + this.notificationsMap.get(id)?.emit("destroy"); + this.notificationsMap.delete(id); + this.rerender(); + } + + subscribe(callback: (value: Gtk.Widget[]) => void): () => void { + return this.notifications.subscribe(callback); + } + + get(): Gtk.Widget[] { + return this.notifications.get(); + } +} + +function getUrgencyClass(n: AstalNotifd.Notification): string { + switch (n.urgency) { + case AstalNotifd.Urgency.LOW: + return "low"; + case AstalNotifd.Urgency.CRITICAL: + return "critical"; + case AstalNotifd.Urgency.NORMAL: + default: + return "normal"; + } +} + +function Notification(n: AstalNotifd.Notification) { + const appName = n.appName; + const time = GLib.DateTime.new_from_unix_local(n.time); + + return <box cssClasses={["Notification", getUrgencyClass(n)]} vertical> + <box cssClasses={["Header"]}> + <label cssClasses={["Application"]} halign={Gtk.Align.START} label={appName || "Unknown"} /> + <label cssClasses={["Time"]} hexpand halign={Gtk.Align.END} label={time.format("%I:%M:%S %p")!} /> + <button cssClasses={["Close"]} onClicked={() => n.dismiss()}><image iconName="window-close-symbolic" /></button> + </box> + <Gtk.Separator visible /> + <box cssClasses={["Contents"]}> + <box vertical> + <label cssClasses={["Summary"]} halign={Gtk.Align.START} label={n.summary} /> + <label cssClasses={["Body"]} useMarkup wrap maxWidthChars={0} justify={Gtk.Justification.FILL} label={n.body} /> + </box> + </box> + + {n.get_actions().length > 0 && <> + <Gtk.Separator visible /> + <box cssClasses={["Actions"]}> + {n.get_actions().map(({ label, id }) => <button hexpand onClicked={() => n.invoke(id)}><label label={label} /></button>)} + </box> + </>} + </box>; +} + +export default function(gdkmonitor: Gdk.Monitor) { + const { TOP, RIGHT } = Astal.WindowAnchor; + const notifications = new NotificationHandler(); + + return <window + // NOTE: is required due to last notification being displayed all the time + visible={bind(notifications).as(v => v.length != 0)} + + name={"Notifications"} + cssClasses={["Notifications"]} + gdkmonitor={gdkmonitor} + exclusivity={Astal.Exclusivity.EXCLUSIVE} + anchor={TOP | RIGHT}> + <box vertical> + {bind(notifications)} + </box> + </window> +} |