feat(im):优化已读上报补偿与群通话探测缓存
- 会话新增 readMessageId,记录已上报到服务端的最大已读消息编号 - readActive 与 WebSocket 自动已读改为基于服务端已上报读位置判断是否跳过接口 - read 接口成功后同步 readMessageId,失败时保留本端已读体验并允许后续重新进入补上报 - 拉取服务端 read 进度时同步更新会话 readMessageId,同时保持本地读位置单调合并 - 群信息新增 activeCallLoaded / activeCallExpired,首登与重连时失效群通话探测缓存 - 群通话胶囊在本地无通话且探测过期时懒加载 getActiveCall,避免离线错过通话后无法发现 - 群通话写入或移除时标记探测已加载,并避免通话探测状态写入 IndexedDB - 为 IndexedDB DO 类型补充存储结构注释im
parent
2172415cad
commit
b6e13c59c7
|
|
@ -67,6 +67,7 @@ import Icon from '@/components/Icon/src/Icon.vue'
|
|||
import UserAvatar from '../user/UserAvatar.vue'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { useRtcStore } from '../../store/rtcStore'
|
||||
import { useGroupStore } from '../../store/groupStore'
|
||||
import { useGroupCallMembers } from '../../composables/useGroupCallMembers'
|
||||
import { joinCall, getActiveCall } from '@/api/im/rtc'
|
||||
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||
|
|
@ -79,6 +80,7 @@ const props = defineProps<{
|
|||
defineOptions({ name: 'ImRtcGroupCallBanner' })
|
||||
|
||||
const rtcStore = useRtcStore()
|
||||
const groupStore = useGroupStore()
|
||||
const message = useMessage()
|
||||
|
||||
const popoverVisible = ref(false)
|
||||
|
|
@ -99,19 +101,43 @@ const pillText = computed(() => {
|
|||
* 用 [groupId, room] 双源监听 + 已填充守卫,避免切群 / 首次填充触发的双次重复拉取
|
||||
*/
|
||||
watch(
|
||||
() => [props.groupId, activeCall.value?.room] as const,
|
||||
() =>
|
||||
[
|
||||
props.groupId,
|
||||
activeCall.value?.room,
|
||||
groupStore.isGroupActiveCallExpired(props.groupId)
|
||||
] as const,
|
||||
async ([groupId, room], oldValues) => {
|
||||
if (!groupId || !activeCall.value) {
|
||||
if (!groupId) {
|
||||
return
|
||||
}
|
||||
|
||||
// 决策是否需要拉取:仅补齐本地已有通话;没有本地通话时等待实时事件创建
|
||||
if (!activeCall.value) {
|
||||
if (!groupStore.isGroupActiveCallExpired(groupId)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await getActiveCall(groupId)
|
||||
if (data) {
|
||||
rtcStore.setGroupCall(data, true)
|
||||
} else {
|
||||
rtcStore.removeGroupCall(groupId)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[GroupCallBanner] getActiveCall 失败', { groupId }, e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 决策是否需要拉取:补齐本地已有通话;没有本地通话时按群缓存过期状态懒探测一次
|
||||
const groupChanged = !oldValues || oldValues[0] !== groupId
|
||||
const roomChanged = oldValues && oldValues[1] !== room
|
||||
const participantsLoaded = (activeCall.value?.joinedUserIds?.length ?? 0) > 1
|
||||
const activeCallExpired = groupStore.isGroupActiveCallExpired(groupId)
|
||||
if (
|
||||
rtcStore.isGroupCallParticipantsLoaded(groupId, room) ||
|
||||
(!groupChanged && !roomChanged && participantsLoaded)
|
||||
!activeCallExpired &&
|
||||
(rtcStore.isGroupCallParticipantsLoaded(groupId, room) ||
|
||||
(!groupChanged && !roomChanged && participantsLoaded))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ export const useMessagePuller = () => {
|
|||
// 1. 清理连接级缓存
|
||||
messageStore.clearPrivateReadMaxIdCache()
|
||||
rtcStore.clearGroupCallCache()
|
||||
groupStore.markAllGroupActiveCallsExpired()
|
||||
groupStore.markAllGroupInfoExpired()
|
||||
groupStore.markAllGroupMembersExpired()
|
||||
// 2. 并发补偿远端状态
|
||||
|
|
|
|||
|
|
@ -237,12 +237,12 @@ export const useMessageSender = () => {
|
|||
0
|
||||
)
|
||||
const maxMessageId = Math.max(loadedMaxMessageId, conversation.lastMessageId || 0)
|
||||
const readCovered = conversationStore.isReadPositionCovered(
|
||||
const readReported = conversationStore.isReportedReadPositionCovered(
|
||||
conversation.type,
|
||||
conversation.targetId,
|
||||
maxMessageId
|
||||
)
|
||||
if (readCovered) {
|
||||
if (readReported) {
|
||||
conversationStore.markConversationRead(conversation.type, conversation.targetId)
|
||||
return
|
||||
}
|
||||
|
|
@ -272,6 +272,11 @@ export const useMessageSender = () => {
|
|||
} else {
|
||||
await apiReadChannelMessages(conversation.targetId, maxMessageId)
|
||||
}
|
||||
conversationStore.markConversationReadReported(
|
||||
conversation.type,
|
||||
conversation.targetId,
|
||||
maxMessageId
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'[IM] 标记已读失败',
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ onMounted(async () => {
|
|||
groupRequestStore.loadGroupRequestList()
|
||||
])
|
||||
childRouteReady.value = true
|
||||
groupStore.markAllGroupActiveCallsExpired()
|
||||
groupStore.markAllGroupMembersExpired()
|
||||
// 1.4 我管理的群下未处理加群申请红点:首登用 unhandled-list(服务端直接过滤未处理,语义精准、启动轻);
|
||||
// pullGroupRequests 只在重连 / 后续补偿时跑(见 useMessagePuller.pullStateEvents),不进首登主链路
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ import type {
|
|||
const PERSIST_DRAFT_DEBOUNCE_MS = 500
|
||||
const pendingDraftConversations = new Set<Conversation>()
|
||||
|
||||
type LegacyConversationDO = ConversationDO & { readMessageId?: number }
|
||||
|
||||
/** 创建会话读位置记录 */
|
||||
function createConversationRead(
|
||||
type: number,
|
||||
|
|
@ -63,6 +61,7 @@ function toConversationDO(conversation: Conversation): ConversationDO {
|
|||
lastReceiptStatus: conversation.lastReceiptStatus,
|
||||
lastSelfSend: conversation.lastSelfSend,
|
||||
lastSenderDisplayName: conversation.lastSenderDisplayName,
|
||||
readMessageId: conversation.readMessageId,
|
||||
deleted: conversation.deleted,
|
||||
top: conversation.top,
|
||||
silent: conversation.silent,
|
||||
|
|
@ -74,10 +73,9 @@ function toConversationDO(conversation: Conversation): ConversationDO {
|
|||
}
|
||||
|
||||
/** IndexedDB 记录转会话 */
|
||||
function fromConversationDO(conversation: LegacyConversationDO): Conversation {
|
||||
function fromConversationDO(conversation: ConversationDO): Conversation {
|
||||
const {
|
||||
clientConversationId: _clientConversationId,
|
||||
readMessageId: _readMessageId,
|
||||
...rest
|
||||
} = conversation
|
||||
return rest
|
||||
|
|
@ -194,32 +192,10 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
const item = fromConversationReadDO(record)
|
||||
nextConversationReads[getClientConversationId(item.conversationType, item.targetId)] = item
|
||||
}
|
||||
const migratedReads: ConversationRead[] = []
|
||||
for (const conversation of conversations as LegacyConversationDO[]) {
|
||||
if (!conversation.readMessageId) {
|
||||
continue
|
||||
}
|
||||
const key = getClientConversationId(conversation.type, conversation.targetId)
|
||||
if (nextConversationReads[key]) {
|
||||
continue
|
||||
}
|
||||
const record = {
|
||||
conversationType: conversation.type,
|
||||
targetId: conversation.targetId,
|
||||
messageId: conversation.readMessageId
|
||||
}
|
||||
nextConversationReads[key] = record
|
||||
migratedReads.push(record)
|
||||
}
|
||||
const nextConversations = (conversations as LegacyConversationDO[]).map(fromConversationDO)
|
||||
const nextConversations = conversations.map(fromConversationDO)
|
||||
this.conversationReads = nextConversationReads
|
||||
await this.applyLocalConversationReads(nextConversations)
|
||||
this.conversations = nextConversations
|
||||
if (migratedReads.length > 0) {
|
||||
void this.saveConversationReadRecord(migratedReads).catch((e) =>
|
||||
console.warn('[IM conversationStore] 会话读位置迁移失败', e)
|
||||
)
|
||||
}
|
||||
if (Array.isArray(recent)) {
|
||||
this.recentForwardConversationKeys = recent.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
|
||||
}
|
||||
|
|
@ -325,6 +301,15 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
return !!record && record.messageId >= messageId
|
||||
},
|
||||
|
||||
/** 判断服务端已读位置是否覆盖消息编号 */
|
||||
isReportedReadPositionCovered(type: number, targetId: number, messageId?: number): boolean {
|
||||
if (!messageId) {
|
||||
return false
|
||||
}
|
||||
const conversation = this.getConversation(type, targetId)
|
||||
return (conversation?.readMessageId || 0) >= messageId
|
||||
},
|
||||
|
||||
/** 应用读位置到会话 */
|
||||
applyReadToConversation(conversation: Conversation, messageId: number): boolean {
|
||||
if (!conversation.lastMessageId || conversation.lastMessageId > messageId) {
|
||||
|
|
@ -378,6 +363,14 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
}
|
||||
const current = this.conversationReads[clientConversationId]
|
||||
const messageId = Math.max(record.messageId, current?.messageId || 0)
|
||||
const conversation = this.getConversation(record.conversationType, record.targetId)
|
||||
if (
|
||||
conversation &&
|
||||
record.messageId > (conversation.readMessageId || 0)
|
||||
) {
|
||||
conversation.readMessageId = record.messageId
|
||||
changedConversations.set(clientConversationId, conversation)
|
||||
}
|
||||
if (!current || messageId > current.messageId) {
|
||||
const next = {
|
||||
conversationType: record.conversationType,
|
||||
|
|
@ -389,7 +382,6 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
changedReads.set(clientConversationId, next)
|
||||
}
|
||||
|
||||
const conversation = this.getConversation(record.conversationType, record.targetId)
|
||||
if (conversation && this.applyReadToConversation(conversation, messageId)) {
|
||||
changedConversations.set(clientConversationId, conversation)
|
||||
} else if (conversation) {
|
||||
|
|
@ -594,11 +586,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
if (!conversation) {
|
||||
return
|
||||
}
|
||||
// 1. 清理会话级未读状态
|
||||
conversation.unreadCount = 0
|
||||
conversation.atMe = false
|
||||
conversation.atAll = false
|
||||
// 2. 懒加载消息并保存会话摘要
|
||||
// 懒加载消息并保存会话摘要
|
||||
void useMessageStore().ensureConversationMessageListLoaded(conversation)
|
||||
this.saveConversation(conversation)
|
||||
},
|
||||
|
|
@ -719,6 +707,19 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
this.saveConversation(conversation)
|
||||
},
|
||||
|
||||
/** 标记会话已上报服务端读位置 */
|
||||
markConversationReadReported(type: number, targetId: number, messageId?: number): void {
|
||||
if (!messageId) {
|
||||
return
|
||||
}
|
||||
const conversation = this.getConversation(type, targetId)
|
||||
if (!conversation || messageId <= (conversation.readMessageId || 0)) {
|
||||
return
|
||||
}
|
||||
conversation.readMessageId = messageId
|
||||
this.saveConversation(conversation)
|
||||
},
|
||||
|
||||
// ==================== 最近转发 ====================
|
||||
|
||||
/** 推送最近转发会话 */
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ const pendingSingleMemberKey = (userId: number, groupId: number, memberUserId: n
|
|||
/** 构建群 IndexedDB 记录 */
|
||||
function buildGroupDO(group: Group): GroupDO {
|
||||
const {
|
||||
activeCallExpired: _activeCallExpired,
|
||||
activeCallLoaded: _activeCallLoaded,
|
||||
infoLoaded: _infoLoaded,
|
||||
members: _members,
|
||||
membersLoaded: _membersLoaded,
|
||||
|
|
@ -233,11 +235,13 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
this.groups = fresh.map((group) => {
|
||||
const existing = groupMap.get(group.id)
|
||||
if (!existing) {
|
||||
return { ...group, infoLoaded: true }
|
||||
return { ...group, activeCallExpired: true, infoLoaded: true }
|
||||
}
|
||||
return {
|
||||
...group,
|
||||
infoLoaded: true,
|
||||
activeCallExpired: existing.activeCallExpired,
|
||||
activeCallLoaded: existing.activeCallLoaded,
|
||||
members: existing.members,
|
||||
memberCount: existing.memberCount ?? group.memberCount,
|
||||
membersLoaded: existing.membersLoaded,
|
||||
|
|
@ -291,6 +295,29 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
}
|
||||
},
|
||||
|
||||
/** 失效全部群通话探测缓存 */
|
||||
markAllGroupActiveCallsExpired() {
|
||||
for (const group of this.groups) {
|
||||
group.activeCallExpired = true
|
||||
}
|
||||
},
|
||||
|
||||
/** 标记群通话探测已加载 */
|
||||
markGroupActiveCallLoaded(groupId: number) {
|
||||
const group = this.getGroup(groupId)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
group.activeCallLoaded = true
|
||||
group.activeCallExpired = false
|
||||
},
|
||||
|
||||
/** 判断群通话是否需要重新探测 */
|
||||
isGroupActiveCallExpired(groupId: number): boolean {
|
||||
const group = this.getGroup(groupId)
|
||||
return !group?.activeCallLoaded || !!group.activeCallExpired
|
||||
},
|
||||
|
||||
/** 失效指定群成员缓存 */
|
||||
markGroupMembersExpired(groupId: number) {
|
||||
const group = this.getGroup(groupId)
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ export const useRtcStore = defineStore('imRtc', () => {
|
|||
if (!payload?.groupId) {
|
||||
return
|
||||
}
|
||||
useGroupStore().markGroupActiveCallLoaded(payload.groupId)
|
||||
// 浅比较:room / mediaType / joinedUserIds / inviteeIds 都没变就跳过,避免下游 watcher 无意义重算
|
||||
const existing = groupActiveCalls.value.get(payload.groupId)
|
||||
const nextParticipantsLoaded = participantsLoaded ?? !!existing?.participantsLoaded
|
||||
|
|
@ -313,6 +314,7 @@ export const useRtcStore = defineStore('imRtc', () => {
|
|||
return
|
||||
}
|
||||
clearGroupCallCache(groupId)
|
||||
useGroupStore().markGroupActiveCallLoaded(groupId)
|
||||
}
|
||||
|
||||
/** 获取群当前活跃通话;用于胶囊条按 groupId 查询 */
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
)
|
||||
if (isActive) {
|
||||
// 窗口打开 = 已读:本端清未读 + 上报服务端读位置,避免读位置滞后
|
||||
const readCovered = conversationStore.isReadPositionCovered(
|
||||
const readReported = conversationStore.isReportedReadPositionCovered(
|
||||
ImConversationType.CHANNEL,
|
||||
websocketMessage.channelId,
|
||||
websocketMessage.id
|
||||
|
|
@ -433,10 +433,17 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
conversationStore.markConversationRead(
|
||||
ImConversationType.CHANNEL,
|
||||
websocketMessage.channelId,
|
||||
readCovered ? undefined : websocketMessage.id
|
||||
websocketMessage.id
|
||||
)
|
||||
if (!readCovered) {
|
||||
if (!readReported) {
|
||||
apiReadChannelMessages(websocketMessage.channelId, websocketMessage.id)
|
||||
.then(() =>
|
||||
conversationStore.markConversationReadReported(
|
||||
ImConversationType.CHANNEL,
|
||||
websocketMessage.channelId,
|
||||
websocketMessage.id
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
console.warn(
|
||||
'[IM WS] 频道自动已读上报失败',
|
||||
|
|
@ -630,7 +637,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
if (isActive) {
|
||||
// 聊天窗口打开 = 实际看到了:本端清未读;私聊已读开启时再上报后端,让对方 UI 立刻切到"已读"
|
||||
// 已读位置直接用刚到的消息 id(这条就是当前会话最大 id)
|
||||
const readCovered = conversationStore.isReadPositionCovered(
|
||||
const readReported = conversationStore.isReportedReadPositionCovered(
|
||||
ImConversationType.PRIVATE,
|
||||
peerId,
|
||||
websocketMessage.id
|
||||
|
|
@ -638,10 +645,17 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
conversationStore.markConversationRead(
|
||||
ImConversationType.PRIVATE,
|
||||
peerId,
|
||||
readCovered ? undefined : websocketMessage.id
|
||||
websocketMessage.id
|
||||
)
|
||||
if (MESSAGE_PRIVATE_READ_ENABLED && !readCovered) {
|
||||
if (MESSAGE_PRIVATE_READ_ENABLED && !readReported) {
|
||||
apiReadPrivateMessages(peerId, websocketMessage.id)
|
||||
.then(() =>
|
||||
conversationStore.markConversationReadReported(
|
||||
ImConversationType.PRIVATE,
|
||||
peerId,
|
||||
websocketMessage.id
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
console.warn(
|
||||
'[IM WS] 私聊自动已读上报失败',
|
||||
|
|
@ -785,7 +799,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
conversationStore.activeConversation?.targetId === websocketMessage.groupId
|
||||
if (isActive) {
|
||||
// 群已读上报需要带 messageId(群消息以"读到第几条"的游标为准,区别于私聊只标 receiverId);群已读关闭时仅本地清零
|
||||
const readCovered = conversationStore.isReadPositionCovered(
|
||||
const readReported = conversationStore.isReportedReadPositionCovered(
|
||||
ImConversationType.GROUP,
|
||||
websocketMessage.groupId,
|
||||
websocketMessage.id
|
||||
|
|
@ -793,10 +807,17 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
conversationStore.markConversationRead(
|
||||
ImConversationType.GROUP,
|
||||
websocketMessage.groupId,
|
||||
readCovered ? undefined : websocketMessage.id
|
||||
websocketMessage.id
|
||||
)
|
||||
if (MESSAGE_GROUP_READ_ENABLED && !readCovered) {
|
||||
if (MESSAGE_GROUP_READ_ENABLED && !readReported) {
|
||||
apiReadGroupMessages(websocketMessage.groupId, websocketMessage.id)
|
||||
.then(() =>
|
||||
conversationStore.markConversationReadReported(
|
||||
ImConversationType.GROUP,
|
||||
websocketMessage.groupId,
|
||||
websocketMessage.id
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
console.warn(
|
||||
'[IM WS] 群聊自动已读上报失败',
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export interface Conversation {
|
|||
silent?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
atMe?: boolean // 群聊:是否有人 @我
|
||||
atAll?: boolean // 群聊:是否有人 @全体成员
|
||||
readMessageId?: number // 已上报到服务端的最大已读消息编号
|
||||
draft?: {
|
||||
html: string // 输入框 HTML
|
||||
plain: string // 输入框纯文本
|
||||
|
|
@ -147,6 +148,7 @@ export interface Message {
|
|||
|
||||
// ==================== IndexedDB 本地存储结构 ====================
|
||||
|
||||
/** 会话 IndexedDB 存储结构 */
|
||||
export interface ConversationDO extends Conversation {
|
||||
clientConversationId: string // `${type}:${targetId}`
|
||||
}
|
||||
|
|
@ -158,16 +160,19 @@ export interface ConversationRead {
|
|||
updateTime?: number // 更新时间
|
||||
}
|
||||
|
||||
/** 会话读位置 IndexedDB 存储结构 */
|
||||
export interface ConversationReadDO extends ConversationRead {
|
||||
clientConversationId: string // `${conversationType}:${targetId}`
|
||||
}
|
||||
|
||||
/** 消息 IndexedDB 存储结构 */
|
||||
export interface MessageDO extends Omit<Message, 'uploadProgress' | '_localFile' | '_ackMerging'> {
|
||||
messageKey: string // `${conversationType}:${id}` 或 `client:${clientMessageId}`
|
||||
conversationType: number // 会话类型,对齐 ImConversationType
|
||||
clientConversationId: string // ConversationDO.clientConversationId
|
||||
}
|
||||
|
||||
/** 设置 IndexedDB 存储结构 */
|
||||
export interface SettingDO<T = unknown> {
|
||||
key: string
|
||||
value: T
|
||||
|
|
@ -195,12 +200,18 @@ export interface Group {
|
|||
groupRemark?: string // 群备注。从当前用户的 GroupMember 回填(当前用户对该群的自定义名)
|
||||
members?: GroupMember[] // 群成员缓存(按需懒加载)
|
||||
infoLoaded?: boolean // 群详情是否已加载,本轮会话内存标记,不持久化
|
||||
activeCallLoaded?: boolean // 群活跃通话是否已探测,本轮会话内存标记,不持久化
|
||||
activeCallExpired?: boolean // 群活跃通话探测是否已过期
|
||||
membersLoaded?: boolean // members 是否"完整加载"——只有整群 loadGroupMemberList / fetchGroupMemberList 命中时为 true;fetchGroupMember 单成员补齐不置位,避免 fetchGroupMemberList(force=false) 命中缓存时误判整群已加载
|
||||
membersExpired?: boolean // 群成员缓存是否已过期;重连 / 重新进入 IM 后只标记不删除,下次进入群会话再刷新
|
||||
memberCount?: number // 成员总数
|
||||
}
|
||||
|
||||
export type GroupDO = Omit<Group, 'infoLoaded' | 'members' | 'membersLoaded' | 'membersExpired'>
|
||||
/** 群 IndexedDB 存储结构 */
|
||||
export type GroupDO = Omit<
|
||||
Group,
|
||||
'activeCallExpired' | 'activeCallLoaded' | 'infoLoaded' | 'members' | 'membersLoaded' | 'membersExpired'
|
||||
>
|
||||
|
||||
// 群成员实体(前端内部结构)
|
||||
export interface GroupMember {
|
||||
|
|
@ -219,6 +230,7 @@ export interface GroupMember {
|
|||
isOwner?: boolean // 是否群主(前端从 Group.ownerUserId 计算)
|
||||
}
|
||||
|
||||
/** 群成员 IndexedDB 存储结构 */
|
||||
export type GroupMemberDO = GroupMember
|
||||
|
||||
// ==================== 好友 ====================
|
||||
|
|
@ -242,6 +254,7 @@ export interface Friend {
|
|||
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
|
||||
}
|
||||
|
||||
/** 好友 IndexedDB 存储结构 */
|
||||
export type FriendDO = Friend
|
||||
|
||||
/**
|
||||
|
|
@ -266,10 +279,13 @@ export interface FriendRequest {
|
|||
toAvatar?: string // 接收方头像
|
||||
}
|
||||
|
||||
/** 好友申请 IndexedDB 存储结构 */
|
||||
export type FriendRequestDO = FriendRequest
|
||||
|
||||
/** 加群申请 IndexedDB 存储结构 */
|
||||
export type GroupRequestDO = import('@/api/im/group/request').ImGroupRequestRespVO
|
||||
|
||||
/** 频道 IndexedDB 存储结构 */
|
||||
export type ChannelDO = import('@/api/im/manager/channel').ImManagerChannelVO
|
||||
|
||||
// ==================== 用户名片 ====================
|
||||
|
|
|
|||
Loading…
Reference in New Issue