| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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ï¼websocketStore.convertPrivateMessage / handlePrivateMessage / computeFriendPeerId |
| | | * å useMessagePuller.getPrivatePeerIdï¼ï¼ç»æç±»ååªè¦ senderId / receiverId ä¸¤ä¸ªåæ®µï¼REST ä¸ WS DTO 齿»¡è¶³ |
| | | */ |
| | | export function getPrivateMessagePeerId( |
| | | message: { receiverId: number; senderId: number; }, |
| | | currentUserId: number |
| | | ): number { |
| | | return message.senderId === currentUserId ? message.receiverId : message.senderId |
| | | } |
| | | |
| | | // ==================== ææ¬çæ®µï¼tip ææ¡ + TEXT æ°æ³¡å
±ç¨ï¼ ==================== |
| | | // æ¢ç¨äºç°æ¡ tipï¼"XX é请 YY å å
¥ç¾¤è"ï¼ï¼ä¹ç¨äº TEXT æ°æ³¡æ£æï¼@xxx é«äº® + URL èªå¨è¯å«ï¼ã |
| | | // mention 段æºå¸¦ userId ç¨äºæç¹å»å¼¹ UserInfoCardï¼link 段æºå¸¦ href ç¨äº <a> è·³è½¬ï¼ |
| | | // text 段æ¯çº¯ææ¬å
åºã渲æå±ï¼tip-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 éåçæ¯å¤æ³¨å / 群æµç§°ï¼ä½æ¥æ¶ç«¯è¦æç宿µç§°æ¸²æãçåºæ¯ï¼ |
| | | * åéå¯ä»¥æºå¸¦å¤ç§åé¢éï¼match ç¨ï¼ï¼ä½ displayName ç»ä¸æå nicknameï¼ |
| | | * 让ææåå² / æ°æ¶æ¯ç @ 齿¶æå°ä¸è´çå±ç¤º |
| | | */ |
| | | displayName?: string |
| | | /** |
| | | * æ§ä¹æ è®°ï¼å name 对åºå¤ä¸ª userId æ¶ä¸º true |
| | | * |
| | | * parser ä»ä¼æ name å½ä¸ï¼æé¿ä¼å
ï¼ï¼ä½å½ä¸åæ´æ®µåææ®éææ¬ââ |
| | | * é¿å
ã@å¼ ä¸ã被åé¤åï¼çåç¼åéã@å¼ ãæ¢å¹é
éç» |
| | | */ |
| | | ambiguous?: boolean |
| | | } |
| | | |
| | | /** URL é宿£åï¼ç»æ¢äºç©ºç½ã䏿ã@ï¼é¿å
åæä¸ä¸ä¸ª 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 ææ®µï¼mention 段æåéæé¿åç¼å¹é
ï¼URL 段走é宿£åï¼å©ä½å½ text |
| | | * |
| | | * mentions ä¸ä¼ æå¹é
ä¸ä¸æ¶ï¼@xxx éå为æ®é textï¼è§£æä¸æ¸²æè§£è¦ï¼å·¥å
·å½æ°æ¬èº«ä¸ä¾èµ 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ï¼ImContentType ä¿ç 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ï¼targetId = ç¨æ·ç¼å·ï¼name = ç¨æ·æµç§°ï¼åç宿µç§°ï¼é夿³¨ï¼ |
| | | * - GROUPï¼targetId = 群ç¼å·ï¼name = 群åï¼å¯å¸¦ memberCount |
| | | */ |
| | | export interface CardMessage extends Quotable { |
| | | /** åç对象类å */ |
| | | targetType: ImConversationTypeValue |
| | | /** ç®æ 对象ç¼å·ï¼PRIVATE æ¶ = userIdï¼GROUP æ¶ = groupId */ |
| | | targetId: number |
| | | /** æ¾ç¤ºåå¿«ç
§ï¼PRIVATE æ¶ = ç¨æ·æµç§°ï¼GROUP æ¶ = 群å */ |
| | | name: string |
| | | /** 头åï¼å¿«ç
§ï¼ */ |
| | | avatar?: string |
| | | /** 群æåæ°ï¼ä»
targetType = GROUPï¼æ¥æ¶ç«¯å±ç¤ºãN 人群èãï¼ */ |
| | | memberCount?: number |
| | | } |
| | | |
| | | /** |
| | | * åç转åçæºå¯¹è±¡ï¼RecommendCardDialog å
¥åï¼ï¼åæ®µä¸ 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 |
| | | } |
| | | } |
| | | |
| | | /** 群对象 â 群åçæºï¼GROUPï¼ï¼ç¼º 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ï¼Unicode 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ï¼JSONï¼ï¼åµå¥åå¹¶æ¶æ¯æ¶ä»ææ¬ç»æå±å±å±å¼ */ |
| | | 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)) |
| | | } |
| | | } |
| | | |
| | | /** ãæ·»å å°è¡¨æ
ãçå¯åèµ·æºï¼FACE / 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 |
| | | } |
| | | |
| | | /** è§£ææ¶æ¯ contentï¼JSON å符串ï¼ä¸ºæå® 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ï¼ack 忝çå® 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 ææï¼IndexedDB æ¢å¤åºæ¥çæ§ 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 ? ' å°ç¦äºè¯¥ç¾¤' : ' è§£å°äºè¯¥ç¾¤')] |
| | | } |
| | | case ImContentType.GROUP_CANCEL_MUTED: { |
| | | return [operatorSegment, tipText(' å
³éäºå
¨ç¾¤ç¦è¨')] |
| | | } |
| | | case ImContentType.GROUP_CREATE: { |
| | | return [operatorSegment, tipText(' å建äºç¾¤è')] |
| | | } |
| | | case ImContentType.GROUP_DISSOLVE: { |
| | | return [operatorSegment, tipText(' è§£æ£äºç¾¤è')] |
| | | } |
| | | case ImContentType.GROUP_INFO_UPDATE: { |
| | | return payload.newAvatar |
| | | ? [operatorSegment, tipText(' æ´æ¢äºç¾¤å¤´å')] |
| | | : [operatorSegment, tipText(' æ´æ°äºç¾¤ä¿¡æ¯')] |
| | | } |
| | | case ImContentType.GROUP_MEMBER_CANCEL_MUTED: { |
| | | return payload.mutedUserId |
| | | ? [ |
| | | operatorSegment, |
| | | tipText(' è§£é¤äº '), |
| | | 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 |
| | | } |
| | | |
| | | /** è§£æ RTC_CALL_START / RTC_CALL_END æ¶æ¯ contentï¼è§£æå¤±è´¥è¿å null */ |
| | | export function parseRtcCallPayload( |
| | | content?: string |
| | | ): null | (RtcCallEndPayload & RtcCallStartPayload) { |
| | | return content ? parseMessage<RtcCallEndPayload & RtcCallStartPayload>(content) : null |
| | | } |
| | | |
| | | /** |
| | | * ä¼è¯å
éè¯äºä»¶ segmentsï¼RTC_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> |
| | | * ç§èï¼START / END é½å±ç¤ºã[è¯é³éè¯]ãï¼å¯¹é½ [å¾ç] / [è¯é³] 飿 ¼ |
| | | * <p> |
| | | * 群èï¼STARTã{inviter} åèµ·äºè¯é³éè¯ããENDãè¯é³éè¯å·²ç»ç»æãï¼ä¸è天 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]ãï¼æ¥éåå¼å¸¸æå¼ï¼duration > 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 'éè¯å·²æå¼' |
| | | } |
| | | } |
| | | } |