✨ feat(im): 优化消息的 format 相关的逻辑,从 user 抽到 message 工具类里,更加统一
parent
841d2cb763
commit
a170ae37ab
|
|
@ -154,7 +154,7 @@
|
||||||
v-else-if="isGroupNotification(message.type)"
|
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)]"
|
class="px-4 py-3 text-12px text-center italic text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||||
>
|
>
|
||||||
<TipSegments :segments="resolveGroupNotificationSegments(message)" />
|
<TipSegments :segments="resolveGroupNotificationSegments(message, resolveGroupMemberName(message))" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 普通消息行 -->
|
<!-- 普通消息行 -->
|
||||||
|
|
@ -252,11 +252,13 @@ import { IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys'
|
||||||
import {
|
import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
getSenderRealNickname,
|
getSenderRealNickname
|
||||||
|
} from '@/views/im/utils/user'
|
||||||
|
import {
|
||||||
resolveFriendNotificationSegments,
|
resolveFriendNotificationSegments,
|
||||||
resolveFriendNotificationText,
|
resolveFriendNotificationText,
|
||||||
resolveGroupNotificationSegments
|
resolveGroupNotificationSegments
|
||||||
} from '@/views/im/utils/user'
|
} from '@/views/im/utils/message'
|
||||||
import {
|
import {
|
||||||
buildFacePreviewText,
|
buildFacePreviewText,
|
||||||
buildRecallTip,
|
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 不掺备注 */
|
/** 单条消息的发送人真实昵称:给 UserAvatar 色卡 / alt 用,永远是 nickname 不掺备注 */
|
||||||
function senderRealNicknameOf(message: Message): string {
|
function senderRealNicknameOf(message: Message): string {
|
||||||
return getSenderRealNickname(
|
return getSenderRealNickname(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,40 @@
|
||||||
<TipSegments :segments="groupNotificationSegments" />
|
<TipSegments :segments="groupNotificationSegments" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 群通话事件(RTC_CALL_START / RTC_CALL_END):居中灰色 tip 两段式 -->
|
||||||
|
<div
|
||||||
|
v-else-if="isRtcCallTipMessage"
|
||||||
|
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
|
<TipSegments :segments="rtcCallTipSegments" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 私聊通话结束(RTC_CALL_END):仿微信「准气泡」;按 selfSend 左右分布;电话图标 + 文案 -->
|
||||||
|
<div v-else-if="isRtcCallPrivateBubbleMessage" class="flex gap-2 items-start px-4 py-2">
|
||||||
|
<div
|
||||||
|
class="flex flex-1 min-w-0 gap-2 items-start"
|
||||||
|
:class="{ 'flex-row-reverse': message.selfSend }"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:id="message.selfSend ? userStore.getUser?.id : message.senderId"
|
||||||
|
:name="senderRealNickname"
|
||||||
|
:url="message.selfSend ? userStore.getUser?.avatar : senderAvatar"
|
||||||
|
:size="36"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex gap-2 items-center px-3.5 py-2 text-sm rounded-lg"
|
||||||
|
:class="
|
||||||
|
message.selfSend
|
||||||
|
? 'text-black bg-[#95ec69]'
|
||||||
|
: 'text-[var(--el-text-color-primary)] bg-[var(--el-fill-color-light)]'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:phone-outlined" :size="16" class="rotate-[135deg] flex-shrink-0" />
|
||||||
|
<span class="whitespace-nowrap">{{ rtcCallPrivateBubbleText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 撤回消息:整行灰色 tip,sender 名段可点击 -->
|
<!-- 撤回消息:整行灰色 tip,sender 名段可点击 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="isRecall"
|
v-else-if="isRecall"
|
||||||
|
|
@ -166,7 +200,8 @@ import {
|
||||||
isFriendChatTip,
|
isFriendChatTip,
|
||||||
isGroupNotification,
|
isGroupNotification,
|
||||||
isMediaMessageType,
|
isMediaMessageType,
|
||||||
isNormalMessage
|
isNormalMessage,
|
||||||
|
isRtcCallTip
|
||||||
} from '@/views/im/utils/constants'
|
} from '@/views/im/utils/constants'
|
||||||
import {
|
import {
|
||||||
MESSAGE_TIME_TIP_GAP_MS,
|
MESSAGE_TIME_TIP_GAP_MS,
|
||||||
|
|
@ -197,10 +232,15 @@ import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getMentionCandidates,
|
getMentionCandidates,
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
getSenderRealNickname,
|
getSenderRealNickname
|
||||||
resolveFriendNotificationSegments,
|
|
||||||
resolveGroupNotificationSegments
|
|
||||||
} from '@/views/im/utils/user'
|
} from '@/views/im/utils/user'
|
||||||
|
import {
|
||||||
|
resolveFriendNotificationSegments,
|
||||||
|
resolveGroupNotificationSegments,
|
||||||
|
resolveRtcCallTipSegments,
|
||||||
|
resolveRtcCallPrivateBubbleText,
|
||||||
|
parseRtcCallPayload
|
||||||
|
} from '@/views/im/utils/message'
|
||||||
import { useImUiStore } from '../../../../store/uiStore'
|
import { useImUiStore } from '../../../../store/uiStore'
|
||||||
import { useMessageSender } from '../../../../composables/useMessageSender'
|
import { useMessageSender } from '../../../../composables/useMessageSender'
|
||||||
import { mediaTypeHandlers, useMediaUploader } from '../../../../composables/useMediaUploader'
|
import { mediaTypeHandlers, useMediaUploader } from '../../../../composables/useMediaUploader'
|
||||||
|
|
@ -294,7 +334,39 @@ const friendChatTipSegments = computed(() => resolveFriendNotificationSegments(p
|
||||||
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
|
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
|
||||||
|
|
||||||
/** 群广播事件 segments */
|
/** 群广播事件 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 ====================
|
// ==================== 消息内容解析 / payload ====================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ import {
|
||||||
setQuietly,
|
setQuietly,
|
||||||
StorageKeys
|
StorageKeys
|
||||||
} from '../../utils/storage'
|
} 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'
|
import type { Group, GroupMember, Message } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,8 @@ import CardLineLabel from '@/views/im/home/components/card/CardLineLabel.vue'
|
||||||
import {
|
import {
|
||||||
parseMessage,
|
parseMessage,
|
||||||
getFileIconInfo,
|
getFileIconInfo,
|
||||||
|
resolveFriendNotificationText,
|
||||||
|
resolveGroupNotificationText,
|
||||||
type ImageMessage,
|
type ImageMessage,
|
||||||
type FileMessage,
|
type FileMessage,
|
||||||
type AudioMessage,
|
type AudioMessage,
|
||||||
|
|
@ -143,10 +145,6 @@ import {
|
||||||
type FaceMessage,
|
type FaceMessage,
|
||||||
type MergeMessage
|
type MergeMessage
|
||||||
} from '@/views/im/utils/message'
|
} from '@/views/im/utils/message'
|
||||||
import {
|
|
||||||
resolveFriendNotificationText,
|
|
||||||
resolveGroupNotificationText
|
|
||||||
} from '@/views/im/utils/user'
|
|
||||||
import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation'
|
import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageContentPreview' })
|
defineOptions({ name: 'ImMessageContentPreview' })
|
||||||
|
|
@ -242,7 +240,7 @@ const friendChatTipText = computed(() => resolveFriendNotificationText({ type: p
|
||||||
/** 是否群广播事件 */
|
/** 是否群广播事件 */
|
||||||
const isGroupNotificationType = computed(() => isGroupNotification(props.type ?? -1))
|
const isGroupNotificationType = computed(() => isGroupNotification(props.type ?? -1))
|
||||||
|
|
||||||
/** 群广播事件文案:复用 utils/user.resolveGroupNotificationText;admin 端 operator 用 senderNickname 直接覆盖,其它 id 退化为 用户(id) */
|
/** 群广播事件文案:admin 端 operator 用 senderNickname 直接覆盖,其它 id 退化为 用户(id) */
|
||||||
const groupNotificationText = computed(() =>
|
const groupNotificationText = computed(() =>
|
||||||
resolveGroupNotificationText(
|
resolveGroupNotificationText(
|
||||||
{ type: props.type, content: props.content },
|
{ type: props.type, content: props.content },
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
// 2. fallbackName 由调用方传入(典型来源:Conversation.lastSenderDisplayName 快照),透传到 getSenderDisplayName 内部,算不出真名时兜底
|
// 2. fallbackName 由调用方传入(典型来源:Conversation.lastSenderDisplayName 快照),透传到 getSenderDisplayName 内部,算不出真名时兜底
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
import { ImMessageType, isFriendChatTip, isGroupNotification } from './constants'
|
import { ImConversationType, ImMessageType, isFriendChatTip, isGroupNotification } from './constants'
|
||||||
import {
|
import {
|
||||||
getCardLabelInfo,
|
getCardLabelInfo,
|
||||||
parseMessage,
|
parseMessage,
|
||||||
|
|
@ -19,11 +19,8 @@ import {
|
||||||
type TextMessage,
|
type TextMessage,
|
||||||
type TipSegment
|
type TipSegment
|
||||||
} from './message'
|
} from './message'
|
||||||
import {
|
import { getSenderDisplayName } from './user'
|
||||||
getSenderDisplayName,
|
import { resolveFriendNotificationText, resolveGroupNotificationText } from './message'
|
||||||
resolveFriendNotificationText,
|
|
||||||
resolveGroupNotificationText
|
|
||||||
} from './user'
|
|
||||||
import type { Message } from '../home/types'
|
import type { Message } from '../home/types'
|
||||||
|
|
||||||
/** 会话主键:`type-targetId` 拼成稳定字符串,给 v-for :key、active 比对、map key 等场景共用 */
|
/** 会话主键:`type-targetId` 拼成稳定字符串,给 v-for :key、active 比对、map key 等场景共用 */
|
||||||
|
|
@ -151,7 +148,9 @@ export function resolveConversationLastContent(
|
||||||
return resolveFriendNotificationText(message)
|
return resolveFriendNotificationText(message)
|
||||||
}
|
}
|
||||||
if (isGroupNotification(message.type)) {
|
if (isGroupNotification(message.type)) {
|
||||||
return resolveGroupNotificationText(message)
|
return resolveGroupNotificationText(message, (id) =>
|
||||||
|
getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return summarizeMessageContent(message)
|
return summarizeMessageContent(message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import { generateUUID } from '@/utils'
|
import { generateUUID } from '@/utils'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
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 { getCurrentUserId } from './storage'
|
||||||
|
import { formatCallDuration } from './time'
|
||||||
import { useFriendStore } from '../home/store/friendStore'
|
import { useFriendStore } from '../home/store/friendStore'
|
||||||
import { useGroupStore } from '../home/store/groupStore'
|
import { useGroupStore } from '../home/store/groupStore'
|
||||||
import type { Conversation, Message, User, GroupLite } from '../home/types'
|
import type { Conversation, Message, User, GroupLite } from '../home/types'
|
||||||
|
|
@ -644,3 +650,303 @@ export const formatJson = (content?: string): string => {
|
||||||
return content
|
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<RtcCallStartPayload & RtcCallEndPayload>(content) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 媒体类型文案;TODO 字典化 */
|
||||||
|
function callMediaText(mediaType: number | undefined): string {
|
||||||
|
return mediaType === 2 ? '视频' : '语音'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话内通话事件 segments(RTC_CALL_START / RTC_CALL_END)
|
||||||
|
* <p>
|
||||||
|
* 群聊两段式:START「{inviter} 发起了{voice/video}通话」+ END「{voice/video}通话已结束 [时长 X]」
|
||||||
|
* <p>
|
||||||
|
* 私聊气泡走 {@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 是不是自己渲染两端不同文案(对齐微信)
|
||||||
|
* <p>
|
||||||
|
* 文案规则:
|
||||||
|
* 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 兜底用
|
||||||
|
* <p>
|
||||||
|
* 缺 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 '通话已断开'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,3 +88,13 @@ export function formatMergeItemTime(timestamp: number): string {
|
||||||
}
|
}
|
||||||
return dayjs(timestamp).format('MM-DD HH:mm')
|
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)}`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,11 @@ import { SystemUserSexEnum } from '@/utils/constants'
|
||||||
import {
|
import {
|
||||||
ImConversationType,
|
ImConversationType,
|
||||||
ImFriendAddSource,
|
ImFriendAddSource,
|
||||||
ImMessageType,
|
|
||||||
IM_AT_ALL_NICKNAME,
|
IM_AT_ALL_NICKNAME,
|
||||||
IM_AT_ALL_USER_ID
|
IM_AT_ALL_USER_ID
|
||||||
} from './constants'
|
} from './constants'
|
||||||
import { getCurrentUserId } from './storage'
|
import { getCurrentUserId } from './storage'
|
||||||
import {
|
import { type MentionCandidate } from './message'
|
||||||
joinMentionSegments,
|
|
||||||
segmentsToText,
|
|
||||||
tipMention,
|
|
||||||
tipText,
|
|
||||||
type MentionCandidate,
|
|
||||||
type TipSegment
|
|
||||||
} from './message'
|
|
||||||
import { useConversationStore } from '../home/store/conversationStore'
|
import { useConversationStore } from '../home/store/conversationStore'
|
||||||
import { useFriendStore } from '../home/store/friendStore'
|
import { useFriendStore } from '../home/store/friendStore'
|
||||||
import { useGroupStore } from '../home/store/groupStore'
|
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 一律不展示,对齐微信留白 */
|
/** 性别图标;UNKNOWN / null / undefined 一律不展示,对齐微信留白 */
|
||||||
export function getGenderIcon(sex?: number): string {
|
export function getGenderIcon(sex?: number): string {
|
||||||
if (sex === SystemUserSexEnum.MALE) {
|
if (sex === SystemUserSexEnum.MALE) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue