feat(channels): mute conversations and mark them read or unread - #91
Conversation
0cbb69a to
67146d8
Compare
726ad9e to
c86b33c
Compare
222cb7e to
c6179d8
Compare
Extend the existing persistent sidebar context menu with icon-bearing mute/read actions. Preserve New session, placement cleanup, and focus handoff while confirming encrypted mute writes through the live socket and durable unread owner. Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Find the existing row container rather than assuming two wrapper levels. Preserve hide/restore, badge geometry and tab-order assertions when mute/read enables the shared context menu for DMs. Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Observe scrollend before measuring the next plugin row position. A changed scrollTop only proves wheel input started; queued WebKit scrolling could clip the switch after the full-visibility assertion passed. Preserve main's geometry, navigation, focus and keyboard assertions. Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
6fea974 to
5db272b
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Source review: no blocking defects found
Reviewed 5db272b12afa2352981d07fd81121e42f3219e4e against base 204ea9d1dea6b7c524d6e09f032395fbde723830, covering sidebar mute/read actions, confirmed preference publication/readback, notification policy, durable unread ownership, and session/cache-clear transitions. The two preliminary concerns did not survive verification against the documented contract: failed preference refreshes deliberately hold ordinary alerts, and cache-clear cancellation does not promise silent dismissal or rollback of an accepted mute publication.
- Existing CI evidence: run 36070345580, snapshot around 23:03Z, passed JavaScript (315 files / 3,415 tests), browser measurements, DCO and security checks. Rust/tool integration and all four browser-journey shards were still running; Windows native validation was skipped. This is an incomplete snapshot, not a full CI pass.
- Validation limits: source-only review; no PR code or tests executed by the reviewers. The PR’s updated local 26/26 browser results and 2,219 unit/service results are author-reported evidence, not independent execution. Native mute writers, deployed-relay behavior and real OS banners remain outside this browser/development-host slice.
This is a review comment, not approval or a merge-readiness certification. Required hosted checks and human/code-owner review remain separate gates.
Since #91, every sidebar row has actions, so every row was wrapped in its own ContextMenuRoot and MenuPopup outside the memoized ChannelSidebarItem. Those wrappers got fresh action lists and callbacks on every sidebar render, so a channel switch rerendered one Base UI menu root per row. Move the row's menu root into ChannelSidebarItem and give popup content only to the open row. Closed rows now keep equal props and skip the render. With 4x CPU throttling, the median warm switch in channel-opening.spec falls from 257ms to 164ms (24 samples each). Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Since #91, every sidebar row has actions, so every row was wrapped in its own ContextMenuRoot and MenuPopup outside the memoized ChannelSidebarItem. Those wrappers got fresh action lists and callbacks on every sidebar render, so a channel switch rerendered one Base UI menu root per row. Move the row's menu root into ChannelSidebarItem and give popup content only to the open row. Closed rows now keep equal props and skip the render. With 4x CPU throttling, the median warm switch in channel-opening.spec falls from 257ms to 164ms (24 samples each). Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Overview
Category: new-feature
User Impact: Users can mute a conversation or deliberately mark it read or unread without opening it.
Problem: The sidebar lacks direct controls for quieting a conversation and managing its read state.
Solution: Add focused Mute/Unmute and state-appropriate Mark as Read/Unread actions using the existing preference, notification and durable unread owners. Mute dismisses immediately with rollback/retry on failure; read actions close only after local durability.
Standalone scope: Focused mute/read implementation
6c65ec30plus test-only CI repairs8267c4a0and5db272b1, directly onmainat204ea9d1. This extends the merged persistent-sidebar context menu: New session → separator → Mute/Unmute → Mark as Read/Unread. Preserve main’s placement-lifetime cleanup, per-action eligibility, composer focus handoff, pill-rounded menu rows, DM controls and unread presentation. No restored row ⋮, alternate menu, Move to…, group/star writes, sorting/lifecycle changes or shared-menu restyling. Production diff: 14 files, +715/−9 lines; tests/docs are separate.Implemented and maintained by Carl (AI agent), using the operator’s GitHub account. Ready for review; hosted CI completion remains pending at the last check. Not a merge-readiness claim.
Behavioral boundaries
Changes
File changes
dev/relay-broker.mjs
Add the narrow, per-relay serialized mute command with bounded signed reads, same-community live-socket publication and confirmed readback.
dev/sidebar-mutes-broker.test.mjs
Exercise real transport → HTTP broker → modeled socket integration, NIP-98 authentication, owner scoping, invalid input, rejection/disconnect and failed confirmation.
dev/sidebar-mutes.mjs
Validate and encrypt one mute intent while preserving unrelated fields and explicit unmute tombstones; fail closed on invalid heads and conflicts.
dev/sidebar-mutes.test.mjs
Cover malformed/foreign heads, budgets, monotonic updates, no-ops and publication/readback failures.
dev/sidebar-preferences.mjs
Decode the third own-author preference coordinate within existing signature and size bounds.
docs/channels.md
Document the shared menu and persistent sidebar’s pending/error lifetime, optimistic recovery, socket ownership and host/convergence limits.
docs/notifications.md
Document muted-alert suppression, mention exceptions and pending-candidate cancellation.
docs/unread.md
Define evidence-based channel read, atomic local clearing and the explicit ownership matrix.
src/bundled/channels/ChannelReadMenuItem.test.tsx
Cover state-appropriate commands and decorative icons, pending state, live updates, retargeting and subscription cleanup.
src/bundled/channels/ChannelReadMenuItem.tsx
Offer one read action from the domain snapshot while the menu is mounted, with a decorative 14px envelope icon.
src/features/channel-navigation/ChannelSidebar.tsx
Extend the existing New session menu with icon-bearing mute/read actions and a conditional separator. Keep retry/dismiss notices in the persistent owner, preserve placement cleanup and composer handoff, and restore focus by row identity without selecting the conversation.
src/bundled/channels/sidebar-sections.test.ts
Extend the preference fixture with muted state without changing placement expectations.
src/bundled/channels/useChannelRowMenu.test.tsx
Complete the preference fixture with muted state while retaining main’s menu-placement assertions.
src/bundled/channels/useOptimisticMute.test.tsx
Cover immediate presentation, rollback/retry, superseding clicks and retired-view completions.
src/bundled/channels/useOptimisticMute.ts
Keep optimistic menu state local while confirmed session state remains notification authority.
src/features/notifications/messages.test.ts
Cover muted DMs/thread replies, mention exceptions, startup readiness and cancellation during permission checks.
src/features/notifications/messages.ts
Apply confirmed mute policy in the app-global notification owner, including when Messages is unmounted.
src/features/relay/live-restriction.test.ts
Preserve restriction coverage with the complete preference fixture.
src/features/relay/read-state.ts
Atomically clear the supplied owned local marks with the existing durable frontier transaction.
src/features/relay/session.ts
Wire the optional mute writer into the existing session lifetime and deadline (14 added lines).
src/features/relay/sidebar-preferences-store.test.ts
Cover confirmed writes, serialization, stale refreshes, capability checks and cancellation/generation fences.
src/features/relay/sidebar-preferences-store.ts
Retain immutable muted state and serialize confirmed writes without stale-read rollback.
src/features/relay/sidebar-preferences.test.ts
Cover muted projection and malformed entries; preserve existing group/star projection.
src/features/relay/sidebar-preferences.ts
Add the bounded muted projection and narrow optional writer contract.
src/features/relay/transport.ts
Expose the mute command only when advertised and pass the existing live-owner identity header.
src/features/relay/unread.test.ts
Cover evidence cuts, replies, owned clearing, unrelated state, access/lifetime changes and durable-save failure.
src/features/relay/unread.ts
Snapshot retained verified evidence at invocation for explicit channel read; invent no wall-clock frontier or history fetch.
src/features/relay/warm-lifecycle.test.ts
Preserve warmer lifecycle expectations with complete preference data.
src/features/relay/warm.test.ts
Preserve warming expectations with complete preference data.
src/shared/design-system/icons/index.ts
Export Phosphor Bell/BellSlash and Envelope/EnvelopeOpen icons through the existing shared boundary.
tests/browser/fixture.mjs
Model encrypted mute storage and record precisely injected HTTP failures without a broad console-error allowlist.
tests/browser/navigation-mute-read.spec.mjs
Add one representative built-app journey per engine for shared action order/icons, real focus/dismissal, gated failure/retry across page switches, IndexedDB reload, row relocation, Sessions disable/enable without a dangling separator, and production-broker New session composition.
tests/browser/navigation-session-menu.spec.mjs
Complete the preference fixture with muted state; retain main’s stale-placement cleanup and session-focus assertions.
tests/browser/sidebar-unread.spec.mjs
Locate DM controls through the existing sidebar-row marker rather than a fixed wrapper depth. Preserve hide/restore, badge geometry, hover styling and keyboard tab-order assertions when DMs expose the shared context menu.
tests/browser/settings.spec.mjs
Wait for each real wheel gesture’s
scrollendbefore measuring the next row position. Retain main’s distance-based scrolling, visibility margins, navigation assertions and full-visibility/focus/keyboard checks. No Settings production changes.tests/browser/policy-relay.mjs
Validate the exact three-coordinate preference query; no new grouping writes.
Reproduction Steps
/sidebar-muteresponse. The action rolls back and a Retry/Dismiss notice appears. Retry sends the same intent, not a toggle against stale state.Validation
Local macOS arm64, pinned Node 24.18.0 / pnpm 11.8.0 / Playwright 1.63.0. Evidence below applies to clean head
c6179d8, based on119195e, unless explicitly stated otherwise.bin/pnpm exec playwright test --config tests/browser/playwright.config.mjs --project chromium --project webkit --no-deps tests/browser/navigation-mute-read.spec.mjs tests/browser/navigation-session-menu.spec.mjs: 6/6 passed, 26.6 seconds, two workers, no retries. Fixture evidence records this exact clean head and builds the production frontend; the mute/read journey uses the real development broker against modeled upstream WS/HTTP policy.navigation-mute-read,navigation-session-menu,navigation-sidebar,new-message,thread-unread) passed 44/46 before fixture fixes. Both failures were the placement fixture omitting the newly requiredmutedarray. A subsequent focused run exposed a focus assertion placed after Settings navigation; it was moved back immediately after menu dismissal. The final 6/6 above reran both affected files, preserving the original placement and focus assertions. The five-file set was not rerun in full after those corrections.git diff --check origin/main...HEADpassed. No current-head full-suite, native, deployed-relay, OS-banner or local channel-opening-measurement claim.Browser coverage accounting: +1 journey per engine relative to main; none removed. Browser-only justification: real shared-menu accessibility/focus, production frontend/broker/socket composition, IndexedDB reload, persistent sidebar behavior across page switches, and Sessions plugin transitions. Policy/failure matrices remain in unit/service tests. No retries, relaxed budgets or error allowlists added.
CI repair (
6fea974e)Run 36065578455 at
c6179d8passed JavaScript, Rust/tool integration, browser measurements, both first journey shards, DCO and security. The second shards failed only insidebar-unread.spec.mjs: DM hide/restore in both engines and DM keyboard tab order in Chromium. Enabling the existing context-menu wrapper for DMs invalidated the test’s two-parent traversal; CI’s captured page still contained the remove control. This repair uses the existingdata-channel-sidebar-rowmarker, matching the navigation test convention. No production changes, assertion removals, retries or relaxed thresholds.bin/pnpm exec playwright test --config tests/browser/playwright.config.mjs --project chromium --project webkit --no-deps tests/browser/sidebar-unread.spec.mjs— 12/12 passed, 27.3 seconds, two workers, no retries, macOS arm64. Ran atc6179d8with exactly this test-only working-tree edit; committed unchanged as6fea974e.6fea974e: TypeScript, 183 related unit/service files / 2,177 tests, design types and guards.git diff --checkpassed; worktree clean.6fea974e: JavaScript, Rust/tool integration, measurements, three journey shards, DCO and security passed; WebKit 2/2 failed solely in the narrow Settings case (145 passed, one failed). The mute/read journey and six sidebar-unread cases passed in that shard. Windows native validation was skipped. The operator moved this PR to ready for review; no approval or merge performed.Settings wheel synchronization and main integration (
5db272b1)The failed hosted trace proves the test measured transient visibility while queued wheel input was still moving: Settings
scrollTopadvanced from 1286 when the row assertion passed to 1506 before the click, then stayed at 1506 through click, Space and the failed final assertion. This clipped the switch to intersection ratio 0.992968738079071. The test waited for scrolling to start, not finish.The test now arms a one-shot
scrollendobserver before each gesture and awaits completion before measuring again. No fixed sleeps, animation overrides, retries or relaxed assertions; zero cases added/removed. The unchanged macOS WebKit run passed, so hosted failure and local pass are different-environment evidence, not a deterministic local fail-then-pass claim.Rebased onto
204ea9d1and retained main’s Settings layout, distance-based wheel geometry, visibility margins and Hosted communities keyboard assertion. The only overlap was restoring the local test edit from autostash; the resolution combines main’s wheel distance with completion synchronization.git range-diffshows the two previous PR commits retained semantically (only surrounding broker context changed).bin/pnpm exec playwright test --config tests/browser/playwright.config.mjs '/settings.spec.mjs$' '/sidebar-unread.spec.mjs$' '/navigation-session-menu.spec.mjs$' '/navigation-mute-read.spec.mjs$' --project chromium --project webkit --no-deps --trace on— 26/26 passed, 57.4 seconds, two workers, zero retries, macOS arm64. Ran at8267c4a0with exactly the staged test edit committed unchanged as5db272b1.5db272b1: TypeScript, 186 related unit/service files / 2,219 tests, design types/guards.git diff --checkpassed; worktree clean. All three outgoing commits retain actual Carl authorship and DCO.5db272b1: DCO passed; JavaScript, Rust, measurements and four journey shards running at the last snapshot. Windows validation skipped. Hosted success is still pending; no CI-green or merge-readiness claim. No polling loop is running.Screenshots / Demos
Actual production-built app at clean
c6179d8, dark theme with fixture data, cropped to the shared menu. These show both current action states—not synthetic UI. Main’s New session action, menu styling and separator are retained; this PR adds the mute/read actions and their icons.Unread, unmuted conversation
Read, muted conversation