From 42a168762f09b0dc395a0419a42e0cc1b6af18ab Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Wed, 20 Dec 2023 17:14:07 -0500
Subject: [PATCH] refactor(ags): typecheck notif logic

---
 devices/wim/config/ags/js/applauncher/main.js |  29 ++--
 .../config/ags/js/bar/buttons/workspaces.js   |   1 +
 .../wim/config/ags/js/media-player/gesture.js |   7 +-
 devices/wim/config/ags/js/misc/cursorbox.js   |  11 +-
 devices/wim/config/ags/js/misc/popup.js       |  10 +-
 .../wim/config/ags/js/notifications/base.js   |  83 ++++++----
 .../wim/config/ags/js/notifications/center.js |  54 +++++--
 .../config/ags/js/notifications/gesture.js    | 143 ++++++++++--------
 .../wim/config/ags/js/notifications/main.js   |   1 -
 .../wim/config/ags/js/notifications/popup.js  |  46 +++---
 10 files changed, 236 insertions(+), 149 deletions(-)

diff --git a/devices/wim/config/ags/js/applauncher/main.js b/devices/wim/config/ags/js/applauncher/main.js
index 0a548bff..8cd31ecb 100644
--- a/devices/wim/config/ags/js/applauncher/main.js
+++ b/devices/wim/config/ags/js/applauncher/main.js
@@ -9,7 +9,10 @@ import { Box, Entry, Icon, Label, ListBox, Revealer, Scrollable } from 'resource
 import PopupWindow from '../misc/popup.js';
 import AppItem from './app-item.js';
 
-/** @typedef {import('types/service/applications.js').Application} App */
+/**
+ * @typedef {import('types/service/applications.js').Application} App
+ * @typedef {typeof imports.gi.Gtk.ListBoxRow} ListBoxRow
+ */
 
 
 const Applauncher = ({ window_name = 'applauncher' } = {}) => {
@@ -32,17 +35,23 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
 
         fzfResults = fzf.find(text);
         // @ts-expect-error
-        list.set_sort_func((a, b) => {
-            const row1 = a.get_children()[0]?.attribute.app.name;
-            const row2 = b.get_children()[0]?.attribute.app.name;
+        list.set_sort_func(
+            /**
+             * @param {ListBoxRow} a
+             * @param {ListBoxRow} b
+             */
+            (a, b) => {
+                const row1 = a.get_children()[0]?.attribute.app.name;
+                const row2 = b.get_children()[0]?.attribute.app.name;
 
-            if (!row1 || !row2) {
-                return 0;
-            }
+                if (!row1 || !row2) {
+                    return 0;
+                }
 
-            return fzfResults.indexOf(row1) -
+                return fzfResults.indexOf(row1) -
             fzfResults.indexOf(row1) || 0;
-        });
+            },
+        );
     };
 
     const makeNewChildren = () => {
@@ -96,7 +105,7 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
             setSort(text);
             let visibleApps = 0;
 
-            /** @type Array<typeof imports.gi.Gtk.ListBoxRow> */
+            /** @type Array<ListBoxRow> */
             // @ts-expect-error
             const rows = list.get_children();
 
diff --git a/devices/wim/config/ags/js/bar/buttons/workspaces.js b/devices/wim/config/ags/js/bar/buttons/workspaces.js
index 5d611ba0..9e6ad5b5 100644
--- a/devices/wim/config/ags/js/bar/buttons/workspaces.js
+++ b/devices/wim/config/ags/js/bar/buttons/workspaces.js
@@ -135,6 +135,7 @@ export default () => {
                     const updateWorkspaces = () => {
                         Hyprland.workspaces.forEach((ws) => {
                             const currentWs = self.children.find((ch) => {
+                                // @ts-expect-error
                                 return ch.attribute.id === ws.id;
                             });
 
diff --git a/devices/wim/config/ags/js/media-player/gesture.js b/devices/wim/config/ags/js/media-player/gesture.js
index a7870340..f206d07a 100644
--- a/devices/wim/config/ags/js/media-player/gesture.js
+++ b/devices/wim/config/ags/js/media-player/gesture.js
@@ -9,12 +9,13 @@ const ANIM_DURATION = 500;
 const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
                                 opacity ${ANIM_DURATION}ms ease;`;
 
-
 /**
  * @typedef {import('types/widgets/overlay').OverlayProps} OverlayProps
  * @typedef {import('types/widgets/overlay').default} Overlay
- *
- * @param {OverlayProps & {
+ */
+
+
+/** @param {OverlayProps & {
  *      setup?: function(Overlay):void
  * }} o
  */
diff --git a/devices/wim/config/ags/js/misc/cursorbox.js b/devices/wim/config/ags/js/misc/cursorbox.js
index daf6ba7f..dd6e9442 100644
--- a/devices/wim/config/ags/js/misc/cursorbox.js
+++ b/devices/wim/config/ags/js/misc/cursorbox.js
@@ -4,12 +4,13 @@ import { EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
 
 const { Gtk } = imports.gi;
 
-
 /**
  * @typedef {import('types/widgets/eventbox').EventBoxProps} EventBoxProps
  * @typedef {import('types/widgets/eventbox').default} EventBox
- *
- * @param {EventBoxProps & {
+ */
+
+
+/** @param {EventBoxProps & {
  *      on_primary_click_release?: function(EventBox):void
  * }} o
  */
@@ -23,7 +24,7 @@ export default ({
     const CanRun = Variable(true);
     const Disabled = Variable(false);
 
-    let widget; // eslint-disable-line
+    let widget = EventBox();
 
     const wrapper = EventBox({
         cursor: 'pointer',
@@ -35,7 +36,7 @@ export default ({
 
             get_child: () => widget.child,
 
-            /** @param {import('types/widget').Widget} new_child */
+            /** @param {import('types/widgets/box').default} new_child */
             set_child: (new_child) => {
                 widget.child = new_child;
             },
diff --git a/devices/wim/config/ags/js/misc/popup.js b/devices/wim/config/ags/js/misc/popup.js
index f8f0fe6a..91fd3b09 100644
--- a/devices/wim/config/ags/js/misc/popup.js
+++ b/devices/wim/config/ags/js/misc/popup.js
@@ -23,8 +23,8 @@ import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
 export default ({
     transition = 'slide_down',
     transition_duration = 500,
-    onOpen = () => { /**/ },
-    onClose = () => { /**/ },
+    onOpen = () => {/**/},
+    onClose = () => {/**/},
 
     // Window props
     name,
@@ -42,8 +42,12 @@ export default ({
         ...props,
 
         attribute: {
+            /**
+             * @param {typeof imports.gi.Gtk.Allocation} alloc
+             * @param {'left'|'right'} side
+             */
             set_x_pos: (
-                alloc = {},
+                alloc,
                 side = 'right',
             ) => {
                 const width = window.get_display()
diff --git a/devices/wim/config/ags/js/notifications/base.js b/devices/wim/config/ags/js/notifications/base.js
index beb589e7..e8061960 100644
--- a/devices/wim/config/ags/js/notifications/base.js
+++ b/devices/wim/config/ags/js/notifications/base.js
@@ -8,28 +8,35 @@ import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js';
 
 const { GLib } = imports.gi;
 
+/** @typedef {import('types/service/notifications').Notification} Notification */
+
+/** @param {number} time */
 const setTime = (time) => {
     return GLib.DateTime
         .new_from_unix_local(time)
         .format('%H:%M');
 };
 
-const getDragState = (box) => box.get_parent().get_parent()
-    .get_parent().get_parent().get_parent()._dragging;
+/** @param {import('types/widgets/eventbox').default} box */
+const getDragState = (box) => box.get_parent()?.get_parent()
+    // @ts-expect-error
+    ?.get_parent()?.get_parent()?.get_parent()?.attribute.dragging;
 
 import Gesture from './gesture.js';
 import CursorBox from '../misc/cursorbox.js';
 
 
+/** @param {Notification} notif */
 const NotificationIcon = (notif) => {
-    let iconCmd = () => { /**/ };
+    /** @type function(import('types/widgets/eventbox').default):void */
+    let iconCmd = () => {/**/};
 
-    if (Applications.query(notif.appEntry).length > 0) {
-        const app = Applications.query(notif.appEntry)[0];
+    if (notif._appEntry && Applications.query(notif._appEntry).length > 0) {
+        const app = Applications.query(notif._appEntry)[0];
 
         let wmClass = app.app.get_string('StartupWMClass');
 
-        if (app.app.get_filename().includes('discord')) {
+        if (app.app?.get_filename()?.includes('discord')) {
             wmClass = 'discord';
         }
 
@@ -45,17 +52,19 @@ const NotificationIcon = (notif) => {
                             'togglespecialworkspace spot');
                     }
                     else {
-                        Hyprland.sendMessage('j/clients').then((out) => {
-                            out = JSON.parse(out);
+                        Hyprland.sendMessage('j/clients').then((msg) => {
+                            /** @type {Array<import('types/service/hyprland').Client>} */
+                            const clients = JSON.parse(msg);
+
                             const classes = [];
 
-                            for (const key of out) {
+                            for (const key of clients) {
                                 if (key.class) {
                                     classes.push(key.class);
                                 }
                             }
 
-                            if (classes.includes(wmClass)) {
+                            if (wmClass && classes.includes(wmClass)) {
                                 Hyprland.sendMessage('dispatch ' +
                                     `focuswindow ^(${wmClass})`);
                             }
@@ -81,7 +90,7 @@ const NotificationIcon = (notif) => {
             child: Box({
                 vpack: 'start',
                 hexpand: false,
-                className: 'icon img',
+                class_name: 'icon img',
                 css: `
                     background-image: url("${notif.image}");
                     background-size: contain;
@@ -96,13 +105,13 @@ const NotificationIcon = (notif) => {
 
     let icon = 'dialog-information-symbolic';
 
-    if (lookUpIcon(notif.appIcon)) {
-        icon = notif.appIcon;
+    if (lookUpIcon(notif._appIcon)) {
+        icon = notif._appIcon;
     }
 
 
-    if (lookUpIcon(notif.appEntry)) {
-        icon = notif.appEntry;
+    if (notif._appEntry && lookUpIcon(notif._appEntry)) {
+        icon = notif._appEntry;
     }
 
 
@@ -112,7 +121,7 @@ const NotificationIcon = (notif) => {
         child: Box({
             vpack: 'start',
             hexpand: false,
-            className: 'icon',
+            class_name: 'icon',
             css: `
                 min-width: 78px;
                 min-height: 78px;
@@ -132,11 +141,18 @@ const NotificationIcon = (notif) => {
 // to know when there are notifs or not
 export const HasNotifs = Variable(false);
 
+/**
+ * @param {{
+ *      notif: Notification
+ *      slideIn?: 'Left'|'Right'
+ *      command?: () => void
+ * }} o
+ */
 export const Notification = ({
     notif,
     slideIn = 'Left',
     command = () => { /**/ },
-} = {}) => {
+}) => {
     if (!notif) {
         return;
     }
@@ -145,7 +161,7 @@ export const Notification = ({
         'Spotify',
     ];
 
-    if (BlockedApps.find((app) => app === notif.appName)) {
+    if (BlockedApps.find((app) => app === notif._appName)) {
         notif.close();
 
         return;
@@ -161,8 +177,9 @@ export const Notification = ({
     });
 
     // Add body to notif
+    // @ts-expect-error
     notifWidget.child.add(Box({
-        className: `notification ${notif.urgency}`,
+        class_name: `notification ${notif.urgency}`,
         vexpand: false,
 
         // Notification
@@ -174,6 +191,7 @@ export const Notification = ({
                 Box({
                     children: [
                         NotificationIcon(notif),
+
                         Box({
                             hexpand: true,
                             vertical: true,
@@ -185,21 +203,21 @@ export const Notification = ({
 
                                         // Title
                                         Label({
-                                            className: 'title',
+                                            class_name: 'title',
                                             xalign: 0,
                                             justification: 'left',
                                             hexpand: true,
-                                            maxWidthChars: 24,
+                                            max_width_chars: 24,
                                             truncate: 'end',
                                             wrap: true,
                                             label: notif.summary,
-                                            useMarkup: notif.summary
+                                            use_markup: notif.summary
                                                 .startsWith('<'),
                                         }),
 
                                         // Time
                                         Label({
-                                            className: 'time',
+                                            class_name: 'time',
                                             vpack: 'start',
                                             label: setTime(notif.time),
                                         }),
@@ -207,9 +225,12 @@ export const Notification = ({
                                         // Close button
                                         CursorBox({
                                             child: Button({
-                                                className: 'close-button',
+                                                class_name: 'close-button',
                                                 vpack: 'start',
-                                                onClicked: () => notif.close(),
+
+                                                on_primary_click_release: () =>
+                                                    notif.close(),
+
                                                 child: Icon('window-close' +
                                                     '-symbolic'),
                                             }),
@@ -219,9 +240,9 @@ export const Notification = ({
 
                                 // Description
                                 Label({
-                                    className: 'description',
+                                    class_name: 'description',
                                     hexpand: true,
-                                    useMarkup: true,
+                                    use_markup: true,
                                     xalign: 0,
                                     justification: 'left',
                                     label: notif.body,
@@ -234,11 +255,13 @@ export const Notification = ({
 
                 // Actions
                 Box({
-                    className: 'actions',
+                    class_name: 'actions',
                     children: notif.actions.map((action) => Button({
-                        className: 'action-button',
-                        onClicked: () => notif.invoke(action.id),
+                        class_name: 'action-button',
                         hexpand: true,
+
+                        on_primary_click_release: () => notif.invoke(action.id),
+
                         child: Label(action.label),
                     })),
                 }),
diff --git a/devices/wim/config/ags/js/notifications/center.js b/devices/wim/config/ags/js/notifications/center.js
index e70be8fa..e2a70f7d 100644
--- a/devices/wim/config/ags/js/notifications/center.js
+++ b/devices/wim/config/ags/js/notifications/center.js
@@ -7,7 +7,16 @@ import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
 import { Notification, HasNotifs } from './base.js';
 import CursorBox from '../misc/cursorbox.js';
 
+/**
+ * @typedef {import('types/service/notifications').Notification} NotifObj
+ * @typedef {import('types/widgets/box').default} Box
+ */
 
+
+/**
+ * @param {Box} box
+ * @param {NotifObj} notif
+ */
 const addNotif = (box, notif) => {
     if (notif) {
         const NewNotif = Notification({
@@ -27,7 +36,7 @@ const NotificationList = () => Box({
     vertical: true,
     vexpand: true,
     vpack: 'start',
-    binds: [['visible', HasNotifs]],
+    visible: HasNotifs.bind(),
 
     setup: (self) => {
         self
@@ -40,31 +49,42 @@ const NotificationList = () => Box({
                 }
 
                 else if (id) {
-                    addNotif(box, Notifications.getNotification(id));
+                    const notifObj = Notifications.getNotification(id);
+
+                    if (notifObj) {
+                        addNotif(box, notifObj);
+                    }
                 }
             }, 'notified')
 
             .hook(Notifications, (box, id) => {
-                const notif = box.children.find((ch) => ch._id === id);
+                // @ts-expect-error
+                const notif = box.children.find((ch) => ch.attribute.id === id);
 
                 if (notif?.sensitive) {
-                    notif.slideAway('Right');
+                    // @ts-expect-error
+                    notif.attribute.slideAway('Right');
                 }
             }, 'closed');
     },
 });
 
+// TODO: use cursorbox feature for this
 // Needs to be wrapped to still have onHover when disabled
 const ClearButton = () => CursorBox({
     child: Button({
-        onPrimaryClickRelease: () => {
+        sensitive: HasNotifs.bind(),
+
+        on_primary_click_release: () => {
             Notifications.clear();
             timeout(1000, () => App.closeWindow('notification-center'));
         },
-        binds: [['sensitive', HasNotifs]],
+
         child: Box({
+
             children: [
                 Label('Clear '),
+
                 Icon({
                     setup: (self) => {
                         self.hook(Notifications, () => {
@@ -80,7 +100,7 @@ const ClearButton = () => CursorBox({
 });
 
 const Header = () => Box({
-    className: 'header',
+    class_name: 'header',
     children: [
         Label({
             label: 'Notifications',
@@ -93,14 +113,17 @@ const Header = () => Box({
 
 const Placeholder = () => Revealer({
     transition: 'crossfade',
-    binds: [['revealChild', HasNotifs, 'value', (value) => !value]],
+    reveal_child: HasNotifs.bind()
+        .transform((v) => !v),
+
     child: Box({
-        className: 'placeholder',
+        class_name: 'placeholder',
         vertical: true,
         vpack: 'center',
         hpack: 'center',
         vexpand: true,
         hexpand: true,
+
         children: [
             Icon('notification-disabled-symbolic'),
             Label('Your inbox is empty'),
@@ -109,22 +132,27 @@ const Placeholder = () => Revealer({
 });
 
 export default () => Box({
-    className: 'notification-center',
+    class_name: 'notification-center',
     vertical: true,
     children: [
         Header(),
+
         Box({
-            className: 'notification-wallpaper-box',
+            class_name: 'notification-wallpaper-box',
+
             children: [
                 Scrollable({
-                    className: 'notification-list-box',
+                    class_name: 'notification-list-box',
                     hscroll: 'never',
                     vscroll: 'automatic',
+
                     child: Box({
-                        className: 'notification-list',
+                        class_name: 'notification-list',
                         vertical: true,
+
                         children: [
                             NotificationList(),
+
                             Placeholder(),
                         ],
                     }),
diff --git a/devices/wim/config/ags/js/notifications/gesture.js b/devices/wim/config/ags/js/notifications/gesture.js
index 94c72098..26b84ec0 100644
--- a/devices/wim/config/ags/js/notifications/gesture.js
+++ b/devices/wim/config/ags/js/notifications/gesture.js
@@ -5,90 +5,97 @@ import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
 
 import { HasNotifs } from './base.js';
 
-import Gtk from 'gi://Gtk';
+const { Gtk } = imports.gi;
 
 const MAX_OFFSET = 200;
 const OFFSCREEN = 300;
 const ANIM_DURATION = 500;
 const SLIDE_MIN_THRESHOLD = 10;
+const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
+const SQUEEZED = 'margin-bottom: -70px; margin-top: -70px;';
+const MAX_LEFT = `
+    margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
+    margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
+`;
+const MAX_RIGHT = `
+    margin-left:   ${Number(MAX_OFFSET + OFFSCREEN)}px;
+    margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
+`;
+
+const slideLeft = `${TRANSITION} ${MAX_LEFT}
+                        margin-top: 0px;
+                        margin-bottom: 0px;
+                        opacity: 0;`;
+const squeezeLeft = `${TRANSITION} ${MAX_LEFT} ${SQUEEZED} opacity: 0;`;
+
+const slideRight = `${TRANSITION} ${MAX_RIGHT}
+                        margin-top: 0px;
+                        margin-bottom: 0px;
+                        opacity: 0;`;
+const squeezeRight = `${TRANSITION} ${MAX_RIGHT} ${SQUEEZED} opacity: 0;`;
+
+const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
 
 
 export default ({
     id,
     slideIn = 'Left',
-    command = () => { /**/ },
+    command = () => {/**/},
     ...props
 }) => {
     const widget = EventBox({
         ...props,
         cursor: 'grab',
-        onHover: (self) => {
-            if (!self._hovered) {
-                self._hovered = true;
+        on_hover: (self) => {
+            if (!self.attribute.hovered) {
+                self.attribute.hovered = true;
             }
         },
-        onHoverLost: (self) => {
-            if (self._hovered) {
-                self._hovered = false;
+        on_hover_lost: (self) => {
+            if (self.attribute.hovered) {
+                self.attribute.hovered = false;
             }
         },
+
+        attribute: {
+            dragging: false,
+            hovered: false,
+            ready: false,
+            id,
+
+            /** @param {'Left'|'Right'} side */
+            slideAway: (side) => {
+                // Slide away
+                // @ts-expect-error
+                widget.child.setCss(side === 'Left' ? slideLeft : slideRight);
+
+                // Make it uninteractable
+                widget.sensitive = false;
+
+                timeout(ANIM_DURATION - 100, () => {
+                    // Reduce height after sliding away
+                    // @ts-expect-error
+                    widget.child?.setCss(side === 'Left' ?
+                        squeezeLeft :
+                        squeezeRight);
+
+                    timeout(ANIM_DURATION, () => {
+                        // Kill notif and update HasNotifs after anim is done
+                        command();
+
+                        HasNotifs.value = Notifications
+                            .notifications.length > 0;
+
+                        // @ts-expect-error
+                        widget.get_parent()?.remove;
+                    });
+                });
+            },
+        },
     });
 
-    // Properties
-    widget._dragging = false;
-    widget._hovered = false;
-    widget._id = id;
-    widget.ready = false;
-
     const gesture = Gtk.GestureDrag.new(widget);
 
-    const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
-    const SQUEEZED = 'margin-bottom: -70px; margin-top: -70px;';
-    const MAX_LEFT = `
-        margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
-        margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
-    `;
-    const MAX_RIGHT = `
-        margin-left:   ${Number(MAX_OFFSET + OFFSCREEN)}px;
-        margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
-    `;
-
-    const slideLeft = `${TRANSITION} ${MAX_LEFT}
-                        margin-top: 0px;
-                        margin-bottom: 0px;
-                        opacity: 0;`;
-    const squeezeLeft = `${TRANSITION} ${MAX_LEFT} ${SQUEEZED} opacity: 0;`;
-
-    const slideRight = `${TRANSITION} ${MAX_RIGHT}
-                        margin-top: 0px;
-                        margin-bottom: 0px;
-                        opacity: 0;`;
-    const squeezeRight = `${TRANSITION} ${MAX_RIGHT} ${SQUEEZED} opacity: 0;`;
-
-    const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
-
-
-    // Notif methods
-    widget.slideAway = (side) => {
-        // Slide away
-        widget.child.setCss(side === 'Left' ? slideLeft : slideRight);
-
-        // Makie it uninteractable
-        widget.sensitive = false;
-
-        timeout(ANIM_DURATION - 100, () => {
-            // Reduce height after sliding away
-            widget.child?.setCss(side === 'Left' ? squeezeLeft : squeezeRight);
-
-            timeout(ANIM_DURATION, () => {
-                // Kill notif and update HasNotifs after anim is done
-                command();
-                HasNotifs.value = Notifications.notifications.length > 0;
-                widget.get_parent()?.remove(widget);
-            });
-        });
-    };
-
     widget.add(Box({
         css: squeezeLeft,
         setup: (self) => {
@@ -123,14 +130,16 @@ export default ({
                     }
 
                     // Put a threshold on if a click is actually dragging
-                    widget._dragging = Math.abs(offset) > SLIDE_MIN_THRESHOLD;
+                    widget.attribute.dragging =
+                        Math.abs(offset) > SLIDE_MIN_THRESHOLD;
+
                     widget.cursor = 'grabbing';
                 }, 'drag-update')
 
                 // On drag end
                 .hook(gesture, () => {
                     // Make it slide in on init
-                    if (!widget.ready) {
+                    if (!widget.attribute.ready) {
                         // Reverse of slideAway, so it started at squeeze, then we go to slide
                         self.setCss(slideIn === 'Left' ?
                             slideLeft :
@@ -140,7 +149,7 @@ export default ({
                             // Then we go to center
                             self.setCss(defaultStyle);
                             timeout(ANIM_DURATION, () => {
-                                widget.ready = true;
+                                widget.attribute.ready = true;
                             });
                         });
 
@@ -152,16 +161,16 @@ export default ({
                     // If crosses threshold after letting go, slide away
                     if (Math.abs(offset) > MAX_OFFSET) {
                         if (offset > 0) {
-                            widget.slideAway('Right');
+                            widget.attribute.slideAway('Right');
                         }
                         else {
-                            widget.slideAway('Left');
+                            widget.attribute.slideAway('Left');
                         }
                     }
                     else {
                         self.setCss(defaultStyle);
                         widget.cursor = 'grab';
-                        widget._dragging = false;
+                        widget.attribute.dragging = false;
                     }
                 }, 'drag-end');
         },
diff --git a/devices/wim/config/ags/js/notifications/main.js b/devices/wim/config/ags/js/notifications/main.js
index 9b1f0a27..1528c4be 100644
--- a/devices/wim/config/ags/js/notifications/main.js
+++ b/devices/wim/config/ags/js/notifications/main.js
@@ -16,7 +16,6 @@ export const NotifPopups = () => PopupWindow({
 
 const TOP_MARGIN = 6;
 
-// FIXME: opens at wrong place
 export const NotifCenter = () => PopupWindow({
     name: 'notification-center',
     anchor: ['top', 'right'],
diff --git a/devices/wim/config/ags/js/notifications/popup.js b/devices/wim/config/ags/js/notifications/popup.js
index 00e8af70..c287d59a 100644
--- a/devices/wim/config/ags/js/notifications/popup.js
+++ b/devices/wim/config/ags/js/notifications/popup.js
@@ -14,6 +14,7 @@ export default () => Box({
     vertical: true,
 
     setup: (self) => {
+        /** @param {number} id */
         const addPopup = (id) => {
             if (!id) {
                 return;
@@ -21,38 +22,49 @@ export default () => Box({
 
             const notif = Notifications.getNotification(id);
 
-            const NewNotif = Notification({
-                notif,
-                command: () => notif.dismiss(),
-            });
+            if (notif) {
+                const NewNotif = Notification({
+                    notif,
+                    command: () => notif.dismiss(),
+                });
 
-            if (NewNotif) {
-                // Use this instead of add to put it at the top
-                self.pack_end(NewNotif, false, false, 0);
-                self.show_all();
+                if (NewNotif) {
+                    // Use this instead of add to put it at the top
+                    self.pack_end(NewNotif, false, false, 0);
+                    self.show_all();
+                }
             }
         };
 
+        /**
+         * @param {number} id
+         * @param {boolean} force
+         */
         const handleDismiss = (id, force = false) => {
-            const notif = self.children.find((ch) => ch._id === id);
+            // @ts-expect-error
+            const notif = self.children.find((ch) => ch.attribute.id === id);
 
             if (!notif) {
                 return;
             }
 
             // If notif isn't hovered or was closed, slide away
-            if (!notif._hovered || force) {
-                notif.slideAway('Left');
+            // @ts-expect-error
+            if (!notif.attribute.hovered || force) {
+                // @ts-expect-error
+                notif.attribute.slideAway('Left');
             }
 
             // If notif is hovered, delay close
-            else if (notif._hovered) {
-                notif.interval = interval(DELAY, () => {
-                    if (!notif._hovered && notif.interval) {
-                        notif.slideAway('Left');
+            // @ts-expect-error
+            else if (notif.attribute.hovered) {
+                const intervalId = interval(DELAY, () => {
+                    // @ts-expect-error
+                    if (!notif.attribute.hovered && intervalId) {
+                        // @ts-expect-error
+                        notif.attribute.slideAway('Left');
 
-                        GLib.source_remove(notif.interval);
-                        notif.interval = null;
+                        GLib.source_remove(intervalId);
                     }
                 });
             }