Skip to content

feat(channels): mute conversations and mark them read or unread - #91

Draft
tellaho wants to merge 4 commits into
tho/sidebar-grouping-starfrom
tho/sidebar-mute-read
Draft

tellaho wants to merge 4 commits into
tho/sidebar-grouping-starfrom
tho/sidebar-mute-read

Conversation

@tellaho

@tellaho tellaho commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Overview

Category: new-feature
User Impact: Users can start a session, mute a conversation, and mark it read or unread from one uncluttered sidebar menu.

Problem: Conversation actions were split between a context menu and a separate three-dot button, while mute decoration added row noise and saving held the menu open.

Solution: Bring New session into the rounded shared menu, remove the extra row controls, and make Mute/Unmute respond immediately while saving in the background. Read actions reflect current unread evidence and commit through the durable unread owner; failed mute saves restore confirmed state with an explicit Retry/Dismiss notice.

Scope / dependency: Stacked on grouping PR #10 via tho/sidebar-grouping-star at 67146d80b73344220f1dc7991871f1f65169759a, already an ancestor of this head. Starred, Move to… and group assignment belong to that predecessor; they are preserved, not reintroduced here. Session creation/persistence and permissions are unchanged—only its entry point moves. Shared menu styling also applies to other consumers. Sorting and lifecycle actions remain separate.

Implemented and maintained by Carl (AI agent) under the operator’s GitHub account. Draft status and stack base are unchanged.

Changes

File changes

dev/relay-broker.mjs
Expose the narrow mute command through existing signed, scoped, serialized preference writes.

dev/sidebar-mutes.mjs
Validate one explicit intent, preserve encrypted fields and unmute tombstones, and confirm the retained result before success.

dev/sidebar-mutes.test.mjs
Cover invalid/foreign heads, budgets, monotonic writes, no-ops, conflicts and publication/readback failures.

dev/sidebar-preference-writes.test.mjs
Exercise Mute through the real HTTP broker, including origin/auth boundaries and failed confirmations.

dev/sidebar-preferences.mjs
Decode the third account-owned preference coordinate within the existing bounded host boundary.

docs/channels.md
Document immediate mute dismissal, background saving, rollback/retry, icon-free rows and the distinct read-action commit behavior.

docs/notifications.md
Document mute eligibility, mention exceptions, pending cancellation and native/sync limits.

docs/sessions/README.md
Document the unified context-menu entry point, keyboard access and focus into retained session drafts.

docs/unread.md
Define channel-read evidence and the explicit clearing matrix, including bounded ownership and local durability.

src/bundled/channels/ChannelReadMenuItem.test.tsx
Cover observed/manual/absent/unknown unread states, command routing, live snapshot updates, pending state, retargeting and StrictMode cleanup in six mounted component tests.

src/bundled/channels/ChannelReadMenuItem.tsx
Subscribe to the shared unread snapshot only while the menu is mounted. Offer exactly one appropriate read action, using the existing device-local manual-unread command.

src/bundled/channels/ChannelSidebarRow.module.css
Remove styling for the redundant three-dot menu while preserving the child-session disclosure.

src/bundled/channels/ChannelSidebarRow.test.tsx
Assert the row has no separate menu button; retain child-session, draft and selection coverage. Real menu-to-composer focus is covered by the browser journey.

src/bundled/channels/ChannelSidebarRow.tsx
Delete the row’s separate New session menu; keep selection, child sessions and draft resumption intact.

src/bundled/channels/ChannelsPage.tsx
Put New session first in the shared context menu and preserve composer focus independently of preference capabilities. Dismiss mute immediately, expose sidebar retry on failure, and keep conditional read actions and identity-based row focus.

src/bundled/channels/sidebar-sections.test.ts
Extend grouping fixtures with the mute projection without changing placement expectations.

src/bundled/channels/useOptimisticMute.test.tsx
Cover immediate projection, confirmation, rollback, explicit retry, rapid reversals, independent channels, StrictMode, retired views and synchronous host failure.

src/bundled/channels/useOptimisticMute.ts
Keep a page-local presentation overlay while the session owns saving. Newer intents supersede older results; failure restores confirmed presentation and offers explicit retry without changing notification truth.

src/bundled/channels/useSidebarPreferences.ts
Expose the session-owned mute capability to the page.

src/features/notifications/messages.test.ts
Cover DM/thread suppression, preserved mentions/unread, failed startup reads and mute/unmute during a permission wait.

src/features/notifications/messages.ts
Apply confirmed mute state in the app-global notification producer, including startup and lifecycle revalidation.

src/features/relay/live-restriction.test.ts
Keep the live-restriction fixture consistent with the added mute projection.

src/features/relay/read-state.ts
Commit an explicit list of owned local clears atomically with the frontier, preserving automatic reading semantics.

src/features/relay/session.ts
Add 14 lines of approved composition-only wiring for the mute writer, lifetime and deadline.

src/features/relay/sidebar-preferences-store.test.ts
Cover confirmed state, shared write ordering, failure/retry, stale reads and session/caller cancellation.

src/features/relay/sidebar-preferences-store.ts
Retain immutable mute state and serialize confirmed writes through the existing preference owner.

src/features/relay/sidebar-preferences.test.ts
Expect the third exact coordinate in production decoder wiring and absent-state projection.

src/features/relay/sidebar-preferences.ts
Validate the legacy channel-mutes schema and expose typed mute intent/projection.

src/features/relay/transport.ts
Discover and invoke the narrow host mute capability, validating its returned projection.

src/features/relay/unread.test.ts
Cover channel-owned clears, latest-reply cuts, later arrivals, access/disposal fences and atomic storage failure/retry.

src/features/relay/unread.ts
Snapshot verified channel evidence at invocation and submit one durable channel-read transaction.

src/features/relay/warm-lifecycle.test.ts
Keep warm-state fixtures consistent with the expanded preference contract.

src/features/relay/warm.test.ts
Keep warm-state fixtures consistent with the expanded preference contract.

src/shared/design-system/DESIGN.md
Describe the shared menu’s roomier inset and pill-shaped highlights.

src/shared/design-system/icons/index.ts
Expose the Phosphor muted-bell icon through the shared gateway; sidebar rows no longer render it.

src/shared/design-system/styles/components.css
Give shared menus an 8px inset and pill-shaped item highlights. Other shared menus inherit the same recipe; global panels and popovers are unchanged.

tests/browser/fixture.mjs
Record encrypted mute publications and match injected failures by exact request URL. Return the real empty agent-library shape so the production-broker session composer can mount.

tests/browser/navigation-groups.spec.mjs
Update keyboard Home/End expectations for New session and conditional read actions while retaining group/star, focus and activity journeys.

tests/browser/navigation-mute-read.spec.mjs
Exercise immediate dismissal with held responses, rollback/retry, encrypted persistence, read/unread reload and unrelated-channel preservation. Retain cold-row-relocation focus coverage and open the session composer through the production broker.

tests/browser/navigation-session-menu.spec.mjs
Exercise right-click and keyboard session creation, Escape focus restoration and retained drafts without preference/read-write host capabilities; assert the three-dot button is absent.

tests/browser/policy-relay.mjs
Require the exact three sidebar preference coordinates in the modeled upstream relay.

Reproduction Steps

  1. Open Messages. Hover a channel: there is no separate three-dot button or mute icon. Right-click it, long-press, or focus the row and press Shift+F10 / the Context Menu key. New session is first; highlighted items follow the rounded menu surface.
  2. Choose New session. Focus moves into the draft composer. Type a draft, switch away, then choose New session again: the draft is retained. Escape from the menu returns focus to the channel row.
  3. On a host supporting mute writes, choose Mute on a non-selected conversation. The menu closes and row focus returns immediately; reopening shows Unmute while saving. Selection and unread evidence do not change. No mute icon appears on the row.
  4. Simulate a failed mute save: confirmed menu state returns and a sidebar notice offers Retry / Dismiss. Retry repeats the same intent. A newer opposite click is not overwritten by an older completion.
  5. Choose Mark as Read on an unread conversation. It closes after the local transaction commits and clears only owned unread evidence. Reopen: Mark as Unread replaces it. Choose that action and reload; its device-local mark persists.
  6. Reload after mute confirmation: the menu still offers Unmute. Eligible DM/thread alerts are suppressed; explicit mentions still follow existing global/category/read/access/permission policy. Unmute restores future eligible alerts without replaying cancelled ones.

Validation and boundaries

  • Published head: 5dc4abfff03aaa835a0a59c69d874e7644aa37b8. Clean tracked working tree; remote head verified. All four slice commits retain actual agent authorship and DCO sign-off.
  • Normal pre-commit/pre-push hooks passed at this head: staged formatting/lint, app TypeScript, 122 related Vitest files / 1,521 tests, design TypeScript and design guards. git diff --check passed against the fetched stack base. Full local scan was not repeated.
  • Final affected mute/read browser file: 2/2 passed, Chromium and WebKit, macOS, pinned Hermit tools, no retries, 11.2s runner wall time. Command: bin/pnpm exec playwright test --config tests/browser/playwright.config.mjs tests/browser/navigation-mute-read.spec.mjs --project chromium --project webkit --no-deps.
  • Grouping and standalone session-menu files passed 10/10 in both engines on 1a95a4b (the earlier local fixture-fix snapshot). Only the mute/read test subsequently changed: an explicit menu-closed/focus-return barrier replaced an immediate Escape→reopen race; that full file was rerun at the published head. No product changes followed those passes.
  • A fresh Chromium capture journey passed at the published head using the actual built frontend, production HTTP broker and modeled upstream relay. It confirms mute/read states, clean rows and focus into a session draft. Screenshot setup initially exposed a stale fixture returning [] for the agent library; the real reader already returns { definitions, identities }. The fixture now matches that contract, and the production-broker journey retains composer coverage.
  • Browser cases relative to the stack base: 2 added / 0 removed, executed in both engines. Browser-only justification: real menu focus/dismissal, composer focus transfer, app→session→broker→IndexedDB reload and row relocation during a held command. State/concurrency/policy matrices remain in Vitest. The removed row-menu component assertions are replaced by the shared-menu browser journey. The capture failed with the invalid library fixture and passed after correcting it; the initial Escape/reopen ordering failure was fixed with observable lifecycle assertions, not sleeps or retries.
  • Hosted DCO Check passed at the published head. No other checks had appeared in the final status snapshot; no green-CI claim. The previous published head had failing browser measurements/journey jobs, so this refresh is not evidence those broader failures are resolved.
  • Deferred: complete browser suite/local-only WebKit checks, isolated performance measurements, native GUI/OS banners, deployed-relay acceptance and independent review. Rust/Clippy/cross-system CLI coverage remains with hosted CI. This draft is not integration approval.
  • Mute changes notification eligibility, not unread truth. Only confirmed preferences affect notifications; the optimistic overlay is page-local, not a durable outbox. Leaving Messages drops its pending/error UI but does not cancel the session-owned save. Session disposal still cancels pending work; an accepted publication cannot be retracted.
  • Mark as Read snapshots retained verified evidence, including replies, without a wall-clock cutoff or extra history query. Later arrivals beyond the cut remain unread. No evidence means only the local channel mark clears; unproven historical marks and remote overrides are preserved. Completion means local durability, not completed relay publication.
  • Mark as Unread is durable on this device/browser only. Only the mounted read-menu item subscribes; no per-row fetches are added. Preference writes remain confirmed whole-record replacements, not atomic cross-device merging, automatic retry or native-adapter parity.

Screenshots / Demos

Fresh captures from the actual built app at 5dc4abf, dark mode with its existing color accent and test conversation data. These are resulting UI states, not synthetic renderings or a component showcase. Sidebar images are cropped to the affected controls. Grouping options shown for context belong to the preceding PR.

Unified menu; unmuted and unread Muted and marked read
New session first in the rounded shared menu, without extra row controls Unmute and Mark as Unread in the rounded shared menu

@tellaho tellaho changed the title feat(channels): mute and mark channels read from the sidebar feat(channels): mute conversations and mark them read or unread Sep 22, 2026
@tellaho
tellaho force-pushed the tho/sidebar-grouping-star branch from 0cbb69a to 67146d8 Compare September 22, 2026 22:02
Carl added 2 commits September 22, 2026 15:10
Use confirmed host-owned encrypted mute preferences in message notification policy, preserving mentions and unread evidence. Advance channel read state through verified evidence and clear only owned local unread marks in one durable transaction.

Expose both actions through the shared row menu with persistence, cancellation, failure recovery and browser coverage.

Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Use the shared unread snapshot to offer Mark as Unread only when there is no unread evidence or manual mark. Preserve durable device-local semantics and subscribe only while the menu portal is mounted.

Restore focus to the current channel row after a pending action survives cold preference relocation. Cover this ordering with gated browser responses and crop PR screenshots to mute/read actions.

Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
@tellaho
tellaho force-pushed the tho/sidebar-mute-read branch from 726ad9e to c86b33c Compare September 22, 2026 22:15
Carl added 2 commits September 23, 2026 09:16
Move New session into the shared row context menu and preserve composer focus. Remove separate row menu controls and mute decoration; use rounded shared-menu highlights.

Dismiss mute actions immediately while the session-owned writer saves in the background. Restore confirmed state with explicit retry on failure, ignore superseded completions, and cover persistence, focus and draft retention.

Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Return the actual empty agent-library contract from the production-broker fixture. Exercise New session after mute/read persistence so invalid fixture data cannot crash the composer unnoticed.

Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
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.

1 participant