web: add replySnippet helper for reply quotes
Pure, testable helper that derives a short single-line preview of a message for a reply quote: collapses whitespace, truncates long text, and falls back to a label for attachment-only messages (e.g. "📷 Photo"). Lives in format.js so the reply UI in chat.js stays testable, with cases in format.test.mjs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 files changed,
+40
−1
test/format.test.mjs+24 −1web/static/format.js+16 −0
diff --git a/test/format.test.mjs b/test/format.test.mjs| index 1eee543..4da162c 100644 |
| --- a/test/format.test.mjs |
| +++ b/test/format.test.mjs |
| @@ -1,6 +1,6 @@ |
| // Tests for text formatting (link detection, file sizes). node test/format.test.mjs |
| import assert from "node:assert/strict"; |
| -import { tokenizeText, formatBytes } from "../web/static/format.js"; |
| +import { tokenizeText, formatBytes, replySnippet } from "../web/static/format.js"; |
| let failures = 0; |
| async function test(name, fn) { |
| @@ -75,5 +75,28 @@ await test("formatBytes is human readable", () => { |
| assert.equal(formatBytes(5 * 1024 * 1024), "5.0 MB"); |
| }); |
| +await test("replySnippet returns trimmed, single-line text", () => { |
| + assert.equal(replySnippet({ text: " hello world\n" }), "hello world"); |
| +}); |
| + |
| +await test("replySnippet truncates long text with an ellipsis", () => { |
| + const s = replySnippet({ text: "a".repeat(200) }, 10); |
| + assert.equal(s.length, 10); |
| + assert.ok(s.endsWith("…")); |
| +}); |
| + |
| +await test("replySnippet labels photo-only messages", () => { |
| + assert.equal(replySnippet({ text: "", attachments: [{ type: "image/png" }] }), "📷 Photo"); |
| +}); |
| + |
| +await test("replySnippet labels non-image attachments by name", () => { |
| + assert.equal(replySnippet({ attachments: [{ type: "application/pdf", name: "spec.pdf" }] }), "📎 spec.pdf"); |
| +}); |
| + |
| +await test("replySnippet is empty for an empty message", () => { |
| + assert.equal(replySnippet({}), ""); |
| + assert.equal(replySnippet(null), ""); |
| +}); |
| + |
| console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); |
| process.exit(failures === 0 ? 0 : 1); |
diff --git a/web/static/format.js b/web/static/format.js| index 08908a2..445664c 100644 |
| --- a/web/static/format.js |
| +++ b/web/static/format.js |
| @@ -39,6 +39,22 @@ export function tokenizeText(text) { |
| return tokens; |
| } |
| +// Build a short, plain-text preview of a message for a reply quote. Collapses |
| +// whitespace and truncates long text; when a message has no text (e.g. a photo |
| +// only), falls back to a label for its first attachment so the quote still says |
| +// something. Pure so the reply UI in chat.js stays testable. |
| +export function replySnippet(payload, max = 140) { |
| + const text = (payload && payload.text ? String(payload.text) : "").replace(/\s+/g, " ").trim(); |
| + if (text) return text.length > max ? text.slice(0, max - 1).trimEnd() + "…" : text; |
| + const atts = payload && Array.isArray(payload.attachments) ? payload.attachments : []; |
| + if (atts.length) { |
| + const a = atts[0] || {}; |
| + if (a.type && a.type.startsWith("image/")) return "📷 Photo"; |
| + return "📎 " + (a.name || "Attachment"); |
| + } |
| + return ""; |
| +} |
| + |
| // Human-readable file size, e.g. 1536 → "1.5 KB". |
| export function formatBytes(n) { |
| if (!Number.isFinite(n) || n < 0) return ""; |