From db70a9148b6a9d34d93ef8246cd7c062a285e29f Mon Sep 17 00:00:00 2001 From: plebeius Date: Tue, 10 Feb 2026 15:51:58 +0800 Subject: [PATCH] fix(reply modal): restore multiline quote insertion --- src/components/reply-modal/reply-modal.tsx | 15 +++++++++--- src/stores/use-reply-modal-store.ts | 27 ++++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index 94cab527..2a237ad5 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -50,6 +50,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa const { selectedText } = useSelectedTextStore(); const quoteInsertRequestId = useReplyModalStore((state) => state.quoteInsertRequestId); const quoteInsertNumber = useReplyModalStore((state) => state.quoteInsertNumber); + const quoteInsertSelectedText = useReplyModalStore((state) => state.quoteInsertSelectedText); const [error, setError] = useState(null); const [lengthError, setLengthError] = useState(null); @@ -239,16 +240,24 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa lastProcessedQuoteInsertRequestIdRef.current = quoteInsertRequestId; const quote = `>>${quoteInsertNumber ?? '?'}`; + const selectedQuote = quoteInsertSelectedText?.trimEnd() || ''; const isFocused = document.activeElement === textarea; const rawStart = isFocused ? (textarea.selectionStart ?? textarea.value.length) : lastSelectionStartRef.current; const selectionEnd = isFocused ? (textarea.selectionEnd ?? rawStart) : lastSelectionEndRef.current; const minStart = contentPrefix.length; const start = Math.max(rawStart, minStart); const end = Math.max(selectionEnd, minStart); - const nextValue = `${textarea.value.slice(0, start)}${quote}${textarea.value.slice(end)}`; + const before = textarea.value.slice(0, start); + const after = textarea.value.slice(end); + const needsLeadingNewline = before.length > minStart && !before.endsWith('\n'); + let insertion = `${needsLeadingNewline ? '\n' : ''}${quote}\n`; + if (selectedQuote) { + insertion += `${selectedQuote}\n`; + } + const nextValue = `${before}${insertion}${after}`; textarea.value = nextValue; - const nextCursor = start + quote.length; + const nextCursor = before.length + insertion.length; textarea.focus(); textarea.setSelectionRange(nextCursor, nextCursor); lastSelectionStartRef.current = nextCursor; @@ -258,7 +267,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa const formattedContent = formatMarkdown(contentWithoutPrefix); setPublishReplyOptions({ content: formattedContent }); checkContentLength(formattedContent, t); - }, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, contentPrefix, setPublishReplyOptions, checkContentLength, t]); + }, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, quoteInsertSelectedText, contentPrefix, setPublishReplyOptions, checkContentLength, t]); // on android, auto upload file to image hosting sites with open api const [isUploading, setIsUploading] = useState(false); diff --git a/src/stores/use-reply-modal-store.ts b/src/stores/use-reply-modal-store.ts index bdc1e02c..0f5b7ac4 100644 --- a/src/stores/use-reply-modal-store.ts +++ b/src/stores/use-reply-modal-store.ts @@ -11,10 +11,26 @@ interface ReplyModalState { scrollY: number; quoteInsertRequestId: number; quoteInsertNumber: number | null; + quoteInsertSelectedText: string | null; closeModal: () => void; openReplyModal: (parentCid: string, parentNumber: number | undefined, postCid: string, threadNumber: number | undefined, subplebbitAddress: string) => void; } +const getQuotedSelection = () => { + const text = document.getSelection()?.toString(); + if (!text) return ''; + + // Keep each selected line as 5chan greentext and normalize newlines. + const normalizedText = text.replace(/\r\n/g, '\n').replace(/\n+$/g, ''); + + if (!normalizedText) return ''; + + return normalizedText + .split('\n') + .map((line) => `>${line}`) + .join('\n'); +}; + const useReplyModalStore = create((set, get) => ({ showReplyModal: false, activeCid: null, @@ -25,6 +41,7 @@ const useReplyModalStore = create((set, get) => ({ scrollY: 0, quoteInsertRequestId: 0, quoteInsertNumber: null, + quoteInsertSelectedText: null, closeModal: () => { // Reset selected text if you're using that store @@ -35,23 +52,25 @@ const useReplyModalStore = create((set, get) => ({ parentNumber: null, threadNumber: null, quoteInsertNumber: null, + quoteInsertSelectedText: null, }); }, openReplyModal: (parentCid, parentNumber, postCid, threadNumber, subplebbitAddress) => { + const quotedSelection = getQuotedSelection(); + // If the reply modal is already open, insert this quote in the current textarea at caret. if (get().showReplyModal) { set((state) => ({ quoteInsertRequestId: state.quoteInsertRequestId + 1, quoteInsertNumber: parentNumber ?? null, + quoteInsertSelectedText: quotedSelection || null, })); return; } - // Get selected text - const text = document.getSelection()?.toString(); - if (text) { - useSelectedTextStore.getState().setSelectedText(`>${text}\n`); + if (quotedSelection) { + useSelectedTextStore.getState().setSelectedText(`${quotedSelection}\n`); } // Handle mobile scrollY