mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +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>
246 lines
7.4 KiB
JavaScript
246 lines
7.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
markAllReadSources,
|
|
activateDesktopNotificationTarget,
|
|
createDesktopNotificationActivationQueue,
|
|
shouldBounceForChannelNotification,
|
|
} from "./AppShell.helpers.ts";
|
|
|
|
test("shouldBounceForChannelNotification_allowsTopLevelChannelMessages", () => {
|
|
assert.equal(shouldBounceForChannelNotification([["h", "channel"]]), true);
|
|
});
|
|
|
|
test("shouldBounceForChannelNotification_suppressesThreadReplies", () => {
|
|
assert.equal(
|
|
shouldBounceForChannelNotification([
|
|
["h", "channel"],
|
|
["e", "root", "", "reply"],
|
|
]),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("shouldBounceForChannelNotification_allowsBroadcastReplies", () => {
|
|
assert.equal(
|
|
shouldBounceForChannelNotification([
|
|
["h", "channel"],
|
|
["e", "root", "", "reply"],
|
|
["broadcast", "1"],
|
|
]),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("notification activation queue preserves click order", async () => {
|
|
const calls = [];
|
|
const resolvers = new Map();
|
|
const queue = createDesktopNotificationActivationQueue((target) => {
|
|
calls.push(`start:${target.channelId}`);
|
|
return new Promise((resolve) => {
|
|
resolvers.set(target.channelId, () => {
|
|
calls.push(`finish:${target.channelId}`);
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
queue.enqueue({ channelId: "first", eventId: null, kind: null });
|
|
queue.enqueue({ channelId: "second", eventId: null, kind: null });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(calls, ["start:first"]);
|
|
|
|
resolvers.get("first")();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(calls, ["start:first", "finish:first", "start:second"]);
|
|
|
|
resolvers.get("second")();
|
|
});
|
|
|
|
test("notification activation queue drops pending targets after cancellation", async () => {
|
|
const calls = [];
|
|
let resolveFirst;
|
|
const queue = createDesktopNotificationActivationQueue((target) => {
|
|
calls.push(target.channelId);
|
|
if (target.channelId === "first") {
|
|
return new Promise((resolve) => {
|
|
resolveFirst = resolve;
|
|
});
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
|
|
queue.enqueue({ channelId: "first", eventId: null, kind: null });
|
|
queue.enqueue({ channelId: "second", eventId: null, kind: null });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(calls, ["first"]);
|
|
|
|
queue.cancel();
|
|
resolveFirst();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(calls, ["first"]);
|
|
});
|
|
|
|
test("notification activation queue aborts an in-flight activation", async () => {
|
|
let observedSignal;
|
|
let resolveActivation;
|
|
const queue = createDesktopNotificationActivationQueue((_target, signal) => {
|
|
observedSignal = signal;
|
|
return new Promise((resolve) => {
|
|
resolveActivation = resolve;
|
|
});
|
|
});
|
|
|
|
queue.enqueue({ channelId: "first", eventId: null, kind: null });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(observedSignal.aborted, false);
|
|
|
|
queue.cancel();
|
|
assert.equal(observedSignal.aborted, true);
|
|
resolveActivation();
|
|
});
|
|
|
|
test("notification activation queue reports failures and continues", async () => {
|
|
const calls = [];
|
|
const errors = [];
|
|
const queue = createDesktopNotificationActivationQueue(
|
|
async (target) => {
|
|
calls.push(target.channelId);
|
|
if (target.channelId === "first") {
|
|
throw new Error("navigation failed");
|
|
}
|
|
},
|
|
(error) => errors.push(error),
|
|
);
|
|
|
|
queue.enqueue({ channelId: "first", eventId: null, kind: null });
|
|
queue.enqueue({ channelId: "second", eventId: null, kind: null });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
assert.deepEqual(calls, ["first", "second"]);
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0].message, /navigation failed/);
|
|
});
|
|
|
|
test("notification activation starts routing before a hung reveal", async () => {
|
|
const calls = [];
|
|
let resolveNavigation;
|
|
let navigationSettled = false;
|
|
const activation = activateDesktopNotificationTarget(
|
|
{
|
|
channelId: "channel",
|
|
eventId: "event",
|
|
kind: 9,
|
|
},
|
|
{
|
|
goChannel: async () => calls.push("channel"),
|
|
goHome: async () => calls.push("home"),
|
|
revealWindow: () => new Promise(() => {}),
|
|
openSearchHit: (_hit, behavior) => {
|
|
calls.push(`message:${String(behavior?.force)}`);
|
|
return new Promise((resolve) => {
|
|
resolveNavigation = () => {
|
|
navigationSettled = true;
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ["message:true"]);
|
|
resolveNavigation();
|
|
await activation;
|
|
assert.equal(navigationSettled, true);
|
|
});
|
|
|
|
test("notification activation falls back to forced channel navigation", async () => {
|
|
const calls = [];
|
|
await activateDesktopNotificationTarget(
|
|
{ channelId: "channel", eventId: null, kind: null },
|
|
{
|
|
goChannel: async (channelId, behavior) =>
|
|
calls.push(`${channelId}:${String(behavior?.force)}`),
|
|
goHome: async () => calls.push("home"),
|
|
openSearchHit: async () => calls.push("message"),
|
|
revealWindow: async () => calls.push("reveal"),
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ["channel:true", "reveal"]);
|
|
});
|
|
|
|
test("notification activation ignores reveal rejection after routing starts", async () => {
|
|
const calls = [];
|
|
await activateDesktopNotificationTarget(
|
|
{ channelId: "channel", eventId: null, kind: null },
|
|
{
|
|
goChannel: async () => calls.push("channel"),
|
|
goHome: async () => calls.push("home"),
|
|
openSearchHit: async () => calls.push("message"),
|
|
revealWindow: async () => {
|
|
calls.push("reveal");
|
|
throw new Error("reveal failed");
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ["channel", "reveal"]);
|
|
});
|
|
|
|
test("notification activation without a channel opens home", async () => {
|
|
const calls = [];
|
|
await activateDesktopNotificationTarget(
|
|
{ channelId: null, eventId: "event", kind: 9 },
|
|
{
|
|
goChannel: async () => calls.push("channel"),
|
|
goHome: async () => calls.push("home"),
|
|
openSearchHit: async () => calls.push("message"),
|
|
revealWindow: async () => calls.push("reveal"),
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ["home", "reveal"]);
|
|
});
|
|
|
|
test("markAllReadSources clears Inbox overrides and active thread activity", () => {
|
|
const calls = [];
|
|
|
|
markAllReadSources({
|
|
activeChannelId: "active-channel",
|
|
channelActivityItems: [
|
|
{ channelId: "another-channel", createdAt: 100 },
|
|
{ channelId: "active-channel", createdAt: 200 },
|
|
{ channelId: "active-channel", createdAt: 300 },
|
|
],
|
|
unreadFeedItemIds: new Set(["first-inbox-item", "second-inbox-item"]),
|
|
undoUnreadFeedItem: (itemId) => calls.push(`inbox:${itemId}`),
|
|
markAllChannelReadMarkers: () => calls.push("channels"),
|
|
markActiveChannelRead: (channelId, createdAt) =>
|
|
calls.push(`active:${channelId}:${createdAt}`),
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
"inbox:first-inbox-item",
|
|
"inbox:second-inbox-item",
|
|
"channels",
|
|
"active:active-channel:300",
|
|
]);
|
|
});
|
|
|
|
test("markAllReadSources skips the active marker without projected activity", () => {
|
|
const calls = [];
|
|
|
|
markAllReadSources({
|
|
activeChannelId: "active-channel",
|
|
channelActivityItems: [],
|
|
unreadFeedItemIds: new Set(),
|
|
undoUnreadFeedItem: () => calls.push("inbox"),
|
|
markAllChannelReadMarkers: () => calls.push("channels"),
|
|
markActiveChannelRead: () => calls.push("active"),
|
|
});
|
|
|
|
assert.deepEqual(calls, ["channels"]);
|
|
});
|