feat(ui): markdown preview toggle in the composer (LC-276) #316

Merged
longjacksonle merged 1 commit from feat/lc-276-markdown-preview into main 2026-06-14 16:07:34 +02:00

What

Adds a Write/Preview toggle to the composer (LC-276). Clicking Preview shows the current draft rendered exactly as it will post; toggling back returns to editing with the draft intact. Pairs with the LC-254 formatting toolbar.

How

  • New POST /room/{id}/preview (routes::room::post_preview) renders the body through the SAME pipeline message posting uses - markdown::render (pulldown-cmark + syntect + custom emoji) - and returns <div class="lc-md leading-snug">...</div>. Access-gated with is_room_accessible (403 otherwise) and body-capped to MAX_MESSAGE_CHARS (LC-153, since the render runs synchronously on the request thread). Mentions are not resolved (the message is unposted), so @user renders as plain text - this is a formatting preview, not a mention-resolution preview.
  • composer.html: a data-lc-preview-toggle button in the toolbar + a hidden data-lc-preview-pane in the textarea's flex slot. A small IIFE fetches the render on entering preview (no re-fetch toggling back), swaps textarea<->pane and the button label, and resets to Write after a successful send so no stale preview lingers. The server returns trusted markdown HTML (same trust model as message bodies emitted with |safe).
  • Strings localized in en + es.

A client-side markdown lib was rejected - it can't match the server's syntect/custom-emoji/sanitization, so the preview would lie. No new DB/env. Not operator-visible.

Tests

routes_drafts: preview_renders_markdown (POST body=**bold** -> <strong>bold</strong> in an lc-md wrapper) and preview_forbidden_when_inaccessible (non-member, private room -> 403). just test and just test-saas pass.

QA note

The toggle is JS-driven (a fetch + show/hide), so worth a quick manual pass: type markdown (bold, a list, a fenced code block, a custom :emoji:), hit Preview (renders matching a real message; code is syntect-highlighted), toggle back (draft intact), confirm a successful send returns to Write, and confirm Preview on a room you can't access is gated.

## What Adds a Write/Preview toggle to the composer (LC-276). Clicking Preview shows the current draft rendered exactly as it will post; toggling back returns to editing with the draft intact. Pairs with the LC-254 formatting toolbar. ## How - New `POST /room/{id}/preview` (`routes::room::post_preview`) renders the body through the SAME pipeline message posting uses - `markdown::render` (pulldown-cmark + syntect + custom emoji) - and returns `<div class="lc-md leading-snug">...</div>`. Access-gated with `is_room_accessible` (403 otherwise) and body-capped to `MAX_MESSAGE_CHARS` (LC-153, since the render runs synchronously on the request thread). Mentions are not resolved (the message is unposted), so `@user` renders as plain text - this is a formatting preview, not a mention-resolution preview. - `composer.html`: a `data-lc-preview-toggle` button in the toolbar + a hidden `data-lc-preview-pane` in the textarea's flex slot. A small IIFE fetches the render on entering preview (no re-fetch toggling back), swaps textarea<->pane and the button label, and resets to Write after a successful send so no stale preview lingers. The server returns trusted markdown HTML (same trust model as message bodies emitted with `|safe`). - Strings localized in en + es. A client-side markdown lib was rejected - it can't match the server's syntect/custom-emoji/sanitization, so the preview would lie. No new DB/env. Not operator-visible. ## Tests `routes_drafts`: `preview_renders_markdown` (POST `body=**bold**` -> `<strong>bold</strong>` in an `lc-md` wrapper) and `preview_forbidden_when_inaccessible` (non-member, private room -> 403). `just test` and `just test-saas` pass. ## QA note The toggle is JS-driven (a `fetch` + show/hide), so worth a quick manual pass: type markdown (bold, a list, a fenced code block, a custom `:emoji:`), hit Preview (renders matching a real message; code is syntect-highlighted), toggle back (draft intact), confirm a successful send returns to Write, and confirm Preview on a room you can't access is gated.
feat(ui): markdown preview toggle in the composer (LC-276)
All checks were successful
check-secrets / Kingfisher (push) Successful in 6s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / TruffleHog (pull_request) Successful in 5s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 2m52s
08f3dbbff1
Adds a Write/Preview toggle to the composer. Clicking Preview renders the current draft through the SAME server markdown pipeline that posting uses (pulldown-cmark + syntect + custom emoji) and shows it in place of the textarea, so you see exactly what will post before sending; toggling back returns to editing with the draft intact.

New POST /room/{id}/preview renders the body via markdown::render and returns the lc-md-wrapped HTML. It is access-gated (403 on a room the caller cannot see) and caps the body to MAX_MESSAGE_CHARS (LC-153, the render is synchronous on the request thread). Mentions are not resolved (the message is unposted), so @user shows as plain text - this is a formatting preview, not a mention-resolution preview. The toggle IIFE fetches once on entering preview (no re-fetch on toggle-back) and resets to Write after a successful send so no stale preview lingers.

A client-side markdown library was rejected: it cannot match the server's syntect highlighting / custom emoji / sanitization, so the preview would not match the post. Strings localized in en + es. Tests cover the render output and the access gate.

#LC-276
#LC-277

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-276-markdown-preview 2026-06-14 16:07:34 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!316
No description provided.