messages: support ||spoiler|| tags (click to reveal)

This commit is contained in:
Jean Chevronnet 2026-07-04 03:19:21 +00:00
parent bc97069e8d
commit ae51c4a3a2
No known key found for this signature in database
2 changed files with 29 additions and 8 deletions

View file

@ -989,6 +989,10 @@ textarea.modal__input { resize: vertical; min-height: 5rem; line-height: 1.45; }
.challenge__busy { font-size: .85rem; font-weight: 700; color: var(--green-d); }
.challenge__link { display: inline-block; margin-top: .3rem; font-weight: 600; color: var(--accent); text-decoration: none; }
.challenge__link:hover { text-decoration: underline; }
/* ||spoiler|| — hidden until clicked */
.spoiler { background: var(--muted); color: transparent; border-radius: 3px; padding: 0 .15em; cursor: pointer; transition: color .12s; }
.spoiler.is-shown { background: rgba(127, 127, 127, .16); color: inherit; cursor: text; }
.turnstile-widget { min-height: 65px; }
/* ---- compact density ---- */

View file

@ -114,15 +114,32 @@ function YouTubeEmbed({ id, url }: { id: string; url: string }) {
);
}
// ||text|| hides content until clicked.
function Spoiler({ text }: { text: string }) {
const [shown, setShown] = useState(false);
return (
<span className={`spoiler ${shown ? 'is-shown' : ''}`} role="button" tabIndex={0}
onClick={() => setShown(true)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setShown(true); } }}>
{text}
</span>
);
}
function linkify(text: string, selfMsg = false): ReactNode {
const parts = text.split(/(https?:\/\/[^\s]+)/g);
return parts.map((p, i) => {
if (!/^https?:\/\//.test(p)) return <span key={i}>{p}</span>;
if (isImageUrl(p)) return <ImageAttachment key={i} url={p} defaultShown={selfMsg} />;
if (isAudioUrl(p)) return <AudioAttachment key={i} url={p} />;
const yt = youtubeId(p);
if (yt) return <YouTubeEmbed key={i} id={yt} url={p} />;
return <a key={i} href={p} target="_blank" rel="noopener noreferrer">{p}</a>;
// Pull out ||spoilers|| first, then linkify the remaining runs.
return text.split(/(\|\|.+?\|\|)/g).map((seg, si) => {
const sp = /^\|\|(.+)\|\|$/.exec(seg);
if (sp) return <Spoiler key={`sp${si}`} text={sp[1]} />;
return seg.split(/(https?:\/\/[^\s]+)/g).map((p, i) => {
const k = `${si}-${i}`;
if (!/^https?:\/\//.test(p)) return <span key={k}>{p}</span>;
if (isImageUrl(p)) return <ImageAttachment key={k} url={p} defaultShown={selfMsg} />;
if (isAudioUrl(p)) return <AudioAttachment key={k} url={p} />;
const yt = youtubeId(p);
if (yt) return <YouTubeEmbed key={k} id={yt} url={p} />;
return <a key={k} href={p} target="_blank" rel="noopener noreferrer">{p}</a>;
});
});
}