eleven / messenger Public

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>
James Adam 638a5292a15e79f84829e98d270e80ee55f7a8f2 parent b054735
2 files changed, +40 −1
  • test/format.test.mjs +24 −1
  • web/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 "";