summaryrefslogtreecommitdiff
path: root/widget/notifications/Notifications.tsx
blob: 6708b96f23126324c00790a6c8fa7de1431cc489 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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>
}