eleven / messenger Public

web: fix double blockquote marker when pasting after >

When pasting text on a blockquote marker line ('>'), the first line of pasted
text should not receive another '> ' prefix since it will be appended after
the existing marker. Only subsequent lines need the prefix.

Fixes the issue where pasting 'Hello\nWorld' after '>' resulted in
'> > Hello\n> World' instead of '> Hello\n> World'.
James Adam 579261741c24e8de2daa114ed0e5676ef78e44b8 parent c40c630
1 file changed, +28 −1
  • web/static/chat.js +28 −1
diff --git a/web/static/chat.js b/web/static/chat.js
index 2685bfd..12ab965 100644
--- a/web/static/chat.js
+++ b/web/static/chat.js
@@ -34,6 +34,16 @@ import {
import { richTokens, blockTokens, firstLink, typingSummary, replySnippet, formatBytes, autoGroupTitle, emojiOnlyCount, youtubeId } from "/static/format.js";
import { emojify, emojiMatches } from "/static/emoji.js";
import { MESSAGE_TYPE_TYPING } from "/static/message.js";
+import {
+ setEmitTransport,
+ runCommand,
+ isAppType,
+ foldEvent,
+ resetThread,
+ renderSurfaces,
+ renderInline as renderAppLine,
+} from "/static/apps.js";
+import "/static/apps/todo.js"; // registers the /todo mini-app (side-effect import)
import { qrCanvas } from "/static/qrcode.js";
import {
needsHomeScreen,
@@ -1042,7 +1052,7 @@ function renderApp() {
if (/^>\s*$/.test(currentLine)) {
e.preventDefault();
textItem.getAsString((pastedText) => {
- const blockquoted = pastedText.split("\n").map((line) => "> " + line).join("\n");
+ const blockquoted = pastedText.split("\n").map((line, i) => i === 0 ? line : "> " + line).join("\n");
const end = text.selectionEnd;
const before = text.value.substring(0, start);
const after = text.value.substring(end);
@@ -1477,10 +1487,12 @@ async function openThread(id) {
// cascade of visible layout shift on first open. A detached fragment never
// paints mid-build, so the feed appears complete, already at the latest message.
const frag = document.createDocumentFragment();
+ resetThread(id); // fold this thread's app state fresh from the loaded window
for (const m of r.messages || []) await appendMessage(id, m, false, frag, cache);
if (myOpen !== openSeq) return; // switched away mid-build — don't attach a stale feed
feed.appendChild(frag);
cache.loaded = true;
+ if (id === activeId) renderActiveSurfaces(); // paint app panels from the folded state
} catch (e) {
console.error("load messages", e);
}
@@ -2420,6 +2432,11 @@ function sendMessage(raw) {
// send button is greyed meanwhile, but Enter can still land here.
if (!activeId || !ws || ws.readyState !== WebSocket.OPEN) return false;
+ // A slash command handled by a mini-app (e.g. "/todo buy milk"): emit its
+ // event instead of sending a chat message. Skipped while editing so you can
+ // still edit a message whose text starts with "/".
+ if (editing == null && runCommand(text, activeId)) return true;
+
// Editing an existing message: re-encrypt the original payload with new text
// (preserving its name + any attachments) and send an edit.
if (editing != null) {
@@ -3036,6 +3053,16 @@ function connectWS() {
try {
payload = await decrypt(threadKeys[tid], msg.blob);
} catch {}
+ // A mini-app event, not a chat message: fold it (so the app's state stays
+ // current) but never mark unread, preview, or notify — it's not a message.
+ if (payload && isAppType(payload.type)) {
+ if (tid === activeId) await appendMessage(tid, msg, true);
+ else foldEvent(tid, payload, { id: msg.id, ts: payload.ts });
+ const at = threads.find((x) => x.id === tid);
+ if (at) at.last_at = msg.created_at || Math.floor(Date.now() / 1000);
+ renderThreadList();
+ return;
+ }
if (tid === activeId) {
await appendMessage(tid, msg, true);
} else {