feat(web): polished chat & channels UI (0198) - #174
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hreads (0198) Phase 1+2 of the chat & channels overhaul. Rendering moves to a reusable set of comms-UI pieces driven by the existing schema/service/hooks (no schema change): - Message grouping (consecutive same-author rows collapse), date separators and a "New messages" divider, smart stick-to-bottom + jump-to-latest pill, role=log. - Avatars (DIDAvatar) + Discord-style presence dots (active/idle/dnd) in rows, the header roster, and DM sidebar rows. - Emoji reactions over the existing Reaction schema: per-message pills with counts + who-reacted, optimistic toggle, dependency-free emoji picker. - Hover action toolbar (quick-react, add-reaction, reply, edit, more) reusing the existing safety menu; inline edit-in-place; up-arrow on empty composer edits last own. - Right-hand thread pane driven by inReplyTo; replies kept out of the main feed, root shows a thread footer (count + participants + last reply). - Composer gains an emoji insert button; @ / # / [[ pickers preserved. - Rebuilt channel header (avatar stack, member count, editable topic, members popover) and sidebar (presence dots, unread emphasis). Deferred (noted in 0198): windowed virtualization + load-older (window capped at 100 today), file-attachment upload, density toggle UI (store landed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3 polish: a comfortable/compact density toggle in the channel header (persisted via the chat-prefs store, shared across open channels), and Escape-to-close on the thread pane when the reply box is empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phases 1+2 and the cheap Phase-3 wins shipped and verified in browser preview; checklist items updated with what landed vs what's deferred (virtualization, file attach, pins, Cmd+K/search, Zulip topics). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
🖼️ UI changes in this PRNo visual differences detected in the changed UI. |
Contributor
|
Preview removed for PR #174. |
| ) | ||
| } | ||
|
|
||
| export function MessageRow({ |
| } | ||
|
|
||
| /** Derive per-root thread summaries from the loaded reply messages. */ | ||
| function buildThreadIndex(replies: ChatRow[]): Map<string, ThreadSummary> { |
| 🛡 {hiddenCount} message(s) hidden by your block/mute list | ||
| </li> | ||
| )} | ||
| {rows.map((row) => { |
| } | ||
| }, [channels]) | ||
|
|
||
| const leadingFor = (channel: ChannelEntry): ReactNode => { |
| import { ReactionBar } from './ReactionBar' | ||
| import { useMessageReactions } from './useMessageReactions' | ||
|
|
||
| function ThreadMessage({ |
| ) | ||
| } | ||
|
|
||
| function MembersButton({ |
| const profiles = useProfiles() | ||
| const [creating, setCreating] = useState<'channel' | 'dm' | null>(null) | ||
|
|
||
| const presenceByDid = useMemo(() => { |
| ) | ||
| } | ||
|
|
||
| function MessageMentionChips({ message }: { message: ChatRow }) { |
| rows={2} | ||
| aria-label="Edit message" | ||
| onChange={(event) => setValue(event.target.value)} | ||
| onKeyDown={(event) => { |
| rows={2} | ||
| placeholder="Reply…" | ||
| onChange={(event) => setText(event.target.value)} | ||
| onKeyDown={(event) => { |
crs48
added a commit
that referenced
this pull request
Jun 18, 2026
## What Backfills the changelog so it reflects what's live on `main`. The per-PR fragment system (exploration 0197) was current through PR #163 (PR #178 backfilled #151/#153/#156/#157/#160/#163), but the recent **0198/0199 batch** and a couple of stragglers shipped without entries. This adds 7 fragments, with prose lifted from each PR's own `## Changelog` block where present. | PR | Entry | Tags | |---|---|---| | #143 | AI chat that knows your workspace | `ai` | | #167 | Linear-grade Tasks | `tasks` `app` | | #169 | Cleaner page editor typography | `editor` | | #170 | Bring your Slack data into xNet | `plugins` `platform` | | #171 | Smoother, more consistent motion | `app` | | #173 | Airtable-grade database grids | `app` | | #174 | A redesigned chat & channels experience | `app` | ## Deliberately excluded - **In-editor AI (#154 / #158)** — shipped the transform engine + diff/approval logic, but both PRs state the `/ai` UI wiring into the running app is **deferred**, so it isn't user-visible yet. An entry would overclaim. - **Slack (#170)** — entry scoped to the migration connector + compatibility layer (genuinely live); avoids claiming the webhook/slash-command hub endpoints work, since they're generic-over-an-injected-sink and not mounted in a running server yet. - Pure infra/ops/docs PRs (cloud M1/M2, CI, runbooks) — not user-facing. ## Verification - `validate-changelog.ts` → **26 fragments valid** (19 existing + 7 new) - `prettier --check` on all new files → clean - Newest-first ordering confirmed — the 7 entries slot in at the top ## Changelog <!-- This PR only adds changelog fragments for already-merged work; it ships no user-facing change of its own. --> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jun 18, 2026
…g, param-route guard) (#185) ## Why Our [Visual UI Capture](scripts/visuals/) system silently misses big UI changes. The trigger: [PR #174](#174) rewrote the entire chat layer (2,364 lines under `apps/web/src/comms/`) and its capture comment read **"No visual differences detected in the changed UI."** — the *absence* of change reported for one of the largest UI changes in the project. Root cause (full write-up in [exploration 0200](docs/explorations/0200_%5Bx%5D_VISUAL_CAPTURE_SILENT_COVERAGE_GAPS.md)): capture targets come from a hand-curated allowlist of param-free routes + co-located stories. Chat matched none — no story, no route glob, and it only renders at the **parameterized** `/channel/$channelId` route (needs an id + seed data, so the static-route capturer can't reach it). `computeCaptureSet` then fell back to capturing `home`, which diffs clean, so the comment said "no differences." A **coverage gap was made indistinguishable from a no-op**. The 0191 drift-guard that was meant to catch this explicitly **skips `$`-routes**, the exact place chat lives. ## What this does **C — make misses loud (keystone).** When changed UI files map to *no* story/route/flow, `computeCaptureSet` now records `fallbackUsed` + `unmappedFiles` (threaded capture-set → capture → diff → comment). The PR comment renders a `> [!WARNING]` listing the unmapped files instead of "No visual differences detected." This converts every future silent miss into an actionable nudge. **A — `chat` flow.** A new interaction flow seeds a channel, posts messages, reacts, and opens a thread — recording the redesigned `/channel/$channelId` surface. Maps `apps/web/src/comms/**`, so a chat-only PR like #174 now resolves to this flow. **B — close the drift-guard hole.** `manifest-coverage.test.mjs` now requires every parameterized route to be flow-covered or listed in `PARAM_EXEMPT` with a reason (+ a stale-exempt guard). `channel` is covered by the chat flow; 8 others are exempted as explicit TODOs. **D — chat primitive stories.** `PresenceDot` + `ReactionBar` stories give the pipeline a stable, seed-free baseline captured by the co-location rule. (`MessageRow` is router/hook-coupled — not a stable isolated story; deferred.) ## Verification - **Fix proven against the real manifest:** PR #174's 19-file diff now yields `{ flows: ['chat'], fallbackUsed: false }` — maps to the chat flow, not the silent home fallback. - **Gap warning end-to-end:** an unmapped file yields `fallbackUsed: true` and `comment.mjs` renders the `[!WARNING]` block listing it. - **Guard bites:** with the chat flow removed, the guard reports `channel.$channelId` uncovered (would go red). - `pnpm test:visuals` green (27 tests); `xnet-web` typecheck clean; eslint + prettier clean; Storybook builds and indexes all 4 new comms stories. - The `visual-capture` workflow on **this PR** (it touches `apps/web/src/comms/**`) will run the new `chat` flow and post the GIF — the live validation of Option A. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilds the chat & channels presentation layer to the standard set by Slack/Discord/Zulip, on top of the existing schema/service/hooks spine (no schema change). Implements exploration 0198 — the core finding was that the data layer already modeled threads, reactions, attachments, edit/delete, and presence status, and the gap was almost entirely in rendering.
What's included
Message feed
DIDAvataravatars + Discord-style presence dots (active/idle/dnd) in rows, the header roster, and DM sidebar rows.role="log"/aria-live,aria-pressedreactions,aria-labelemoji, andprefers-reduced-motionguards.Interactions
Reactionschema — per-message pills with counts + who-reacted, optimistic toggle, and a dependency-free emoji picker.inReplyTo; replies stay out of the main feed and the root shows a "N replies · last reply" footer.↑on an empty composer edits your last message); composer emoji-insert button;@/#/[[pickers preserved.Header & sidebar
ChatsPanel: presence dots on DMs, unread emphasis.Verification
groupMessages+ reaction grouping (17 tests); all 75 comms tests green;tsc --noEmitclean; eslint clean (0 errors).<main>): grouping, reaction toggle (👍 1,aria-pressed), thread reply + footer, editable topic, density toggle. No console errors.Deferred (noted in the doc)
Windowed virtualization + load-older history (window capped at 100 today), file-attachment upload, message pinning,
Cmd+Kquick-switch + message search, optional TipTap composer /motion/react, and the optional Zulip-style topic mode.Changelog
🤖 Generated with Claude Code