From 07e970e81f656dd9cea76c628dd958887f43d3fc Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 14:12:12 +0300 Subject: [PATCH] p2: add quote and code-block text formatting --- docs/core-checklist-status.md | 2 +- web/src/components/MessageComposer.tsx | 31 ++++++++++++++++++++++++++ web/src/utils/formatMessage.tsx | 25 ++++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 3fefa8b..18893b4 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -28,7 +28,7 @@ Legend: 19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites) 20. GIF - `PARTIAL` (web GIF picker with Tenor search + preset fallback + favorites) 21. Message History/Search - `DONE` (history/pagination/chat+global search) -22. Text Formatting - `PARTIAL` (bold/italic/underline/spoiler/mono/links supported; toolbar still evolving) +22. Text Formatting - `PARTIAL` (bold/italic/underline/spoiler/mono/links + strikethrough + quote/code block; toolbar still evolving) 23. Groups - `PARTIAL` (create/add/remove/invite link; advanced moderation partial) 24. Roles - `DONE` (owner/admin/member) 25. Admin Rights - `PARTIAL` (delete/ban-like remove/pin/edit info; full ban system limited) diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index 910d531..d40ae60 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -872,6 +872,31 @@ export function MessageComposer() { }); } + function insertQuoteBlock() { + const textarea = textareaRef.current; + if (!textarea) { + return; + } + const start = textarea.selectionStart ?? text.length; + const end = textarea.selectionEnd ?? text.length; + const selected = text.slice(start, end); + const source = selected || "quote"; + const quoted = source + .split("\n") + .map((line) => `> ${line}`) + .join("\n"); + const nextValue = `${text.slice(0, start)}${quoted}${text.slice(end)}`; + setText(nextValue); + if (activeChatId) { + setDraft(activeChatId, nextValue); + } + requestAnimationFrame(() => { + textarea.focus(); + const pos = start + quoted.length; + textarea.setSelectionRange(pos, pos); + }); + } + return (
{activeChatId && replyToByChat[activeChatId] ? ( @@ -950,6 +975,12 @@ export function MessageComposer() { + + diff --git a/web/src/utils/formatMessage.tsx b/web/src/utils/formatMessage.tsx index 89ec1b7..dafcbb5 100644 --- a/web/src/utils/formatMessage.tsx +++ b/web/src/utils/formatMessage.tsx @@ -108,5 +108,28 @@ function parseInline(text: string): string { } export function formatMessageHtml(text: string): string { - return parseInline(text); + const blocks: string[] = []; + const parts = text.split(/```/); + for (let i = 0; i < parts.length; i += 1) { + const part = parts[i]; + const isCode = i % 2 === 1; + if (isCode) { + const value = escapeHtml(part.replace(/^\n+|\n+$/g, "")); + blocks.push(`
${value}
`); + continue; + } + const lines = part.split("\n"); + for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { + const line = lines[lineIndex]; + if (line.startsWith("> ")) { + blocks.push(`
${parseInline(line.slice(2))}
`); + } else { + blocks.push(parseInline(line)); + } + if (lineIndex < lines.length - 1) { + blocks.push("
"); + } + } + } + return blocks.join(""); }