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);
|
$hover-overlay: transparentize($base05, 0.92);
|
||||||
$active-hover: transparentize($primary-bg, 0.15);
|
$active-hover: transparentize($primary-bg, 0.15);
|
||||||
$active-gradient: linear-gradient(to right, $primary-bg, darken($primary-bg, 4%));
|
$active-gradient: linear-gradient(to right, $primary-bg, darken($primary-bg, 4%));
|
||||||
|
$toast-bg: transparentize($base02, 0.15);
|
||||||
|
|
||||||
$bar-alpha: 0.7;
|
$bar-alpha: 0.7;
|
||||||
$bar-bg: transparentize($base01, 1 - $bar-alpha);
|
$bar-bg: transparentize($base01, 1 - $bar-alpha);
|
||||||
|
|
@ -582,6 +583,16 @@ scale {
|
||||||
&.critical {
|
&.critical {
|
||||||
border-color: $base08;
|
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 {
|
.notif-header {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@ export const dndIcon = Variable.derive([bind(notifd, "dontDisturb")], (dnd) =>
|
||||||
: "preferences-system-notifications-symbolic",
|
: "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() {
|
export function DndToggle() {
|
||||||
return (
|
return (
|
||||||
<button
|
<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
|
// notifications restored from the daemon's cache on startup are stale
|
||||||
for (const n of notifd.notifications) {
|
for (const n of notifd.notifications) {
|
||||||
n.dismiss()
|
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) => {
|
notifd.connect("notified", (_, id: number) => {
|
||||||
const n = notifd.get_notification(id)
|
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) {
|
// re-notified ids update the existing entry instead of adding a duplicate
|
||||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, MAX_TIMEOUT, () => {
|
if (!history.get().some((m) => m.id === id)) {
|
||||||
if (notifd.notifications.includes(n)) n.dismiss()
|
history.set([...history.get(), n])
|
||||||
return GLib.SOURCE_REMOVE
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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) {
|
function formatTime(n: Notifd.Notification) {
|
||||||
return GLib.DateTime.new_from_unix_local(n.time).format("%H:%M")!
|
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
|
const critical = n.urgency === Notifd.Urgency.CRITICAL
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`notif${critical ? " critical" : ""}`}
|
className={`notif${critical ? " critical" : ""}${toast ? " toast" : ""}`}
|
||||||
onClicked={() => n.dismiss()}
|
onClicked={() => dismissNotification(n)}
|
||||||
|
{...(toast
|
||||||
|
? {
|
||||||
|
onHover: () => hovered.add(n.id),
|
||||||
|
onHoverLost: () => hovered.delete(n.id),
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
>
|
>
|
||||||
<box vertical>
|
<box vertical>
|
||||||
<box className="notif-header">
|
<box className="notif-header">
|
||||||
|
|
@ -77,7 +126,7 @@ export function NotificationCard(n: Notifd.Notification) {
|
||||||
<label className="notif-time" label={formatTime(n)} />
|
<label className="notif-time" label={formatTime(n)} />
|
||||||
<button
|
<button
|
||||||
className="notif-close"
|
className="notif-close"
|
||||||
onClicked={() => n.dismiss()}
|
onClicked={() => dismissNotification(n)}
|
||||||
>
|
>
|
||||||
<icon icon="window-close-symbolic" pixelSize={12} />
|
<icon icon="window-close-symbolic" pixelSize={12} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -137,12 +186,15 @@ export default function Notifications(gdkmonitor: Gdk.Monitor) {
|
||||||
marginTop={54}
|
marginTop={54}
|
||||||
marginRight={8}
|
marginRight={8}
|
||||||
visible={bind(notifd, "notifications").as((ns) => ns.length > 0)}
|
visible={bind(notifd, "notifications").as((ns) => ns.length > 0)}
|
||||||
|
onNotifyVisible={(self) => {
|
||||||
|
if (!self.visible) hovered.clear()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<box className="notifications" vertical spacing={8} widthRequest={360}>
|
<box className="notifications" vertical spacing={8} widthRequest={360}>
|
||||||
{bind(notifd, "notifications").as((ns) =>
|
{bind(notifd, "notifications").as((ns) =>
|
||||||
[...ns]
|
[...ns]
|
||||||
.sort((a, b) => b.time - a.time)
|
.sort((a, b) => b.time - a.time)
|
||||||
.map((n) => NotificationCard(n)),
|
.map((n) => NotificationCard(n, true)),
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
</window>
|
</window>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import Gtk from "gi://Gtk?version=3.0"
|
import Gtk from "gi://Gtk?version=3.0"
|
||||||
import GLib from "gi://GLib"
|
import GLib from "gi://GLib"
|
||||||
import AstalWp from "gi://AstalWp"
|
import AstalWp from "gi://AstalWp"
|
||||||
import Notifd from "gi://AstalNotifd"
|
|
||||||
import { execAsync, Variable, bind } from "astal"
|
import { execAsync, Variable, bind } from "astal"
|
||||||
import Dropdown from "./Dropdown"
|
import Dropdown from "./Dropdown"
|
||||||
import { wifiIcon, WifiSection } from "./Wifi"
|
import { wifiIcon, WifiSection } from "./Wifi"
|
||||||
|
|
@ -9,13 +8,17 @@ import { bluetoothIndicator, BluetoothSection } from "./Bluetooth"
|
||||||
import { BrightnessRow } from "./Brightness"
|
import { BrightnessRow } from "./Brightness"
|
||||||
import { MediaSection } from "./Media"
|
import { MediaSection } from "./Media"
|
||||||
import { batteryIcon, batteryPercent, BatterySection } from "./Battery"
|
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 speaker = AstalWp.get_default().audio.default_speaker!
|
||||||
const mic = AstalWp.get_default().audio.default_microphone
|
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(
|
const speakerIcon = Variable.derive(
|
||||||
[bind(speaker, "volume"), bind(speaker, "mute")],
|
[bind(speaker, "volume"), bind(speaker, "mute")],
|
||||||
|
|
@ -162,7 +165,7 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
||||||
<box hexpand />
|
<box hexpand />
|
||||||
<button
|
<button
|
||||||
className="notif-clear"
|
className="notif-clear"
|
||||||
onClicked={() => [...notifd.notifications].forEach((n) => n.dismiss())}
|
onClicked={clearNotifications}
|
||||||
>
|
>
|
||||||
<label label="Clear" />
|
<label label="Clear" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -173,7 +176,7 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
|
||||||
label="no notifications"
|
label="no notifications"
|
||||||
visible={notifCount.as((n) => n === 0)}
|
visible={notifCount.as((n) => n === 0)}
|
||||||
/>
|
/>
|
||||||
{bind(notifd, "notifications").as((ns) =>
|
{bind(history).as((ns) =>
|
||||||
[...ns]
|
[...ns]
|
||||||
.sort((a, b) => b.time - a.time)
|
.sort((a, b) => b.time - a.time)
|
||||||
.map((n) => NotificationCard(n)),
|
.map((n) => NotificationCard(n)),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue