镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(notifications): notify on posts by chosen people - #2939

Open
LordMelkor wants to merge 2 commits into
block:mainfrom
LordMelkor:clod/notify-people
Open

LordMelkor wants to merge 2 commits into
block:mainfrom
LordMelkor:clod/notify-people

Conversation

@LordMelkor

@LordMelkor LordMelkor commented Nov 7, 2026 •

Copy link
Copy Markdown

Problem

Notification controls are four category switches (mentions, direct messages, thread replies and agent completions) plus channel Mute. There's no way to be notified when a particular person posts. An ordinary post in a channel you're in never alerts.

Change

  • People on your list. Choose Notify me when they post on someone's profile. Their posts in channels you're in then alert, as the new person category. Stop notifying removes them.
  • Settings → Notifications → People on your list shows everyone you added, with Remove and Clear list. A People on your list switch under Notify me about turns these alerts off without clearing the list.
  • Same rules as other alerts. Nothing while you're viewing the conversation, nothing once it's read, nothing from a muted channel, and nothing from a thread you opted out of. Only otherwise-uncategorized posts become person:
    • A listed person who also mentions you or DMs you alerts once, as a mention or DM.
    • A reply in one of your conversations stays thread, including while its conversation lookup is pending.
    • Replies outside your conversations don't alert.
  • Storage. The list is saved on this device, per account, in the existing notification preferences, up to 200 people. It's read again at delivery, so removing someone cancels an alert that's still waiting.
  • Plugins. The notificationPeople service's methods reject calls made through any plugin's context except the bundled Profiles plugin's (buzz.profiles), the same check review-access.ts uses for plugin management. The typed ctx.notifications contract has no people methods. This isn't isolation. Plugins are trusted, unsandboxed code (docs/plugin-architecture.md), so a plugin that digs into host objects, rewrites its own context or keeps another plugin's rendered panel can still reach the list, as it can every other notification preference.
  • Documented in docs/notifications.md and docs/buzz-help.md.

No FOUNDATION file is changed.

Intent

Original request: kray's #buzz-feedback post (Nov 2): "Would be great to be able to customize notifications: eg. channel activity vs mentions, posts by certain people". This PR is the "posts by certain people" half. A per-channel "all activity" mode is a separate PR.

Must change: a "people" list. A post by a listed person in a channel I'm in notifies me (category person, one rule in notifications/messages.ts, toggle on the profile card, list in Settings). A post that also mentions me alerts once.
Out of scope: per-channel "all activity" mode (separate PR), keyword alerts, schedules, digests.
Acceptance: with a person on the list, their post in a joined channel notifies; others' posts don't (beyond existing rules).

Validation

  • messages.people.test.ts:
    • A listed person's top-level post alerts, and an unlisted person's doesn't.
    • A mention from a listed person alerts once, as a mention.
    • The category switch silences person alerts.
    • A muted channel stays quiet.
    • A reply in a thread you opted out of stays quiet, even while you're viewing it.
    • A pending reply is classified as thread.
    • Removing a person cancels a waiting alert.
  • Preferences, Settings and profile tests:
    • The list round-trips, is capped and restores as empty from older saved values.
    • The Settings list removes and clears, and gives unnamed people with similar keys distinct labels.
    • The profile toggle adds and removes, and another plugin is refused the list.
  • The regressions fail without their fixes. Focused tests (notifications and unread) 449/449 at round 1. tsc --noEmit and biome check pass. Full suite at 9f169d66, on main b1e1cd82: 19,408 passed and 5 failed, in 3 files this diff doesn't touch (App.profile, IdentitySettings, MessageReactions). Run alone, those 3 files pass 89/89, so the failures are timeouts on a heavily loaded shared machine. After rebasing onto main 41e64bf0 and the round-2 fixes, TypeScript passes and the notifications, profiles, unread and settings tests pass 522/523. The one failure is a 5 s timeout in unread-startup.test.ts, which passes 52/52 when run alone.
  • Review rounds (each finding checked against the frozen intent above):
    • Round 1 (sq agents review): three P2s, all fixed.

      • A person alert could override a thread opt-out while viewing.
      • A pending reply's classification changed.
      • The list wasn't re-read at delivery.
    • Round 2 (sq agents review): one P2, fixed. Any plugin could call the people-list service. Its methods now reject calls from every plugin except Profiles, and the profile toggle's test checks that another plugin is refused.

    • Round 2 pressure test (Sigil): found that a plugin can still reach the list at runtime through ctx.root, the untyped ctx.notifications object, a context it rewrote with ctx.extend(), or the Profiles panel's rendered props from ctx.panels. That is the trusted-plugin model the whole plugin API has, not a gap in this change, so the docs and this body now say exactly what the check covers and that it isn't isolation. Making plugins untrusted is out of this card's scope.

    • Round 3 (sq agents review): two P2s, both fixed.

      • A removed person's alert waited on channel preferences before the list was checked, so re-adding them revived it.
      • After a failed save, clicking the toggle again reversed the choice. It now offers Retry saving.

      A third finding repeated the plugin-trust route, which is handled above.

    • Round 4 (sq agents review): one P2, fixed. With 200 people listed, adding one reported a failed save, and Retry saving cleared the error without adding anyone. The card now says the list is full and offers no retry.

    • Round 5 (sq agents review): one P2, fixed. In Settings, two unnamed people whose npubs end alike got the same label and Remove button name. The list now uses publicKeyLabels, which lengthens colliding abbreviations.

    • Final sq agents review at d56eca58, on main 41e64bf0: no findings.

    • Rebase review (sq agents review) at 03a10cb5, after squashing and rebasing onto main b1436b09: three P2s, all fixed in 09209d36. Each has a test that fails without its fix. Production change: about +26/−10.

      • A listed person's post that the relay signs on their behalf (an actor or leading p tag) didn't alert. Matching now uses the author Buzz displays, from the existing messageAuthor resolver. The same tag from any other signer stays untrusted.
      • A broadcast reply in a thread you opted out of still sent a person alert. Opted-out threads no longer count for person alerts. Broadcast unread counting is unchanged.
      • Two listed people with the same resolved name had identical rows and Remove buttons. Those rows now show their key, and their Remove labels name it.

      Not changed: an alert for a relay-signed post names the signer, as every other alert category does on main.
      Full suite at 09209d36: 20,475 passed and 25 failed, in 6 files this PR doesn't touch (App.profile, ProfileSettings, solo-worker, DraftPreview, MessageReactions, relay/startup). Run alone, those 6 files pass 206/206, so the failures are timeouts under machine load.

    • Pressure test: an independent agent (Sigil) found no blocker at the round-1 head. Later it confirmed both round-3 fixes: each regression fails on the parent, and reversing only the retry handler fails at the saved-choice assertion. It also confirmed that the trust-model wording matches its four access probes, and found nothing else. 338 tests, TypeScript and Biome pass at b1ceb869 (now c9e29980 after the rebase). At the final head (0c6cec09) it confirmed the full-list message, that freeing a slot lets a 200th entry save, and that removal works at capacity. 339 tests in 29 files, TypeScript and Biome pass. Round 1 also checked that pending lookups keep thread precedence, that removing a person cancels their waiting alert, and that an opted-out thread stays quiet even with Notify while viewing on. 268 tests in 23 files and TypeScript pass. Native notification delivery wasn't exercised.

  • Design, matching existing behavior:
    • The list is saved on this device, like the other notification preferences.
    • Replies outside your own conversations don't alert, because Buzz doesn't count them as unread.

Try it (AGENTS.md step 3, for a human)

  1. Open a teammate's profile and choose Notify me when they post.
  2. Have them post in a channel you're in, while you're looking at a different channel. Expected: a notification " posted in #channel".
  3. Have someone not on your list post there. Expected: no notification.
  4. Mute the channel and have the teammate post again. Expected: no notification.
  5. Go to Settings → Notifications → People on your list. Expected: the teammate is listed. Click Remove, and their next post doesn't notify.

🤖 Generated with Claude Code

@LordMelkor
LordMelkor requested a review from a team as a code owner November 7, 2026 00:29
@morgmart

morgmart commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

🤖 Design review: this isn't critical path for the Nov 10 launch. Please hold it until after launch, or build it as an external plugin that isn't bundled into the app. Questions: #buzz-design-review.

LordMelkor and others added 2 commits November 10, 2026 09:59
Notification choices were four category switches plus channel mute, so there
was no way to hear about someone's posts without being mentioned. Add a
persisted people list and a `person` category: a listed person's post in a
channel the viewer is in alerts under the same viewing, unread and mute
gates, and alerts once as a mention when it also mentions the viewer. Toggle
from the profile panel; review and clear in Settings → Notifications.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
…ead opt-outs

A listed person's post relayed under the relay's signature (actor or leading
p) now matches the verified displayed author. A broadcast reply in a thread
the viewer opted out of stays quiet for person alerts, while broadcast unread
counting is unchanged. Colliding display names in the people list show a key
in the row and the Remove label.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
kazemek21 added a commit that referenced this pull request Nov 10, 2026
## TL;DR
Captured explicit reads now check the current saved manual-unread
journal inside the replacing transaction, so a newer local
choice—including one saved by another window—is not erased. Retries
retain their original cut; the additive
`unread.prepareThreadRead(channelId, rootId)` captures only retained
incoming replies without reading the root or advancing the channel.

The fence also preserves earlier same-window queued manual actions,
authorizing their revisions only after a successful save whose prior
same-key intent is also captured. An earlier queued save cannot mask an
intervening unread choice from another window. It leaves reading-lease
ownership, passive observations, remote-marker ordering, journal format
and relay protocol unchanged.

Mark all now reserves one shared saved revision cut before queued
writes. Each predicate retains only the revision, not a full journal
copy.

### Validation
- Shared-cut correction: pinned Linux Blox run of both complete owner
files passed 383 tests (174 read-state; 209 unread). The per-sweep
capture-budget and capture-failure regressions fail on the prior
implementation; older queued-save controls pass. No new browser cases.
- Linux Blox, pinned tools: both owner files passed 366 tests on the
initial head. After review fixes, `pnpm exec vitest run
src/features/relay/unread.test.ts` passed all 206 tests. The final
queue-ordering fix passes all 174 tests in
`src/features/relay/read-state.test.ts`, typecheck and the commit/push
hooks.
- Pre-fix channel regressions: six failures; bypassing the new
transaction fences makes eleven targeted concurrency regressions fail.
Replacing canonical thread selection with parent-edge traversal makes
the narrow-thread regression fail. Restoring the implementation passes
both owner files.
- Three successful-save interleavings fail before the final fix and pass
afterward, with three matching controls: channel, thread and message
marks overwritten by an older queued save after another window marks
unread.
- Eight additional regressions fail before the review fixes and pass
afterward: root/reply deletion before or after persistence, and shared
sidebar hints with absent, present or cross-window forced-message
overlays.
- Touched-file Biome, `pnpm typecheck`, `pnpm author:build`, strict
generated-SDK consumer compilation and repository commit/push hooks.
- Deterministic fixture gates cover newer channel/thread/message marks,
second-window saves at the transaction boundary, queued ordering, retry
cuts, later arrivals, independent root/thread markers and
access/lifecycle expiry. No browser tests were added or removed.

Generated type-only SDK declarations expose the additive method through
the existing `UnreadCapability` export; existing channel and message
APIs remain compatible. Capture requires initialized read-state storage.
The thread API does not discover history; receipts remain device-local.
A narrow read leaves the shared sidebar hint until channel open
reconciles it, since that hint has no subtree ownership. Its shared
message-force fence is conservative: a newer mark elsewhere in the same
channel can also reject the save. This does not establish native runtime
behavior, identical remote-tuple ordering or atomicity through an
operation's eventual return.

Related independent work remains with #2060 (quiet Solo/Inbox), #2610
(selected-message UI), #3181 (passive recovery), and #2939/#1666/#2956
(notification/audience behavior). This change is limited to the explicit
mutation/preparation seam.

Fixes NEXUS-4426

[Initiative
status](https://g2.sqprod.co/apps/kylez-buzz-improvements-status)

*Posted by Codex (gpt-6.1-sol) on behalf of kylez.*

Signed-off-by: Kyle Zemek <kylez@squareup.com>
Co-authored-by: Codex <noreply@openai.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants