From a170ae37ab542d906bff0fe02ccaa5b4dac6129c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 13 May 2026 23:27:02 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=9A=84=20format=20=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BB=8E=20user=20?= =?UTF-8?q?=E6=8A=BD=E5=88=B0=20message=20=E5=B7=A5=E5=85=B7=E7=B1=BB?= =?UTF-8?q?=E9=87=8C=EF=BC=8C=E6=9B=B4=E5=8A=A0=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/message/MessageHistory.vue | 14 +- .../components/message/MessageItem.vue | 82 ++++- src/views/im/home/store/groupStore.ts | 3 +- .../manager/message/MessageContentPreview.vue | 8 +- src/views/im/utils/conversation.ts | 13 +- src/views/im/utils/message.ts | 308 +++++++++++++++++- src/views/im/utils/time.ts | 10 + src/views/im/utils/user.ts | 187 +---------- 8 files changed, 417 insertions(+), 208 deletions(-) diff --git a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue index f149263e4..6260b60e7 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue @@ -154,7 +154,7 @@ v-else-if="isGroupNotification(message.type)" class="px-4 py-3 text-12px text-center italic text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]" > - + @@ -252,11 +252,13 @@ import { IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys' import { getMemberDisplayName, getSenderDisplayName, - getSenderRealNickname, + getSenderRealNickname +} from '@/views/im/utils/user' +import { resolveFriendNotificationSegments, resolveFriendNotificationText, resolveGroupNotificationSegments -} from '@/views/im/utils/user' +} from '@/views/im/utils/message' import { buildFacePreviewText, buildRecallTip, @@ -322,6 +324,12 @@ function senderDisplayNameOf(message: Message): string { ) } +/** 群广播事件 segments 的成员名解析器;按当前会话 targetId 走 getSenderDisplayName */ +function resolveGroupMemberName(message: Message): (userId: number) => string { + return (id: number) => + getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0) +} + /** 单条消息的发送人真实昵称:给 UserAvatar 色卡 / alt 用,永远是 nickname 不掺备注 */ function senderRealNicknameOf(message: Message): string { return getSenderRealNickname( diff --git a/src/views/im/home/pages/conversation/components/message/MessageItem.vue b/src/views/im/home/pages/conversation/components/message/MessageItem.vue index 24a88f7f5..3d7b02419 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageItem.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageItem.vue @@ -23,6 +23,40 @@ + +
+ +
+ + +
+
+ +
+ + {{ rtcCallPrivateBubbleText }} +
+
+
+
resolveFriendNotificationSegments(p const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type)) /** 群广播事件 segments */ -const groupNotificationSegments = computed(() => resolveGroupNotificationSegments(props.message)) +const groupNotificationSegments = computed(() => + resolveGroupNotificationSegments(props.message, (id: number) => + getSenderDisplayName(id, ImConversationType.GROUP, props.message.targetId ?? 0) + ) +) + +/** 私聊 RTC_CALL_END 走「准气泡」(左右分布 + 电话图标 + 文案);非私聊场景为 null */ +const rtcCallEndPrivatePayload = computed(() => { + if (props.message.type !== ImMessageType.RTC_CALL_END) { + return null + } + const payload = parseRtcCallPayload(props.message.content) + return payload?.conversationType === ImConversationType.PRIVATE ? payload : null +}) + +/** 是否私聊通话气泡 */ +const isRtcCallPrivateBubbleMessage = computed(() => rtcCallEndPrivatePayload.value !== null) + +/** 私聊通话气泡文案(按 operatorUserId 是否当前用户区分;对齐微信两端不同视角) */ +const rtcCallPrivateBubbleText = computed(() => + resolveRtcCallPrivateBubbleText(rtcCallEndPrivatePayload.value) +) + +/** 是否会话内通话事件居中 tip:仅群聊场景(START 总是群聊;END 私聊走气泡分支,群聊走 tip) */ +const isRtcCallTipMessage = computed(() => { + if (!isRtcCallTip(props.message.type)) { + return false + } + return !isRtcCallPrivateBubbleMessage.value +}) + +/** 通话事件 segments;仅群聊 tip 用 */ +const rtcCallTipSegments = computed(() => resolveRtcCallTipSegments(props.message)) // ==================== 消息内容解析 / payload ==================== diff --git a/src/views/im/home/store/groupStore.ts b/src/views/im/home/store/groupStore.ts index af6e8cffa..594709149 100644 --- a/src/views/im/home/store/groupStore.ts +++ b/src/views/im/home/store/groupStore.ts @@ -23,7 +23,8 @@ import { setQuietly, StorageKeys } from '../../utils/storage' -import { getGroupDisplayName, type GroupNotificationPayload } from '../../utils/user' +import { getGroupDisplayName } from '../../utils/user' +import { type GroupNotificationPayload } from '../../utils/message' import type { Group, GroupMember, Message } from '../types' /** diff --git a/src/views/im/manager/message/MessageContentPreview.vue b/src/views/im/manager/message/MessageContentPreview.vue index 50a95c41d..527d0e3c4 100644 --- a/src/views/im/manager/message/MessageContentPreview.vue +++ b/src/views/im/manager/message/MessageContentPreview.vue @@ -134,6 +134,8 @@ import CardLineLabel from '@/views/im/home/components/card/CardLineLabel.vue' import { parseMessage, getFileIconInfo, + resolveFriendNotificationText, + resolveGroupNotificationText, type ImageMessage, type FileMessage, type AudioMessage, @@ -143,10 +145,6 @@ import { type FaceMessage, type MergeMessage } from '@/views/im/utils/message' -import { - resolveFriendNotificationText, - resolveGroupNotificationText -} from '@/views/im/utils/user' import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation' defineOptions({ name: 'ImMessageContentPreview' }) @@ -242,7 +240,7 @@ const friendChatTipText = computed(() => resolveFriendNotificationText({ type: p /** 是否群广播事件 */ const isGroupNotificationType = computed(() => isGroupNotification(props.type ?? -1)) -/** 群广播事件文案:复用 utils/user.resolveGroupNotificationText;admin 端 operator 用 senderNickname 直接覆盖,其它 id 退化为 用户(id) */ +/** 群广播事件文案:admin 端 operator 用 senderNickname 直接覆盖,其它 id 退化为 用户(id) */ const groupNotificationText = computed(() => resolveGroupNotificationText( { type: props.type, content: props.content }, diff --git a/src/views/im/utils/conversation.ts b/src/views/im/utils/conversation.ts index 572db6b5a..652d2e1a0 100644 --- a/src/views/im/utils/conversation.ts +++ b/src/views/im/utils/conversation.ts @@ -6,7 +6,7 @@ // 2. fallbackName 由调用方传入(典型来源:Conversation.lastSenderDisplayName 快照),透传到 getSenderDisplayName 内部,算不出真名时兜底 // ==================================================================== -import { ImMessageType, isFriendChatTip, isGroupNotification } from './constants' +import { ImConversationType, ImMessageType, isFriendChatTip, isGroupNotification } from './constants' import { getCardLabelInfo, parseMessage, @@ -19,11 +19,8 @@ import { type TextMessage, type TipSegment } from './message' -import { - getSenderDisplayName, - resolveFriendNotificationText, - resolveGroupNotificationText -} from './user' +import { getSenderDisplayName } from './user' +import { resolveFriendNotificationText, resolveGroupNotificationText } from './message' import type { Message } from '../home/types' /** 会话主键:`type-targetId` 拼成稳定字符串,给 v-for :key、active 比对、map key 等场景共用 */ @@ -151,7 +148,9 @@ export function resolveConversationLastContent( return resolveFriendNotificationText(message) } if (isGroupNotification(message.type)) { - return resolveGroupNotificationText(message) + return resolveGroupNotificationText(message, (id) => + getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0) + ) } return summarizeMessageContent(message) } diff --git a/src/views/im/utils/message.ts b/src/views/im/utils/message.ts index 75236c84c..0b9f3a043 100644 --- a/src/views/im/utils/message.ts +++ b/src/views/im/utils/message.ts @@ -1,7 +1,13 @@ import { generateUUID } from '@/utils' import { useUserStore } from '@/store/modules/user' -import { ImConversationType, ImMessageType, type ImConversationTypeValue } from './constants' +import { + ImCallEndReason, + ImConversationType, + ImMessageType, + type ImConversationTypeValue +} from './constants' import { getCurrentUserId } from './storage' +import { formatCallDuration } from './time' import { useFriendStore } from '../home/store/friendStore' import { useGroupStore } from '../home/store/groupStore' import type { Conversation, Message, User, GroupLite } from '../home/types' @@ -644,3 +650,303 @@ export const formatJson = (content?: string): string => { return content } } + +// ==================== 群广播事件 payload + 文案 ==================== + +// 群广播事件 payload;对齐后端 GroupNotificationMessage 子类聚合字段 +export type GroupNotificationPayload = { + operatorUserId?: number + memberUserIds?: number[] + newOwnerUserId?: number + oldName?: string + newName?: string + oldNotice?: string + newNotice?: string + oldAvatar?: string + newAvatar?: string + displayUserName?: string + messageId?: number + // 禁言事件 + mutedUserId?: number + muteEndTime?: string + // 全群封禁 + banned?: boolean + // 自由进群事件 + entrantUserId?: number + addSource?: number + // PIN 事件携带的完整被置顶消息对象 + message?: { + id: number + clientMessageId?: string + senderId: number + groupId: number + type: number + content: string + status: number + sendTime: string + atUserIds?: number[] + receiverUserIds?: number[] + receiptStatus?: number + readCount?: number + } +} + +/** + * 群广播事件 segments + * + * resolveName 由调用方注入(默认场景传 getSenderDisplayName); + * operatorNameOverride 仅覆盖 operator 段文案,mention userId 仍用 payload.operatorUserId + */ +export function resolveGroupNotificationSegments( + message: { type?: number; content?: string; targetId?: 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 === ImMessageType.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 ImMessageType.GROUP_CREATE: + return [operatorSegment, tipText(' 创建了群聊')] + case ImMessageType.GROUP_NAME_UPDATE: + return [operatorSegment, tipText(` 将群名修改为 "${payload.newName ?? ''}"`)] + case ImMessageType.GROUP_NOTICE_UPDATE: + return [operatorSegment, tipText(' 更新了群公告')] + case ImMessageType.GROUP_INFO_UPDATE: + return payload.newAvatar + ? [operatorSegment, tipText(' 更换了群头像')] + : [operatorSegment, tipText(' 更新了群信息')] + case ImMessageType.GROUP_DISSOLVE: + return [operatorSegment, tipText(' 解散了群聊')] + case ImMessageType.GROUP_MEMBER_INVITE: + return [operatorSegment, tipText(' 邀请 '), ...memberSegments, tipText(' 加入群聊')] + case ImMessageType.GROUP_MEMBER_QUIT: + return [operatorSegment, tipText(' 退出了群聊')] + case ImMessageType.GROUP_MEMBER_KICK: + return [operatorSegment, tipText(' 移出了 '), ...memberSegments] + case ImMessageType.GROUP_MEMBER_NICKNAME_UPDATE: + return [operatorSegment, tipText(` 修改群昵称为 "${payload.displayUserName ?? ''}"`)] + case ImMessageType.GROUP_ADMIN_ADD: + return [operatorSegment, tipText(' 将 '), ...memberSegments, tipText(' 设为管理员')] + case ImMessageType.GROUP_ADMIN_REMOVE: + return [operatorSegment, tipText(' 撤销了 '), ...memberSegments, tipText(' 的管理员身份')] + case ImMessageType.GROUP_OWNER_TRANSFER: + return payload.newOwnerUserId + ? [ + operatorSegment, + tipText(' 已将群主转让给 '), + tipMention(payload.newOwnerUserId, resolveName(payload.newOwnerUserId)) + ] + : [] + case ImMessageType.GROUP_MESSAGE_PIN: + return [operatorSegment, tipText(' 置顶了一条消息')] + case ImMessageType.GROUP_MESSAGE_UNPIN: + return [operatorSegment, tipText(' 取消了一条置顶消息')] + case ImMessageType.GROUP_MEMBER_MUTED: + return payload.mutedUserId + ? [ + operatorSegment, + tipText(' 将 '), + tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)), + tipText(' 禁言') + ] + : [] + case ImMessageType.GROUP_MEMBER_CANCEL_MUTED: + return payload.mutedUserId + ? [ + operatorSegment, + tipText(' 解除了 '), + tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)), + tipText(' 的禁言') + ] + : [] + case ImMessageType.GROUP_MUTED: + return [operatorSegment, tipText(' 开启了全群禁言')] + case ImMessageType.GROUP_CANCEL_MUTED: + return [operatorSegment, tipText(' 关闭了全群禁言')] + case ImMessageType.GROUP_BANNED: + return [operatorSegment, tipText(payload.banned ? ' 封禁了该群' : ' 解封了该群')] + default: + return [] + } +} + +/** 群广播事件中文文案 */ +export function resolveGroupNotificationText( + message: { type?: number; content?: string; targetId?: 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 ImMessageType.FRIEND_ADD: + return [tipText('你们已经是好友了,开始聊天吧')] + case ImMessageType.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 = { + room?: string + conversationType?: number + mediaType?: number + inviterUserId?: number + inviterNickname?: string + inviterAvatar?: string +} + +// RTC_CALL_END payload;私聊准气泡 + 群聊「通话已结束 [时长 X]」共用 +export type RtcCallEndPayload = { + room?: string + conversationType?: number + mediaType?: number + endReason?: number + durationSeconds?: number + operatorUserId?: number + operatorNickname?: string + operatorAvatar?: string +} + +/** 解析 RTC_CALL_START / RTC_CALL_END 消息 content;解析失败返回 null */ +export function parseRtcCallPayload( + content?: string +): (RtcCallStartPayload & RtcCallEndPayload) | null { + return content ? parseMessage(content) : null +} + +/** 媒体类型文案;TODO 字典化 */ +function callMediaText(mediaType: number | undefined): string { + return mediaType === 2 ? '视频' : '语音' +} + +/** + * 会话内通话事件 segments(RTC_CALL_START / RTC_CALL_END) + *

+ * 群聊两段式:START「{inviter} 发起了{voice/video}通话」+ END「{voice/video}通话已结束 [时长 X]」 + *

+ * 私聊气泡走 {@link resolveRtcCallPrivateBubbleText} + */ +export function resolveRtcCallTipSegments(message: { + type?: number + content?: string + selfSend?: boolean +}): TipSegment[] { + const payload = parseRtcCallPayload(message.content) + if (!payload) { + return [] + } + const media = callMediaText(payload.mediaType) + if (message.type === ImMessageType.RTC_CALL_START) { + const inviter = payload.inviterNickname?.trim() || `用户 ${payload.inviterUserId ?? ''}` + return [tipText(`${inviter} 发起了${media}通话`)] + } + if (message.type === ImMessageType.RTC_CALL_END) { + if (payload.durationSeconds && payload.durationSeconds > 0) { + return [tipText(`${media}通话已结束(时长 ${formatCallDuration(payload.durationSeconds)})`)] + } + return [tipText(`${media}通话已结束`)] + } + return [] +} + +/** + * 私聊 RTC_CALL_END 气泡内文案;按 operatorUserId 是不是自己渲染两端不同文案(对齐微信) + *

+ * 文案规则: + * HANGUP duration > 0 → 「通话时长 N」(双方一致) + * HANGUP duration ≤ 0 → 「通话中断」(未接通的兜底) + * CANCEL → 操作者「已取消」/ 另一方「对方已取消」 + * REJECT → 操作者「已拒绝」/ 另一方「对方已拒绝」 + * BUSY → 操作者「忙线未接听」/ 另一方「对方忙线中」 + * ERROR → 「通话中断 [N]」(接通后异常断开;duration > 0 时带时长) + */ +export function resolveRtcCallPrivateBubbleText(payload: RtcCallEndPayload | null): string { + if (!payload) { + return '通话已结束' + } + const duration = payload.durationSeconds ?? 0 + const hasDuration = duration > 0 + const isOperator = payload.operatorUserId === getCurrentUserId() + switch (payload.endReason) { + case ImCallEndReason.HANGUP: + return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话中断' + case ImCallEndReason.CANCEL: + return isOperator ? '已取消' : '对方已取消' + case ImCallEndReason.REJECT: + return isOperator ? '已拒绝' : '对方已拒绝' + case ImCallEndReason.BUSY: + return isOperator ? '忙线未接听' : '对方忙线中' + case ImCallEndReason.ERROR: + return hasDuration ? `通话中断 ${formatCallDuration(duration)}` : '通话中断' + default: + return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话已结束' + } +} + +/** 会话内通话事件文案 */ +export function resolveRtcCallTipText(message: { + type?: number + content?: string + selfSend?: boolean +}): string { + return segmentsToText(resolveRtcCallTipSegments(message)) +} + +/** + * RTC_CALL_END 结束原因兜底文案;前端 toast / console 兜底用 + *

+ * 缺 operator 信息(同步响应 + WS push 兜底场景)时的通用文案;细分文案(按发送方视角)走 {@link resolveRtcCallPrivateBubbleText} + */ +export function resolveCallEndReasonText(reason: number | undefined): string { + switch (reason) { + case ImCallEndReason.REJECT: + return '对方已拒绝' + case ImCallEndReason.CANCEL: + return '对方已取消' + case ImCallEndReason.BUSY: + return '对方忙线中' + case ImCallEndReason.HANGUP: + return '通话已结束' + case ImCallEndReason.ERROR: + return '通话异常' + default: + return '通话已断开' + } +} diff --git a/src/views/im/utils/time.ts b/src/views/im/utils/time.ts index 3848f81a3..a84bdac21 100644 --- a/src/views/im/utils/time.ts +++ b/src/views/im/utils/time.ts @@ -88,3 +88,13 @@ export function formatMergeItemTime(timestamp: number): string { } return dayjs(timestamp).format('MM-DD HH:mm') } + +/** RTC 通话时长(秒)→ "00:06" / "1:23:45" */ +export function formatCallDuration(seconds: number | undefined): string { + const total = Math.max(0, Math.floor(seconds || 0)) + const h = Math.floor(total / 3600) + const m = Math.floor((total % 3600) / 60) + const s = total % 60 + const pad = (n: number) => String(n).padStart(2, '0') + return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}` +} diff --git a/src/views/im/utils/user.ts b/src/views/im/utils/user.ts index 5d76335a6..0260798ba 100644 --- a/src/views/im/utils/user.ts +++ b/src/views/im/utils/user.ts @@ -16,19 +16,11 @@ import { SystemUserSexEnum } from '@/utils/constants' import { ImConversationType, ImFriendAddSource, - ImMessageType, IM_AT_ALL_NICKNAME, IM_AT_ALL_USER_ID } from './constants' import { getCurrentUserId } from './storage' -import { - joinMentionSegments, - segmentsToText, - tipMention, - tipText, - type MentionCandidate, - type TipSegment -} from './message' +import { type MentionCandidate } from './message' import { useConversationStore } from '../home/store/conversationStore' import { useFriendStore } from '../home/store/friendStore' import { useGroupStore } from '../home/store/groupStore' @@ -301,183 +293,6 @@ export function openMentionUserInfoCardAtEvent( ) } -/** - * 群广播事件(GROUP_* 系列)的中文文案 - * - * 按 message.type 取 content payload 字段,昵称默认走 getSenderDisplayName(备注 / 群昵称 / 真实昵称兜底); - * 管理后台无 store,可传入 resolveName 自定义 id → 名字(如 senderNickname + 用户(id) 兜底); - * home 端 MessageItem.vue / ConversationItem.vue / MessageHistory.vue 与 admin 端 MessageContentPreview.vue 共用 - */ -export type GroupNotificationPayload = { - operatorUserId?: number - memberUserIds?: number[] - newOwnerUserId?: number - oldName?: string - newName?: string - oldNotice?: string - newNotice?: string - oldAvatar?: string - newAvatar?: string - displayUserName?: string - messageId?: number - mutedUserId?: number // 禁言目标用户 - muteEndTime?: string // 禁言到期时间 - banned?: boolean // 封禁状态 - entrantUserId?: number // 自由进群事件:进群者用户编号 - addSource?: number // 自由进群事件:来源(搜索 / 二维码 / 分享链接) - /** PIN 事件携带的完整被置顶消息对象 */ - message?: { - id: number - clientMessageId?: string - senderId: number - groupId: number - type: number - content: string - status: number - sendTime: string - atUserIds?: number[] - receiverUserIds?: number[] - receiptStatus?: number - readCount?: number - } -} - -/** - * 群广播事件 segments - * - * resolveName 默认走 getSenderDisplayName,可注入自定义 resolver; - * operatorNameOverride 仅覆盖 operator 段文案,mention userId 仍用 payload.operatorUserId - */ -export function resolveGroupNotificationSegments( - message: { type?: number; content?: string; targetId?: number }, - resolveName?: (userId: number) => string, - operatorNameOverride?: string -): TipSegment[] { - let payload: GroupNotificationPayload = {} - try { - payload = JSON.parse(message.content || '{}') - } catch { - return [] - } - const resolve = - resolveName || - ((id: number) => getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0)) - - // ENTER 主语是 entrant 而非 operator,独立处理;其它 case 都以 operatorUserId 为主语 - if (message.type === ImMessageType.GROUP_MEMBER_ENTER) { - const entrantId = payload.entrantUserId ?? payload.operatorUserId - return entrantId ? [tipMention(entrantId, resolve(entrantId)), tipText(' 加入了群聊')] : [] - } - if (!payload.operatorUserId) { - return [] - } - const operatorSegment = tipMention( - payload.operatorUserId, - operatorNameOverride ?? resolve(payload.operatorUserId) - ) - const memberSegments = joinMentionSegments(payload.memberUserIds || [], '、', resolve) - - switch (message.type) { - case ImMessageType.GROUP_CREATE: - return [operatorSegment, tipText(' 创建了群聊')] - case ImMessageType.GROUP_NAME_UPDATE: - return [operatorSegment, tipText(` 将群名修改为 "${payload.newName ?? ''}"`)] - case ImMessageType.GROUP_NOTICE_UPDATE: - return [operatorSegment, tipText(' 更新了群公告')] - case ImMessageType.GROUP_INFO_UPDATE: - return payload.newAvatar - ? [operatorSegment, tipText(' 更换了群头像')] - : [operatorSegment, tipText(' 更新了群信息')] - case ImMessageType.GROUP_DISSOLVE: - return [operatorSegment, tipText(' 解散了群聊')] - case ImMessageType.GROUP_MEMBER_INVITE: - return [operatorSegment, tipText(' 邀请 '), ...memberSegments, tipText(' 加入群聊')] - case ImMessageType.GROUP_MEMBER_QUIT: - return [operatorSegment, tipText(' 退出了群聊')] - case ImMessageType.GROUP_MEMBER_KICK: - return [operatorSegment, tipText(' 移出了 '), ...memberSegments] - case ImMessageType.GROUP_MEMBER_NICKNAME_UPDATE: - return [operatorSegment, tipText(` 修改群昵称为 "${payload.displayUserName ?? ''}"`)] - case ImMessageType.GROUP_ADMIN_ADD: - return [operatorSegment, tipText(' 将 '), ...memberSegments, tipText(' 设为管理员')] - case ImMessageType.GROUP_ADMIN_REMOVE: - return [ - operatorSegment, - tipText(' 撤销了 '), - ...memberSegments, - tipText(' 的管理员身份') - ] - case ImMessageType.GROUP_OWNER_TRANSFER: - return payload.newOwnerUserId - ? [ - operatorSegment, - tipText(' 已将群主转让给 '), - tipMention(payload.newOwnerUserId, resolve(payload.newOwnerUserId)) - ] - : [] - case ImMessageType.GROUP_MESSAGE_PIN: - return [operatorSegment, tipText(' 置顶了一条消息')] - case ImMessageType.GROUP_MESSAGE_UNPIN: - return [operatorSegment, tipText(' 取消了一条置顶消息')] - case ImMessageType.GROUP_MEMBER_MUTED: - return payload.mutedUserId - ? [ - operatorSegment, - tipText(' 将 '), - tipMention(payload.mutedUserId, resolve(payload.mutedUserId)), - tipText(' 禁言') - ] - : [] - case ImMessageType.GROUP_MEMBER_CANCEL_MUTED: - return payload.mutedUserId - ? [ - operatorSegment, - tipText(' 解除了 '), - tipMention(payload.mutedUserId, resolve(payload.mutedUserId)), - tipText(' 的禁言') - ] - : [] - case ImMessageType.GROUP_MUTED: - return [operatorSegment, tipText(' 开启了全群禁言')] - case ImMessageType.GROUP_CANCEL_MUTED: - return [operatorSegment, tipText(' 关闭了全群禁言')] - case ImMessageType.GROUP_BANNED: - return [operatorSegment, tipText(payload.banned ? ' 封禁了该群' : ' 解封了该群')] - default: - return [] - } -} - -/** 群广播事件中文文案 */ -export function resolveGroupNotificationText( - message: { type?: number; content?: string; targetId?: 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 ImMessageType.FRIEND_ADD: - return [tipText('你们已经是好友了,开始聊天吧')] - case ImMessageType.FRIEND_DELETE: - return [tipText('你已删除好友')] - default: - return [] - } -} - -/** 会话内好友事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡,文案固定不依赖 payload */ -export function resolveFriendNotificationText(message: { type?: number }): string { - return segmentsToText(resolveFriendNotificationSegments(message)) -} - /** 性别图标;UNKNOWN / null / undefined 一律不展示,对齐微信留白 */ export function getGenderIcon(sex?: number): string { if (sex === SystemUserSexEnum.MALE) {