diff --git a/web/src/features/messages/ConversationView.tsx b/web/src/features/messages/ConversationView.tsx index 17babf83..9fdc8c1a 100644 --- a/web/src/features/messages/ConversationView.tsx +++ b/web/src/features/messages/ConversationView.tsx @@ -25,8 +25,8 @@ import { FETCH_PAGE_TRIGGER, MESSAGES_PAGE } from '@/lib/config'; import { useClickOutside } from '@/lib/hooks'; import { availableActions, recallRemainingSeconds, type MessageAction } from '@/lib/messages/actions'; import { isRecalled } from '@/lib/messages/dispatch'; -import { renderDrafty } from '@/lib/messages/drafty'; import { buildMentionedContent, detectMentionTrigger, type PendingMention } from '@/lib/messages/mentions'; +import { buildReplyInfo, replySnapshotText } from '@/lib/messages/reply'; import { groupSenderLabel } from '@/lib/messages/sender-name'; import { sendFile, sendImage, sendVoice } from '@/lib/tinode/attachments'; import { isTopicGone } from '@/lib/tinode/errors'; @@ -607,6 +607,16 @@ export function ConversationView() { return peerName; }; + /** Arms a media send with the pending quote and clears the composer's reply state in one step: + * the quote applies to the NEXT outgoing message only (cross-client rule), and a picked image + * or recorded voice is that message. Returns null when nothing is armed or the quoted message + * has become unquotable in the meantime (send proceeds as a plain media message). */ + const consumePendingReply = () => { + const pending = replyTo ? buildReplyInfo(replyTo) : null; + setReplyTo(null); + return pending; + }; + /** * "加载更多" (README §3.2/old app's messages-view.jsx): not simple since/before paging -- computes * which seq ranges are actually missing from the local cache within the currently-visible window @@ -670,7 +680,7 @@ export function ConversationView() { // Mentions inside a reply don't get spliced into real MN entities -- a quoted reply that // also pings someone is a rare enough compound case that it isn't worth a second // stitching path; the "@name" text still sends fine, just without the entity/highlight. - return sendReply(target, text, pendingReplyTo, quotedSenderLabel(pendingReplyTo.from)); + return sendReply(target, text, pendingReplyTo); } if (pendingMentions.length > 0) { return sendPrebuiltContent(target, buildMentionedContent(text, pendingMentions)); @@ -868,13 +878,19 @@ export function ConversationView() { const handlePickImage = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; e.target.value = ''; - if (file) sendImage(topic, file).catch((err) => showError(err, '图片发送失败')); + if (file) { + const reply = consumePendingReply(); + sendImage(topic, file, reply).catch((err) => showError(err, '图片发送失败')); + } }; const handlePickFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; e.target.value = ''; - if (file) sendFile(topic, file).catch((err) => showError(err, '文件发送失败')); + if (file) { + const reply = consumePendingReply(); + sendFile(topic, file, reply).catch((err) => showError(err, '文件发送失败')); + } }; return ( @@ -1002,6 +1018,7 @@ export function ConversationView() { highlighted={typeof msg.seq === 'number' && highlightedSeq === msg.seq} onToggleSelect={toggleSelect} onContextMenu={(e, m) => setMenu({ x: e.clientX, y: e.clientY, msg: m })} + resolveQuotedName={quotedSenderLabel} onError={setErrorToast} /> ); @@ -1042,7 +1059,7 @@ export function ConversationView() { {replyTo ? (
- 回复 {quotedSenderLabel(replyTo.from)}:{renderDrafty(replyTo.content)} + 回复 {quotedSenderLabel(replyTo.from)}:{replySnapshotText(replyTo)}