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:
+1
-1
@@ -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 @@
|
|||||||
{"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}]]}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user