✨ feat(im): 修一组细节:会话 silent 跟随新消息同步、合并末尾刷摘要 + 群 @ 标记、录音 1s 下限、邀请 reload 透 friendIds、pull 游标取最大 id
parent
b63492199a
commit
f7cda1fc4e
|
|
@ -118,7 +118,8 @@ export const useMessagePuller = () => {
|
||||||
type: ImConversationType.PRIVATE,
|
type: ImConversationType.PRIVATE,
|
||||||
targetId,
|
targetId,
|
||||||
name: friend ? getFriendDisplayName(friend) : String(targetId), // 会话列表 / 顶部标题展示:好友备注 > 真实昵称
|
name: friend ? getFriendDisplayName(friend) : String(targetId), // 会话列表 / 顶部标题展示:好友备注 > 真实昵称
|
||||||
avatar: friend?.avatar || ''
|
avatar: friend?.avatar || '',
|
||||||
|
silent: friend?.silent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,11 +130,12 @@ export const useMessagePuller = () => {
|
||||||
type: ImConversationType.GROUP,
|
type: ImConversationType.GROUP,
|
||||||
targetId: message.groupId,
|
targetId: message.groupId,
|
||||||
name: group?.name || String(message.groupId),
|
name: group?.name || String(message.groupId),
|
||||||
avatar: group?.avatar || ''
|
avatar: group?.avatar || '',
|
||||||
|
silent: group?.silent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 循环拉取指定会话类型的消息:以列表最后一条 id 作为下次 minId,直到接口返回空列表 */
|
/** 循环拉取指定会话类型的消息:以本批最大 id 作为下次 minId,直到接口返回空列表或游标不再前进 */
|
||||||
const pullByType = async (conversationType: number, startMinId: number) => {
|
const pullByType = async (conversationType: number, startMinId: number) => {
|
||||||
// 私聊 / 群聊 / 频道各自一套接口;按 conversationType 在循环内分支调度
|
// 私聊 / 群聊 / 频道各自一套接口;按 conversationType 在循环内分支调度
|
||||||
let minId = startMinId || 0
|
let minId = startMinId || 0
|
||||||
|
|
@ -208,11 +210,17 @@ export const useMessagePuller = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 游标推进到本批最后一条 id,下一轮从此处续翻
|
// 游标推进到本批最大 id,与后端返回顺序无关;无有效 id 直接 break 避免死翻同一批
|
||||||
const lastId = list[list.length - 1].id
|
const validIds = list.map((message) => message.id).filter((id): id is number => id != null)
|
||||||
if (lastId != null) {
|
if (validIds.length === 0) {
|
||||||
minId = lastId
|
break
|
||||||
}
|
}
|
||||||
|
const nextMinId = Math.max(...validIds)
|
||||||
|
// 游标没前进就停:当前后端契约是 id > minId,理论不会出现;防御后端契约变更或边界数据死翻
|
||||||
|
if (nextMinId <= minId) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
minId = nextMinId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -371,7 +371,7 @@
|
||||||
|
|
||||||
<!-- ==================== 子对话框 ==================== -->
|
<!-- ==================== 子对话框 ==================== -->
|
||||||
<!-- 邀请新成员 / 选成员移除 -->
|
<!-- 邀请新成员 / 选成员移除 -->
|
||||||
<GroupMemberAddDialog ref="inviteDialogRef" @reload="$emit('reload')" />
|
<GroupMemberAddDialog ref="inviteDialogRef" @reload="(ids) => $emit('reload', ids)" />
|
||||||
<GroupMemberRemoveDialog ref="removeDialogRef" @reload="$emit('reload')" />
|
<GroupMemberRemoveDialog ref="removeDialogRef" @reload="$emit('reload')" />
|
||||||
|
|
||||||
<!-- 群主操作:管理员设置(一个弹窗合并增 / 删,提交时 diff)+ 群主管理权转让 -->
|
<!-- 群主操作:管理员设置(一个弹窗合并增 / 删,提交时 diff)+ 群主管理权转让 -->
|
||||||
|
|
@ -432,7 +432,7 @@ const props = withDefaults(
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: boolean]
|
'update:modelValue': [value: boolean]
|
||||||
reload: [] // 邀请 / 移除 / 修改群资料后,父组件重新拉群数据
|
reload: [friendIds?: number[]] // 邀请 / 移除 / 修改群资料后,父组件重新拉群数据;邀请新成员场景透出 friendIds 让上层可选精准刷新
|
||||||
'open-history': [] // 点击 "查找聊天内容" 行 → 父组件打开 MessageHistory 弹窗
|
'open-history': [] // 点击 "查找聊天内容" 行 → 父组件打开 MessageHistory 弹窗
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,13 @@ async function startRecord() {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 停止录制:进入 preview 阶段,由用户决定重录或发送 */
|
/** 停止录制:进入 preview 阶段,由用户决定重录或发送;< 1s 视为误触,直接 warning + 回 idle 不进 preview */
|
||||||
function stopRecord() {
|
function stopRecord() {
|
||||||
|
if (duration.value < 1) {
|
||||||
|
message.warning('录音时间太短')
|
||||||
|
resetAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||||
mediaRecorder.stop()
|
mediaRecorder.stop()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,31 @@ function recomputeConversationLast(conversation: Conversation): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群聊未读消息把 @ 标记同步到会话;非群聊 / 自己发的 / 已读 / 没 @ 全跳过
|
||||||
|
*
|
||||||
|
* 同时被新消息插入路径和合并末尾消息路径调用,让 pull 拿到不完整 atUserIds 后 WS 补齐的场景,
|
||||||
|
* 也能正确点亮 conversation.atMe / atAll 红字徽标
|
||||||
|
*/
|
||||||
|
function syncConversationAtFlags(conversation: Conversation, message: Message): void {
|
||||||
|
if (
|
||||||
|
message.selfSend ||
|
||||||
|
conversation.type !== ImConversationType.GROUP ||
|
||||||
|
!message.atUserIds ||
|
||||||
|
message.atUserIds.length === 0 ||
|
||||||
|
message.status === ImMessageStatus.READ
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentUserId = getCurrentUserId()
|
||||||
|
if (currentUserId && message.atUserIds.includes(currentUserId)) {
|
||||||
|
conversation.atMe = true
|
||||||
|
}
|
||||||
|
if (message.atUserIds.includes(IM_AT_ALL_USER_ID)) {
|
||||||
|
conversation.atAll = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把服务端字段(REST ack / WS 推送 / pull 回填)合并到本地消息
|
* 把服务端字段(REST ack / WS 推送 / pull 回填)合并到本地消息
|
||||||
*
|
*
|
||||||
|
|
@ -369,12 +394,13 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 创建空会话(抽取公共逻辑,供 insertMessage / openConversation 复用) */
|
/** 创建空会话(抽取公共逻辑,供 insertMessage / openConversation 复用);调用方传 silent 时按 friend / group store 的值落地,未传保持默认 false */
|
||||||
createEmptyConversation(
|
createEmptyConversation(
|
||||||
type: number,
|
type: number,
|
||||||
targetId: number,
|
targetId: number,
|
||||||
name: string,
|
name: string,
|
||||||
avatar: string
|
avatar: string,
|
||||||
|
silent: boolean = false
|
||||||
): Conversation {
|
): Conversation {
|
||||||
return {
|
return {
|
||||||
targetId,
|
targetId,
|
||||||
|
|
@ -387,7 +413,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
messages: [],
|
messages: [],
|
||||||
deleted: false,
|
deleted: false,
|
||||||
top: false,
|
top: false,
|
||||||
silent: false,
|
silent,
|
||||||
atMe: false,
|
atMe: false,
|
||||||
atAll: false
|
atAll: false
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +487,13 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
* 4. 收尾:更新游标 + 持久化
|
* 4. 收尾:更新游标 + 持久化
|
||||||
*/
|
*/
|
||||||
insertMessage(
|
insertMessage(
|
||||||
conversationInfo: { type: number; targetId: number; name: string; avatar: string },
|
conversationInfo: {
|
||||||
|
type: number
|
||||||
|
targetId: number
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
silent?: boolean // 调用方按 friend / group store 当前 silent 传入;未传不动会话已有值
|
||||||
|
},
|
||||||
messageInfo: Message
|
messageInfo: Message
|
||||||
) {
|
) {
|
||||||
// 0. 群广播事件旁路:按 type 局部更新 groupStore 的 role / ownerUserId / 成员列表等状态
|
// 0. 群广播事件旁路:按 type 局部更新 groupStore 的 role / ownerUserId / 成员列表等状态
|
||||||
|
|
@ -477,19 +509,26 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.1 查找或自动创建会话;命中软删会话需要复活(场景:A 退群后被重新拉入、用户主动删了对话又收到新消息)
|
// 1.1 查找或自动创建会话;命中软删会话需要复活(场景:A 退群后被重新拉入、用户主动删了对话又收到新消息)
|
||||||
|
// silent 跟随调用方:新建写入;复活 / 已有会话仅在调用方明确传值时覆盖,避免本地 silent 与 friend / group store 漂移
|
||||||
let conversation = this.getConversation(conversationInfo.type, conversationInfo.targetId)
|
let conversation = this.getConversation(conversationInfo.type, conversationInfo.targetId)
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
conversation = this.createEmptyConversation(
|
conversation = this.createEmptyConversation(
|
||||||
conversationInfo.type,
|
conversationInfo.type,
|
||||||
conversationInfo.targetId,
|
conversationInfo.targetId,
|
||||||
conversationInfo.name,
|
conversationInfo.name,
|
||||||
conversationInfo.avatar
|
conversationInfo.avatar,
|
||||||
|
conversationInfo.silent
|
||||||
)
|
)
|
||||||
this.conversations.unshift(conversation)
|
this.conversations.unshift(conversation)
|
||||||
} else if (conversation.deleted) {
|
} else if (conversation.deleted) {
|
||||||
conversation.deleted = false
|
conversation.deleted = false
|
||||||
conversation.name = conversationInfo.name || conversation.name
|
conversation.name = conversationInfo.name || conversation.name
|
||||||
conversation.avatar = conversationInfo.avatar || conversation.avatar
|
conversation.avatar = conversationInfo.avatar || conversation.avatar
|
||||||
|
if (conversationInfo.silent !== undefined) {
|
||||||
|
conversation.silent = conversationInfo.silent
|
||||||
|
}
|
||||||
|
} else if (conversationInfo.silent !== undefined) {
|
||||||
|
conversation.silent = conversationInfo.silent
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.2 去重合并:优先按 id,其次按 clientMessageId。命中则覆盖更新并返回
|
// 1.2 去重合并:优先按 id,其次按 clientMessageId。命中则覆盖更新并返回
|
||||||
|
|
@ -507,7 +546,11 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
// 覆盖更新:与 ackMessage 走同一份 applyServerMessageUpdate;
|
// 覆盖更新:与 ackMessage 走同一份 applyServerMessageUpdate;
|
||||||
// WebSocket / pull 比 REST ack 先到的场景下,blob revoke 和 uploadProgress / _localFile 清理在这里完成
|
// WebSocket / pull 比 REST ack 先到的场景下,blob revoke 和 uploadProgress / _localFile 清理在这里完成
|
||||||
applyServerMessageUpdate(conversation.messages[existingIndex], messageInfo)
|
applyServerMessageUpdate(conversation.messages[existingIndex], messageInfo)
|
||||||
conversation.lastSendTime = messageInfo.sendTime || conversation.lastSendTime
|
// 仅合并到末尾那条时刷会话摘要 + 群 @ 标记;中间位置合并不动会话级 last*,避免后到的早消息把排序时间倒着拉回去
|
||||||
|
if (existingIndex === conversation.messages.length - 1) {
|
||||||
|
recomputeConversationLast(conversation)
|
||||||
|
syncConversationAtFlags(conversation, messageInfo)
|
||||||
|
}
|
||||||
this.updateMaxId(conversationInfo.type, messageInfo.id)
|
this.updateMaxId(conversationInfo.type, messageInfo.id)
|
||||||
this.saveConversations(conversation)
|
this.saveConversations(conversation)
|
||||||
return
|
return
|
||||||
|
|
@ -529,21 +572,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
conversation.lastSenderDisplayName = senderDisplayName
|
conversation.lastSenderDisplayName = senderDisplayName
|
||||||
|
|
||||||
// 2.2 群聊 @ 标记(仅对方消息 + 未读态有效)
|
// 2.2 群聊 @ 标记(仅对方消息 + 未读态有效)
|
||||||
if (
|
syncConversationAtFlags(conversation, messageInfo)
|
||||||
!messageInfo.selfSend &&
|
|
||||||
conversation.type === ImConversationType.GROUP &&
|
|
||||||
messageInfo.atUserIds &&
|
|
||||||
messageInfo.atUserIds.length > 0 &&
|
|
||||||
messageInfo.status !== ImMessageStatus.READ
|
|
||||||
) {
|
|
||||||
const currentUserId = getCurrentUserId()
|
|
||||||
if (currentUserId && messageInfo.atUserIds.includes(currentUserId)) {
|
|
||||||
conversation.atMe = true
|
|
||||||
}
|
|
||||||
if (messageInfo.atUserIds.includes(IM_AT_ALL_USER_ID)) {
|
|
||||||
conversation.atAll = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2.3 未读数:非当前会话 + 非自己发送 + 普通消息 + 非已读 => +1
|
// 2.3 未读数:非当前会话 + 非自己发送 + 普通消息 + 非已读 => +1
|
||||||
const isActive =
|
const isActive =
|
||||||
|
|
@ -632,8 +661,9 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
let changed = false
|
let changed = false
|
||||||
for (const key in patch) {
|
for (const key in patch) {
|
||||||
if (
|
if (
|
||||||
Object.prototype.hasOwnProperty.call(patch, key)
|
Object.prototype.hasOwnProperty.call(patch, key) &&
|
||||||
&& (patch as Record<string, unknown>)[key] !== (message as unknown as Record<string, unknown>)[key]
|
(patch as Record<string, unknown>)[key] !==
|
||||||
|
(message as unknown as Record<string, unknown>)[key]
|
||||||
) {
|
) {
|
||||||
changed = true
|
changed = true
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -449,7 +449,8 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
type: ImConversationType.PRIVATE,
|
type: ImConversationType.PRIVATE,
|
||||||
targetId: peerId,
|
targetId: peerId,
|
||||||
name: peerDisplayName || String(peerId),
|
name: peerDisplayName || String(peerId),
|
||||||
avatar: friend?.avatar || ''
|
avatar: friend?.avatar || '',
|
||||||
|
silent: friend?.silent
|
||||||
},
|
},
|
||||||
message
|
message
|
||||||
)
|
)
|
||||||
|
|
@ -561,7 +562,8 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
type: ImConversationType.GROUP,
|
type: ImConversationType.GROUP,
|
||||||
targetId: websocketMessage.groupId,
|
targetId: websocketMessage.groupId,
|
||||||
name: group?.name || String(websocketMessage.groupId),
|
name: group?.name || String(websocketMessage.groupId),
|
||||||
avatar: group?.avatar || ''
|
avatar: group?.avatar || '',
|
||||||
|
silent: group?.silent
|
||||||
},
|
},
|
||||||
message
|
message
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue