import { GLib, register, timeout } from "astal"; import { Astal, Gtk, Widget } from "astal/gtk3"; import AstalNotifd from "gi://AstalNotifd"; import { notifpopups as config } from "../config"; import { desktopEntrySubs } from "../utils/icons"; import { setupChildClickthrough } from "../utils/widgets"; const urgencyToString = (urgency: AstalNotifd.Urgency) => { switch (urgency) { case AstalNotifd.Urgency.LOW: return "low"; case AstalNotifd.Urgency.NORMAL: return "normal"; case AstalNotifd.Urgency.CRITICAL: return "critical"; } }; const getTime = (time: number) => { const messageTime = GLib.DateTime.new_from_unix_local(time); const todayDay = GLib.DateTime.new_now_local().get_day_of_year(); if (messageTime.get_day_of_year() === todayDay) { const aMinuteAgo = GLib.DateTime.new_now_local().add_seconds(-60); return aMinuteAgo !== null && messageTime.compare(aMinuteAgo) > 0 ? "Now" : messageTime.format("%H:%M"); } else if (messageTime.get_day_of_year() === todayDay - 1) return "Yesterday"; return messageTime.format("%d/%m"); }; const AppIcon = ({ appIcon, desktopEntry }: { appIcon: string; desktopEntry: string }) => { // Try app icon let icon = Astal.Icon.lookup_icon(appIcon) && appIcon; // Try desktop entry if (!icon) { if (desktopEntrySubs.hasOwnProperty(desktopEntry)) icon = desktopEntrySubs[desktopEntry]; else if (Astal.Icon.lookup_icon(desktopEntry)) icon = desktopEntry; } return icon ? : null; }; const Image = ({ icon }: { icon: string }) => { if (GLib.file_test(icon, GLib.FileTest.EXISTS)) return ( ); if (Astal.Icon.lookup_icon(icon)) return ; return null; }; @register() class NotifPopup extends Widget.Box { readonly #revealer; #destroyed = false; constructor({ notification }: { notification: AstalNotifd.Notification }) { super(); this.#revealer = ( ) as Widget.Revealer; this.add(this.#revealer); // Init animation const width = this.get_preferred_width()[1]; this.css = `margin-left: ${width}px; margin-right: -${width}px;`; timeout(1, () => { this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`; }); // Close popup after timeout if transient or expire enabled in config if (config.expire || notification.transient) timeout( notification.expireTimeout > 0 ? notification.expireTimeout : notification.urgency === AstalNotifd.Urgency.CRITICAL ? 10000 : 5000, () => this.destroyWithAnims() ); } destroyWithAnims() { if (this.#destroyed) return; this.#destroyed = true; const animTime = 120; const animMargin = this.get_allocated_width(); this.css = `transition: ${animTime}ms cubic-bezier(0.85, 0, 0.15, 1); margin-left: ${animMargin}px; margin-right: -${animMargin}px;`; timeout(animTime, () => { this.#revealer.revealChild = false; timeout(this.#revealer.transitionDuration, () => this.destroy()); }); } } export default () => ( { const notifd = AstalNotifd.get_default(); const map = new Map(); self.hook(notifd, "notified", (self, id) => { const notification = notifd.get_notification(id); const popup = () as NotifPopup; popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id)); map.get(notification.id)?.destroyWithAnims(); map.set(notification.id, popup); self.add( event.button === Astal.MouseButton.MIDDLE && notification.dismiss()} // Close on hover lost onHoverLost={() => popup.destroyWithAnims()} > {popup} ); // Limit number of popups if (config.maxPopups > 0 && self.children.length > config.maxPopups) map.values().next().value?.destroyWithAnims(); }); self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims()); // Change input region to child region so can click through empty space setupChildClickthrough(self); }} /> );