// ==================================================================== // IM 会话 / 撤回展示 utility // ==================================================================== // 基于 conversation 上下文 + sender 信息实时算"展示文案"。 // 1. 发送人名一律由 utils/user.getSenderDisplayName 实时算,本文件只负责拼"撤回 tip / 摘要"等文案。 // 2. fallbackName 由调用方传入(典型来源:Conversation.lastSenderDisplayName 快照),透传到 getSenderDisplayName 内部,算不出真名时兜底 // ==================================================================== import { ImMessageType, isFriendChatTip, isGroupNotification } from './constants' import { getCardLabelInfo, parseMessage, segmentsToText, tipMention, tipText, type CardMessage, type FaceMessage, type FileMessage, type TextMessage, type TipSegment } from './message' import { getSenderDisplayName, resolveFriendNotificationText, resolveGroupNotificationText } from './user' import type { Message } from '../home/types' /** 会话主键:`type-targetId` 拼成稳定字符串,给 v-for :key、active 比对、map key 等场景共用 */ export function getConversationKey(conversation: { type: number; targetId: number }): string { return `${conversation.type}-${conversation.targetId}` } /** 按昵称模糊过滤会话列表:空 keyword 原样返回,命中走 toLowerCase 不区分大小写 */ export function filterConversationsByKeyword( list: T[], keyword: string ): T[] { const trimmed = keyword.trim().toLowerCase() if (!trimmed) { return list } return list.filter((c) => (c.name || '').toLowerCase().includes(trimmed)) } /** * 表情消息的统一文本预览:摘要 / 历史搜索 / 引用块 / 管理后台预览共用一份 * 有 name 时走 `[表情] name`(系统包),无 name 时走 `[表情]`(个人表情通常无 name) */ export function buildFacePreviewText(facePayload: { name?: string } | null | undefined): string { return facePayload?.name ? `[表情] ${facePayload.name}` : '[表情]' } /** * 撤回提示 segments * * senderId 缺失时不挂 mention 段,避免点出错号;算不出真名降级为「对方」纯文本 */ export function buildRecallTipSegments( senderId: number, selfSend: boolean, conversationType: number, conversationTargetId: number, fallbackName?: string ): TipSegment[] { if (selfSend) { return [tipText('你撤回了一条消息')] } const senderDisplayName = getSenderDisplayName( senderId, conversationType, conversationTargetId, fallbackName ) if (!senderId) { return [tipText(`${senderDisplayName || '对方'} 撤回了一条消息`)] } return [ tipMention(senderId, senderDisplayName || '对方'), tipText(' 撤回了一条消息') ] } /** 撤回提示文案:自己撤回固定文案,对方撤回带 sender 名(实时算 + fallbackName 兜底) */ export function buildRecallTip( senderId: number, selfSend: boolean, conversationType: number, conversationTargetId: number, fallbackName?: string ): string { return segmentsToText( buildRecallTipSegments(senderId, selfSend, conversationType, conversationTargetId, fallbackName) ) } /** * 单条消息的纯文本摘要;不依赖 conversation 上下文 * * - withFileName=true 时文件消息附带文件名(合并转发详情用) * - 不处理 RECALL / 群广播 / 好友通知,由调用方包一层 */ export function summarizeMessageContent( message: Pick, opts?: { withFileName?: boolean } ): string { switch (message.type) { case ImMessageType.TEXT: return parseMessage(message.content)?.content ?? '' case ImMessageType.IMAGE: return '[图片]' case ImMessageType.VOICE: return '[语音]' case ImMessageType.VIDEO: return '[视频]' case ImMessageType.FILE: { if (opts?.withFileName) { const file = parseMessage(message.content) return file?.name ? `[文件] ${file.name}` : '[文件]' } return '[文件]' } case ImMessageType.CARD: return `[${getCardLabelInfo(parseMessage(message.content)).label}]` case ImMessageType.FACE: return buildFacePreviewText(parseMessage(message.content)) case ImMessageType.MERGE: return '[聊天记录]' default: return '' } } /** 会话列表最后一条摘要:RECALL 走 buildRecallTip + fallbackName;其它按消息类型派生 */ export function resolveConversationLastContent( message: Message, conversationType: number, conversationTargetId: number, fallbackName?: string ): string { if (message.type === ImMessageType.RECALL) { return buildRecallTip( message.senderId, message.selfSend, conversationType, conversationTargetId, fallbackName ) } if (isFriendChatTip(message.type)) { return resolveFriendNotificationText(message) } if (isGroupNotification(message.type)) { return resolveGroupNotificationText(message) } return summarizeMessageContent(message) }