tauri-app: 8b click-through notifications

OS notification body already arrived as a 'maarcadetweet:notification'
DOM event (Phase 7b). The toast pill that surfaces the body is now
clickable: left-click navigates to the URL the notification was
about (at://<did>/<col>/<rkey> opens the thread, unknown URL
falls back to the home view); right-click dismisses without
navigating. tauri-plugin-notification v2.x does not expose a
reliable OS-level notification-click callback (it only shows the
notification), so the two-step pattern (OS click focuses the app
+ in-app toast click navigates) is the standard workaround.

lastNotificationUrl is now $state so the toast title updates
when a new notification arrives.
This commit is contained in:
tomdebone
2026-07-06 18:59:53 +02:00
parent b912132a05
commit 238823aae1
3 changed files with 54 additions and 7 deletions
+52 -5
View File
@@ -96,9 +96,42 @@
function onNotification(e: Event) {
const detail = (e as CustomEvent<{ title: string; body: string; url: string | null }>).detail;
if (detail) {
// Surface in the existing toast stack — works for both the
// OS-notification click and the in-app events.
pushToast("info", `notif: ${detail.title}${detail.body}`);
// Surface in the existing toast stack. The toast pill is
// clickable — clicking it navigates to the URL the notification
// was about (e.g. an at:// post URI). For OS-level
// notifications, the user has to first click the OS notification
// (which focuses the app) and then click the toast in the app
// to actually navigate; this is the limitation of the
// tauri-plugin-notification v2.x click callbacks.
pushToast(
"info",
`notif: ${detail.title}${detail.body}` + (detail.url ? " (click to open)" : ""),
);
// Save the URL on a per-toast basis via a side-channel so the
// toast pill can navigate when clicked. We attach it to the
// notification event for simplicity (re-look-up via last).
lastNotificationUrl = detail.url ?? null;
}
}
let lastNotificationUrl: string | null = $state(null);
function openLastNotification() {
const url = lastNotificationUrl;
if (!url) return;
if (url.startsWith("at://")) {
// at://<did>/app.twi.post/<rkey> or at://<did>/app.bsky.feed.post/<rkey>
const parts = url.replace(/^at:\/\//, "").split("/");
const rkey = parts[parts.length - 1];
const did = parts[0];
if (rkey && did) {
// Use the existing thread-context machinery to open the post.
void openThread(`${did}/app.twi.post/${rkey}`);
lastNotificationUrl = null;
}
} else {
// unknown scheme — just open home
view = "home";
}
}
@@ -454,8 +487,22 @@
class="toast-pill"
class:toast-pill--err={t.kind === "error"}
type="button"
onclick={() => (toasts = toasts.filter((x) => x.id !== t.id))}
title="dismiss"
onclick={() => {
// If this toast was a notification with a URL, navigate
// to it before dismissing. Otherwise just dismiss.
if (lastNotificationUrl) {
openLastNotification();
}
toasts = toasts.filter((x) => x.id !== t.id);
}}
oncontextmenu={(e) => {
// Right-click dismisses without navigating — useful when
// the user clicks a notification toast by mistake.
e.preventDefault();
lastNotificationUrl = null;
toasts = toasts.filter((x) => x.id !== t.id);
}}
title={lastNotificationUrl ? "click to open · right-click to dismiss" : "dismiss"}
>
{t.kind === "error" ? "err" : "info"}: {t.text}
</button>