Skip to content

feat(notifications): choose All new posts, Just mentions or Mute per channel - #2918

Open
koltenrobison wants to merge 3 commits into
mainfrom
kolten/channel-notify-levels
Open

koltenrobison wants to merge 3 commits into
mainfrom
kolten/channel-notify-levels

Conversation

@koltenrobison

@koltenrobison koltenrobison commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Slack-style per-channel notification levels. A channel's sidebar row menu and header menu now have a Notifications submenu (in place of the Mute toggle) with:

  • All new posts: alerts for every top-level post from someone else, under a new Channel posts category (switch + sound in Settings → Notifications). Banner reads "Name posted in #channel".
  • Just mentions: the default, unchanged behavior (mentions, DMs, replies in your threads).
  • Mute: the existing synced channel-mutes preference. Mute wins over All new posts.

DMs and Solo chats keep their existing menus.

Screenshots

Row menu with the Notifications submenu:

Dark Light
Notifications submenu, dark Notifications submenu, light

Settings → Notifications with the new Channel posts category:

Dark Light
Notify me about settings, dark Notify me about settings, light

Design

  • All new posts is an account-local list (allPosts) in the existing notification preferences. No relay coordinate, Rust or browser-host change. Tradeoff: it doesn't sync across devices. Syncing would need a new encrypted sidebar coordinate across the native host, browser broker and frontend.
  • unread.attention() exposes post: true for someone else's top-level, non-DM, non-quiet message. Its status stays ineligible, so every existing caller is unchanged; only the notification binding consumes it.
  • Revalidation recomputes the level live: muting, switching back to Just mentions, or turning off Channel posts cancels pending post candidates.
  • Leaving Mute applies the new level only after the unmute is confirmed, so a failed unmute keeps the earlier choice.

Production diff is about 175 lines; the rest is tests and docs.

Tests

  • New Vitest coverage: post alerts in an All new posts channel, quiet under Just mentions and Mute, replies follow thread rules (fails without the fix); menu level changes, failed unmute keeps the choice, DMs keep Mute; preference parsing and back-compat; post banner title.
  • Browser journeys: no cases added or removed. Six specs that clicked a top-level Mute/Unmute item now go through the submenu, using shared helpers in tests/browser/navigation.mjs. The helpers wait for focus to enter the submenu before sending keys (a Linux WebKit race). Notification settings expects the extra Channel posts switch. Rebased on current main and reran locally in Chromium and WebKit: 88 passed.
  • After the rebase: typecheck, lint and the pre-push unit suite pass.
  • Full Vitest run before the rebase: 1 failure, browser-host/media-preparation.integration.test.mjs (tiled HEIC). It fails the same way on the base commit b3409542, so it is unrelated.

Deferred

  • Human UI test (not done yet).
  • Real OS banners in the packaged desktop app were not exercised.

🤖 Generated with Claude Code

baxen added a commit that referenced this pull request Nov 7, 2026
## Summary

These are fixes for three browser-journey flakes that are failing open
PRs which don't touch the code under test. The change is test-only. No
browser cases are added or removed, and no assertion is relaxed.

The red `Browser journeys (chromium, 3/9)` / `(webkit, 4/14)` shards on
most open PRs come from a different problem:
`sidebar-geometry.spec.mjs:224` is broken on main. #2945 and #2946
already fix it, so this PR leaves it alone.

### 1. `channel-completion.spec.mjs`: cold dev-server load timed out

**Evidence:** `Flake check` on #2935 failed
`channel-completion.spec.mjs:7` on both engines with `page.goto: Timeout
15000ms exceeded` on
`/tests/fixtures/mentions.html?channels&stream&test-controls`
([chromium](https://github.com/block/buzz-app/actions/runs/42317084302/job/128232679087),
[webkit](https://github.com/block/buzz-app/actions/runs/42317084302/job/128232678918)).
#2935 doesn't touch that page.

**Cause:** this is the same cold load #2776 fixed for
`mentions.spec.mjs`. Each test here started its own Vite dev server for
the same ~1,600-module fixture.

**Fix:**
- `beforeAll` builds `mentions.html` once and serves it with
`build`/`preview`, using the same config as `mentions.spec.mjs`.
- The build keeps the dev server's CSS. Tailwind's optimizer and Vite's
CSS minifier each round the 20/14 line-height token to `1.42857`, so
line boxes become `19.99998px`. In WebKit that put the popup `4.03125px`
above the composer, and the test asserts exactly `4`. I confirmed this
locally: it fails 5/5 with either setting left on and passes with both
off. Turning both off keeps the geometry the test checked before.

### 2. `profiles.spec.mjs:940`: reopening Members right after closing
the profile

**Evidence:** `Flake check (webkit, 8/9)` on #2909 failed in repeat 8 of
10
([job](https://github.com/block/buzz-app/actions/runs/42313241314/job/128219877861)).
The trace shows this sequence:
1. `Escape` closes the profile panel.
2. Focus returns to "Manage members of One" within 40ms.
3. `Enter` is pressed 10ms later.
4. The DOM after that `Enter` shows the Profile panel again and no
Members dialog, so the next `row.click` times out.

**Fix:** after each `Escape`, wait for the address to leave the profile
(no `"detail"` in the `#buzz=` hash) before reopening Members. This is a
lifecycle barrier, not a delay. One step in this test already waited for
the panel to unmount before reopening; the other steps didn't wait at
all.

**Product follow-up (not fixed here):** the order of a dismissal's
history return and a quick reopen matters.
- If I delay `history.go` by 300ms, the unpatched test takes a different
path: the dismissal's `go(-1)` never runs, and the URL stays on the
profile visit under the reopened dialog.
- A `browser-history` unit sketch (deferred traversal: `push(channel)`,
`push(panel)`, `dismiss([panel])`, then `push(settings)` before the
traversal commits) lands on `channel`. The new `settings` visit is
undone.

Someone who owns navigation should decide whether a push during a
pending dismissal return should be deferred or replayed. That is a
navigation-history change, not a test fix.

### 3. `timeline.mjs` `edge()`: one wheel gesture stopped short

**Evidence:** `Browser journeys (webkit, 6/14)` on #2937 failed
`history-prefix.spec.mjs:37` with `wheel reaches timeline edge`,
`Received: 199`
([job](https://github.com/block/buzz-app/actions/runs/42320781857/job/128244819852)).
The trace shows the one `-3251px` wheel ended with `scrollend` and the
geometry then held still, so the poll could never pass.

**Fix:** `edge()` repeats the gesture, at most four times, until the
edge is reached. Every extra gesture must reduce the distance, so an
immobile timeline still fails. `upper()` already follows this rule. The
case of already being at the edge is unchanged: it still sends one small
wheel so production can start paging.

## Not in this PR, from the same survey of the 40 most recently updated
open PRs

- `sidebar-geometry.spec.mjs:224` (Drafts & sent row) is broken on main
since #2733 + #2728 crossed. #2945 is green and #2946 is in CI; either
one fixes it.
- `solo-chats.spec.mjs:322` (Share, webkit, on #2927) is being handled
in draft #2933.
- `layout.spec.mjs:803` on #2916 is already fixed on main by #2750.
#2916's merge ref predates it.
- #2911 (thread virtualization) and #2918 (per-channel notifications)
fail consistently in tests their own changes reach, so those look like
regressions for their authors, not flakes.

## Validation

All runs were local on macOS with
`tests/browser/playwright.ci.config.mjs` and `--no-deps`. These are not
hosted CI measurements.

- `channel-completion.spec.mjs`, `--repeat-each=5`, both engines: 20/20
passed with the change. Before the CSS settings were added, the built
fixture failed WebKit 5/5 (`4.03125`).
- `profiles.spec.mjs:940`, `--repeat-each=24 --workers=6`, WebKit:
passed. It also passed with `history.go/back/forward` delayed by 300ms,
in both engines.
- `history-prefix`, `history-loading`, `scroll`, `timeline-setup` and
`new-message-boundary` (all callers of `edge()`/`end()`), both engines:
70/70 passed.
- Full `history-prefix`, `channel-completion` and `profiles` files, both
engines: 43/44 passed. The one failure was `profiles.spec.mjs:6` in
WebKit, a test this PR doesn't touch. It then passed 4/4 both with and
without this change.

**Deferred and remaining gaps:**
- I couldn't reproduce the cold-load timeout or the WebKit stopping
short locally, so I have no local fail-before for those two. The
evidence is the CI traces linked above.
- The other `edge()` callers (24 spec files) are left to CI.
- A green run doesn't prove these flakes are gone.

Signed-off-by: Bradley Axen <baxen@squareup.com>
Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
@koltenrobison
koltenrobison force-pushed the kolten/channel-notify-levels branch from 1fc369f to 0ece1bf Compare November 9, 2026 20:39
@koltenrobison
koltenrobison marked this pull request as ready for review November 9, 2026 20:40
@koltenrobison
koltenrobison requested a review from a team as a code owner November 9, 2026 20:40
@tellaho

tellaho commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

🤖 Could we hold the new notification choices and Channel posts setting until after the Nov 10 event? They expand the core notification experience beyond the contained repairs allowed during the current UI/UX frost. New UX follows the external-plugin policy.

koltenrobison and others added 3 commits November 10, 2026 11:36
…channel

A channel's row and header menus now offer a Notifications submenu in
place of the Mute toggle. All new posts alerts every top-level post from
someone else under a new Channel posts category; Just mentions keeps
today's behavior; Mute keeps the synced channel-mutes preference and
wins over All new posts. The All new posts list lives in the existing
account-local notification preferences, so it needs no relay or host
change. DMs and Solo chats keep their existing menus.

Co-authored-by: Wumbo <cba8cea567445730d41093b28915fac9a55aba4315a05864995e81bc53bfc724@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kolten Robison <kolten@squareup.com>
Leaving Mute now applies All new posts or Just mentions only after the
unmute is confirmed. A failed unmute stays muted with the earlier choice
intact, and its Retry only unmutes.

Co-authored-by: Wumbo <cba8cea567445730d41093b28915fac9a55aba4315a05864995e81bc53bfc724@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kolten Robison <kolten@squareup.com>
Notification settings now shows nine switches with Channel posts, and the
collapsed-heading spec picks Mute and Just mentions from the submenu. The
submenu helpers wait for focus to enter the levels before sending keys and
check it returns to Notifications, fixing a Linux WebKit race.

Co-authored-by: Wumbo <cba8cea567445730d41093b28915fac9a55aba4315a05864995e81bc53bfc724@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kolten Robison <kolten@squareup.com>
@koltenrobison
koltenrobison force-pushed the kolten/channel-notify-levels branch from b9758e8 to 2566391 Compare November 10, 2026 18:39

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