Keep notifications in dropdown after toast timeout and polish toasts
This commit is contained in:
parent
a9cfd9e5c2
commit
5fab76c41f
|
|
@ -16,6 +16,7 @@ $hover-bg: transparentize($base02, 0.2);
|
|||
$hover-overlay: transparentize($base05, 0.92);
|
||||
$active-hover: transparentize($primary-bg, 0.15);
|
||||
$active-gradient: linear-gradient(to right, $primary-bg, darken($primary-bg, 4%));
|
||||
$toast-bg: transparentize($base02, 0.15);
|
||||
|
||||
$bar-alpha: 0.7;
|
||||
$bar-bg: transparentize($base01, 1 - $bar-alpha);
|
||||
|
|
@ -582,6 +583,16 @@ scale {
|
|||
&.critical {
|
||||
border-color: $base08;
|
||||
}
|
||||
|
||||
// popup toasts are more opaque than list entries and become
|
||||
// completely opaque while hovered
|
||||
&.toast {
|
||||
background-color: $toast-bg;
|
||||
|
||||
&:hover {
|
||||
background-color: $base02;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notif-header {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ export const dndIcon = Variable.derive([bind(notifd, "dontDisturb")], (dnd) =>
|
|||
: "preferences-system-notifications-symbolic",
|
||||
)
|
||||
|
||||
// every notification received this session is kept until it is explicitly
|
||||
// cleared, so popups that time out remain in the dropdown's list. only
|
||||
// notifications dismissed directly from their popup skip the list.
|
||||
export const history = Variable<Notifd.Notification[]>([])
|
||||
|
||||
export const notifCount = bind(history).as((ns) => ns.length)
|
||||
|
||||
export function DndToggle() {
|
||||
return (
|
||||
<button
|
||||
|
|
@ -28,39 +35,81 @@ export function DndToggle() {
|
|||
)
|
||||
}
|
||||
|
||||
notifd.default_timeout = MAX_TIMEOUT
|
||||
// popups are timed out entirely client-side: the daemon never resolves them
|
||||
// on its own, so a popup under the pointer can be kept alive indefinitely
|
||||
notifd.ignore_timeout = true
|
||||
notifd.default_timeout = -1
|
||||
|
||||
// notifications restored from the daemon's cache on startup are stale
|
||||
for (const n of notifd.notifications) {
|
||||
n.dismiss()
|
||||
}
|
||||
|
||||
// the daemon honors app-requested timeouts, so enforce our own cap for
|
||||
// notifications that ask for more (or none at all). critical notifications
|
||||
// are exempt and stay until dismissed.
|
||||
notifd.connect("notified", (_, id: number) => {
|
||||
const n = notifd.get_notification(id)
|
||||
if (!n || n.urgency === Notifd.Urgency.CRITICAL) return
|
||||
if (!n) return
|
||||
|
||||
if (n.expire_timeout <= 0 || n.expire_timeout > MAX_TIMEOUT) {
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, MAX_TIMEOUT, () => {
|
||||
if (notifd.notifications.includes(n)) n.dismiss()
|
||||
return GLib.SOURCE_REMOVE
|
||||
})
|
||||
// re-notified ids update the existing entry instead of adding a duplicate
|
||||
if (!history.get().some((m) => m.id === id)) {
|
||||
history.set([...history.get(), n])
|
||||
}
|
||||
})
|
||||
|
||||
// ids of notifications whose popup is currently under the pointer
|
||||
const hovered = new Set<number>()
|
||||
|
||||
// enforce our own cap for popups, honoring shorter app-requested timeouts.
|
||||
// critical notifications are exempt and stay until dismissed, as are popups
|
||||
// being hovered.
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
|
||||
const now = GLib.DateTime.new_now_local().to_unix()
|
||||
|
||||
for (const n of [...notifd.notifications]) {
|
||||
if (n.urgency === Notifd.Urgency.CRITICAL || hovered.has(n.id)) continue
|
||||
|
||||
const timeout =
|
||||
n.expire_timeout > 0
|
||||
? Math.min(n.expire_timeout, MAX_TIMEOUT)
|
||||
: MAX_TIMEOUT
|
||||
|
||||
if (now - n.time >= timeout / 1000) n.dismiss()
|
||||
}
|
||||
|
||||
return GLib.SOURCE_CONTINUE
|
||||
})
|
||||
|
||||
// drop a notification from the list and withdraw its popup; used when a
|
||||
// notification is dismissed directly instead of timing out
|
||||
export function dismissNotification(n: Notifd.Notification) {
|
||||
hovered.delete(n.id)
|
||||
history.set(history.get().filter((m) => m.id !== n.id))
|
||||
notifd.get_notification(n.id)?.dismiss()
|
||||
}
|
||||
|
||||
export function clearNotifications() {
|
||||
for (const n of [...notifd.notifications]) {
|
||||
dismissNotification(n)
|
||||
}
|
||||
history.set([])
|
||||
}
|
||||
|
||||
function formatTime(n: Notifd.Notification) {
|
||||
return GLib.DateTime.new_from_unix_local(n.time).format("%H:%M")!
|
||||
}
|
||||
|
||||
export function NotificationCard(n: Notifd.Notification) {
|
||||
export function NotificationCard(n: Notifd.Notification, toast = false) {
|
||||
const critical = n.urgency === Notifd.Urgency.CRITICAL
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`notif${critical ? " critical" : ""}`}
|
||||
onClicked={() => n.dismiss()}
|
||||
className={`notif${critical ? " critical" : ""}${toast ? " toast" : ""}`}
|
||||
onClicked={() => dismissNotification(n)}
|
||||
{...(toast
|
||||
? {
|
||||
onHover: () => hovered.add(n.id),
|
||||
onHoverLost: () => hovered.delete(n.id),
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<box vertical>
|
||||
<box className="notif-header">
|
||||
|
|
@ -77,7 +126,7 @@ export function NotificationCard(n: Notifd.Notification) {
|
|||
<label className="notif-time" label={formatTime(n)} />
|
||||
<button
|
||||
className="notif-close"
|
||||
onClicked={() => n.dismiss()}
|
||||
onClicked={() => dismissNotification(n)}
|
||||
>
|
||||
<icon icon="window-close-symbolic" pixelSize={12} />
|
||||
</button>
|
||||
|
|
@ -137,12 +186,15 @@ export default function Notifications(gdkmonitor: Gdk.Monitor) {
|
|||
marginTop={54}
|
||||
marginRight={8}
|
||||
visible={bind(notifd, "notifications").as((ns) => ns.length > 0)}
|
||||
onNotifyVisible={(self) => {
|
||||
if (!self.visible) hovered.clear()
|
||||
}}
|
||||
>
|
||||
<box className="notifications" vertical spacing={8} widthRequest={360}>
|
||||
{bind(notifd, "notifications").as((ns) =>
|
||||
[...ns]
|
||||
.sort((a, b) => b.time - a.time)
|
||||
.map((n) => NotificationCard(n)),
|
||||
.map((n) => NotificationCard(n, true)),
|
||||
)}
|
||||
</box>
|
||||
</window>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Gtk from "gi://Gtk?version=3.0"
|
||||
import GLib from "gi://GLib"
|
||||
import AstalWp from "gi://AstalWp"
|
||||
import Notifd from "gi://AstalNotifd"
|
||||
import { execAsync, Variable, bind } from "astal"
|
||||
import Dropdown from "./Dropdown"
|
||||
import { wifiIcon, WifiSection } from "./Wifi"
|
||||
|
|
@ -9,13 +8,17 @@ import { bluetoothIndicator, BluetoothSection } from "./Bluetooth"
|
|||
import { BrightnessRow } from "./Brightness"
|
||||
import { MediaSection } from "./Media"
|
||||
import { batteryIcon, batteryPercent, BatterySection } from "./Battery"
|
||||
import { NotificationCard, DndToggle, dndIcon } from "./Notifications"
|
||||
import {
|
||||
clearNotifications,
|
||||
dndIcon,
|
||||
DndToggle,
|
||||
history,
|
||||
notifCount,
|
||||
NotificationCard,
|
||||
} from "./Notifications"
|
||||
|
||||
const speaker = AstalWp.get_default().audio.default_speaker!
|
||||
const mic = AstalWp.get_default().audio.default_microphone
|
||||
const notifd = Notifd.get_default()
|
||||
|
||||
const notifCount = bind(notifd, "notifications").as((ns) => ns.length)
|
||||
|
||||
const speakerIcon = Variable.derive(
|
||||
[bind(speaker, "volume"), bind(speaker, "mute")],
|
||||
|
|
@ -162,7 +165,7 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
|||
<box hexpand />
|
||||
<button
|
||||
className="notif-clear"
|
||||
onClicked={() => [...notifd.notifications].forEach((n) => n.dismiss())}
|
||||
onClicked={clearNotifications}
|
||||
>
|
||||
<label label="Clear" />
|
||||
</button>
|
||||
|
|
@ -173,7 +176,7 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
|||
label="no notifications"
|
||||
visible={notifCount.as((n) => n === 0)}
|
||||
/>
|
||||
{bind(notifd, "notifications").as((ns) =>
|
||||
{bind(history).as((ns) =>
|
||||
[...ns]
|
||||
.sort((a, b) => b.time - a.time)
|
||||
.map((n) => NotificationCard(n)),
|
||||
|
|
|
|||
Loading…
Reference in New Issue