🐛 fix(im): 群聊离线拉取看不到撤回提示,pull 路径接入 recallMessage
pullByType 之前对 RECALL 信号一律 skip、只靠原消息 status=RECALL 走 OR 兜底渲染。 当 pull 的 minId 卡在原消息处、回拉只返回信号时,本地缓存里的老消息没人翻成 RECALL,会一直停在原态——配合后端群聊 mapper 过滤掉 status=RECALL 的原消息,群聊 离线撤回完全不可见。 改成 pull / WS 走同一套 dispatch: - pullByType 信号转 conversationStore.recallMessage(),跟 WS 路径一致 - recallMessage 把 parseRecallMessageId 收敛进内部,第 3 个参数从 messageId: number 改成 recallSignalContent: string,4 个调用点都缩成一行 - MessageItem.isRecall 只判 type=RECALL,去掉 status=RECALL OR 分支 (conversationStore 里跳未读 / 跳已读那两处对 status 的判断是业务逻辑保留)im
parent
66514fc597
commit
a35698fc07
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import { useConversationStore } from '../store/conversationStore'
|
||||||
|
import { useImWebSocketStore } from '../store/websocketStore'
|
||||||
|
import {
|
||||||
|
pullPrivateMessages as apiPullPrivateMessages,
|
||||||
|
type ImPrivateMessageRespVO
|
||||||
|
} from '@/api/im/message/private'
|
||||||
|
import {
|
||||||
|
pullGroupMessages as apiPullGroupMessages,
|
||||||
|
type ImGroupMessageRespVO
|
||||||
|
} from '@/api/im/message/group'
|
||||||
|
import {
|
||||||
|
ImConversationType,
|
||||||
|
ImMessageType,
|
||||||
|
PRIVATE_MESSAGE_PULL_SIZE,
|
||||||
|
GROUP_MESSAGE_PULL_SIZE
|
||||||
|
} from '../../utils/constants'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import type { Message } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息增量拉取:登录后分页拉取离线期间的新消息
|
||||||
|
*
|
||||||
|
* 设计要点:
|
||||||
|
* 1. 同时拉取私聊 + 群聊,使用各自的 `minId` 游标(privateMessageMaxId / groupMessageMaxId)
|
||||||
|
* 2. 后端一次最多返回 size 条;前端按 minId 持续翻页,直到接口返回空列表为止
|
||||||
|
* 3. 拉取期间 conversationStore.loading=true:
|
||||||
|
* - conversationStore 跳过 localStorage 持久化,避免频繁写入卡顿
|
||||||
|
* - websocketStore 把新来的 WS 普通消息丢进缓冲区,等循环结束后统一回放
|
||||||
|
* 4. WebSocket 重连后会再触发一次拉取,补齐断网期间错过的消息
|
||||||
|
*/
|
||||||
|
export const useMessagePuller = () => {
|
||||||
|
const conversationStore = useConversationStore()
|
||||||
|
const wsStore = useImWebSocketStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const currentUserId = Number(userStore.getUser?.id) || 0
|
||||||
|
|
||||||
|
/** 服务端私聊消息 -> 本地 Message */
|
||||||
|
const convertPrivateMessage = (message: ImPrivateMessageRespVO): Message => {
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
clientMessageId: message.clientMessageId || '',
|
||||||
|
type: message.type,
|
||||||
|
content: message.content,
|
||||||
|
status: message.status,
|
||||||
|
sendTime: new Date(message.sendTime).getTime(),
|
||||||
|
senderId: message.senderId,
|
||||||
|
senderNickName: '',
|
||||||
|
targetId: message.receiverId,
|
||||||
|
selfSend: message.senderId === currentUserId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 服务端群聊消息 -> 本地 Message */
|
||||||
|
const convertGroupMessage = (message: ImGroupMessageRespVO): Message => {
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
clientMessageId: message.clientMessageId || '',
|
||||||
|
type: message.type,
|
||||||
|
content: message.content,
|
||||||
|
status: message.status,
|
||||||
|
sendTime: new Date(message.sendTime).getTime(),
|
||||||
|
senderId: message.senderId,
|
||||||
|
senderNickName: '',
|
||||||
|
targetId: message.groupId,
|
||||||
|
selfSend: message.senderId === currentUserId,
|
||||||
|
atUserIds: message.atUserIds || [],
|
||||||
|
receiverUserIds: message.receiverUserIds || [],
|
||||||
|
receiptStatus: message.receiptStatus,
|
||||||
|
readCount: message.readCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 私聊:会话归属到对端 userId */
|
||||||
|
const convertPrivateConversation = (message: ImPrivateMessageRespVO) => {
|
||||||
|
const targetId = message.senderId === currentUserId ? message.receiverId : message.senderId
|
||||||
|
return {
|
||||||
|
type: ImConversationType.PRIVATE,
|
||||||
|
targetId,
|
||||||
|
name: String(targetId),
|
||||||
|
avatar: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群聊:会话归属到 groupId */
|
||||||
|
const convertGroupConversation = (message: ImGroupMessageRespVO) => {
|
||||||
|
return {
|
||||||
|
type: ImConversationType.GROUP,
|
||||||
|
targetId: message.groupId,
|
||||||
|
name: String(message.groupId),
|
||||||
|
avatar: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 循环拉取指定会话类型的消息:以列表最后一条 id 作为下次 minId,直到接口返回空列表 */
|
||||||
|
const pullByType = async (conversationType: number, startMinId: number) => {
|
||||||
|
// 私聊 / 群聊各自一套接口和分页大小,按 isPrivate 在循环内分支调度
|
||||||
|
let minId = startMinId || 0
|
||||||
|
const isPrivate = conversationType === ImConversationType.PRIVATE
|
||||||
|
const size = isPrivate ? PRIVATE_MESSAGE_PULL_SIZE : GROUP_MESSAGE_PULL_SIZE
|
||||||
|
while (true) {
|
||||||
|
const list = isPrivate
|
||||||
|
? await apiPullPrivateMessages({ minId, size })
|
||||||
|
: await apiPullGroupMessages({ minId, size })
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐条 dispatch:原消息走 insertMessage;RECALL 信号走 recallMessage 把同批内已 insert 的原消息翻成撤回提示。
|
||||||
|
// 后端按 id 升序返回,且信号 id 一定 > 原消息 id(先翻 status 再插信号),所以原消息一定先到、recallMessage 找得到
|
||||||
|
for (const raw of list) {
|
||||||
|
if (isPrivate) {
|
||||||
|
const message = raw as ImPrivateMessageRespVO
|
||||||
|
if (message.type === ImMessageType.RECALL) {
|
||||||
|
conversationStore.recallMessage(
|
||||||
|
ImConversationType.PRIVATE,
|
||||||
|
message.senderId === currentUserId ? message.receiverId : message.senderId,
|
||||||
|
message.content,
|
||||||
|
'',
|
||||||
|
message.senderId === currentUserId
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conversationStore.insertMessage(
|
||||||
|
convertPrivateConversation(message),
|
||||||
|
convertPrivateMessage(message)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const message = raw as ImGroupMessageRespVO
|
||||||
|
if (message.type === ImMessageType.RECALL) {
|
||||||
|
conversationStore.recallMessage(
|
||||||
|
ImConversationType.GROUP,
|
||||||
|
message.groupId,
|
||||||
|
message.content,
|
||||||
|
'',
|
||||||
|
message.senderId === currentUserId
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conversationStore.insertMessage(
|
||||||
|
convertGroupConversation(message),
|
||||||
|
convertGroupMessage(message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游标推进到本批最后一条 id,下一轮从此处续翻
|
||||||
|
const lastId = list[list.length - 1].id
|
||||||
|
if (lastId != null) {
|
||||||
|
minId = lastId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同一时刻只允许一次 pull:Index.vue 的手动调用与重连 watch 触发可能并发,共用同一个 promise 即可去重 */
|
||||||
|
let pullPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
/** 执行一次全量增量拉取(重入安全:进行中再次调用复用同一个 promise) */
|
||||||
|
const pullOnce = (): Promise<void> => {
|
||||||
|
if (!currentUserId) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
if (pullPromise) {
|
||||||
|
return pullPromise
|
||||||
|
}
|
||||||
|
pullPromise = (async () => {
|
||||||
|
conversationStore.loading = true
|
||||||
|
try {
|
||||||
|
// 并发拉取私聊 + 群聊,降低初始加载耗时
|
||||||
|
await Promise.all([
|
||||||
|
pullByType(ImConversationType.PRIVATE, conversationStore.privateMessageMaxId),
|
||||||
|
pullByType(ImConversationType.GROUP, conversationStore.groupMessageMaxId)
|
||||||
|
])
|
||||||
|
|
||||||
|
// 回放 WebSocket 在 loading 期间收到的缓冲消息
|
||||||
|
const buffered = wsStore.flushBuffer()
|
||||||
|
for (const item of buffered) {
|
||||||
|
if (item.conversationType === ImConversationType.PRIVATE) {
|
||||||
|
wsStore.handlePrivateMessage(item.payload)
|
||||||
|
} else {
|
||||||
|
wsStore.handleGroupMessage(item.payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[IM] 拉取离线消息失败:', e)
|
||||||
|
} finally {
|
||||||
|
conversationStore.loading = false
|
||||||
|
conversationStore.sortConversations()
|
||||||
|
pullPromise = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return pullPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断网期间 WS 收不到推送,期间产生的消息只能靠拉取接口按 minId 游标补齐;
|
||||||
|
* 首次连接由 Index.vue 显式调 pullOnce,这里订阅 isConnected 的 false→true 转换,覆盖后续每次重连
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => wsStore.isConnected,
|
||||||
|
(isConnected) => {
|
||||||
|
if (isConnected) {
|
||||||
|
void pullOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return { pullOnce }
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
buildRecallTip,
|
buildRecallTip,
|
||||||
generateClientMessageId,
|
generateClientMessageId,
|
||||||
parseMessage,
|
parseMessage,
|
||||||
|
parseRecallMessageId,
|
||||||
type TextMessage
|
type TextMessage
|
||||||
} from '../../utils/message'
|
} from '../../utils/message'
|
||||||
import type { Conversation, ConversationStoreMeta, Message } from '../types'
|
import type { Conversation, ConversationStoreMeta, Message } from '../types'
|
||||||
|
|
@ -475,17 +476,18 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
this.saveConversations(conversation)
|
this.saveConversations(conversation)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/** 撤回消息:解析撤回信号 content(`{"messageId": xxx}`),找到原消息翻成 RECALL 态 + 刷新会话摘要 */
|
||||||
* 撤回消息:将原消息 type 改为 RECALL,并刷新会话摘要
|
|
||||||
* 对应后端 RECALL 事件:按原 messageId 更新
|
|
||||||
*/
|
|
||||||
recallMessage(
|
recallMessage(
|
||||||
conversationType: number,
|
conversationType: number,
|
||||||
targetId: number,
|
targetId: number,
|
||||||
messageId: number,
|
recallSignalContent: string,
|
||||||
senderNickName: string,
|
senderNickName: string,
|
||||||
selfSend: boolean
|
selfSend: boolean
|
||||||
) {
|
) {
|
||||||
|
const messageId = parseRecallMessageId(recallSignalContent)
|
||||||
|
if (messageId <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const conversation = this.getConversation(conversationType, targetId)
|
const conversation = this.getConversation(conversationType, targetId)
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const list = await apiGetMyFriendList()
|
const list = await apiGetMyFriendList()
|
||||||
this.friends = (list || []).map(toFriend)
|
this.friends = (list || []).map(convertFriend)
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
// 同步 conversationStore 私聊会话的展示名 / 头像 / 免打扰
|
// 同步 conversationStore 私聊会话的展示名 / 头像 / 免打扰
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
|
|
@ -72,7 +72,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.upsertFriend(toFriend(data))
|
this.upsertFriend(convertFriend(data))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[IM friendStore] loadFriendInfo 失败', e)
|
console.warn('[IM friendStore] loadFriendInfo 失败', e)
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +154,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function toFriend(vo: ImFriendRespVO): Friend {
|
function convertFriend(vo: ImFriendRespVO): Friend {
|
||||||
return {
|
return {
|
||||||
id: vo.id,
|
id: vo.id,
|
||||||
friendUserId: vo.friendUserId,
|
friendUserId: vo.friendUserId,
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
}
|
}
|
||||||
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 loadGroupMembers)
|
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 loadGroupMembers)
|
||||||
const list = await apiGetMyGroupList()
|
const list = await apiGetMyGroupList()
|
||||||
this.groups = (list || []).map(toGroup)
|
this.groups = (list || []).map(convertGroup)
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
for (const g of this.groups) {
|
for (const g of this.groups) {
|
||||||
|
|
@ -63,7 +63,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.upsertGroup(toGroup(data))
|
this.upsertGroup(convertGroup(data))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[IM groupStore] loadGroupInfo 失败', e)
|
console.warn('[IM groupStore] loadGroupInfo 失败', e)
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +79,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
|
|
||||||
// 拉取该群所有成员(聚合自 AdminUser,含 nickname / avatar / displayUserName)
|
// 拉取该群所有成员(聚合自 AdminUser,含 nickname / avatar / displayUserName)
|
||||||
const list = await apiGetGroupMemberList(groupId)
|
const list = await apiGetGroupMemberList(groupId)
|
||||||
const members = (list || []).map((member) => toGroupMember(member, groupId))
|
const members = (list || []).map((member) => convertGroupMember(member, groupId))
|
||||||
// 成员列表可能在群列表之前触发,此时需要占位一个 group
|
// 成员列表可能在群列表之前触发,此时需要占位一个 group
|
||||||
if (!group) {
|
if (!group) {
|
||||||
this.upsertGroup({
|
this.upsertGroup({
|
||||||
|
|
@ -138,7 +138,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function toGroup(vo: ImGroupRespVO): Group {
|
function convertGroup(vo: ImGroupRespVO): Group {
|
||||||
return {
|
return {
|
||||||
id: vo.id,
|
id: vo.id,
|
||||||
name: vo.name,
|
name: vo.name,
|
||||||
|
|
@ -148,7 +148,7 @@ function toGroup(vo: ImGroupRespVO): Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember {
|
function convertGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember {
|
||||||
return {
|
return {
|
||||||
id: member.id,
|
id: member.id,
|
||||||
userId: member.userId,
|
userId: member.userId,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { getRefreshToken } from '@/utils/auth'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
import { ImWebSocketMessageType, ImMessageType, ImConversationType } from '../../utils/constants'
|
import { ImWebSocketMessageType, ImMessageType, ImConversationType } from '../../utils/constants'
|
||||||
import { parseRecallMessageId, playAudioTip } from '../../utils/message'
|
import { playAudioTip } from '../../utils/message'
|
||||||
import { useConversationStore } from './conversationStore'
|
import { useConversationStore } from './conversationStore'
|
||||||
import { useFriendStore } from './friendStore'
|
import { useFriendStore } from './friendStore'
|
||||||
import { useGroupStore } from './groupStore'
|
import { useGroupStore } from './groupStore'
|
||||||
|
|
@ -17,6 +17,44 @@ import type {
|
||||||
Message
|
Message
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
|
/** WebSocket 私聊 DTO -> 前端 Message:sendTime 转毫秒;senderNickName 由调用方按好友信息补 */
|
||||||
|
const convertPrivateMessage = (
|
||||||
|
websocketMessage: ImPrivateMessageDTO,
|
||||||
|
currentUserId: number,
|
||||||
|
senderNickName: string
|
||||||
|
): Message => ({
|
||||||
|
id: websocketMessage.id,
|
||||||
|
clientMessageId: websocketMessage.clientMessageId,
|
||||||
|
type: websocketMessage.type,
|
||||||
|
content: websocketMessage.content,
|
||||||
|
status: websocketMessage.status,
|
||||||
|
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||||
|
senderId: websocketMessage.senderId,
|
||||||
|
senderNickName,
|
||||||
|
targetId: websocketMessage.receiverId,
|
||||||
|
selfSend: websocketMessage.senderId === currentUserId
|
||||||
|
})
|
||||||
|
|
||||||
|
/** WebSocket 群聊 DTO -> 前端 Message:群消息额外带 atUserIds / receiverUserIds,给 @ 标记和回执用 */
|
||||||
|
const convertGroupMessage = (
|
||||||
|
websocketMessage: ImGroupMessageDTO,
|
||||||
|
currentUserId: number,
|
||||||
|
senderNickName: string
|
||||||
|
): Message => ({
|
||||||
|
id: websocketMessage.id,
|
||||||
|
clientMessageId: websocketMessage.clientMessageId,
|
||||||
|
type: websocketMessage.type,
|
||||||
|
content: websocketMessage.content,
|
||||||
|
status: websocketMessage.status,
|
||||||
|
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||||
|
senderId: websocketMessage.senderId,
|
||||||
|
senderNickName,
|
||||||
|
targetId: websocketMessage.groupId,
|
||||||
|
selfSend: websocketMessage.senderId === currentUserId,
|
||||||
|
atUserIds: websocketMessage.atUserIds || [],
|
||||||
|
receiverUserIds: websocketMessage.receiverUserIds || []
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IM WebSocket Store
|
* IM WebSocket Store
|
||||||
*
|
*
|
||||||
|
|
@ -38,8 +76,8 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
reconnectTimer: null as ReturnType<typeof setTimeout> | null,
|
reconnectTimer: null as ReturnType<typeof setTimeout> | null,
|
||||||
heartbeatTimer: null as ReturnType<typeof setInterval> | null,
|
heartbeatTimer: null as ReturnType<typeof setInterval> | null,
|
||||||
messageBuffer: [] as Array<
|
messageBuffer: [] as Array<
|
||||||
| { kind: 'private'; payload: ImPrivateMessageDTO }
|
| { conversationType: typeof ImConversationType.PRIVATE; payload: ImPrivateMessageDTO }
|
||||||
| { kind: 'group'; payload: ImGroupMessageDTO }
|
| { conversationType: typeof ImConversationType.GROUP; payload: ImGroupMessageDTO }
|
||||||
> // 初始化加载期内,先把普通消息丢进缓冲区,pull 完成后再一次性回放
|
> // 初始化加载期内,先把普通消息丢进缓冲区,pull 完成后再一次性回放
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -228,7 +266,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
// 1. 离线加载期间先缓冲,等 pull 完成后再统一回放,避免重复或顺序错乱
|
// 1. 离线加载期间先缓冲,等 pull 完成后再统一回放,避免重复或顺序错乱
|
||||||
if (conversationStore.loading) {
|
if (conversationStore.loading) {
|
||||||
this.messageBuffer.push({ kind: 'private', payload: websocketMessage })
|
this.messageBuffer.push({
|
||||||
|
conversationType: ImConversationType.PRIVATE,
|
||||||
|
payload: websocketMessage
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,32 +288,18 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
// 3. 后端撤回:下发一条 RECALL 消息,content 为 `{"messageId": xxx}`(对齐 ImMessageTypeEnum.RECALL → RecallMessage)
|
// 3. 后端撤回:下发一条 RECALL 消息,content 为 `{"messageId": xxx}`(对齐 ImMessageTypeEnum.RECALL → RecallMessage)
|
||||||
// 这里拦截下来改走 recallMessage(把原消息翻转为 RECALL 态),不让它作为新消息进列表
|
// 这里拦截下来改走 recallMessage(把原消息翻转为 RECALL 态),不让它作为新消息进列表
|
||||||
if (websocketMessage.type === ImMessageType.RECALL) {
|
if (websocketMessage.type === ImMessageType.RECALL) {
|
||||||
const recallMessageId = parseRecallMessageId(websocketMessage.content)
|
conversationStore.recallMessage(
|
||||||
if (recallMessageId) {
|
ImConversationType.PRIVATE,
|
||||||
conversationStore.recallMessage(
|
peerId,
|
||||||
ImConversationType.PRIVATE,
|
websocketMessage.content,
|
||||||
peerId,
|
friend?.nickname || '',
|
||||||
recallMessageId,
|
selfSend
|
||||||
friend?.nickname || '',
|
)
|
||||||
selfSend
|
return
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 后端 DTO → 前端 Message:sendTime 转毫秒;selfSend / senderNickName 是前端补的
|
// 4. 后端 DTO → 前端 Message
|
||||||
const message: Message = {
|
const message = convertPrivateMessage(websocketMessage, currentUserId, friend?.nickname || '')
|
||||||
id: websocketMessage.id,
|
|
||||||
clientMessageId: websocketMessage.clientMessageId,
|
|
||||||
type: websocketMessage.type,
|
|
||||||
content: websocketMessage.content,
|
|
||||||
status: websocketMessage.status,
|
|
||||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
|
||||||
senderId: websocketMessage.senderId,
|
|
||||||
senderNickName: friend?.nickname || '',
|
|
||||||
targetId: websocketMessage.receiverId,
|
|
||||||
selfSend
|
|
||||||
}
|
|
||||||
conversationStore.insertMessage(
|
conversationStore.insertMessage(
|
||||||
{
|
{
|
||||||
type: ImConversationType.PRIVATE,
|
type: ImConversationType.PRIVATE,
|
||||||
|
|
@ -339,7 +366,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
// 1. 离线加载期缓冲(与私聊对称)
|
// 1. 离线加载期缓冲(与私聊对称)
|
||||||
if (conversationStore.loading) {
|
if (conversationStore.loading) {
|
||||||
this.messageBuffer.push({ kind: 'group', payload: websocketMessage })
|
this.messageBuffer.push({
|
||||||
|
conversationType: ImConversationType.GROUP,
|
||||||
|
payload: websocketMessage
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
@ -359,34 +389,18 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
// 3. 后端撤回:下发一条 RECALL 消息,content 为 `{"messageId": xxx}`
|
// 3. 后端撤回:下发一条 RECALL 消息,content 为 `{"messageId": xxx}`
|
||||||
// 这里拦截下来改走 recallMessage(把原消息翻转为 RECALL 态)
|
// 这里拦截下来改走 recallMessage(把原消息翻转为 RECALL 态)
|
||||||
if (websocketMessage.type === ImMessageType.RECALL) {
|
if (websocketMessage.type === ImMessageType.RECALL) {
|
||||||
const recallMessageId = parseRecallMessageId(websocketMessage.content)
|
conversationStore.recallMessage(
|
||||||
if (recallMessageId) {
|
ImConversationType.GROUP,
|
||||||
conversationStore.recallMessage(
|
websocketMessage.groupId,
|
||||||
ImConversationType.GROUP,
|
websocketMessage.content,
|
||||||
websocketMessage.groupId,
|
senderNickName,
|
||||||
recallMessageId,
|
selfSend
|
||||||
senderNickName,
|
)
|
||||||
selfSend
|
return
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 后端 DTO → 前端 Message:群消息额外带 atUserIds / receiverUserIds,给 @ 标记和回执用
|
// 4. 后端 DTO → 前端 Message
|
||||||
const message: Message = {
|
const message = convertGroupMessage(websocketMessage, currentUserId, senderNickName)
|
||||||
id: websocketMessage.id,
|
|
||||||
clientMessageId: websocketMessage.clientMessageId,
|
|
||||||
type: websocketMessage.type,
|
|
||||||
content: websocketMessage.content,
|
|
||||||
status: websocketMessage.status,
|
|
||||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
|
||||||
senderId: websocketMessage.senderId,
|
|
||||||
senderNickName,
|
|
||||||
targetId: websocketMessage.groupId,
|
|
||||||
selfSend,
|
|
||||||
atUserIds: websocketMessage.atUserIds || [],
|
|
||||||
receiverUserIds: websocketMessage.receiverUserIds || []
|
|
||||||
}
|
|
||||||
conversationStore.insertMessage(
|
conversationStore.insertMessage(
|
||||||
{
|
{
|
||||||
type: ImConversationType.GROUP,
|
type: ImConversationType.GROUP,
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,8 @@ export interface Friend {
|
||||||
avatar?: string // 好友头像
|
avatar?: string // 好友头像
|
||||||
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||||
status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除/墓碑)
|
status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除/墓碑)
|
||||||
addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 toFriend 转换)
|
addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
|
||||||
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 toFriend 转换)
|
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 用户名片 ====================
|
// ==================== 用户名片 ====================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue