summaryrefslogtreecommitdiff
path: root/widget/notifications/Notifications.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'widget/notifications/Notifications.tsx')
-rw-r--r--widget/notifications/Notifications.tsx108
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>
+}