From b64a0deae5b5d33f9e20671a68936b27f0b9b00b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 21 七月 2026 18:03:03 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0
---
src/views/im/utils/message.ts | 1035 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 1,035 insertions(+), 0 deletions(-)
diff --git a/src/views/im/utils/message.ts b/src/views/im/utils/message.ts
new file mode 100644
index 0000000..5bea3af
--- /dev/null
+++ b/src/views/im/utils/message.ts
@@ -0,0 +1,1035 @@
+import type { Conversation, GroupLite, Message, QuoteMessage, User } from '../home/types'
+
+import { useUserStore } from '#/packages/stores/src'
+
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { useFriendStore } from '../home/store/friendStore'
+import { useGroupStore } from '../home/store/groupStore'
+import {
+ ImContentType,
+ ImConversationType,
+ type ImConversationTypeValue,
+ ImRtcCallEndReason
+} from './constants'
+import { formatCallDuration } from './time'
+
+export type { QuoteMessage } from '../home/types'
+
+// ====================================================================
+// IM 娑堟伅 content 缂栬В鐮� & 灞曠ず宸ュ叿
+// ====================================================================
+// 绾﹀畾锛氭秷鎭殑 content 瀛楁缁熶竴瀛� JSON 瀛楃涓诧紝瀛楁鍚嶃�佺粨鏋勫榻愬悗绔�
+// cn.iocoder.yudao.module.im.service.websocket.notification.message.* 涓嬬殑 DTO銆�
+// 鍚勭被娑堟伅 payload interface 瀛楁瀵归綈鍚庣锛涜В鏋愮粺涓�鐢� parseMessage<T>锛�
+// 搴忓垪鍖栫洿鎺� JSON.stringify(payload)銆�
+// ====================================================================
+
+// ==================== 瀹㈡埛绔� ID ====================
+
+/** 鐢熸垚瀹㈡埛绔秷鎭� ID锛堢函 UUID锛夛紝鐢ㄤ簬鍓嶇鍘婚噸 & ACK 鍥炲啓 */
+export const generateClientMessageId = (): string => {
+ if (globalThis.crypto?.randomUUID) {
+ return globalThis.crypto.randomUUID()
+ }
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll(/[xy]/g, (char) => {
+ const random = Math.trunc(Math.random() * 16)
+ const value = char === 'x' ? random : (random & 0x3) | 0x8
+ return value.toString(16)
+ })
+}
+
+// ==================== 绉佽亰瀵圭 userId ====================
+
+/**
+ * 绉佽亰娑堟伅 / DTO 鐨勫绔� userId锛氳嚜宸卞彂鐨勫绔槸 receiver锛屽埆浜哄彂鐨勫绔槸 sender
+ *
+ * 鏀跺彛 4 澶勬棫 inline锛坵ebsocketStore.convertPrivateMessage / handlePrivateMessage / computeFriendPeerId
+ * 鍜� useMessagePuller.getPrivatePeerId锛夛紝缁撴瀯绫诲瀷鍙 senderId / receiverId 涓や釜瀛楁锛孯EST 涓� WS DTO 閮芥弧瓒�
+ */
+export function getPrivateMessagePeerId(
+ message: { receiverId: number; senderId: number; },
+ currentUserId: number
+): number {
+ return message.senderId === currentUserId ? message.receiverId : message.senderId
+}
+
+// ==================== 鏂囨湰鐗囨锛坱ip 鏂囨 + TEXT 姘旀场鍏辩敤锛� ====================
+// 鏃㈢敤浜庣伆鏉� tip锛�"XX 閭�璇� YY 鍔犲叆缇よ亰"锛夛紝涔熺敤浜� TEXT 姘旀场姝f枃锛園xxx 楂樹寒 + URL 鑷姩璇嗗埆锛夈��
+// mention 娈垫惡甯� userId 鐢ㄤ簬鎸傜偣鍑诲脊 UserInfoCard锛沴ink 娈垫惡甯� href 鐢ㄤ簬 <a> 璺宠浆锛�
+// text 娈垫槸绾枃鏈厹搴曘�傛覆鏌撳眰锛坱ip-segments.vue锛夋寜 type 鍒嗗彂缁熶竴澶勭悊銆�
+
+export type TipSegment =
+ | { href: string; text: string; type: 'link'; }
+ | { text: string; type: 'mention'; userId: number; }
+ | { text: string; type: 'text'; }
+
+export const tipText = (text: string): TipSegment => ({ type: 'text', text })
+
+export const tipMention = (userId: number, text: string): TipSegment => ({
+ type: 'mention',
+ userId,
+ text
+})
+
+export const tipLink = (href: string, text: string): TipSegment => ({
+ type: 'link',
+ href,
+ text
+})
+
+export const segmentsToText = (segments: TipSegment[]): string =>
+ segments.map((s) => s.text).join('')
+
+/** 澶氫釜 userId 鐢ㄥ悓涓�涓垎闅旂鎻掑�兼垚 segments锛屾瘡涓� user 鍗曠嫭鎴� mention 娈� */
+export function joinMentionSegments(
+ userIds: number[],
+ separator: string,
+ resolveName: (userId: number) => string
+): TipSegment[] {
+ return userIds.flatMap((id, index) =>
+ index === 0
+ ? [tipMention(id, resolveName(id))]
+ : [tipText(separator), tipMention(id, resolveName(id))]
+ )
+}
+
+/** mention 鍊欓�夛紱name 鐢ㄤ簬鍖归厤鏂囨湰瀛楅潰閲忥紝displayName 鐢ㄤ簬瑕嗙洊娓叉煋锛堜笉浼犲垯鎸� name 娓叉煋锛� */
+export interface MentionCandidate {
+ userId: number
+ /** 鐢ㄦ潵鍦� content 涓墠缂�鍖归厤鐨勫瓧闈㈤噺锛堜笉鍚� @锛� */
+ name: string
+ /**
+ * 娓叉煋鏃跺己鍒朵娇鐢ㄧ殑鏄剧ず鍚嶏紙涓嶅惈 @锛�
+ *
+ * 鐢ㄤ簬銆屽巻鍙叉秷鎭� content 閲屽啓鐨勬槸澶囨敞鍚� / 缇ゆ樀绉帮紝浣嗘帴鏀剁瑕佹寜鐪熷疄鏄电О娓叉煋銆嶇殑鍦烘櫙锛�
+ * 鍊欓�夊彲浠ユ惡甯﹀绉嶅瓧闈㈤噺锛坢atch 鐢級锛屼絾 displayName 缁熶竴鎸囧悜 nickname锛�
+ * 璁╂墍鏈夊巻鍙� / 鏂版秷鎭殑 @ 閮芥敹鏁涘埌涓�鑷寸殑灞曠ず
+ */
+ displayName?: string
+ /**
+ * 姝т箟鏍囪锛氬悓 name 瀵瑰簲澶氫釜 userId 鏃朵负 true
+ *
+ * parser 浠嶄細鎸� name 鍛戒腑锛堟渶闀夸紭鍏堬級锛屼絾鍛戒腑鍚庢暣娈靛悆鎴愭櫘閫氭枃鏈�斺��
+ * 閬垮厤銆孈寮犱笁銆嶈鍓旈櫎鍚庯紝鐭墠缂�鍊欓�夈�孈寮犮�嶆姠鍖归厤閿欑粦
+ */
+ ambiguous?: boolean
+}
+
+/** URL 閿氬畾姝e垯锛涚粓姝簬绌虹櫧銆佷腑鏂囥�丂锛堥伩鍏嶅悶鎺変笅涓�涓� mention锛夈��< > " '锛堥槻 HTML / 寮曞彿涓插叆锛夛紱y 鏍囧織璧� sticky 鍖归厤锛岀渷 text.slice */
+const URL_STICKY_REGEX = /(https?:\/\/[^\s涓�-榫<>"']+|www\.[^\s涓�-榫<>"']+)/iy
+
+/** URL 鏈熬甯歌鏍囩偣鍓旈櫎锛岄伩鍏嶅悶鎺夊彞鏈腑鑻辨枃鏍囩偣 */
+const URL_TRAILING_PUNCTUATION = /[.,!?;:)\]銆侊紝銆傦紒锛燂紱锛氾級銆慮+$/
+
+/** 鏈�鐭� URL锛歚www.ab` / `http:/` 杩欑瀛ゆ涓嶇畻閾炬帴 */
+const URL_MIN_LENGTH = 6
+
+/**
+ * 鏂囨湰姘旀场 content 鎷嗘锛歮ention 娈垫寜鍊欓�夋渶闀垮墠缂�鍖归厤锛孶RL 娈佃蛋閿氬畾姝e垯锛屽墿浣欏綊 text
+ *
+ * mentions 涓嶄紶鎴栧尮閰嶄笉涓婃椂锛孈xxx 閫�鍖栦负鏅�� text锛涜В鏋愪笌娓叉煋瑙h�︼紝宸ュ叿鍑芥暟鏈韩涓嶄緷璧� store
+ */
+export function parseTextSegments(text: string, mentions: MentionCandidate[] = []): TipSegment[] {
+ if (!text) {
+ return []
+ }
+ // 銆孈寮犱笁涓般�嶄笉鑳借銆孈寮犱笁銆嶆埅鑳★紝鍊欓�夋寜 name 闀垮害鍊掑簭
+ const sortedMentions =
+ mentions.length > 1 ? mentions.toSorted((a, b) => b.name.length - a.name.length) : mentions
+ const out: TipSegment[] = []
+ let buffer = ''
+
+ const flush = () => {
+ if (buffer) {
+ out.push(tipText(buffer))
+ buffer = ''
+ }
+ }
+
+ let i = 0
+ while (i < text.length) {
+ if (text[i] === '@' && sortedMentions.length > 0) {
+ const matched = sortedMentions.find((m) => m.name && text.startsWith(m.name, i + 1))
+ if (matched) {
+ if (matched.ambiguous) {
+ // 鍚屽瓧闈㈤噺瀵瑰簲澶� userId锛屾棤娉曞垽瀹氭剰鍥撅紱鏁存绱叆 buffer 璁╃煭鍓嶇紑鍊欓�夋病鏈轰細鍐嶆壂杩欐
+ buffer += '@' + matched.name
+ } else {
+ flush()
+ // 娓叉煋缁熶竴璧� displayName锛堝嵆鐪熷疄鏄电О锛夛紝璁╁巻鍙� content 閲屾畫鐣欑殑澶囨敞 / 缇ゆ樀绉颁篃鏀舵暃鍒� nickname
+ out.push(tipMention(matched.userId, '@' + (matched.displayName || matched.name)))
+ }
+ i += 1 + matched.name.length
+ continue
+ }
+ }
+ const head = text[i]
+ if (head === 'h' || head === 'H' || head === 'w' || head === 'W') {
+ URL_STICKY_REGEX.lastIndex = i
+ const linkMatch = URL_STICKY_REGEX.exec(text)
+ if (linkMatch) {
+ const urlText = linkMatch[0].replace(URL_TRAILING_PUNCTUATION, '')
+ if (urlText.length >= URL_MIN_LENGTH) {
+ flush()
+ const href = /^https?:\/\//i.test(urlText) ? urlText : `https://${urlText}`
+ out.push(tipLink(href, urlText))
+ i += urlText.length
+ continue
+ }
+ }
+ }
+ buffer += text[i]
+ i += 1
+ }
+ flush()
+ return out
+}
+
+// ==================== 寮曠敤娑堟伅 ====================
+
+/** 寮曠敤瀹瑰櫒锛�5 绉嶆櫘閫氭秷鎭�(TEXT / IMAGE / FILE / VOICE / VIDEO)閮藉彲鎼哄甫 quote */
+interface Quotable {
+ quote?: QuoteMessage
+}
+
+// ==================== 娑堟伅 payload ====================
+
+/** 鏂囨湰娑堟伅 payload锛堝榻愬悗绔� TextMessage锛� */
+export interface TextMessage extends Quotable {
+ content: string
+}
+
+/** 鍥剧墖娑堟伅 payload锛堝榻愬悗绔� ImageMessage锛� */
+export interface ImageMessage extends Quotable {
+ url: string
+ /** 缂╃暐鍥� URL */
+ thumbnailUrl?: string
+ /** 鍥剧墖瀹藉害 */
+ width?: number
+ /** 鍥剧墖楂樺害 */
+ height?: number
+ /** 鏂囦欢澶у皬锛堝瓧鑺傦級 */
+ size?: number
+}
+
+/** 璇煶娑堟伅 payload锛堝榻愬悗绔� AudioMessage锛汭mContentType 淇濈暀 VOICE 鍛藉悕锛� */
+export interface AudioMessage extends Quotable {
+ url: string
+ /** 鏃堕暱锛堢锛� */
+ duration: number
+ /** 鏂囦欢澶у皬锛堝瓧鑺傦級 */
+ size?: number
+}
+
+/** 鏂囦欢娑堟伅 payload锛堝榻愬悗绔� FileMessage锛� */
+export interface FileMessage extends Quotable {
+ url: string
+ name: string
+ size: number
+ /** MIME 绫诲瀷 */
+ type?: string
+}
+
+/** 瑙嗛娑堟伅 payload锛堝榻愬悗绔� VideoMessage锛涙殏鏈帴鍏ユ覆鏌擄級 */
+export interface VideoMessage extends Quotable {
+ url: string
+ /** 灏侀潰 URL */
+ coverUrl?: string
+ /** 鏃堕暱锛堢锛� */
+ duration?: number
+ width?: number
+ height?: number
+ /** 鏂囦欢澶у皬锛堝瓧鑺傦級 */
+ size?: number
+}
+
+/**
+ * 鍚嶇墖娑堟伅 payload锛堝榻愬悗绔� CardMessage锛�
+ *
+ * 鎸� targetType 鍖哄垎鐢ㄦ埛鍚嶇墖 / 缇ゅ悕鐗囷細
+ * - PRIVATE锛歵argetId = 鐢ㄦ埛缂栧彿锛宯ame = 鐢ㄦ埛鏄电О锛堝彇鐪熷疄鏄电О锛岄潪澶囨敞锛�
+ * - GROUP锛歵argetId = 缇ょ紪鍙凤紝name = 缇ゅ悕锛屽彲甯� memberCount
+ */
+export interface CardMessage extends Quotable {
+ /** 鍚嶇墖瀵硅薄绫诲瀷 */
+ targetType: ImConversationTypeValue
+ /** 鐩爣瀵硅薄缂栧彿锛歅RIVATE 鏃� = userId锛汫ROUP 鏃� = groupId */
+ targetId: number
+ /** 鏄剧ず鍚嶅揩鐓э細PRIVATE 鏃� = 鐢ㄦ埛鏄电О锛汫ROUP 鏃� = 缇ゅ悕 */
+ name: string
+ /** 澶村儚锛堝揩鐓э級 */
+ avatar?: string
+ /** 缇ゆ垚鍛樻暟锛堜粎 targetType = GROUP锛涙帴鏀剁灞曠ず銆孨 浜虹兢鑱娿�嶏級 */
+ memberCount?: number
+}
+
+/**
+ * 鍚嶇墖杞彂鐨勬簮瀵硅薄锛圧ecommendCardDialog 鍏ュ弬锛夛紱瀛楁涓� CardMessage 1:1锛屾棤 quote
+ */
+export type CardTarget = Omit<CardMessage, 'quote'>
+
+/** 鐢ㄦ埛瀵硅薄 鈫� 鐢ㄦ埛鍚嶇墖婧愶紙PRIVATE锛夛紱缂� id 杩斿洖 null 璁╄皟鐢ㄦ柟 v-bind 缁戝畾涓轰笉娓叉煋 */
+export function toUserCardTarget(user: null | undefined | User): CardTarget | null {
+ if (!user?.id) {
+ return null
+ }
+ return {
+ targetType: ImConversationType.PRIVATE,
+ targetId: user.id,
+ name: user.nickname || '',
+ avatar: user.avatar
+ }
+}
+
+/** 缇ゅ璞� 鈫� 缇ゅ悕鐗囨簮锛圙ROUP锛夛紱缂� id 杩斿洖 null */
+export function toGroupCardTarget(group: GroupLite | null | undefined): CardTarget | null {
+ if (!group?.id) {
+ return null
+ }
+ return {
+ targetType: ImConversationType.GROUP,
+ targetId: group.id,
+ name: group.name || '',
+ avatar: group.showImage || group.showImageThumb,
+ memberCount: group.memberCount
+ }
+}
+
+/**
+ * 鍚嶇墖鏍囩 + 鍥炬爣锛堟寜 targetType 浜屽垎锛夛紝缁熶竴鑱婂ぉ姘旀场 / 寮曠敤棰勮 / 鍘嗗彶鎽樿 / 鍚庡彴棰勮鐨勬枃妗堜笌鍥炬爣
+ *
+ * 缂哄け / 闈炴硶 targetType 璧般�屼釜浜哄悕鐗囥�嶅厹搴曪紝閬垮厤鑰佹秷鎭垨鑴忔暟鎹鑷� UI 绌虹櫧
+ */
+export function getCardLabelInfo(card: null | undefined | { targetType?: number }): {
+ icon: string
+ label: string
+} {
+ if (card?.targetType === ImConversationType.GROUP) {
+ return { label: '缇ゅ悕鐗�', icon: 'ant-design:usergroup-outlined' }
+ }
+ return { label: '涓汉鍚嶇墖', icon: 'ant-design:user-outlined' }
+}
+
+/** 琛ㄦ儏娑堟伅 payload锛堝榻愬悗绔� FaceMessage锛沀nicode emoji 浠嶈蛋 TEXT锛屾湰绫诲瀷鍙壙杞借创鍥� / 鑷畾涔夎〃鎯呭寘锛� */
+export interface FaceMessage extends Quotable {
+ /** 琛ㄦ儏鍥� URL */
+ url: string
+ /** 娓叉煋瀹藉害锛堝儚绱狅級锛岄伩鍏嶅竷灞�鎶栧姩锛涘彲閫夛紝缂哄け鏃惰皟鐢ㄦ柟璧� CSS max-w 鍏滃簳 */
+ width?: number
+ /** 娓叉煋楂樺害锛堝儚绱狅級锛屽彲閫� */
+ height?: number
+ /** 琛ㄦ儏鍚嶏紙绯荤粺鍖呴�氬父鏈夛紝涓汉琛ㄦ儏鍖呴�氬父鏃狅級 */
+ name?: string
+}
+
+/** 鍚堝苟杞彂鐨勫崟鏉″唴宓屾秷鎭揩鐓э紙瀵归綈鍚庣 MergeMessage.Item锛� */
+export interface MergeMessageItem {
+ /** 鍘熸秷鎭紪鍙凤紱浠呭仛婧簮鏍囪瘑 */
+ messageId: number
+ /** 鍙戦�佷汉缂栧彿 */
+ senderId: number
+ /** 鍙戦�佷汉鏄电О蹇収锛涘绔彲鑳戒笉鍦ㄥ師浼氳瘽閲岋紝鏃犳硶瀹炴椂鏌ュ埌 */
+ senderNickname: string
+ /** 鍙戦�佷汉澶村儚蹇収 */
+ senderAvatar?: string
+ /** 鍐呭绫诲瀷锛屽榻� ImContentType */
+ type: number
+ /** 鍘熸秷鎭� content锛圝SON锛夛紱宓屽鍚堝苟娑堟伅鏃朵粛鎸夋湰缁撴瀯灞傚眰灞曞紑 */
+ content: string
+ /** 鍙戦�佹椂闂存埑锛堟绉掞級 */
+ sendTime: number
+}
+
+/** 鍚堝苟杞彂娑堟伅 payload锛堝榻愬悗绔� MergeMessage锛� */
+export interface MergeMessage {
+ title: string // 鍚堝苟鏍囬锛涗緥锛氥�屽紶涓夊拰鏉庡洓鐨勮亰澶╄褰曘�嶃�岀兢鑱婄殑鑱婂ぉ璁板綍銆�
+ messages: MergeMessageItem[] // 鍐呭祵鐨勫畬鏁存秷鎭揩鐓�
+}
+
+/** 棰戦亾绱犳潗娑堟伅 payload锛堝榻愬悗绔� MaterialMessage锛� */
+export interface MaterialMessage {
+ materialId?: number
+ channelId?: number // 棰戦亾缂栧彿锛涜浆鍙戝悗娓叉煋鍗$墖搴曢儴鐨勯閬撳ご鍍� + 鍚嶇О
+ title?: string
+ coverUrl?: string
+ summary?: string
+ url?: string // 璺宠浆閾炬帴锛涗负绌烘椂鐐瑰嚮鍦ㄥ鎴风鍐呯疆璇︽儏椤垫寜 materialId 鎷� content 娓叉煋锛涢潪绌哄垯璺� url
+}
+
+// ==================== 鍚堝苟杞彂 payload 鏋勯�� ====================
+
+/** 鍗曚釜鍙戦�佷汉鐨勫揩鐓ф樀绉� / 澶村儚 */
+interface SenderSnapshot {
+ nickname: string
+ avatar: string
+}
+
+/**
+ * 涓�娆℃�ф瀯閫� senderId 鈫� SenderSnapshot 鏄犲皠锛涢伩鍏� N 鏉℃秷鎭�愭潯 find 缇ゆ垚鍛�
+ *
+ * 缇よ亰浠� group.members 涓�娆℃�� indexBy锛涚鑱婂彧闇� self + friend锛涘鍔犲叏浣� senderId 涓婂仛 friend 鍏滃簳
+ */
+function buildSenderSnapshotMap(
+ senderIds: number[],
+ conversation: Conversation
+): Map<number, SenderSnapshot> {
+ const userStore = useUserStore()
+ const friendStore = useFriendStore()
+ const selfUserId = getCurrentUserId()
+ const result = new Map<number, SenderSnapshot>()
+
+ let groupMembers: Map<number, { avatar?: string; nickname: string; }> | null = null
+ if (conversation.type === ImConversationType.GROUP) {
+ const group = useGroupStore().getGroup(conversation.targetId)
+ groupMembers = new Map((group?.members || []).map((m) => [m.userId, m]))
+ }
+
+ for (const senderId of senderIds) {
+ if (result.has(senderId)) {
+ continue
+ }
+ if (senderId === selfUserId) {
+ result.set(senderId, {
+ nickname: userStore.userInfo?.nickname || String(senderId),
+ avatar: userStore.userInfo?.avatar || ''
+ })
+ continue
+ }
+ const member = groupMembers?.get(senderId)
+ if (member?.nickname) {
+ result.set(senderId, { nickname: member.nickname, avatar: member.avatar || '' })
+ continue
+ }
+ const friend = friendStore.getFriend(senderId)
+ result.set(senderId, {
+ nickname: friend?.nickname || String(senderId),
+ avatar: friend?.avatar || ''
+ })
+ }
+ return result
+}
+
+/** 鎶婂崟鏉� Message 杞垚 MergeMessageItem 蹇収锛涘墺绂� quote 闃插紩鐢ㄧ┛閫� */
+function mapMessageToMergeItem(
+ message: Message,
+ senderSnapshots: Map<number, SenderSnapshot>
+): MergeMessageItem {
+ const snapshot = senderSnapshots.get(message.senderId)
+ return {
+ messageId: message.id || 0,
+ senderId: message.senderId,
+ senderNickname: snapshot?.nickname ?? String(message.senderId),
+ senderAvatar: snapshot?.avatar ?? '',
+ type: message.type,
+ content: removeQuotePayload(message.content),
+ sendTime: message.sendTime
+ }
+}
+
+/** 鍚堝苟杞彂鏍囬锛氱鑱娿�寋瀵规柟} 鍜� {鑷繁} 鐨勮亰澶╄褰曘�嶏紱缇よ亰銆寋缇ゅ悕} 鐨勮亰澶╄褰曘�� */
+export function buildMergeTitle(conversation: Conversation): string {
+ if (conversation.type === ImConversationType.GROUP) {
+ return `${conversation.name || '缇よ亰'} 鐨勮亰澶╄褰昤
+ }
+ const myName = useUserStore().userInfo?.nickname || '鎴�'
+ return `${conversation.name || '瀵规柟'} 鍜� ${myName} 鐨勮亰澶╄褰昤
+}
+
+/** 鎶婁竴缁� Message 鎵撳寘鎴� MergeMessage payload锛涜皟鐢ㄦ柟璐熻矗鎸� sendTime 鎺掑簭鍚庝紶鍏� */
+export function buildMergeMessagePayload(
+ messages: Message[],
+ conversation: Conversation
+): MergeMessage {
+ const senderIds = messages.map((m) => m.senderId)
+ const senderSnapshots = buildSenderSnapshotMap(senderIds, conversation)
+ return {
+ title: buildMergeTitle(conversation),
+ messages: messages.map((m) => mapMessageToMergeItem(m, senderSnapshots))
+ }
+}
+
+/** 銆屾坊鍔犲埌琛ㄦ儏銆嶇殑鍙彂璧锋簮锛欶ACE / IMAGE 閮藉厑璁革紙GIF 鍥剧墖涔熷父琚敹钘忥級 */
+export interface AddableFacePayload {
+ url: string
+ width: number
+ height: number
+ name?: string
+}
+
+/**
+ * 浠庢秷鎭娊鍙栥�屾坊鍔犲埌琛ㄦ儏銆嶇殑 payload锛涘綋鍓嶅唴瀹圭被鍨嬩笉鍙坊鍔犺繑鍥� null
+ *
+ * 璋冪敤鏂癸紙MessageItem 鐨勫彸閿彍鍗曪級鎸� nullable 鍐冲畾鏄惁灞曠ず銆屾坊鍔犲埌琛ㄦ儏銆嶅叆鍙�
+ */
+export function extractAddableFace(message: Message): AddableFacePayload | null {
+ if (message.type === ImContentType.FACE) {
+ const face = parseMessage<FaceMessage>(message.content)
+ if (!face?.url) {
+ return null
+ }
+ return { url: face.url, width: face.width || 200, height: face.height || 200, name: face.name }
+ }
+ if (message.type === ImContentType.IMAGE) {
+ const image = parseMessage<ImageMessage>(message.content)
+ if (!image?.url) {
+ return null
+ }
+ return { url: image.url, width: image.width || 200, height: image.height || 200 }
+ }
+ return null
+}
+
+/** 瑙f瀽娑堟伅 content锛圝SON 瀛楃涓诧級涓烘寚瀹� payload锛岄潪娉� JSON 杩斿洖 null */
+export const parseMessage = <T>(content: string): null | T => {
+ try {
+ return JSON.parse(content) as T
+ } catch {
+ return null
+ }
+}
+
+/** 搴忓垪鍖栨秷鎭� payload 涓� content JSON 瀛楃涓诧紱涓� parseMessage 瀵圭О */
+export const serializeMessage = <T>(payload: T): string => JSON.stringify(payload)
+
+/** `URL.createObjectURL(file)` 鐢熸垚鐨� URL 鍓嶇紑锛涘崰浣� / revoke / 閲嶄紶鏃у�艰瘑鍒叡鐢� */
+export const BLOB_URL_PREFIX = 'blob:'
+
+/**
+ * 濯掍綋 payload 閲屽彲鑳藉寘鍚� blob URL 鐨勫瓧娈碉紙鍥剧墖/鏂囦欢/瑙嗛/璇煶閮藉榻愯繖濂� url 瀛楁鍛藉悕锛�
+ *
+ * 璺熼殢 ImageMessage / VideoMessage / FileMessage / AudioMessage interface 瀹氫箟鍚屾锛�
+ * - url锛氫富浣撹祫婧愶紙鍗犱綅鏃舵槸 blob URL锛宎ck 鍚庢槸鐪熷疄 URL锛�
+ * - coverUrl锛氳棰戝皝闈紙commit 鍚庢槸鐪熷疄 URL锛涘崰浣嶉樁娈典笉璁句互閬垮厤浼� blob 褰� poster 鍦ㄩ儴鍒嗘祻瑙堝櫒閫�鍖栵級
+ * - thumbnailUrl锛氬浘鐗囩缉鐣ュ浘锛堝綋鍓嶆湭鍗犱綅鏃朵娇鐢� blob锛岄鐣欙級
+ */
+const MEDIA_BLOB_URL_FIELDS = ['url', 'coverUrl', 'thumbnailUrl'] as const
+
+/**
+ * 閲婃斁 content 涓獟浣� payload 瀛楁涓婄殑 blob URL 鍐呭瓨鏄犲皠
+ *
+ * 浠呮壂鎻� url / coverUrl / thumbnailUrl 涓変釜宸茬煡瀛楁锛岄伩鍏� regex 鍏ㄦ枃 grep 璇激 quote.content 閲屽祵濂楃殑鍚屽悕 blob URL銆�
+ * 浠呭褰撳墠 document 鍐呭垱寤虹殑 blob URL 鏈夋晥锛汭ndexedDB 鎭㈠鍑烘潵鐨勬棫 blob URL 宸查殢鏃� document 澶辨晥锛岃皟瀹冩棤瀹充絾鏃犳剰涔�
+ */
+export const revokeBlobUrlsInContent = (content: string): void => {
+ if (!content || !content.includes(BLOB_URL_PREFIX)) {
+ return
+ }
+ const payload = parseMessage<Record<string, unknown>>(content)
+ if (!payload) {
+ return
+ }
+ for (const field of MEDIA_BLOB_URL_FIELDS) {
+ const value = payload[field]
+ if (typeof value === 'string' && value.startsWith(BLOB_URL_PREFIX)) {
+ URL.revokeObjectURL(value)
+ }
+ }
+}
+
+// ==================== 寮曠敤娑堟伅 helper ====================
+
+/** 鎶� quote 鍚堣繘 payload(搴忓垪鍖栧墠璋冪敤);quote 缂哄け鏃跺師鏍疯繑鍥� */
+export const withQuotePayload = <T extends Quotable>(payload: T, quote?: QuoteMessage): T => {
+ if (!quote) {
+ return payload
+ }
+ return { ...payload, quote }
+}
+
+/**
+ * 浠� content JSON 瀛楃涓查噷娓呮帀 quote 瀛楁
+ *
+ * 瀹㈡埛绔箰瑙傛覆鏌撴瀯閫� quote 鏃惰皟鐢�,閬垮厤"鍥炲涓�鏉″甫寮曠敤鐨勬秷鎭�"閫犳垚 quote 宓屽婊氶洩鐞�;
+ * 涓庡悗绔� ImMessageUtils.removeQuote 瀵归綈
+ */
+export const removeQuotePayload = (content: string): string => {
+ if (!content || !content.includes('"quote"')) {
+ return content
+ }
+ const parsed = parseMessage<Record<string, unknown>>(content)
+ if (!parsed || !('quote' in parsed)) {
+ return content
+ }
+ delete parsed.quote
+ return JSON.stringify(parsed)
+}
+
+/** 鐢� Message 娲剧敓 QuoteMessage 鐢ㄤ簬涔愯娓叉煋;ack 鍚庝細琚湇鍔$鏉冨▉鐗堟湰瑕嗙洊 */
+export const buildQuoteFromMessage = (message: Message): QuoteMessage => {
+ return {
+ messageId: message.id || 0,
+ senderId: message.senderId,
+ type: message.type,
+ content: removeQuotePayload(message.content)
+ }
+}
+
+/** 浠庡凡搴忓垪鍖� message.content 涓В鍑� quote;闈� JSON / 鏃� quote 杩斿洖 null */
+export const getQuoteFromMessage = (content: string): null | QuoteMessage => {
+ // 闀夸細璇濇瘡鏉℃秷鎭覆鏌撻兘璧� quote computed,闈炲紩鐢ㄦ秷鎭瓧绗︿覆棰勬壂鐩存帴杩斿洖,鍏嶄竴娆� JSON.parse
+ if (!content || !content.includes('"quote"')) {
+ return null
+ }
+ const parsed = parseMessage<Quotable>(content)
+ return parsed?.quote ?? null
+}
+
+// ==================== 鎾ゅ洖 ====================
+
+/**
+ * 浠庡悗绔笅鍙戠殑鎾ゅ洖 RecallMessage content 涓В鏋愬嚭琚挙鍥炵殑鍘熸秷鎭� id
+ * content 褰㈠ `{"messageId": 123}`锛岃嫢涓嶅惈 messageId 鍒欒繑鍥� 0锛堣〃绀鸿繖鏉′笉鏄挙鍥炴秷鎭級
+ */
+export const parseRecallMessageId = (content: string): number => {
+ try {
+ const parsed = JSON.parse(content)
+ return parsed?.messageId == null ? 0 : Number(parsed.messageId)
+ } catch {
+ return 0
+ }
+}
+
+// ==================== 鏂版秷鎭彁绀洪煶 ====================
+
+import tipAudioUrl from '#/assets/audio/im/message-tip.mp3'
+
+/**
+ * 鏂版秷鎭彁绀洪煶锛屽甫 1 绉掕妭娴侊細鐭椂闂村唴澶氭潯娑堟伅鍙搷涓�娆★紝閬垮厤鐤插姵杞扮偢銆�
+ *
+ * 娴忚鍣ㄨ嚜鍔ㄦ挱鏀剧瓥鐣ヨ姹傞〉闈㈡湁杩囩敤鎴蜂氦浜掞紱鑻ユ棤娉曟挱鏀句細闈欓粯澶辫触銆�
+ */
+let __lastPlayAudioTipAt = 0
+let __tipAudio: HTMLAudioElement | null = null
+export const playAudioTip = () => {
+ const now = Date.now()
+ if (now - __lastPlayAudioTipAt < 1000) {
+ return
+ }
+ __lastPlayAudioTipAt = now
+ try {
+ if (!__tipAudio) {
+ __tipAudio = new Audio(tipAudioUrl)
+ __tipAudio.preload = 'auto'
+ }
+ __tipAudio.currentTime = 0
+ const playPromise = __tipAudio.play()
+ if (playPromise && typeof playPromise.catch === 'function') {
+ playPromise.catch((error) => console.debug('[IM] playAudioTip 澶辫触', error))
+ }
+ } catch (error) {
+ console.debug('[IM] playAudioTip 澶辫触', error)
+ }
+}
+
+// ==================== 鏂囦欢鍥炬爣 ====================
+
+/**
+ * 鎸夋枃浠舵墿灞曞悕鎸戞枃浠跺浘鏍� + 棰滆壊锛屽榻愬井淇¤鎰�
+ *
+ * message-item.vue锛堜富姘旀场锛夊拰 reply-preview.vue锛堝紩鐢ㄩ瑙堬級鍏辩敤鍚屼竴浠芥槧灏勶紝閬垮厤瑙嗚涓ゅ涓嶄竴鑷�
+ */
+export function getFileIconInfo(filename: string | undefined): { color: string; icon: string; } {
+ const ext = (filename || '').split('.').pop()?.toLowerCase() || ''
+ if (ext === 'pdf') {
+ return { icon: 'ant-design:file-pdf-filled', color: '#ed5757' }
+ }
+ if (['doc', 'docx'].includes(ext)) {
+ return { icon: 'ant-design:file-word-filled', color: '#2b7cd3' }
+ }
+ if (['xls', 'xlsx'].includes(ext)) {
+ return { icon: 'ant-design:file-excel-filled', color: '#1f7244' }
+ }
+ if (['ppt', 'pptx'].includes(ext)) {
+ return { icon: 'ant-design:file-ppt-filled', color: '#d24726' }
+ }
+ if (['7z', 'gz', 'rar', 'tar', 'zip'].includes(ext)) {
+ return { icon: 'ant-design:file-zip-filled', color: '#f0ad4e' }
+ }
+ if (['bmp', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp'].includes(ext)) {
+ return { icon: 'ant-design:file-image-filled', color: '#9c27b0' }
+ }
+ if (['avi', 'flv', 'mkv', 'mov', 'mp4', 'wmv'].includes(ext)) {
+ return { icon: 'ant-design:video-camera-filled', color: '#9c27b0' }
+ }
+ if (['aac', 'flac', 'mp3', 'ogg', 'wav'].includes(ext)) {
+ return { icon: 'ant-design:audio-filled', color: '#9c27b0' }
+ }
+ if (['json', 'log', 'md', 'txt', 'xml'].includes(ext)) {
+ return { icon: 'ant-design:file-text-filled', color: '#909399' }
+ }
+ return { icon: 'ant-design:file-filled', color: '#909399' }
+}
+
+// ==================== 绠$悊鍚庡彴灞曠ず宸ュ叿 ====================
+
+/** 璇︽儏寮圭獥閲屾妸 content JSON 缇庡寲鎴� 2 缂╄繘 */
+export const formatJson = (content?: string): string => {
+ if (!content) return ''
+ try {
+ return JSON.stringify(JSON.parse(content), null, 2)
+ } catch {
+ return content
+ }
+}
+
+// ==================== 缇ゅ箍鎾簨浠� payload + 鏂囨 ====================
+
+// 缇ゅ箍鎾簨浠� payload锛涘榻愬悗绔� GroupNotificationMessage 瀛愮被鑱氬悎瀛楁
+export type GroupNotificationPayload = {
+ addSource?: number
+ // 鍏ㄧ兢灏佺
+ banned?: boolean
+ displayUserName?: string
+ // 鑷敱杩涚兢浜嬩欢
+ entrantUserId?: number
+ memberUserIds?: number[]
+ // PIN 浜嬩欢鎼哄甫鐨勮缃《娑堟伅灞曠ず鏁版嵁
+ message?: {
+ atUserIds?: number[]
+ content: string
+ groupId: number
+ id: number
+ receiverUserIds?: number[]
+ senderId: number
+ sendTime: string
+ type: number
+ }
+ messageId?: number
+ // 绂佽█浜嬩欢
+ mutedUserId?: number
+ muteEndTime?: string
+ newAvatar?: string
+ newJoinApproval?: boolean
+ newName?: string
+ newNotice?: string
+ newOwnerUserId?: number
+ oldAvatar?: string
+ oldJoinApproval?: boolean
+ oldName?: string
+ oldNotice?: string
+ operatorUserId?: number
+}
+
+/**
+ * 缇ゅ箍鎾簨浠� segments
+ *
+ * resolveName 鐢辫皟鐢ㄦ柟娉ㄥ叆锛堥粯璁ゅ満鏅紶 getSenderDisplayName锛夛紱
+ * operatorNameOverride 浠呰鐩� operator 娈垫枃妗堬紝mention userId 浠嶇敤 payload.operatorUserId
+ */
+export function resolveGroupNotificationSegments(
+ message: { content?: string; targetId?: number; type?: number; },
+ resolveName: (userId: number) => string,
+ operatorNameOverride?: string
+): TipSegment[] {
+ let payload: GroupNotificationPayload
+ try {
+ payload = JSON.parse(message.content || '{}')
+ } catch {
+ return []
+ }
+ // ENTER 涓昏鏄� entrant 鑰岄潪 operator锛岀嫭绔嬪鐞嗭紱鍏跺畠 case 閮戒互 operatorUserId 涓轰富璇�
+ if (message.type === ImContentType.GROUP_MEMBER_ENTER) {
+ const entrantId = payload.entrantUserId ?? payload.operatorUserId
+ return entrantId ? [tipMention(entrantId, resolveName(entrantId)), tipText(' 鍔犲叆浜嗙兢鑱�')] : []
+ }
+ if (!payload.operatorUserId) {
+ return []
+ }
+ const operatorSegment = tipMention(
+ payload.operatorUserId,
+ operatorNameOverride ?? resolveName(payload.operatorUserId)
+ )
+ const memberSegments = joinMentionSegments(payload.memberUserIds || [], '銆�', resolveName)
+
+ switch (message.type) {
+ case ImContentType.GROUP_ADMIN_ADD: {
+ return [operatorSegment, tipText(' 灏� '), ...memberSegments, tipText(' 璁句负绠$悊鍛�')]
+ }
+ case ImContentType.GROUP_ADMIN_REMOVE: {
+ return [operatorSegment, tipText(' 鎾ら攢浜� '), ...memberSegments, tipText(' 鐨勭鐞嗗憳韬唤')]
+ }
+ case ImContentType.GROUP_BANNED: {
+ return [operatorSegment, tipText(payload.banned ? ' 灏佺浜嗚缇�' : ' 瑙e皝浜嗚缇�')]
+ }
+ case ImContentType.GROUP_CANCEL_MUTED: {
+ return [operatorSegment, tipText(' 鍏抽棴浜嗗叏缇ょ瑷�')]
+ }
+ case ImContentType.GROUP_CREATE: {
+ return [operatorSegment, tipText(' 鍒涘缓浜嗙兢鑱�')]
+ }
+ case ImContentType.GROUP_DISSOLVE: {
+ return [operatorSegment, tipText(' 瑙f暎浜嗙兢鑱�')]
+ }
+ case ImContentType.GROUP_INFO_UPDATE: {
+ return payload.newAvatar
+ ? [operatorSegment, tipText(' 鏇存崲浜嗙兢澶村儚')]
+ : [operatorSegment, tipText(' 鏇存柊浜嗙兢淇℃伅')]
+ }
+ case ImContentType.GROUP_MEMBER_CANCEL_MUTED: {
+ return payload.mutedUserId
+ ? [
+ operatorSegment,
+ tipText(' 瑙i櫎浜� '),
+ tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)),
+ tipText(' 鐨勭瑷�')
+ ]
+ : []
+ }
+ case ImContentType.GROUP_MEMBER_INVITE: {
+ return [operatorSegment, tipText(' 閭�璇� '), ...memberSegments, tipText(' 鍔犲叆缇よ亰')]
+ }
+ case ImContentType.GROUP_MEMBER_KICK: {
+ return [operatorSegment, tipText(' 绉诲嚭浜� '), ...memberSegments]
+ }
+ case ImContentType.GROUP_MEMBER_MUTED: {
+ return payload.mutedUserId
+ ? [
+ operatorSegment,
+ tipText(' 灏� '),
+ tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)),
+ tipText(' 绂佽█')
+ ]
+ : []
+ }
+ case ImContentType.GROUP_MEMBER_NICKNAME_UPDATE: {
+ return [operatorSegment, tipText(` 淇敼缇ゆ樀绉颁负 "${payload.displayUserName ?? ''}"`)]
+ }
+ case ImContentType.GROUP_MEMBER_QUIT: {
+ return [operatorSegment, tipText(' 閫�鍑轰簡缇よ亰')]
+ }
+ case ImContentType.GROUP_MESSAGE_PIN: {
+ return [operatorSegment, tipText(' 缃《浜嗕竴鏉℃秷鎭�')]
+ }
+ case ImContentType.GROUP_MESSAGE_UNPIN: {
+ return [operatorSegment, tipText(' 鍙栨秷浜嗕竴鏉$疆椤舵秷鎭�')]
+ }
+ case ImContentType.GROUP_MUTED: {
+ return [operatorSegment, tipText(' 寮�鍚簡鍏ㄧ兢绂佽█')]
+ }
+ case ImContentType.GROUP_NAME_UPDATE: {
+ return [operatorSegment, tipText(` 灏嗙兢鍚嶄慨鏀逛负 "${payload.newName ?? ''}"`)]
+ }
+ case ImContentType.GROUP_NOTICE_UPDATE: {
+ return [operatorSegment, tipText(' 鏇存柊浜嗙兢鍏憡')]
+ }
+ case ImContentType.GROUP_OWNER_TRANSFER: {
+ return payload.newOwnerUserId
+ ? [
+ operatorSegment,
+ tipText(' 宸插皢缇や富杞缁� '),
+ tipMention(payload.newOwnerUserId, resolveName(payload.newOwnerUserId))
+ ]
+ : []
+ }
+ default: {
+ return []
+ }
+ }
+}
+
+/** 缇ゅ箍鎾簨浠朵腑鏂囨枃妗� */
+export function resolveGroupNotificationText(
+ message: { content?: string; targetId?: number; type?: number; },
+ resolveName: (userId: number) => string,
+ operatorNameOverride?: string
+): string {
+ return segmentsToText(
+ resolveGroupNotificationSegments(message, resolveName, operatorNameOverride)
+ )
+}
+
+// ==================== 濂藉弸浜嬩欢 ====================
+
+/** 浼氳瘽鍐呭ソ鍙嬩簨浠� segments */
+export function resolveFriendNotificationSegments(message: { type?: number }): TipSegment[] {
+ switch (message.type) {
+ case ImContentType.FRIEND_ADD: {
+ return [tipText('浣犱滑宸茬粡鏄ソ鍙嬩簡锛屽紑濮嬭亰澶╁惂')]
+ }
+ case ImContentType.FRIEND_DELETE: {
+ return [tipText('浣犲凡鍒犻櫎濂藉弸')]
+ }
+ default: {
+ return []
+ }
+ }
+}
+
+/** 浼氳瘽鍐呭ソ鍙嬩簨浠舵枃妗堬細FRIEND_ADD / FRIEND_DELETE 娓叉煋鎴愮伆鑹叉彁绀烘皵娉★紝鏂囨鍥哄畾涓嶄緷璧� payload */
+export function resolveFriendNotificationText(message: { type?: number }): string {
+ return segmentsToText(resolveFriendNotificationSegments(message))
+}
+
+// ==================== RTC 閫氳瘽浜嬩欢 ====================
+
+// RTC_CALL_START payload锛涗粎缇よ亰锛涚敤浜庤亰澶� tip 鏂囨銆寋inviter} 鍙戣捣浜唟voice/video}閫氳瘽銆�
+export type RtcCallStartPayload = {
+ conversationType?: number
+ inviterAvatar?: string
+ inviterNickname?: string
+ inviterUserId?: number
+ mediaType?: number
+ room?: string
+}
+
+// RTC_CALL_END payload锛涚鑱婂噯姘旀场 + 缇よ亰銆岄�氳瘽宸茬粨鏉� [鏃堕暱 X]銆嶅叡鐢�
+export type RtcCallEndPayload = {
+ conversationType?: number
+ durationSeconds?: number
+ endReason?: number
+ mediaType?: number
+ operatorAvatar?: string
+ operatorNickname?: string
+ operatorUserId?: number
+ room?: string
+}
+
+/** 瑙f瀽 RTC_CALL_START / RTC_CALL_END 娑堟伅 content锛涜В鏋愬け璐ヨ繑鍥� null */
+export function parseRtcCallPayload(
+ content?: string
+): null | (RtcCallEndPayload & RtcCallStartPayload) {
+ return content ? parseMessage<RtcCallEndPayload & RtcCallStartPayload>(content) : null
+}
+
+/**
+ * 浼氳瘽鍐呴�氳瘽浜嬩欢 segments锛圧TC_CALL_START / RTC_CALL_END锛�
+ * <p>
+ * 缇よ亰涓ゆ寮忥細START銆寋inviter} 鍙戣捣浜嗚闊抽�氳瘽銆�+ END銆岃闊抽�氳瘽宸茬粡缁撴潫銆�
+ * <p>
+ * 绉佽亰姘旀场璧� {@link resolveRtcCallPrivateBubbleText}
+ */
+export function resolveRtcCallTipSegments(message: {
+ content?: string
+ selfSend?: boolean
+ type?: number
+}): TipSegment[] {
+ const payload = parseRtcCallPayload(message.content)
+ if (!payload) {
+ return []
+ }
+ if (message.type === ImContentType.RTC_CALL_START) {
+ return payload.inviterUserId
+ ? [
+ tipMention(payload.inviterUserId, resolveRtcInviterLabel(payload)),
+ tipText(' 鍙戣捣浜嗚闊抽�氳瘽')
+ ]
+ : []
+ }
+ if (message.type === ImContentType.RTC_CALL_END) {
+ return [tipText('璇煶閫氳瘽宸茬粡缁撴潫')]
+ }
+ return []
+}
+
+/** 鍙� RTC 閫氳瘽鍙戣捣浜哄睍绀哄悕锛涙樀绉颁负绌烘椂鍥為��銆岀敤鎴� {id}銆� */
+function resolveRtcInviterLabel(payload: RtcCallStartPayload): string {
+ return payload.inviterNickname?.trim() || `鐢ㄦ埛 ${payload.inviterUserId ?? ''}`
+}
+
+/**
+ * 浼氳瘽鍒楄〃鏈�鍚庝竴鏉¢瑙堟枃妗堬紙RTC_CALL_START / RTC_CALL_END锛�
+ * <p>
+ * 绉佽亰锛歋TART / END 閮藉睍绀恒�孾璇煶閫氳瘽]銆嶏紝瀵归綈 [鍥剧墖] / [璇煶] 椋庢牸
+ * <p>
+ * 缇よ亰锛歋TART銆寋inviter} 鍙戣捣浜嗚闊抽�氳瘽銆嶃�丒ND銆岃闊抽�氳瘽宸茬粡缁撴潫銆嶏紝涓庤亰澶� tip 鍚屾簮
+ */
+export function resolveRtcCallLastContent(
+ message: { content?: string; type?: number; },
+ conversationType: number
+): string {
+ if (conversationType === ImConversationType.PRIVATE) {
+ return '[璇煶閫氳瘽]'
+ }
+ if (message.type === ImContentType.RTC_CALL_END) {
+ return '璇煶閫氳瘽宸茬粡缁撴潫'
+ }
+ if (message.type === ImContentType.RTC_CALL_START) {
+ const payload = parseRtcCallPayload(message.content)
+ if (payload) {
+ return `${resolveRtcInviterLabel(payload)} 鍙戣捣浜嗚闊抽�氳瘽`
+ }
+ }
+ return ''
+}
+
+/**
+ * 绉佽亰 RTC_CALL_END 姘旀场鍐呮枃妗堬紱鎸� operatorUserId 鏄笉鏄嚜宸辨覆鏌撲袱绔笉鍚屾枃妗堬紙瀵归綈寰俊锛�
+ * <p>
+ * 鏂囨瑙勫垯锛�
+ * HANGUP duration > 0 鈫� 銆岄�氳瘽鏃堕暱 N銆嶏紙鍙屾柟涓�鑷达級
+ * HANGUP duration 鈮� 0 鈫� 銆岄�氳瘽涓柇銆嶏紙鏈帴閫氱殑鍏滃簳锛�
+ * CANCEL 鈫� 鎿嶄綔鑰呫�屽凡鍙栨秷銆�/ 鍙︿竴鏂广�屽鏂瑰凡鍙栨秷銆�
+ * REJECT 鈫� 鎿嶄綔鑰呫�屽凡鎷掔粷銆�/ 鍙︿竴鏂广�屽鏂瑰凡鎷掔粷銆�
+ * BUSY 鈫� 鎿嶄綔鑰呫�屽繖绾挎湭鎺ュ惉銆�/ 鍙︿竴鏂广�屽鏂瑰繖绾夸腑銆�
+ * NO_ANSWER 鈫� 鎿嶄綔鑰呫�屾湭鎺ュ惉銆�/ 鍙︿竴鏂广�屽鏂规湭鎺ュ惉銆嶏紙鎸搩瓒呮椂 Job 瑙﹀彂锛�
+ * ERROR 鈫� 銆岄�氳瘽涓柇 [N]銆嶏紙鎺ラ�氬悗寮傚父鏂紑锛沝uration > 0 鏃跺甫鏃堕暱锛�
+ */
+export function resolveRtcCallPrivateBubbleText(payload: null | RtcCallEndPayload): string {
+ if (!payload) {
+ return '閫氳瘽宸茬粨鏉�'
+ }
+ const duration = payload.durationSeconds ?? 0
+ const hasDuration = duration > 0
+ const isOperator = payload.operatorUserId === getCurrentUserId()
+ switch (payload.endReason) {
+ case ImRtcCallEndReason.BUSY: {
+ return isOperator ? '蹇欑嚎鏈帴鍚�' : '瀵规柟蹇欑嚎涓�'
+ }
+ case ImRtcCallEndReason.CANCEL: {
+ return isOperator ? '宸插彇娑�' : '瀵规柟宸插彇娑�'
+ }
+ case ImRtcCallEndReason.ERROR: {
+ return hasDuration ? `閫氳瘽涓柇 ${formatCallDuration(duration)}` : '閫氳瘽涓柇'
+ }
+ case ImRtcCallEndReason.HANGUP: {
+ return hasDuration ? `閫氳瘽鏃堕暱 ${formatCallDuration(duration)}` : '閫氳瘽涓柇'
+ }
+ case ImRtcCallEndReason.NO_ANSWER: {
+ return isOperator ? '鏈帴鍚�' : '瀵规柟鏈帴鍚�'
+ }
+ case ImRtcCallEndReason.REJECT: {
+ return isOperator ? '宸叉嫆缁�' : '瀵规柟宸叉嫆缁�'
+ }
+ default: {
+ return hasDuration ? `閫氳瘽鏃堕暱 ${formatCallDuration(duration)}` : '閫氳瘽宸茬粨鏉�'
+ }
+ }
+}
+
+/** 浼氳瘽鍐呴�氳瘽浜嬩欢鏂囨 */
+export function resolveRtcCallTipText(message: {
+ content?: string
+ selfSend?: boolean
+ type?: number
+}): string {
+ return segmentsToText(resolveRtcCallTipSegments(message))
+}
+
+/**
+ * RTC_CALL_END 缁撴潫鍘熷洜鍏滃簳鏂囨锛涘墠绔� toast / console 鍏滃簳鐢�
+ * <p>
+ * 缂� operator 淇℃伅锛堝悓姝ュ搷搴� + WS push 鍏滃簳鍦烘櫙锛夋椂鐨勯�氱敤鏂囨锛涚粏鍒嗘枃妗堬紙鎸夊彂閫佹柟瑙嗚锛夎蛋 {@link resolveRtcCallPrivateBubbleText}
+ */
+export function resolveCallEndReasonText(reason: number | undefined): string {
+ switch (reason) {
+ case ImRtcCallEndReason.BUSY: {
+ return '瀵规柟蹇欑嚎涓�'
+ }
+ case ImRtcCallEndReason.CANCEL: {
+ return '瀵规柟宸插彇娑�'
+ }
+ case ImRtcCallEndReason.ERROR: {
+ return '閫氳瘽寮傚父'
+ }
+ case ImRtcCallEndReason.HANGUP: {
+ return '閫氳瘽宸茬粨鏉�'
+ }
+ case ImRtcCallEndReason.REJECT: {
+ return '瀵规柟宸叉嫆缁�'
+ }
+ default: {
+ return '閫氳瘽宸叉柇寮�'
+ }
+ }
+}
--
Gitblit v1.9.3