import type { StoredMessage, Topic } from 'tinode-sdk'; import { Icon } from '@/app/Icon'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { isRecalled, isServerDeleted, systemNoticeText } from '@/lib/messages/dispatch'; import { renderDrafty } from '@/lib/messages/drafty'; import { formatBubbleTime, formatEventTimestamp } from '@/lib/messages/time'; import { isFailedStatus, isSendingStatus, outgoingStatusLabel } from '@/lib/messages/status'; import { retryMessage } from '@/lib/tinode/send'; interface MessageRowProps { topic: Topic; msg: StoredMessage; mine: boolean; isGroup: boolean; peerName: string; myUserId: string | null; selectMode?: boolean; selected?: boolean; /** README §3.5: "选中气泡加 outline:2px solid #1677ff" -- true while this message is the open * context menu's target. */ menuActive?: boolean; onToggleSelect?: (seq: number) => void; onContextMenu?: (e: React.MouseEvent, msg: StoredMessage) => void; } export function MessageRow({ topic, msg, mine, isGroup, peerName, myUserId, selectMode, selected, menuActive, onToggleSelect, onContextMenu, }: MessageRowProps) { if (isServerDeleted(msg)) { return
此消息已被删除
; } const noticeText = systemNoticeText(msg); if (noticeText) { return (
{noticeText}
{msg.ts ?
{formatEventTimestamp(msg.ts)}
: null}
); } const failed = isFailedStatus(topic, msg); const sending = isSendingStatus(topic, msg); const statusLabel = mine ? outgoingStatusLabel(topic, msg, isGroup) : msg.ts ? formatBubbleTime(msg.ts) : ''; const retry = () => { if (typeof msg.seq === 'number') retryMessage(topic, msg.seq); }; return (
{selectMode ? ( ) : null}
{ if (selectMode) return; e.preventDefault(); onContextMenu?.(e, msg); }} className={`flex flex-1 items-end gap-2.5 ${mine ? 'flex-row-reverse' : ''}`} > {avatarLetter(mine ? '我' : peerName)}
{/* README §3.2: the failed-send icon sits beside the bubble, not below it with the retry text. */}
{failed ? : null}
{isRecalled(msg) ? '⚠ 消息已删除' : renderDrafty(msg.content)}
{failed ? ( ) : ( {sending ? '发送中…' : statusLabel} )}
); }