feat(im): 重构普通消息类型,和 openim 的消息编号对齐
parent
055d4bab27
commit
a9f54fdee1
|
|
@ -127,7 +127,7 @@ const lastSenderDisplayName = computed(() => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 群聊 + 有最后发送者 + 最后一条是普通消息时,显示发送者前缀(TIP_TEXT / RECALL / 草稿态不带前缀) */
|
/** 群聊 + 有最后发送者 + 最后一条是普通消息时,显示发送者前缀(FRIEND_* / GROUP_* / RECALL / 草稿态不带前缀) */
|
||||||
const showSendName = computed(() => {
|
const showSendName = computed(() => {
|
||||||
if (draft.value) {
|
if (draft.value) {
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -143,16 +143,16 @@
|
||||||
v-for="message in currentList"
|
v-for="message in currentList"
|
||||||
:key="message.id || message.clientMessageId"
|
:key="message.id || message.clientMessageId"
|
||||||
>
|
>
|
||||||
<!-- TIP_TEXT 系统提示("你们已成为好友"等):居中灰色,不挂头像 / sender,
|
<!-- 好友会话事件(FRIEND_ADD / FRIEND_DELETE):居中灰色,不挂头像 / sender,
|
||||||
跟主聊天面板里 MessageItem 的渲染语义对齐 -->
|
跟主聊天面板里 MessageItem 的渲染语义对齐 -->
|
||||||
<div
|
<div
|
||||||
v-if="message.type === ImMessageType.TIP_TEXT"
|
v-if="isFriendChatTip(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)]"
|
||||||
>
|
>
|
||||||
{{ resolveTipText(message.content) }}
|
{{ resolveFriendNotificationText(message) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群广播事件文案:跟 TIP_TEXT 同样的居中灰色样式 -->
|
<!-- 群广播事件文案:跟好友事件同灰色样式 -->
|
||||||
<div
|
<div
|
||||||
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)]"
|
||||||
|
|
@ -312,14 +312,19 @@ import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
getSenderRealNickname,
|
getSenderRealNickname,
|
||||||
|
resolveFriendNotificationText,
|
||||||
resolveGroupNotificationText
|
resolveGroupNotificationText
|
||||||
} from '@/views/im/utils/user'
|
} from '@/views/im/utils/user'
|
||||||
import { buildRecallTip } from '@/views/im/utils/conversation'
|
import { buildRecallTip } from '@/views/im/utils/conversation'
|
||||||
import { useMessagePuller } from '@/views/im/home/composables/useMessagePuller'
|
import { useMessagePuller } from '@/views/im/home/composables/useMessagePuller'
|
||||||
import { ImConversationType, ImMessageType, isGroupNotification } from '@/views/im/utils/constants'
|
import {
|
||||||
|
ImConversationType,
|
||||||
|
ImMessageType,
|
||||||
|
isFriendChatTip,
|
||||||
|
isGroupNotification
|
||||||
|
} from '@/views/im/utils/constants'
|
||||||
import {
|
import {
|
||||||
parseMessage,
|
parseMessage,
|
||||||
resolveTipText,
|
|
||||||
getFileIconInfo,
|
getFileIconInfo,
|
||||||
type TextMessage,
|
type TextMessage,
|
||||||
type ImageMessage,
|
type ImageMessage,
|
||||||
|
|
@ -664,11 +669,12 @@ function audioOf(message: Message): AudioMessage | null {
|
||||||
|
|
||||||
/** 关键字命中文本:文本类返回原文、文件返回文件名(利于按文件名搜)、其他返回占位词 */
|
/** 关键字命中文本:文本类返回原文、文件返回文件名(利于按文件名搜)、其他返回占位词 */
|
||||||
function textSnippetOf(message: Message): string {
|
function textSnippetOf(message: Message): string {
|
||||||
|
if (isFriendChatTip(message.type)) {
|
||||||
|
return resolveFriendNotificationText(message)
|
||||||
|
}
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case ImMessageType.TEXT:
|
case ImMessageType.TEXT:
|
||||||
return parseMessage<TextMessage>(message.content)?.content ?? ''
|
return parseMessage<TextMessage>(message.content)?.content ?? ''
|
||||||
case ImMessageType.TIP_TEXT:
|
|
||||||
return resolveTipText(message.content)
|
|
||||||
case ImMessageType.IMAGE:
|
case ImMessageType.IMAGE:
|
||||||
return '[图片]'
|
return '[图片]'
|
||||||
case ImMessageType.FILE:
|
case ImMessageType.FILE:
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@
|
||||||
{{ formatTipTime(message.sendTime) }}
|
{{ formatTipTime(message.sendTime) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 系统提示文案(TIP_TEXT=21) -->
|
<!-- 好友会话事件(FRIEND_ADD / FRIEND_DELETE):跟群广播事件同灰色样式,文案固定 -->
|
||||||
<div
|
<div
|
||||||
v-if="isTipText"
|
v-if="isFriendChatTipMessage"
|
||||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
{{ tipText }}
|
{{ friendChatTipText }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群广播事件:跟 TIP_TEXT 同样的居中灰色样式,文案按 type 拼装 -->
|
<!-- 群广播事件:跟好友事件同灰色样式,文案按 type 拼装 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="isGroupNotificationMessage"
|
v-else-if="isGroupNotificationMessage"
|
||||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||||
|
|
@ -227,6 +227,7 @@ import {
|
||||||
ImConversationType,
|
ImConversationType,
|
||||||
ImGroupMemberRole,
|
ImGroupMemberRole,
|
||||||
TIME_TIP_GAP_MS,
|
TIME_TIP_GAP_MS,
|
||||||
|
isFriendChatTip,
|
||||||
isGroupNotification,
|
isGroupNotification,
|
||||||
isNormalMessage
|
isNormalMessage
|
||||||
} from '@/views/im/utils/constants'
|
} from '@/views/im/utils/constants'
|
||||||
|
|
@ -236,7 +237,6 @@ import {
|
||||||
buildQuoteFromMessage,
|
buildQuoteFromMessage,
|
||||||
getQuoteFromMessage,
|
getQuoteFromMessage,
|
||||||
parseMessage,
|
parseMessage,
|
||||||
resolveTipText,
|
|
||||||
getFileIconInfo,
|
getFileIconInfo,
|
||||||
type TextMessage,
|
type TextMessage,
|
||||||
type ImageMessage,
|
type ImageMessage,
|
||||||
|
|
@ -256,10 +256,12 @@ import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
getSenderRealNickname,
|
getSenderRealNickname,
|
||||||
|
resolveFriendNotificationText,
|
||||||
resolveGroupNotificationText
|
resolveGroupNotificationText
|
||||||
} from '@/views/im/utils/user'
|
} from '@/views/im/utils/user'
|
||||||
import { useImUiStore } from '../../../../store/uiStore'
|
import { useImUiStore } from '../../../../store/uiStore'
|
||||||
import { useMessageSender } from '../../../../composables/useMessageSender'
|
import { useMessageSender } from '../../../../composables/useMessageSender'
|
||||||
|
import { useMuteOverlay } from '../../../../composables/useMuteOverlay'
|
||||||
import type { Message } from '../../../../types'
|
import type { Message } from '../../../../types'
|
||||||
import MessageReadStatus from './MessageReadStatus.vue'
|
import MessageReadStatus from './MessageReadStatus.vue'
|
||||||
import ReplyPreview from './ReplyPreview.vue'
|
import ReplyPreview from './ReplyPreview.vue'
|
||||||
|
|
@ -292,6 +294,7 @@ const friendStore = useFriendStore()
|
||||||
const draftStore = useDraftStore()
|
const draftStore = useDraftStore()
|
||||||
const uiStore = useImUiStore()
|
const uiStore = useImUiStore()
|
||||||
const { recall, sendRaw } = useMessageSender()
|
const { recall, sendRaw } = useMessageSender()
|
||||||
|
const muteOverlay = useMuteOverlay()
|
||||||
// 仅用 confirm,避免 message 跟 props.message 同名冲突(vue/no-dupe-keys)
|
// 仅用 confirm,避免 message 跟 props.message 同名冲突(vue/no-dupe-keys)
|
||||||
const { confirm: confirmDialog, success: successMessage } = useMessage()
|
const { confirm: confirmDialog, success: successMessage } = useMessage()
|
||||||
|
|
||||||
|
|
@ -300,8 +303,8 @@ const { confirm: confirmDialog, success: successMessage } = useMessage()
|
||||||
/** 是否已撤回:pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL,渲染只需识别 type */
|
/** 是否已撤回:pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL,渲染只需识别 type */
|
||||||
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
|
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
|
||||||
|
|
||||||
/** 系统提示文案 */
|
/** 是否会话内好友事件气泡(FRIEND_ADD / FRIEND_DELETE) */
|
||||||
const isTipText = computed(() => props.message.type === ImMessageType.TIP_TEXT)
|
const isFriendChatTipMessage = computed(() => isFriendChatTip(props.message.type))
|
||||||
|
|
||||||
/** 是否在当前消息上方渲染时间分隔条:列表第一条 / 距上一条超过阈值;缺 sendTime 不渲染 */
|
/** 是否在当前消息上方渲染时间分隔条:列表第一条 / 距上一条超过阈值;缺 sendTime 不渲染 */
|
||||||
const shouldShowTimeTip = computed(() => {
|
const shouldShowTimeTip = computed(() => {
|
||||||
|
|
@ -384,8 +387,8 @@ function formatTipTime(timestamp: number): string {
|
||||||
/** 文本内容 */
|
/** 文本内容 */
|
||||||
const textContent = computed(() => parseMessage<TextMessage>(props.message.content)?.content ?? '')
|
const textContent = computed(() => parseMessage<TextMessage>(props.message.content)?.content ?? '')
|
||||||
|
|
||||||
/** TIP_TEXT 文案:与 conversationStore.resolveLastContent / MessageHistory.renderContent 共用 helper,避免兼容性逻辑分裂 */
|
/** 好友会话事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示,文案固定 */
|
||||||
const tipText = computed(() => resolveTipText(props.message.content))
|
const friendChatTipText = computed(() => resolveFriendNotificationText(props.message))
|
||||||
|
|
||||||
/** 群广播事件 */
|
/** 群广播事件 */
|
||||||
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
|
const isGroupNotificationMessage = computed(() => isGroupNotification(props.message.type))
|
||||||
|
|
@ -561,10 +564,10 @@ const RECALL_WINDOW_MS = 2 * 60 * 1000
|
||||||
* - 引用:已落库(id≠0)+ 未撤回的消息可引用,引用块写入 draftStore.reply
|
* - 引用:已落库(id≠0)+ 未撤回的消息可引用,引用块写入 draftStore.reply
|
||||||
* - 撤回 / 删除:互斥;自己发送 + 已落库 + 未撤回 + 2 分钟内显示「撤回」(推服务器),其它显示「删除」(仅本地清)
|
* - 撤回 / 删除:互斥;自己发送 + 已落库 + 未撤回 + 2 分钟内显示「撤回」(推服务器),其它显示「删除」(仅本地清)
|
||||||
*
|
*
|
||||||
* TIP_TEXT 态不弹菜单
|
* 好友事件气泡态不弹菜单
|
||||||
*/
|
*/
|
||||||
async function handleContextMenu(e: MouseEvent) {
|
async function handleContextMenu(e: MouseEvent) {
|
||||||
if (isTipText.value) {
|
if (isFriendChatTipMessage.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -761,6 +764,10 @@ async function handleResend() {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 禁言 / 封禁时拦截:避免重试绕过 MessageInput 的 muteOverlay 又走一次 sendRaw 让后端拒
|
||||||
|
if (muteOverlay.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
conversationStore.removeMessage(conversation.type, conversation.targetId, {
|
conversationStore.removeMessage(conversation.type, conversation.targetId, {
|
||||||
id: props.message.id,
|
id: props.message.id,
|
||||||
clientMessageId: props.message.clientMessageId
|
clientMessageId: props.message.clientMessageId
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ImWebSocketMessageType,
|
ImWebSocketMessageType,
|
||||||
ImMessageType,
|
ImMessageType,
|
||||||
ImConversationType,
|
ImConversationType,
|
||||||
|
isFriendChatTip,
|
||||||
isFriendNotification,
|
isFriendNotification,
|
||||||
isNormalMessage
|
isNormalMessage
|
||||||
} from '../../utils/constants'
|
} from '../../utils/constants'
|
||||||
|
|
@ -77,9 +78,9 @@ const convertGroupMessage = (
|
||||||
* 2. 帧分发:dispatchFrame → dispatchPrivateFrame / dispatchGroupFrame,按 ImMessageType 再分流
|
* 2. 帧分发:dispatchFrame → dispatchPrivateFrame / dispatchGroupFrame,按 ImMessageType 再分流
|
||||||
* 3. 缓冲:初始化加载期(conversationStore.loading=true)暂存消息,等 pull 完成后由 useMessagePuller 调 flushBuffer 回放
|
* 3. 缓冲:初始化加载期(conversationStore.loading=true)暂存消息,等 pull 完成后由 useMessagePuller 调 flushBuffer 回放
|
||||||
* 4. 事件处理(按类型分发到对应 handle*,联动 conversation / friend / group store):
|
* 4. 事件处理(按类型分发到对应 handle*,联动 conversation / friend / group store):
|
||||||
* - 普通消息(TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT):入库 + 当前会话自动已读 / 提示音
|
* - 普通消息(TEXT / IMAGE / FILE / VOICE / VIDEO):入库 + 当前会话自动已读 / 提示音
|
||||||
* - 已读 / 回执(READ / RECEIPT):多端已读同步、对方读后回执
|
* - 已读 / 回执(READ / RECEIPT):多端已读同步、对方读后回执
|
||||||
* - 好友变更(FRIEND_ADD / DELETE / UPDATE):同步 friendStore + 级联刷新私聊会话
|
* - 好友变更(FRIEND_*):同步 friendStore + 级联刷新私聊会话;FRIEND_ADD / FRIEND_DELETE 额外插入会话气泡
|
||||||
* - 群个人信号(GROUP_MEMBER_SETTING_UPDATE):同步 groupStore + 级联刷新群聊会话
|
* - 群个人信号(GROUP_MEMBER_SETTING_UPDATE):同步 groupStore + 级联刷新群聊会话
|
||||||
* - 群广播事件(GROUP_*):走 handleGroupMessage + applyGroupNotification 旁路(含 DISSOLVE / QUIT / KICK 自判清群)
|
* - 群广播事件(GROUP_*):走 handleGroupMessage + applyGroupNotification 旁路(含 DISSOLVE / QUIT / KICK 自判清群)
|
||||||
*/
|
*/
|
||||||
|
|
@ -224,8 +225,13 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
default:
|
default:
|
||||||
if (isFriendNotification(websocketMessage.type)) {
|
if (isFriendNotification(websocketMessage.type)) {
|
||||||
this.handleFriendNotification(websocketMessage)
|
this.handleFriendNotification(websocketMessage)
|
||||||
|
// FRIEND_ADD / FRIEND_DELETE 同时作为会话事件气泡插入消息列表(becomeFriends 入库
|
||||||
|
// 帧 + silent / delete 单边推送帧统一走入库去重路径,前端按 type 渲染灰色提示)
|
||||||
|
if (isFriendChatTip(websocketMessage.type)) {
|
||||||
|
this.handlePrivateMessage(websocketMessage)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT 等普通消息
|
// TEXT / IMAGE / FILE / VOICE / VIDEO 等普通消息
|
||||||
this.handlePrivateMessage(websocketMessage)
|
this.handlePrivateMessage(websocketMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +259,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
this.handleGroupMemberSettingUpdate(websocketMessage)
|
this.handleGroupMemberSettingUpdate(websocketMessage)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
// TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT + GROUP_* 群广播事件
|
// TEXT / IMAGE / FILE / VOICE / VIDEO + GROUP_* 群广播事件
|
||||||
this.handleGroupMessage(websocketMessage)
|
this.handleGroupMessage(websocketMessage)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -263,7 +269,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 私聊普通消息(TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT)入库 + 自动已读
|
* 私聊普通消息(TEXT / IMAGE / FILE / VOICE / VIDEO)入库 + 自动已读
|
||||||
*
|
*
|
||||||
* 流程:
|
* 流程:
|
||||||
* 1. 离线加载期缓冲(避开与 pull 回填的竞态)
|
* 1. 离线加载期缓冲(避开与 pull 回填的竞态)
|
||||||
|
|
@ -334,7 +340,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
console.warn('[IM WS] 自动已读上报失败', e)
|
console.warn('[IM WS] 自动已读上报失败', e)
|
||||||
})
|
})
|
||||||
} else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
} else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
||||||
// 非当前会话且未免打扰:响一下提示音(带节流,详见 playAudioTip);TIP_TEXT 等系统提示不响
|
// 非当前会话且未免打扰:响一下提示音(带节流,详见 playAudioTip);FRIEND_* 等系统事件不响
|
||||||
playAudioTip()
|
playAudioTip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -440,7 +446,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
console.warn('[IM WS] 自动已读上报失败', e)
|
console.warn('[IM WS] 自动已读上报失败', e)
|
||||||
})
|
})
|
||||||
} else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
} else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
||||||
// GROUP_* 群广播事件 / TIP_TEXT 等系统提示不响提示音
|
// GROUP_* 群广播事件等系统消息不响提示音
|
||||||
playAudioTip()
|
playAudioTip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- 文本 / 系统提示文本:直接显示纯文本 -->
|
<!-- 文本:直接显示纯文本 -->
|
||||||
<span v-if="isText" class="whitespace-pre-wrap break-all">{{ textContent }}</span>
|
<span v-if="isText" class="whitespace-pre-wrap break-all">{{ textContent }}</span>
|
||||||
|
|
||||||
<!-- 图片:缩略图 + 点击放大 -->
|
<!-- 图片:缩略图 + 点击放大 -->
|
||||||
|
|
@ -84,7 +84,15 @@
|
||||||
{{ groupNotificationText }}
|
{{ groupNotificationText }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 系统事件类(FRIEND_*):content 通常是结构化 JSON,回退原始预览 -->
|
<!-- 好友会话事件(FRIEND_ADD / FRIEND_DELETE):固定中文文案 -->
|
||||||
|
<span
|
||||||
|
v-else-if="isFriendChatTipType"
|
||||||
|
class="text-12px text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
|
{{ friendChatTipText }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- 其它系统事件 / 未知类型:content 通常是结构化 JSON,回退原始预览 -->
|
||||||
<span v-else class="whitespace-pre-wrap break-all">{{ fallbackText }}</span>
|
<span v-else class="whitespace-pre-wrap break-all">{{ fallbackText }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -93,16 +101,23 @@ import { computed } from 'vue'
|
||||||
import Icon from '@/components/Icon/src/Icon.vue'
|
import Icon from '@/components/Icon/src/Icon.vue'
|
||||||
import { formatFileSize } from '@/utils/file'
|
import { formatFileSize } from '@/utils/file'
|
||||||
import { formatSeconds } from '@/utils/formatTime'
|
import { formatSeconds } from '@/utils/formatTime'
|
||||||
import { ImMessageType, isGroupNotification } from '@/views/im/utils/constants'
|
import {
|
||||||
|
ImMessageType,
|
||||||
|
isFriendChatTip,
|
||||||
|
isGroupNotification
|
||||||
|
} from '@/views/im/utils/constants'
|
||||||
import {
|
import {
|
||||||
parseMessage,
|
parseMessage,
|
||||||
resolveTipText,
|
|
||||||
type ImageMessage,
|
type ImageMessage,
|
||||||
type FileMessage,
|
type FileMessage,
|
||||||
type AudioMessage,
|
type AudioMessage,
|
||||||
type VideoMessage
|
type VideoMessage,
|
||||||
|
type TextMessage
|
||||||
} from '@/views/im/utils/message'
|
} from '@/views/im/utils/message'
|
||||||
import { resolveGroupNotificationText } from '@/views/im/utils/user'
|
import {
|
||||||
|
resolveFriendNotificationText,
|
||||||
|
resolveGroupNotificationText
|
||||||
|
} from '@/views/im/utils/user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageContentPreview' })
|
defineOptions({ name: 'ImMessageContentPreview' })
|
||||||
|
|
||||||
|
|
@ -116,16 +131,16 @@ const props = defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 各类型判定 */
|
/** 各类型判定 */
|
||||||
const isText = computed(
|
const isText = computed(() => props.type === ImMessageType.TEXT)
|
||||||
() => props.type === ImMessageType.TEXT || props.type === ImMessageType.TIP_TEXT
|
|
||||||
)
|
|
||||||
const isImage = computed(() => props.type === ImMessageType.IMAGE)
|
const isImage = computed(() => props.type === ImMessageType.IMAGE)
|
||||||
const isFile = computed(() => props.type === ImMessageType.FILE)
|
const isFile = computed(() => props.type === ImMessageType.FILE)
|
||||||
const isVoice = computed(() => props.type === ImMessageType.VOICE)
|
const isVoice = computed(() => props.type === ImMessageType.VOICE)
|
||||||
const isVideo = computed(() => props.type === ImMessageType.VIDEO)
|
const isVideo = computed(() => props.type === ImMessageType.VIDEO)
|
||||||
|
|
||||||
/** 文本内容:兼容 JSON 包裹和裸字符串两种形态 */
|
/** 文本内容:从 TextMessage payload 取 .content */
|
||||||
const textContent = computed(() => resolveTipText(props.content || ''))
|
const textContent = computed(
|
||||||
|
() => parseMessage<TextMessage>(props.content || '')?.content ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
const imagePayload = computed(() =>
|
const imagePayload = computed(() =>
|
||||||
isImage.value ? parseMessage<ImageMessage>(props.content || '') : null
|
isImage.value ? parseMessage<ImageMessage>(props.content || '') : null
|
||||||
|
|
@ -197,6 +212,12 @@ const fallbackText = computed(() => {
|
||||||
return raw
|
return raw
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 是否好友会话事件气泡(FRIEND_ADD / FRIEND_DELETE) */
|
||||||
|
const isFriendChatTipType = computed(() => isFriendChatTip(props.type ?? -1))
|
||||||
|
|
||||||
|
/** 好友会话事件文案:固定文案,不依赖 payload */
|
||||||
|
const friendChatTipText = computed(() => resolveFriendNotificationText({ type: props.type }))
|
||||||
|
|
||||||
/** 是否群广播事件 */
|
/** 是否群广播事件 */
|
||||||
const isGroupNotificationType = computed(() => isGroupNotification(props.type ?? -1))
|
const isGroupNotificationType = computed(() => isGroupNotification(props.type ?? -1))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
/** IM 消息类型枚举(对齐后端 ImMessageTypeEnum) */
|
/** IM 消息类型枚举(对齐后端 ImMessageTypeEnum) */
|
||||||
export const ImMessageType = {
|
export const ImMessageType = {
|
||||||
TEXT: 0, // 文本
|
// ========== 用户聊天消息(101-105 直接复用 OpenIM 段位编号) ==========
|
||||||
IMAGE: 1, // 图片
|
TEXT: 101, // 文本(对应 OpenIM Text=101)
|
||||||
FILE: 2, // 文件
|
IMAGE: 102, // 图片(对应 OpenIM Picture=102)
|
||||||
VOICE: 3, // 语音
|
VOICE: 103, // 语音(对应 OpenIM Sound=103)
|
||||||
VIDEO: 4, // 视频
|
VIDEO: 104, // 视频(对应 OpenIM Video=104)
|
||||||
RECALL: 10, // 撤回
|
FILE: 105, // 文件(对应 OpenIM File=105)
|
||||||
READ: 11, // 已读
|
// ========== 信号类(2101 / 2200 直接复用 OpenIM 段位编号;2201 自有扩展) ==========
|
||||||
RECEIPT: 12, // 回执
|
RECALL: 2101, // 撤回(对应 OpenIM RevokeNotification=2101)
|
||||||
TIP_TEXT: 21, // 提示文本(撤回提示等)
|
RECEIPT: 2200, // 回执(对应 OpenIM HasReadReceipt=2200)
|
||||||
|
READ: 2201, // 已读(多端同步,OpenIM 无对应;自有扩展)
|
||||||
// ========== 好友通知(1201-1210 直接复用 OpenIM 段位编号) ==========
|
// ========== 好友通知(1201-1210 直接复用 OpenIM 段位编号) ==========
|
||||||
FRIEND_REQUEST_APPROVED: 1201, // 好友申请被同意
|
FRIEND_REQUEST_APPROVED: 1201, // 好友申请被同意
|
||||||
FRIEND_REQUEST_REJECTED: 1202, // 好友申请被拒绝
|
FRIEND_REQUEST_REJECTED: 1202, // 好友申请被拒绝
|
||||||
|
|
@ -62,6 +63,11 @@ export function isFriendNotification(type: number): boolean {
|
||||||
return type >= ImMessageType.FRIEND_REQUEST_APPROVED && type <= ImMessageType.FRIEND_UPDATE
|
return type >= ImMessageType.FRIEND_REQUEST_APPROVED && type <= ImMessageType.FRIEND_UPDATE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 判断是否「会话内的好友事件气泡」:FRIEND_ADD / FRIEND_DELETE 直接渲染成灰色提示,与群事件同处理 */
|
||||||
|
export function isFriendChatTip(type: number): boolean {
|
||||||
|
return type === ImMessageType.FRIEND_ADD || type === ImMessageType.FRIEND_DELETE
|
||||||
|
}
|
||||||
|
|
||||||
/** IM 普通消息类型集合(聊天气泡中显示,并作为会话最后一条摘要) */
|
/** IM 普通消息类型集合(聊天气泡中显示,并作为会话最后一条摘要) */
|
||||||
const ImMessageTypeNormals: number[] = [
|
const ImMessageTypeNormals: number[] = [
|
||||||
ImMessageType.TEXT,
|
ImMessageType.TEXT,
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,13 @@
|
||||||
// 2. fallbackName 由调用方传入(典型来源:Conversation.lastSenderDisplayName 快照),透传到 getSenderDisplayName 内部,算不出真名时兜底
|
// 2. fallbackName 由调用方传入(典型来源:Conversation.lastSenderDisplayName 快照),透传到 getSenderDisplayName 内部,算不出真名时兜底
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
import { ImMessageType, isGroupNotification } from './constants'
|
import { ImMessageType, isFriendChatTip, isGroupNotification } from './constants'
|
||||||
import { parseMessage, resolveTipText, type TextMessage } from './message'
|
import { parseMessage, type TextMessage } from './message'
|
||||||
import { getSenderDisplayName, resolveGroupNotificationText } from './user'
|
import {
|
||||||
|
getSenderDisplayName,
|
||||||
|
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 等场景共用 */
|
||||||
|
|
@ -62,10 +66,10 @@ export function resolveConversationLastContent(
|
||||||
)
|
)
|
||||||
case ImMessageType.TEXT:
|
case ImMessageType.TEXT:
|
||||||
return parseMessage<TextMessage>(message.content)?.content ?? ''
|
return parseMessage<TextMessage>(message.content)?.content ?? ''
|
||||||
case ImMessageType.TIP_TEXT:
|
|
||||||
// TIP_TEXT 后端常发裸字符串(私聊好友建立 / 解除等),不能按 TextMessage JSON 解析,否则摘要变空
|
|
||||||
return resolveTipText(message.content)
|
|
||||||
default:
|
default:
|
||||||
|
if (isFriendChatTip(message.type)) {
|
||||||
|
return resolveFriendNotificationText(message)
|
||||||
|
}
|
||||||
if (isGroupNotification(message.type)) {
|
if (isGroupNotification(message.type)) {
|
||||||
return resolveGroupNotificationText(message)
|
return resolveGroupNotificationText(message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ import type { Message } from '../home/types'
|
||||||
// cn.iocoder.yudao.module.im.service.websocket.dto.message.* 下的 DTO。
|
// cn.iocoder.yudao.module.im.service.websocket.dto.message.* 下的 DTO。
|
||||||
// 各类消息 payload interface 字段对齐后端;解析统一用 parseMessage<T>,
|
// 各类消息 payload interface 字段对齐后端;解析统一用 parseMessage<T>,
|
||||||
// 序列化直接 JSON.stringify(payload)。
|
// 序列化直接 JSON.stringify(payload)。
|
||||||
//
|
|
||||||
// 例外:TIP_TEXT(私聊好友建立 / 解除等系统提示)后端会直接发裸字符串,
|
|
||||||
// 展示侧需走 resolveTipText 兼容裸字符串 + 老接口可能的 {"content":"..."} 两种形态。
|
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
// ==================== 客户端 ID ====================
|
// ==================== 客户端 ID ====================
|
||||||
|
|
@ -146,34 +143,11 @@ export const getQuoteFromMessage = (content: string): QuoteMessage | null => {
|
||||||
return parsed?.quote ?? null
|
return parsed?.quote ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== TIP_TEXT ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 TIP_TEXT(系统提示)文案
|
|
||||||
*
|
|
||||||
* 后端:私聊好友建立 / 解除等系统提示直接发裸字符串;老接口可能包成 {"content": "..."}。
|
|
||||||
* 解析得到 .content 就用,否则当裸文案返回,避免出现空行。
|
|
||||||
*
|
|
||||||
* MessageItem / conversationStore.resolveLastContent / MessageHistory.renderContent 三处共用,
|
|
||||||
* 修一处兼容性问题不会漏到另两处
|
|
||||||
*/
|
|
||||||
export const resolveTipText = (content: string): string => {
|
|
||||||
const raw = content || ''
|
|
||||||
if (!raw) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const parsed = parseMessage<TextMessage>(raw)
|
|
||||||
if (parsed && typeof parsed.content === 'string') {
|
|
||||||
return parsed.content
|
|
||||||
}
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 撤回 ====================
|
// ==================== 撤回 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从后端下发的撤回 TIP_TEXT content 中解析出被撤回的原消息 id
|
* 从后端下发的撤回 RecallMessage content 中解析出被撤回的原消息 id
|
||||||
* content 形如 `{"messageId": 123}`,若不含 messageId 则返回 0(表示这条不是撤回 tip)
|
* content 形如 `{"messageId": 123}`,若不含 messageId 则返回 0(表示这条不是撤回消息)
|
||||||
*/
|
*/
|
||||||
export const parseRecallMessageId = (content: string): number => {
|
export const parseRecallMessageId = (content: string): number => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,18 @@ export function resolveGroupNotificationText(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 会话内好友事件文案:FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡,文案固定不依赖 payload */
|
||||||
|
export function resolveFriendNotificationText(message: { type?: number }): string {
|
||||||
|
switch (message.type) {
|
||||||
|
case ImMessageType.FRIEND_ADD:
|
||||||
|
return '你们已经是好友了,开始聊天吧'
|
||||||
|
case ImMessageType.FRIEND_DELETE:
|
||||||
|
return '你已删除好友'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 性别图标:男 1 / 女 2,0 / null / undefined 一律不展示,对齐微信留白 */
|
/** 性别图标:男 1 / 女 2,0 / null / undefined 一律不展示,对齐微信留白 */
|
||||||
export function getGenderIcon(sex?: number): string {
|
export function getGenderIcon(sex?: number): string {
|
||||||
if (sex === 1) {
|
if (sex === 1) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue