feat(composer): live character counter near the message-length cap (LC-332) #344

Merged
longjacksonle merged 1 commit from feat/lc-332-composer-char-counter into main 2026-06-18 04:03:46 +02:00

What

Adds a live character counter to the message composer so a user gets feedback as they approach the 16,000-char message-length cap (MAX_MESSAGE_CHARS, LC-153) instead of only discovering it as a generic 400 failure banner after the send fails.

Behavior:

  • Hidden while comfortably under the cap (more than 500 chars of headroom).
  • Within 500 chars of the cap: warning-colored "N left".
  • Over the cap: danger-colored "N over limit" and the Send button is disabled until the body is trimmed back under the cap.

How

  • The cap is surfaced to the templates by a max_message_chars() method on RoomPage, DmPage, and ComposerFragment (the three structs that render room/composer.html), emitted as data-lc-maxchars on the form. Single source of truth stays the Rust const; no magic number in the template.
  • Length is counted by Unicode code point (Array.from(value).length) so the client count matches the server's body.chars().count() for emoji / astral input; value.length (UTF-16 code units) would over-count and disagree with the server gate.
  • The over-limit state is published on window.__lcOverLimit; the existing window.__lcRefreshSend predicate was extended to keep Send disabled while over-limit, composing with the existing "text or attachment present" rule rather than fighting it.
  • The counter IIFE uses the same #composer cleanup-root teardown pattern as the other composer scripts and clears the stale flag on form swap.
  • Counter strings are localized in en + es. They use a brace-free %n% token (a plain string, not a Fluent placeable) substituted client-side, so the Fluent text round-trips without brace-escaping.

Testing

  • just check (server compiles; Askama validates the template method call)
  • just test and just test-saas both green (incl. i18n en/es parity)

No operator action: pure UI, no env var / config / contract change.

🤖 Generated with Claude Code

## What Adds a live character counter to the message composer so a user gets feedback as they approach the 16,000-char message-length cap (`MAX_MESSAGE_CHARS`, LC-153) instead of only discovering it as a generic 400 failure banner after the send fails. Behavior: - Hidden while comfortably under the cap (more than 500 chars of headroom). - Within 500 chars of the cap: warning-colored "N left". - Over the cap: danger-colored "N over limit" and the Send button is disabled until the body is trimmed back under the cap. ## How - The cap is surfaced to the templates by a `max_message_chars()` method on `RoomPage`, `DmPage`, and `ComposerFragment` (the three structs that render `room/composer.html`), emitted as `data-lc-maxchars` on the form. Single source of truth stays the Rust const; no magic number in the template. - Length is counted by Unicode code point (`Array.from(value).length`) so the client count matches the server's `body.chars().count()` for emoji / astral input; `value.length` (UTF-16 code units) would over-count and disagree with the server gate. - The over-limit state is published on `window.__lcOverLimit`; the existing `window.__lcRefreshSend` predicate was extended to keep Send disabled while over-limit, composing with the existing "text or attachment present" rule rather than fighting it. - The counter IIFE uses the same `#composer` cleanup-root teardown pattern as the other composer scripts and clears the stale flag on form swap. - Counter strings are localized in `en` + `es`. They use a brace-free `%n%` token (a plain string, not a Fluent placeable) substituted client-side, so the Fluent text round-trips without brace-escaping. ## Testing - `just check` (server compiles; Askama validates the template method call) - `just test` and `just test-saas` both green (incl. i18n en/es parity) No operator action: pure UI, no env var / config / contract change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(composer): live character counter near the message-length cap
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 9s
check-secrets / Kingfisher (pull_request) Successful in 7s
Check / clippy + fmt + tests (pull_request) Successful in 2m36s
Create release / Create release from merged PR (pull_request) Has been skipped
9f35028079
The composer textarea had no length feedback: exceeding MAX_MESSAGE_CHARS (16,000, LC-153) only surfaced as a generic 400 failure banner after the send. Add a counter that reveals within 500 chars of the cap, warns inside that window, and turns danger + disables Send once the body would exceed the cap.

The cap is surfaced to the templates via a max_message_chars() method on RoomPage, DmPage, and ComposerFragment (all three render room/composer.html) and emitted as data-lc-maxchars, so the single source of truth stays the Rust const with no magic number in the template. Length is counted by Unicode code point (Array.from(...).length) to match the server's body.chars().count() for emoji/astral input. The over-limit gate composes with the existing send-enable predicate via window.__lcOverLimit. Counter strings are localized in en + es with a brace-free %n% token (not a Fluent placeable) substituted client-side.

#LC-332
#LC-333

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-332-composer-char-counter 2026-06-18 04:03:46 +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!344
No description provided.