You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Chatto stores and exchanges message bodies as plain text while bundled clients render the deliberately limited Markdown subset documented in FDR-032. People should be able to hide plot details, puzzle answers, sensitive-looking screenshots described in text, and similar content until each reader deliberately reveals it.
Use the familiar Discord-style syntax:
The answer is ||42||.
This remains a presentation feature. Spoiler markers do not encrypt content or change authorization: message source, APIs, exports, moderation tools, and explicit copy/reply actions can still expose the underlying text.
Syntax and parsing
Parse paired ||...|| delimiters outside inline code and fenced code.
An opening delimiter must be followed by non-whitespace and a closing delimiter must be preceded by non-whitespace. This keeps ordinary prose/code-like text such as a || b || c literal.
Empty, unmatched, and improperly spaced delimiters stay literal.
Do not nest spoilers or cross paragraph/block boundaries in the first version.
Inline Markdown inside a spoiler—emphasis, links, autolinks, mentions, and timestamps—should render normally once revealed.
Define and test precedence with GFM tables. A paired spoiler delimiter inside a cell must be treated as cell content rather than as column separators; existing empty-cell and escaped-pipe behavior must remain intact.
Preserve the exact source through compose, send, edit, copy, reply quoting, realtime delivery, history hydration, and third-party API use.
Reader experience
Initially render each spoiler as a clearly discoverable inline Reveal spoiler control without exposing its content through colour contrast, text selection, hover, focus, or the accessibility tree.
Reveal on pointer activation, Enter, or Space. The revealed content must become selectable and its links/mentions/timestamps must retain their normal behavior.
Give the control an accessible name/state and a visible keyboard focus indicator. Do not rely on colour alone.
Keep reveal state local to the rendered message. It need not be durable or synchronized between devices.
Support multiple independent spoilers in one message and every surface that renders message bodies, including room/thread timelines, reply and message preview cards, and search results.
Keep layout stable enough that revealing content does not cause an avoidable large timeline jump.
All user-visible control text must use the normal i18n catalogs.
Prevent accidental disclosure
Visual concealment is not useful if adjacent UI immediately repeats the hidden text.
Message/search excerpts and reply previews must preserve the spoiler boundary and start concealed.
Notification and push-notification text must replace concealed ranges with a neutral localized marker such as [spoiler] while retaining safe surrounding prose. If notification text is assembled outside the frontend, add the minimum shared/server-side source detection needed for consistent behavior.
URLs inside concealed ranges must not create visible link-preview cards or trigger preview fetches before reveal.
Mentions may retain their normal delivery semantics, but notification copy must not reveal the concealed text.
Search may continue indexing the underlying message because this is not an authorization boundary, but result snippets must not reveal matched spoiler text until the reader activates it.
Explicit actions that already copy or quote the original message body may retain the ||...|| source; document that behavior.
Implementation direction
Extend the owned markdown-it pipeline in apps/frontend/src/lib/markdown.ts with spoiler tokens and keep source HTML disabled. All generated tags and attributes must remain renderer-owned and flow through the audited MarkdownHtml.svelte boundary; do not add feature-level {@html}.
Interactive reveal behavior belongs at the reusable message-content boundary so it works consistently across callers. Concealed content should be genuinely excluded from selection and assistive-technology output until reveal rather than merely painted with matching foreground/background colours.
Keep parsing bounded by existing message-size limits and add targeted adversarial cases for long delimiter runs and many spoilers.
Acceptance criteria
||spoiler|| renders as an initially concealed, explicitly revealable inline region.
Pointer, Enter, and Space activation work; focus, accessible name/state, and screen-reader behavior are verified.
Concealed text cannot be read accidentally through selection, hover/focus styling, or the accessibility tree.
Multiple spoilers reveal independently and remain responsive in narrow, light, dark, and RTL layouts.
Empty/unmatched/whitespace-adjacent delimiters and a || b || c remain literal.
Code spans and fenced code never parse spoiler markers.
Spoilers coexist with emphasis, links, mentions, timestamps, and GFM tables without corrupting syntax or layout.
Composer posting/editing, copy/reply actions, realtime delivery, and receiver-side rendering preserve the original Markdown source.
Room/thread messages, preview cards, reply previews, search results, and other message-rendering surfaces start concealed consistently.
Notifications and search excerpts redact concealed ranges; hidden URLs do not produce visible/fetched link previews before reveal.
The implementation adds no raw source-HTML support and preserves the current Trusted Types/CSP boundary.
Parser edge cases, table precedence, post-processing, notification/excerpt redaction, mounted interaction/accessibility tests, Storybook variants, and sender/receiver browser coverage are included.
FDR-032, public Message Body Tokens documentation, and user-facing formatting guidance explain the syntax and its non-security guarantee.
Out of scope
Encrypting spoiler content or changing who is authorized to read it.
Durable/per-user reveal preferences.
Spoilered attachments or link-preview cards.
A composer toolbar button; the first version can be source-syntax only.
Context
Chatto stores and exchanges message bodies as plain text while bundled clients render the deliberately limited Markdown subset documented in FDR-032. People should be able to hide plot details, puzzle answers, sensitive-looking screenshots described in text, and similar content until each reader deliberately reveals it.
Use the familiar Discord-style syntax:
This remains a presentation feature. Spoiler markers do not encrypt content or change authorization: message source, APIs, exports, moderation tools, and explicit copy/reply actions can still expose the underlying text.
Syntax and parsing
||...||delimiters outside inline code and fenced code.a || b || cliteral.Reader experience
All user-visible control text must use the normal i18n catalogs.
Prevent accidental disclosure
Visual concealment is not useful if adjacent UI immediately repeats the hidden text.
[spoiler]while retaining safe surrounding prose. If notification text is assembled outside the frontend, add the minimum shared/server-side source detection needed for consistent behavior.||...||source; document that behavior.Implementation direction
Extend the owned
markdown-itpipeline inapps/frontend/src/lib/markdown.tswith spoiler tokens and keep source HTML disabled. All generated tags and attributes must remain renderer-owned and flow through the auditedMarkdownHtml.svelteboundary; do not add feature-level{@html}.Interactive reveal behavior belongs at the reusable message-content boundary so it works consistently across callers. Concealed content should be genuinely excluded from selection and assistive-technology output until reveal rather than merely painted with matching foreground/background colours.
Keep parsing bounded by existing message-size limits and add targeted adversarial cases for long delimiter runs and many spoilers.
Acceptance criteria
||spoiler||renders as an initially concealed, explicitly revealable inline region.a || b || cremain literal.Out of scope
Reference