feat: 实现跨端引用回复协议,新增 QuoteBar 组件支持消息引用

本次提交实现了与 iOS 对齐的跨端消息引用协议,新增 QuoteBar 组件用于渲染引用消息栏,重构发送逻辑支持文本和媒体消息的引用功能。主要改动包括:
- 新增 reply.ts 处理引用数据的构建和解析
- 新增 QuoteBar 组件渲染引用消息预览
- 重构发送逻辑,将引用快照放入消息头部而非内容
- 调整代理配置指向本地开发环境
- 优化消息操作权限,支持名片和媒体消息引用
This commit is contained in:
Austin
2026-09-23 20:40:47 +08:00
parent fb92f4f703
commit a8d3504424
8 changed files with 452 additions and 38 deletions
@@ -16,6 +16,8 @@ import {
} from '@/lib/messages/dispatch';
import { renderDrafty } from '@/lib/messages/drafty';
import { AudioAttachment, FileAttachment, ImageAttachment, VideoAttachment } from '@/features/messages/AttachmentPreview';
import { QuoteBar } from '@/features/messages/QuoteBar';
import { parseReplyHead } from '@/lib/messages/reply';
import { formatBubbleTime, formatEventTimestamp } from '@/lib/messages/time';
import { isFailedStatus, isSendingStatus, outgoingStatusLabel } from '@/lib/messages/status';
import { groupSenderLabel } from '@/lib/messages/sender-name';
@@ -74,6 +76,10 @@ interface MessageRowProps {
highlighted?: boolean;
onToggleSelect?: (seq: number) => void;
onContextMenu?: (e: React.MouseEvent, msg: StoredMessage) => void;
/** Resolves a QUOTED message's sender uid (from head["x-im-reply"].from) to a display label.
* Owned by ConversationView so group remark/nickname resolution stays in one place; QuoteBar
* must never print a raw uid. */
resolveQuotedName?: (uid: string | undefined) => string;
/** Fired when the inline "重试" retry (below) itself fails -- e.g. still offline. Without this the
* failed-send icon just silently stays put with no feedback at all that the retry attempt was
* even made. */
@@ -95,6 +101,7 @@ export function MessageRow({
highlighted,
onToggleSelect,
onContextMenu,
resolveQuotedName,
onError,
}: MessageRowProps) {
if (isServerDeleted(msg)) {
@@ -121,6 +128,9 @@ export function MessageRow({
const callResult = callResultDisplay(msg, mine);
const contactCard = contactCardContentOf(msg);
const mobileAttachment = mobileAttachmentContentOf(msg);
// Quoted-reply snapshot (x-im-reply). Not rendered for a recalled message -- its whole bubble
// collapses to the "消息已删除" placeholder, quote bar included.
const replyInfo = isRecalled(msg) ? null : parseReplyHead(msg.head);
const retry = () => {
if (typeof msg.seq !== 'number') return;
@@ -201,6 +211,9 @@ export function MessageRow({
mine ? 'rounded-[12px_4px_12px_12px] bg-bubble-mine' : 'rounded-[4px_12px_12px_12px] bg-white shadow-bubble'
} ${isRecalled(msg) ? 'text-text-tertiary-4 italic' : ''} ${menuActive ? 'outline outline-2 outline-offset-2 outline-accent' : ''}`}
>
{replyInfo ? (
<QuoteBar topicName={topic.name} reply={replyInfo} resolveName={resolveQuotedName ?? (() => '消息')} />
) : null}
{isRecalled(msg) ? (
'⚠ 消息已删除'
) : callResult ? (