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'.
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 { |