mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
> Opened by the agents Brain and Pinky on behalf of @wesbillman. Fixes two desktop notification issues (requested in Buzz channel `desktop-notification-improvements`): ## 1. Notifications now show who sent the message Live DM and thread-reply notifications showed only "Direct message" / "Reply in #channel", while home-feed mention toasts already carried the sender's name. All message-notification copy is now centralized: - **`formatMessageNotification`** (`notifications/lib/notificationFormat.ts`) — canonical title/body for all five sources (mention, approval, needs-action, DM, thread reply). Sender-first titles with neutral fallbacks, never a raw pubkey: - DM: `Taylor` instead of `Direct message` - Thread reply: `Taylor replied in #ship-room` instead of `Reply in #ship-room` - **`useNotificationSenderName`** — synchronous cache-only lookup (react-query users-batch entry cache → persisted label cache); a cold miss ships the fallback title immediately and warms the cache in the background. No toast delay, no new network machinery. - **`buildEventNotificationTarget` / `buildFeedItemNotificationTarget`** (`notifications/lib/target.ts`) — the click-through target payload is built in one place instead of three hand-rolled copies. ## 2. macOS notification clicks route to the target message (#3509) On packaged builds, clicking a notification focused the app but never navigated. Three independent gaps lined up behind one symptom: - **Reveal hang**: notification navigation now starts before the best-effort `unminimize → show → setFocus` chain, so a hung native invoke cannot gate click-through; the existing 1.5s reveal timeout remains as a secondary guard. - **Lost emit**: the Rust delegate queues the activation target *before* emitting `native-notification-activated`; a lost emit stranded the target with nothing re-draining the queue. The macOS listener now also drains on window `focus` / `visibilitychange` — delivered by WebKit independently of the Tauri event channel, and always produced by the click's own foregrounding. - **Silent no-op**: `commitNavigation` skips same-href destinations; `goChannel` / `goForumPost` / `openSearchHit` now accept `force`, and the notification activation handler passes it so a click always routes. Multiple queued activations are serialized FIFO with rejection containment so an older click cannot finish after and overwrite a newer one. Queue teardown now aborts already-running activations as well as pending ones; async forum-comment destination resolution rechecks ownership before community-scoped cache writes or routing. Diagnostic evidence from the macOS unified log (packaged v0.5.17): delegate confirmed live in release builds (`willPresent` honored — `(["list"])` presentations); a real click response at 09:25:06 reached `usernoted`, the app was fronted by LaunchServices, and no navigation followed. ## Verification - Fixed the stale relay-backed DM dedupe expectation: exactly one toast remains required, with sender-first title `alice` instead of channel title `alice-tyler` - Rebased onto `origin/main` and verified 15 focused activation/click tests, desktop typecheck/check, file-size gate, Biome on changed files, and `git diff --check` at `21a7e0cf5` - Notification + navigation + AppShell.helpers unit tests: 89/89 pass (includes new `notificationFormat.test.mjs`, `target.test.mjs`, `desktopActivations.test.mjs`) - Full desktop JS suite run by Pinky at f03979ea: 5133/5133 pass; pre-push hooks (desktop-test, desktop-typecheck, desktop-tauri-checks, rust-tests, file-size ratchet) all green on this branch - `tsc --noEmit` and changed-file Biome checks clean; re-verified after rebasing current `origin/main` - Click-through on a packaged build still needs human verification — @wesbillman, next release build is the real test. --------- Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { finalizeEvent } from "nostr-tools/pure";
|
|
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
|
|
import { installRelayBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
|
import { assertRelaySeeded } from "../helpers/seed";
|
|
|
|
const isCi = Boolean(process.env.CI);
|
|
const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000;
|
|
|
|
const RELAY_HTTP_URL =
|
|
process.env.BUZZ_E2E_RELAY_URL ?? "http://localhost:3000";
|
|
|
|
// setup-desktop-test-data.sh: uuid5(NAMESPACE_DNS, "buzz.channel.dm.alice-tyler")
|
|
const ALICE_TYLER_DM_CHANNEL_ID = "5a9c064e-0411-5242-ae6b-0363ba99b8e6";
|
|
|
|
async function getLoggedNotifications(page: import("@playwright/test").Page) {
|
|
return page.evaluate(() => {
|
|
const win = window as Window & {
|
|
__BUZZ_E2E_NOTIFICATIONS__?: Array<{
|
|
body: string | null;
|
|
title: string;
|
|
}>;
|
|
};
|
|
|
|
return win.__BUZZ_E2E_NOTIFICATIONS__ ?? [];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Publishes a REAL signed DM message from alice through the relay ingest
|
|
* path. Like every DM send in the product, it carries a recipient `p` tag
|
|
* (see messageMentionPubkeys) — which is exactly what makes the event match
|
|
* both the live DM subscription and the home-feed mention query.
|
|
*/
|
|
async function publishAliceDm(content: string) {
|
|
const event = finalizeEvent(
|
|
{
|
|
kind: 9,
|
|
content,
|
|
tags: [
|
|
["h", ALICE_TYLER_DM_CHANNEL_ID],
|
|
["p", TEST_IDENTITIES.tyler.pubkey],
|
|
],
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
},
|
|
hexToBytes(TEST_IDENTITIES.alice.privateKey),
|
|
);
|
|
|
|
const response = await fetch(`${RELAY_HTTP_URL}/events`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Pubkey": event.pubkey,
|
|
},
|
|
body: JSON.stringify(event),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`POST /events failed (${response.status}): ${await response.text()}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
test.beforeAll(async () => {
|
|
test.setTimeout(relaySeedHookTimeoutMs);
|
|
await assertRelaySeeded();
|
|
});
|
|
|
|
test("an incoming DM produces exactly one desktop notification", async ({
|
|
page,
|
|
}) => {
|
|
await installRelayBridge(page, "tyler");
|
|
|
|
// Deterministically wait for the home feed's initial mention query
|
|
// (kinds 9/... + #p filter) to complete before publishing: the feed
|
|
// dedupe-seen set must be initialized, otherwise the duplicate is
|
|
// accidentally swallowed as "initial backlog" and the repro goes flaky.
|
|
const feedInitialized = page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes("/query") &&
|
|
(response.request().postData() ?? "").includes('"#p"'),
|
|
{ timeout: 15_000 },
|
|
);
|
|
await page.goto("/");
|
|
|
|
// Wait until the DM channel is loaded — live subscriptions (channel + home
|
|
// feed mention) are established once the channel list resolves.
|
|
await expect(page.getByTestId("dm-list")).toContainText("alice-tyler");
|
|
await feedInitialized;
|
|
// Small buffer for the feed effect (seen-set initialization) to run.
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const message = `dm dedupe probe ${Date.now()}`;
|
|
await publishAliceDm(message);
|
|
|
|
// Wait for the DM toast to arrive.
|
|
await expect
|
|
.poll(async () => (await getLoggedNotifications(page)).length, {
|
|
timeout: 15_000,
|
|
})
|
|
.toBeGreaterThan(0);
|
|
|
|
// Give the duplicate (home-feed mention path) time to fire — it arrives via
|
|
// the onLiveMention → feed refetch round trip, which lags the WS toast.
|
|
await page.waitForTimeout(5_000);
|
|
|
|
const notifications = await getLoggedNotifications(page);
|
|
expect(
|
|
notifications,
|
|
`expected exactly one notification for a single DM, got: ${JSON.stringify(notifications)}`,
|
|
).toHaveLength(1);
|
|
|
|
// The survivor must be the live WebSocket DM toast (titled with the
|
|
// sender name), not the home-feed mention duplicate ("… mentioned you in …").
|
|
expect(notifications[0].title).toBe("alice");
|
|
});
|