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
+1 -1
View File
@@ -17,7 +17,7 @@ Cargo.lock.bak
crates/tauri-app/node_modules/ crates/tauri-app/node_modules/
crates/tauri-app/dist/ crates/tauri-app/dist/
crates/tauri-app/.svelte-kit/ crates/tauri-app/.svelte-kit/
crates/tauri-app/.vite/ .vite/
crates/tauri-app/src-tauri/target/ crates/tauri-app/src-tauri/target/
crates/tauri-app/src-tauri/gen/ crates/tauri-app/src-tauri/gen/
crates/tauri-app/src/assets/fonts/*.ttf crates/tauri-app/src/assets/fonts/*.ttf
+1 -1
View File
@@ -1 +1 @@
{"version":"2.1.9","results":[[":crates/tauri-app/src/lib/api/client.test.ts",{"duration":31.70154099999999,"failed":false}],[":crates/tauri-app/src/lib/utils/localstorage.test.ts",{"duration":6.449833000000012,"failed":false}]]} {"version":"2.1.9","results":[[":crates/tauri-app/src/lib/api/client.test.ts",{"duration":31.23195799999999,"failed":false}],[":crates/tauri-app/src/lib/utils/localstorage.test.ts",{"duration":5.796832999999992,"failed":false}]]}
+52 -5
View File
@@ -96,9 +96,42 @@
function onNotification(e: Event) { function onNotification(e: Event) {
const detail = (e as CustomEvent<{ title: string; body: string; url: string | null }>).detail; const detail = (e as CustomEvent<{ title: string; body: string; url: string | null }>).detail;
if (detail) { if (detail) {
// Surface in the existing toast stack — works for both the // Surface in the existing toast stack. The toast pill is
// OS-notification click and the in-app events. // clickable — clicking it navigates to the URL the notification
pushToast("info", `notif: ${detail.title}${detail.body}`); // 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"
class:toast-pill--err={t.kind === "error"} class:toast-pill--err={t.kind === "error"}
type="button" type="button"
onclick={() => (toasts = toasts.filter((x) => x.id !== t.id))} onclick={() => {
title="dismiss" // 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} {t.kind === "error" ? "err" : "info"}: {t.text}
</button> </button>