fix(im): 修复重新登录会话未读闪烁
- 新增会话读位置本地存储,独立维护 conversationReads - 启动时先恢复本地读位置,并在会话列表渲染前修正未读状态 - 消息入库时基于读位置过滤已读历史消息,避免重新累计未读 - READ 同步与主动已读统一走 conversationStore,保证读位置单调推进 - 兼容旧会话 readMessageId 数据迁移pull/884/MERGE
parent
8ba76813ae
commit
07c8f143ea
|
|
@ -19,7 +19,6 @@ import {
|
||||||
pullChannelMessages as apiPullChannelMessages,
|
pullChannelMessages as apiPullChannelMessages,
|
||||||
type ImChannelMessageRespVO
|
type ImChannelMessageRespVO
|
||||||
} from '@/api/im/message/channel'
|
} from '@/api/im/message/channel'
|
||||||
import { pullMyConversationReadList as apiPullMyConversationReadList } from '@/api/im/conversation/read'
|
|
||||||
import {
|
import {
|
||||||
ImConversationType,
|
ImConversationType,
|
||||||
ImMessageStatus,
|
ImMessageStatus,
|
||||||
|
|
@ -34,8 +33,7 @@ import {
|
||||||
} from '../../utils/config'
|
} from '../../utils/config'
|
||||||
import { buildChannelConversationStub } from '../../utils/channel'
|
import { buildChannelConversationStub } from '../../utils/channel'
|
||||||
import { generateClientMessageId, getPrivateMessagePeerId } from '../../utils/message'
|
import { generateClientMessageId, getPrivateMessagePeerId } from '../../utils/message'
|
||||||
import { runIncrementalPull, runMinIdPull } from '../../utils/pull'
|
import { runMinIdPull } from '../../utils/pull'
|
||||||
import { StorageKeys } from '../../utils/db'
|
|
||||||
import { getCurrentUserId } from '@/utils/auth'
|
import { getCurrentUserId } from '@/utils/auth'
|
||||||
import type { Message } from '../types'
|
import type { Message } from '../types'
|
||||||
|
|
||||||
|
|
@ -285,25 +283,6 @@ export const useMessagePuller = () => {
|
||||||
wsStore.discardBuffer()
|
wsStore.discardBuffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 增量拉取我的会话读位置并合并到本地展示态 */
|
|
||||||
const pullConversationReads = async (isActive: () => boolean): Promise<void> => {
|
|
||||||
await runIncrementalPull(
|
|
||||||
StorageKeys.settings.conversationReadPullCursor,
|
|
||||||
apiPullMyConversationReadList,
|
|
||||||
async (records) => {
|
|
||||||
if (!isActive()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
await messageStore.applyConversationReadList(records, isActive)
|
|
||||||
if (!isActive()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
isActive
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态事件补偿:好友 / 好友申请走增量;群列表和群申请红点走快照刷新
|
* 状态事件补偿:好友 / 好友申请走增量;群列表和群申请红点走快照刷新
|
||||||
*
|
*
|
||||||
|
|
@ -315,6 +294,7 @@ export const useMessagePuller = () => {
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
friendStore.pullFriends(),
|
friendStore.pullFriends(),
|
||||||
friendStore.pullFriendRequests(),
|
friendStore.pullFriendRequests(),
|
||||||
|
conversationStore.pullConversationReads(),
|
||||||
groupStore.fetchGroupList(true),
|
groupStore.fetchGroupList(true),
|
||||||
groupRequestStore.pullGroupRequests(),
|
groupRequestStore.pullGroupRequests(),
|
||||||
groupRequestStore.fetchUnhandledGroupRequestList()
|
groupRequestStore.fetchUnhandledGroupRequestList()
|
||||||
|
|
@ -415,12 +395,6 @@ export const useMessagePuller = () => {
|
||||||
// pull + replay 都完成后再排序,避免回放消息打乱顺序
|
// pull + replay 都完成后再排序,避免回放消息打乱顺序
|
||||||
conversationStore.sortConversationList()
|
conversationStore.sortConversationList()
|
||||||
|
|
||||||
// 消息和缓冲帧落库后再补读位置,避免读位置游标先推进导致新消息展示态漏更新
|
|
||||||
await pullConversationReads(isCurrentPull)
|
|
||||||
if (!isCurrentPull()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重连 / 冷启动后补齐当前激活私聊会话的「对方已读位置」
|
// 重连 / 冷启动后补齐当前激活私聊会话的「对方已读位置」
|
||||||
// 离线期间错过的 RECEIPT 推送会被这里补回;其他私聊会话等用户点开时由 Index.vue 的 watch 触发
|
// 离线期间错过的 RECEIPT 推送会被这里补回;其他私聊会话等用户点开时由 Index.vue 的 watch 触发
|
||||||
// 私聊已读关闭时跳过,避免打到已禁用接口触发错误日志
|
// 私聊已读关闭时跳过,避免打到已禁用接口触发错误日志
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,6 @@ export const useMessageSender = () => {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 本地标记已读:未读数清零(UI 立刻响应)
|
|
||||||
conversationStore.markConversationRead(conversation.type, conversation.targetId)
|
|
||||||
const loadedMaxMessageId = messageStore
|
const loadedMaxMessageId = messageStore
|
||||||
.getMessages(getClientConversationId(conversation.type, conversation.targetId))
|
.getMessages(getClientConversationId(conversation.type, conversation.targetId))
|
||||||
.reduce<number>(
|
.reduce<number>(
|
||||||
|
|
@ -239,6 +237,8 @@ export const useMessageSender = () => {
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const maxMessageId = Math.max(loadedMaxMessageId, conversation.lastMessageId || 0)
|
const maxMessageId = Math.max(loadedMaxMessageId, conversation.lastMessageId || 0)
|
||||||
|
// 本地标记已读:未读数清零(UI 立刻响应)
|
||||||
|
conversationStore.markConversationRead(conversation.type, conversation.targetId, maxMessageId)
|
||||||
if (!maxMessageId) {
|
if (!maxMessageId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,11 +125,16 @@ onMounted(async () => {
|
||||||
.pullFriendRequests()
|
.pullFriendRequests()
|
||||||
.catch((e) => console.warn('[IM] 后台增量拉好友申请失败', e))
|
.catch((e) => console.warn('[IM] 后台增量拉好友申请失败', e))
|
||||||
|
|
||||||
// 3. 实时通信:建 WebSocket 长连接 + 拉离线消息(pullOnce finally 把 loading 归位)
|
// 3. 会话读位置先补偿,消息入库时可直接过滤已读历史消息
|
||||||
|
await conversationStore
|
||||||
|
.pullConversationReads()
|
||||||
|
.catch((e) => console.warn('[IM] 拉取会话读位置失败', e))
|
||||||
|
|
||||||
|
// 4. 实时通信:建 WebSocket 长连接 + 拉离线消息(pullOnce finally 把 loading 归位)
|
||||||
webSocketStore.connect()
|
webSocketStore.connect()
|
||||||
await pullOnce()
|
await pullOnce()
|
||||||
|
|
||||||
// 4. 默认选中第一个会话;若置顶分组处于折叠态,需跳过被折叠隐藏的置顶项,避免自动展开折叠
|
// 5. 默认选中第一个会话;若置顶分组处于折叠态,需跳过被折叠隐藏的置顶项,避免自动展开折叠
|
||||||
const sorted = conversationStore.getSortedConversationList
|
const sorted = conversationStore.getSortedConversationList
|
||||||
const firstVisible = pickFirstVisibleConversation(sorted)
|
const firstVisible = pickFirstVisibleConversation(sorted)
|
||||||
if (firstVisible && !conversationStore.activeConversation) {
|
if (firstVisible && !conversationStore.activeConversation) {
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,33 @@ import { debounce } from 'lodash-es'
|
||||||
import { store } from '@/store'
|
import { store } from '@/store'
|
||||||
|
|
||||||
import { CONVERSATION_RECENT_FORWARD_MAX } from '../../utils/config'
|
import { CONVERSATION_RECENT_FORWARD_MAX } from '../../utils/config'
|
||||||
import { ImConversationType } from '../../utils/constants'
|
import {
|
||||||
|
ImConversationType,
|
||||||
|
ImMessageReceiptStatus,
|
||||||
|
ImMessageStatus,
|
||||||
|
isNormalMessage
|
||||||
|
} from '../../utils/constants'
|
||||||
import { getClientConversationId, getDb, StorageKeys, type DbTransaction } from '../../utils/db'
|
import { getClientConversationId, getDb, StorageKeys, type DbTransaction } from '../../utils/db'
|
||||||
|
import { runIncrementalPull } from '../../utils/pull'
|
||||||
import { getCurrentUserId } from '@/utils/auth'
|
import { getCurrentUserId } from '@/utils/auth'
|
||||||
import { useMessageStore } from './messageStore'
|
import { useMessageStore } from './messageStore'
|
||||||
import type { Conversation, ConversationDO } from '../types'
|
import {
|
||||||
|
pullMyConversationReadList as apiPullMyConversationReadList,
|
||||||
|
type ImConversationReadRespVO
|
||||||
|
} from '@/api/im/conversation/read'
|
||||||
|
import type {
|
||||||
|
Conversation,
|
||||||
|
ConversationDO,
|
||||||
|
ConversationRead,
|
||||||
|
ConversationReadDO,
|
||||||
|
MessageDO
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
const PERSIST_DRAFT_DEBOUNCE_MS = 500
|
const PERSIST_DRAFT_DEBOUNCE_MS = 500
|
||||||
const pendingDraftConversations = new Set<Conversation>()
|
const pendingDraftConversations = new Set<Conversation>()
|
||||||
|
|
||||||
|
type LegacyConversationDO = ConversationDO & { readMessageId?: number }
|
||||||
|
|
||||||
/** 会话转 IndexedDB 记录 */
|
/** 会话转 IndexedDB 记录 */
|
||||||
function toConversationDO(conversation: Conversation): ConversationDO {
|
function toConversationDO(conversation: Conversation): ConversationDO {
|
||||||
const draft = conversation.draft
|
const draft = conversation.draft
|
||||||
|
|
@ -42,14 +60,59 @@ function toConversationDO(conversation: Conversation): ConversationDO {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IndexedDB 记录转会话 */
|
/** IndexedDB 记录转会话 */
|
||||||
function fromConversationDO(conversation: ConversationDO): Conversation {
|
function fromConversationDO(conversation: LegacyConversationDO): Conversation {
|
||||||
const { clientConversationId: _clientConversationId, ...rest } = conversation
|
const {
|
||||||
|
clientConversationId: _clientConversationId,
|
||||||
|
readMessageId: _readMessageId,
|
||||||
|
...rest
|
||||||
|
} = conversation
|
||||||
return rest
|
return rest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 会话读位置转 IndexedDB 记录 */
|
||||||
|
function toConversationReadDO(record: ConversationRead): ConversationReadDO {
|
||||||
|
return {
|
||||||
|
conversationType: record.conversationType,
|
||||||
|
targetId: record.targetId,
|
||||||
|
messageId: record.messageId,
|
||||||
|
updateTime: record.updateTime,
|
||||||
|
clientConversationId: getClientConversationId(record.conversationType, record.targetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IndexedDB 记录转会话读位置 */
|
||||||
|
function fromConversationReadDO(record: ConversationReadDO): ConversationRead {
|
||||||
|
const { clientConversationId: _clientConversationId, ...rest } = record
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否为有效会话读位置 */
|
||||||
|
function isValidConversationReadRecord(record: ImConversationReadRespVO): boolean {
|
||||||
|
return !!record.conversationType && !!record.targetId && !!record.messageId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取对方普通消息最大编号 */
|
||||||
|
function getMaxIncomingNormalMessageId(
|
||||||
|
messages: Array<Pick<MessageDO, 'id' | 'selfSend' | 'type' | 'status'>>
|
||||||
|
): number {
|
||||||
|
return messages.reduce((maxMessageId, message) => {
|
||||||
|
if (
|
||||||
|
message.id &&
|
||||||
|
!message.selfSend &&
|
||||||
|
isNormalMessage(message.type) &&
|
||||||
|
message.status !== ImMessageStatus.RECALL &&
|
||||||
|
message.id > maxMessageId
|
||||||
|
) {
|
||||||
|
return message.id
|
||||||
|
}
|
||||||
|
return maxMessageId
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
export const useConversationStore = defineStore('imConversationStore', {
|
export const useConversationStore = defineStore('imConversationStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
conversations: [] as Conversation[], // 全量会话列表(私聊 + 群聊 + 频道)
|
conversations: [] as Conversation[], // 全量会话列表(私聊 + 群聊 + 频道)
|
||||||
|
conversationReads: {} as Record<string, ConversationRead>, // 会话读位置
|
||||||
activeConversation: null as Conversation | null, // 当前激活的会话
|
activeConversation: null as Conversation | null, // 当前激活的会话
|
||||||
loading: false, // 是否正在批量加载
|
loading: false, // 是否正在批量加载
|
||||||
recentForwardConversationKeys: [] as string[] // 最近转发会话 key 列表
|
recentForwardConversationKeys: [] as string[] // 最近转发会话 key 列表
|
||||||
|
|
@ -83,7 +146,13 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
(type: number, targetId: number): Conversation | undefined =>
|
(type: number, targetId: number): Conversation | undefined =>
|
||||||
state.conversations.find(
|
state.conversations.find(
|
||||||
(conversation) => conversation.type === type && conversation.targetId === targetId
|
(conversation) => conversation.type === type && conversation.targetId === targetId
|
||||||
)
|
),
|
||||||
|
|
||||||
|
/** 查找会话读位置 */
|
||||||
|
getConversationRead:
|
||||||
|
(state) =>
|
||||||
|
(type: number, targetId: number): ConversationRead | undefined =>
|
||||||
|
state.conversationReads[getClientConversationId(type, targetId)]
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
@ -101,11 +170,42 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
this.clear()
|
this.clear()
|
||||||
// 2. 从 IndexedDB 读取会话和轻量设置
|
// 2. 从 IndexedDB 读取会话和轻量设置
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
const [conversations, recent] = await Promise.all([
|
const [conversations, conversationReads, recent] = await Promise.all([
|
||||||
db.getAll<ConversationDO>('conversations'),
|
db.getAll<ConversationDO>('conversations'),
|
||||||
|
db.getAll<ConversationReadDO>('conversationReads'),
|
||||||
db.getSetting<string[]>(StorageKeys.settings.recentForwardConversationKeys)
|
db.getSetting<string[]>(StorageKeys.settings.recentForwardConversationKeys)
|
||||||
])
|
])
|
||||||
this.conversations = conversations.map(fromConversationDO)
|
const nextConversationReads: Record<string, ConversationRead> = {}
|
||||||
|
for (const record of conversationReads) {
|
||||||
|
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)
|
||||||
|
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)) {
|
if (Array.isArray(recent)) {
|
||||||
this.recentForwardConversationKeys = recent.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
|
this.recentForwardConversationKeys = recent.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
|
||||||
}
|
}
|
||||||
|
|
@ -126,10 +226,236 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
saveDraftConversationListDebounced.cancel()
|
saveDraftConversationListDebounced.cancel()
|
||||||
pendingDraftConversations.clear()
|
pendingDraftConversations.clear()
|
||||||
this.conversations = []
|
this.conversations = []
|
||||||
|
this.conversationReads = {}
|
||||||
this.activeConversation = null
|
this.activeConversation = null
|
||||||
this.recentForwardConversationKeys = []
|
this.recentForwardConversationKeys = []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 持久化会话读位置 */
|
||||||
|
async saveConversationReadRecord(
|
||||||
|
target: ConversationRead | ConversationRead[] | null | undefined,
|
||||||
|
tx?: DbTransaction
|
||||||
|
): Promise<void> {
|
||||||
|
const records = (Array.isArray(target) ? target : target ? [target] : []).map(
|
||||||
|
toConversationReadDO
|
||||||
|
)
|
||||||
|
if (records.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const db = getDb()
|
||||||
|
if (tx) {
|
||||||
|
for (const record of records) {
|
||||||
|
await db.put('conversationReads', record, tx)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await db.transaction(['conversationReads'], 'readwrite', async (tx) => {
|
||||||
|
for (const record of records) {
|
||||||
|
await db.put('conversationReads', record, tx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 应用本地会话读位置 */
|
||||||
|
async applyLocalConversationReads(conversations?: Conversation[]) {
|
||||||
|
const targetConversations = conversations || this.conversations
|
||||||
|
const changedConversations: Conversation[] = []
|
||||||
|
for (const conversation of targetConversations) {
|
||||||
|
const record = this.getConversationRead(conversation.type, conversation.targetId)
|
||||||
|
if (!record) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (this.applyReadToConversation(conversation, record.messageId)) {
|
||||||
|
changedConversations.push(conversation)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (conversation.unreadCount === 0 && !conversation.atMe && !conversation.atAll) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const messages = await getDb().getAllByIndex<MessageDO>(
|
||||||
|
'messages',
|
||||||
|
'clientConversationId',
|
||||||
|
getClientConversationId(conversation.type, conversation.targetId)
|
||||||
|
)
|
||||||
|
const maxIncomingMessageId = getMaxIncomingNormalMessageId(messages)
|
||||||
|
if (maxIncomingMessageId > 0 && maxIncomingMessageId <= record.messageId) {
|
||||||
|
conversation.unreadCount = 0
|
||||||
|
conversation.atMe = false
|
||||||
|
conversation.atAll = false
|
||||||
|
changedConversations.push(conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changedConversations.length > 0) {
|
||||||
|
await this.saveConversationRecord(changedConversations)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 判断消息是否已被会话读位置覆盖 */
|
||||||
|
isMessageCoveredByReadPosition(
|
||||||
|
conversation: Pick<Conversation, 'type' | 'targetId'>,
|
||||||
|
message?: { id?: number } | null
|
||||||
|
): boolean {
|
||||||
|
if (!message?.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const record = this.getConversationRead(conversation.type, conversation.targetId)
|
||||||
|
return !!record && message.id <= record.messageId
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 应用读位置到会话 */
|
||||||
|
applyReadToConversation(conversation: Conversation, messageId: number): boolean {
|
||||||
|
if (!conversation.lastMessageId || conversation.lastMessageId > messageId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (conversation.unreadCount === 0 && !conversation.atMe && !conversation.atAll) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
conversation.unreadCount = 0
|
||||||
|
conversation.atMe = false
|
||||||
|
conversation.atAll = false
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 应用会话读位置 */
|
||||||
|
async applyConversationReadList(
|
||||||
|
records: ImConversationReadRespVO[],
|
||||||
|
isActive?: () => boolean
|
||||||
|
): Promise<void> {
|
||||||
|
if (records.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const changedReads = new Map<string, ConversationRead>()
|
||||||
|
const changedConversations = new Map<string, Conversation>()
|
||||||
|
const changedMessages = new Map<string, MessageDO>()
|
||||||
|
const db = getDb()
|
||||||
|
const messageStore = useMessageStore()
|
||||||
|
|
||||||
|
// 1. 按读位置更新会话未读和频道已读态
|
||||||
|
for (const record of records) {
|
||||||
|
if (isActive && !isActive()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isValidConversationReadRecord(record)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const clientConversationId = getClientConversationId(
|
||||||
|
record.conversationType,
|
||||||
|
record.targetId
|
||||||
|
)
|
||||||
|
let storedMessages: MessageDO[] | undefined
|
||||||
|
const getStoredMessages = async () => {
|
||||||
|
if (!storedMessages) {
|
||||||
|
storedMessages = await db.getAllByIndex<MessageDO>(
|
||||||
|
'messages',
|
||||||
|
'clientConversationId',
|
||||||
|
clientConversationId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return storedMessages
|
||||||
|
}
|
||||||
|
const current = this.conversationReads[clientConversationId]
|
||||||
|
const messageId = Math.max(record.messageId, current?.messageId || 0)
|
||||||
|
if (!current || messageId > current.messageId) {
|
||||||
|
const next = {
|
||||||
|
conversationType: record.conversationType,
|
||||||
|
targetId: record.targetId,
|
||||||
|
messageId,
|
||||||
|
updateTime: record.updateTime
|
||||||
|
}
|
||||||
|
this.conversationReads[clientConversationId] = next
|
||||||
|
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) {
|
||||||
|
const maxIncomingMessageId = getMaxIncomingNormalMessageId(await getStoredMessages())
|
||||||
|
if (maxIncomingMessageId > 0 && maxIncomingMessageId <= messageId) {
|
||||||
|
conversation.unreadCount = 0
|
||||||
|
conversation.atMe = false
|
||||||
|
conversation.atAll = false
|
||||||
|
changedConversations.set(clientConversationId, conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (record.conversationType !== ImConversationType.CHANNEL) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const memoryMessages = messageStore.getMessages(clientConversationId)
|
||||||
|
for (const message of memoryMessages) {
|
||||||
|
if (
|
||||||
|
message.id &&
|
||||||
|
message.id <= messageId &&
|
||||||
|
message.receiptStatus !== ImMessageReceiptStatus.DONE
|
||||||
|
) {
|
||||||
|
message.receiptStatus = ImMessageReceiptStatus.DONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const message of await getStoredMessages()) {
|
||||||
|
if (
|
||||||
|
message.id &&
|
||||||
|
message.id <= messageId &&
|
||||||
|
message.receiptStatus !== ImMessageReceiptStatus.DONE
|
||||||
|
) {
|
||||||
|
message.receiptStatus = ImMessageReceiptStatus.DONE
|
||||||
|
changedMessages.set(message.messageKey, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 持久化本轮变更
|
||||||
|
if (
|
||||||
|
changedReads.size === 0 &&
|
||||||
|
changedConversations.size === 0 &&
|
||||||
|
changedMessages.size === 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isActive && !isActive()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const stores: Array<'conversationReads' | 'conversations' | 'messages'> = []
|
||||||
|
if (changedReads.size > 0) {
|
||||||
|
stores.push('conversationReads')
|
||||||
|
}
|
||||||
|
if (changedConversations.size > 0) {
|
||||||
|
stores.push('conversations')
|
||||||
|
}
|
||||||
|
if (changedMessages.size > 0) {
|
||||||
|
stores.push('messages')
|
||||||
|
}
|
||||||
|
await db.transaction(stores, 'readwrite', async (tx) => {
|
||||||
|
if (changedReads.size > 0) {
|
||||||
|
await this.saveConversationReadRecord([...changedReads.values()], tx)
|
||||||
|
}
|
||||||
|
if (changedConversations.size > 0) {
|
||||||
|
await this.saveConversationRecord([...changedConversations.values()], tx)
|
||||||
|
}
|
||||||
|
for (const message of changedMessages.values()) {
|
||||||
|
await db.put('messages', message, tx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 增量拉取会话读位置 */
|
||||||
|
async pullConversationReads(isActive?: () => boolean): Promise<void> {
|
||||||
|
await runIncrementalPull(
|
||||||
|
StorageKeys.settings.conversationReadPullCursor,
|
||||||
|
apiPullMyConversationReadList,
|
||||||
|
async (records) => {
|
||||||
|
if (isActive && !isActive()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
await this.applyConversationReadList(records, isActive)
|
||||||
|
if (isActive && !isActive()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
isActive
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
/** 执行会话记录持久化 */
|
/** 执行会话记录持久化 */
|
||||||
async saveConversationRecord(
|
async saveConversationRecord(
|
||||||
target: Conversation | Conversation[] | null | undefined,
|
target: Conversation | Conversation[] | null | undefined,
|
||||||
|
|
@ -326,17 +652,41 @@ export const useConversationStore = defineStore('imConversationStore', {
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 标记会话已读 */
|
/** 标记会话已读 */
|
||||||
markConversationRead(type: number, targetId: number) {
|
markConversationRead(type: number, targetId: number, messageId?: number) {
|
||||||
const conversation = this.getConversation(type, targetId)
|
const conversation = this.getConversation(type, targetId)
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (conversation.unreadCount === 0 && !conversation.atMe && !conversation.atAll) {
|
const key = getClientConversationId(type, targetId)
|
||||||
|
const current = this.conversationReads[key]
|
||||||
|
const readMessageIdAdvanced = !!messageId && messageId > (current?.messageId || 0)
|
||||||
|
if (
|
||||||
|
conversation.unreadCount === 0 &&
|
||||||
|
!conversation.atMe &&
|
||||||
|
!conversation.atAll &&
|
||||||
|
!readMessageIdAdvanced
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conversation.unreadCount = 0
|
conversation.unreadCount = 0
|
||||||
conversation.atMe = false
|
conversation.atMe = false
|
||||||
conversation.atAll = false
|
conversation.atAll = false
|
||||||
|
if (readMessageIdAdvanced) {
|
||||||
|
const record = {
|
||||||
|
conversationType: type,
|
||||||
|
targetId,
|
||||||
|
messageId,
|
||||||
|
updateTime: Date.now()
|
||||||
|
}
|
||||||
|
this.conversationReads[key] = record
|
||||||
|
void getDb()
|
||||||
|
.transaction(['conversations', 'conversationReads'], 'readwrite', async (tx) => {
|
||||||
|
await this.saveConversationRecord(conversation, tx)
|
||||||
|
await this.saveConversationReadRecord(record, tx)
|
||||||
|
})
|
||||||
|
.catch((e) => console.warn('[IM conversationStore] 会话已读写入失败', e))
|
||||||
|
return
|
||||||
|
}
|
||||||
this.saveConversation(conversation)
|
this.saveConversation(conversation)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import { getCurrentUserId } from '@/utils/auth'
|
||||||
import { isGroupQuit, tryGetSenderDisplayName } from '../../utils/user'
|
import { isGroupQuit, tryGetSenderDisplayName } from '../../utils/user'
|
||||||
import { useGroupStore } from './groupStore'
|
import { useGroupStore } from './groupStore'
|
||||||
import { useConversationStore } from './conversationStore'
|
import { useConversationStore } from './conversationStore'
|
||||||
import type { ImConversationReadRespVO } from '@/api/im/conversation/read'
|
|
||||||
import type { Conversation, Message, MessageDO } from '../types'
|
import type { Conversation, Message, MessageDO } from '../types'
|
||||||
|
|
||||||
const MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT = 5
|
const MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT = 5
|
||||||
|
|
@ -238,24 +237,6 @@ function isSameMessage(left: Message, right: Message): boolean {
|
||||||
return !!left.clientMessageId && left.clientMessageId === right.clientMessageId
|
return !!left.clientMessageId && left.clientMessageId === right.clientMessageId
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取对方普通消息最大编号 */
|
|
||||||
function getMaxIncomingNormalMessageId(
|
|
||||||
messages: Array<Pick<Message, 'id' | 'selfSend' | 'type' | 'status'>>
|
|
||||||
): number {
|
|
||||||
return messages.reduce((maxMessageId, message) => {
|
|
||||||
if (
|
|
||||||
message.id &&
|
|
||||||
!message.selfSend &&
|
|
||||||
isNormalMessage(message.type) &&
|
|
||||||
message.status !== ImMessageStatus.RECALL &&
|
|
||||||
message.id > maxMessageId
|
|
||||||
) {
|
|
||||||
return message.id
|
|
||||||
}
|
|
||||||
return maxMessageId
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMessageStore = defineStore('imMessageStore', {
|
export const useMessageStore = defineStore('imMessageStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
messagesByConversation: {} as Record<string, Message[]>,
|
messagesByConversation: {} as Record<string, Message[]>,
|
||||||
|
|
@ -527,6 +508,7 @@ export const useMessageStore = defineStore('imMessageStore', {
|
||||||
if (
|
if (
|
||||||
!message.selfSend &&
|
!message.selfSend &&
|
||||||
!isActive &&
|
!isActive &&
|
||||||
|
!conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
|
||||||
isNormalMessage(message.type) &&
|
isNormalMessage(message.type) &&
|
||||||
message.status !== ImMessageStatus.RECALL
|
message.status !== ImMessageStatus.RECALL
|
||||||
) {
|
) {
|
||||||
|
|
@ -632,6 +614,7 @@ export const useMessageStore = defineStore('imMessageStore', {
|
||||||
if (
|
if (
|
||||||
!message.selfSend &&
|
!message.selfSend &&
|
||||||
!isActive &&
|
!isActive &&
|
||||||
|
!conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
|
||||||
isNormalMessage(message.type) &&
|
isNormalMessage(message.type) &&
|
||||||
message.status !== ImMessageStatus.RECALL
|
message.status !== ImMessageStatus.RECALL
|
||||||
) {
|
) {
|
||||||
|
|
@ -844,127 +827,6 @@ export const useMessageStore = defineStore('imMessageStore', {
|
||||||
.catch((e) => console.warn('[IM messageStore] 回执写入失败', e))
|
.catch((e) => console.warn('[IM messageStore] 回执写入失败', e))
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 应用会话读位置补偿 */
|
|
||||||
async applyConversationReadList(
|
|
||||||
records: ImConversationReadRespVO[],
|
|
||||||
isActive?: () => boolean
|
|
||||||
): Promise<void> {
|
|
||||||
if (records.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const conversationStore = useConversationStore()
|
|
||||||
const changedConversations = new Map<string, Conversation>()
|
|
||||||
const changedMessages = new Map<string, MessageDO>()
|
|
||||||
const db = getDb()
|
|
||||||
|
|
||||||
// 1. 按读位置更新会话未读和频道已读态
|
|
||||||
for (const record of records) {
|
|
||||||
if (isActive && !isActive()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!record.conversationType || !record.targetId || !record.messageId) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const clientConversationId = getClientConversationId(
|
|
||||||
record.conversationType,
|
|
||||||
record.targetId
|
|
||||||
)
|
|
||||||
let storedMessages: MessageDO[] | undefined
|
|
||||||
const getStoredMessages = async () => {
|
|
||||||
if (!storedMessages) {
|
|
||||||
storedMessages = await db.getAllByIndex<MessageDO>(
|
|
||||||
'messages',
|
|
||||||
'clientConversationId',
|
|
||||||
clientConversationId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return storedMessages
|
|
||||||
}
|
|
||||||
const conversation = conversationStore.getConversation(
|
|
||||||
record.conversationType,
|
|
||||||
record.targetId
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
conversation &&
|
|
||||||
(conversation.unreadCount > 0 || conversation.atMe || conversation.atAll)
|
|
||||||
) {
|
|
||||||
const memoryMessages = this.messagesByConversation[clientConversationId]
|
|
||||||
let readCovered =
|
|
||||||
!!conversation.lastMessageId && conversation.lastMessageId <= record.messageId
|
|
||||||
const latestMessageLoaded =
|
|
||||||
!!conversation.lastMessageId &&
|
|
||||||
memoryMessages?.some((message) => message.id === conversation.lastMessageId)
|
|
||||||
if (!readCovered && latestMessageLoaded && memoryMessages) {
|
|
||||||
const maxIncomingMessageId = getMaxIncomingNormalMessageId(memoryMessages)
|
|
||||||
readCovered = maxIncomingMessageId > 0 && maxIncomingMessageId <= record.messageId
|
|
||||||
}
|
|
||||||
if (!readCovered && !latestMessageLoaded) {
|
|
||||||
const storedMessages = await getStoredMessages()
|
|
||||||
const latestMessageStored =
|
|
||||||
!!conversation.lastMessageId &&
|
|
||||||
storedMessages.some((message) => message.id === conversation.lastMessageId)
|
|
||||||
if (latestMessageStored) {
|
|
||||||
const storedMaxIncomingMessageId = getMaxIncomingNormalMessageId(storedMessages)
|
|
||||||
readCovered =
|
|
||||||
storedMaxIncomingMessageId > 0 && storedMaxIncomingMessageId <= record.messageId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (readCovered) {
|
|
||||||
conversation.unreadCount = 0
|
|
||||||
conversation.atMe = false
|
|
||||||
conversation.atAll = false
|
|
||||||
changedConversations.set(clientConversationId, conversation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (record.conversationType !== ImConversationType.CHANNEL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const memoryMessages = this.messagesByConversation[clientConversationId] || []
|
|
||||||
for (const message of memoryMessages) {
|
|
||||||
if (
|
|
||||||
message.id &&
|
|
||||||
message.id <= record.messageId &&
|
|
||||||
message.receiptStatus !== ImMessageReceiptStatus.DONE
|
|
||||||
) {
|
|
||||||
message.receiptStatus = ImMessageReceiptStatus.DONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const message of await getStoredMessages()) {
|
|
||||||
if (
|
|
||||||
message.id &&
|
|
||||||
message.id <= record.messageId &&
|
|
||||||
message.receiptStatus !== ImMessageReceiptStatus.DONE
|
|
||||||
) {
|
|
||||||
message.receiptStatus = ImMessageReceiptStatus.DONE
|
|
||||||
changedMessages.set(message.messageKey, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 持久化本轮变更
|
|
||||||
if (changedConversations.size === 0 && changedMessages.size === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isActive && !isActive()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const stores: Array<'conversations' | 'messages'> = []
|
|
||||||
if (changedConversations.size > 0) {
|
|
||||||
stores.push('conversations')
|
|
||||||
}
|
|
||||||
if (changedMessages.size > 0) {
|
|
||||||
stores.push('messages')
|
|
||||||
}
|
|
||||||
await db.transaction(stores, 'readwrite', async (tx) => {
|
|
||||||
if (changedConversations.size > 0) {
|
|
||||||
await conversationStore.saveConversationRecord([...changedConversations.values()], tx)
|
|
||||||
}
|
|
||||||
for (const message of changedMessages.values()) {
|
|
||||||
await db.put('messages', message, tx)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 前置历史消息 */
|
/** 前置历史消息 */
|
||||||
prependMessageList(conversationType: number, targetId: number, earlierMessages: Message[]) {
|
prependMessageList(conversationType: number, targetId: number, earlierMessages: Message[]) {
|
||||||
if (earlierMessages.length === 0) {
|
if (earlierMessages.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
|
|
||||||
/** 频道 READ:自己其它终端在某频道里标为已读,本端同步清零该频道未读 */
|
/** 频道 READ:自己其它终端在某频道里标为已读,本端同步清零该频道未读 */
|
||||||
handleChannelRead(websocketMessage: ImChannelMessageRespVO) {
|
handleChannelRead(websocketMessage: ImChannelMessageRespVO) {
|
||||||
void useMessageStore()
|
void useConversationStore()
|
||||||
.applyConversationReadList([
|
.applyConversationReadList([
|
||||||
{
|
{
|
||||||
id: websocketMessage.id,
|
id: websocketMessage.id,
|
||||||
|
|
@ -425,7 +425,8 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
// 窗口打开 = 已读:本端清未读 + 上报服务端读位置,避免读位置滞后
|
// 窗口打开 = 已读:本端清未读 + 上报服务端读位置,避免读位置滞后
|
||||||
conversationStore.markConversationRead(
|
conversationStore.markConversationRead(
|
||||||
ImConversationType.CHANNEL,
|
ImConversationType.CHANNEL,
|
||||||
websocketMessage.channelId
|
websocketMessage.channelId,
|
||||||
|
websocketMessage.id
|
||||||
)
|
)
|
||||||
apiReadChannelMessages(websocketMessage.channelId, websocketMessage.id).catch((e) => {
|
apiReadChannelMessages(websocketMessage.channelId, websocketMessage.id).catch((e) => {
|
||||||
console.warn('[IM WS] 频道自动已读上报失败', e)
|
console.warn('[IM WS] 频道自动已读上报失败', e)
|
||||||
|
|
@ -606,7 +607,11 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
// 聊天窗口打开 = 实际看到了:本端清未读;私聊已读开启时再上报后端,让对方 UI 立刻切到"已读"
|
// 聊天窗口打开 = 实际看到了:本端清未读;私聊已读开启时再上报后端,让对方 UI 立刻切到"已读"
|
||||||
// 已读位置直接用刚到的消息 id(这条就是当前会话最大 id)
|
// 已读位置直接用刚到的消息 id(这条就是当前会话最大 id)
|
||||||
conversationStore.markConversationRead(ImConversationType.PRIVATE, peerId)
|
conversationStore.markConversationRead(
|
||||||
|
ImConversationType.PRIVATE,
|
||||||
|
peerId,
|
||||||
|
websocketMessage.id
|
||||||
|
)
|
||||||
if (MESSAGE_PRIVATE_READ_ENABLED) {
|
if (MESSAGE_PRIVATE_READ_ENABLED) {
|
||||||
apiReadPrivateMessages(peerId, websocketMessage.id).catch((e) => {
|
apiReadPrivateMessages(peerId, websocketMessage.id).catch((e) => {
|
||||||
console.warn('[IM WS] 自动已读上报失败', e)
|
console.warn('[IM WS] 自动已读上报失败', e)
|
||||||
|
|
@ -628,7 +633,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
if (!websocketMessage.id || !websocketMessage.receiverId) {
|
if (!websocketMessage.id || !websocketMessage.receiverId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void useMessageStore()
|
void useConversationStore()
|
||||||
.applyConversationReadList([
|
.applyConversationReadList([
|
||||||
{
|
{
|
||||||
id: websocketMessage.id,
|
id: websocketMessage.id,
|
||||||
|
|
@ -741,7 +746,11 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
conversationStore.activeConversation?.targetId === websocketMessage.groupId
|
conversationStore.activeConversation?.targetId === websocketMessage.groupId
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
// 群已读上报需要带 messageId(群消息以"读到第几条"的游标为准,区别于私聊只标 receiverId);群已读关闭时仅本地清零
|
// 群已读上报需要带 messageId(群消息以"读到第几条"的游标为准,区别于私聊只标 receiverId);群已读关闭时仅本地清零
|
||||||
conversationStore.markConversationRead(ImConversationType.GROUP, websocketMessage.groupId)
|
conversationStore.markConversationRead(
|
||||||
|
ImConversationType.GROUP,
|
||||||
|
websocketMessage.groupId,
|
||||||
|
websocketMessage.id
|
||||||
|
)
|
||||||
if (MESSAGE_GROUP_READ_ENABLED) {
|
if (MESSAGE_GROUP_READ_ENABLED) {
|
||||||
apiReadGroupMessages(websocketMessage.groupId, websocketMessage.id).catch((e) => {
|
apiReadGroupMessages(websocketMessage.groupId, websocketMessage.id).catch((e) => {
|
||||||
console.warn('[IM WS] 自动已读上报失败', e)
|
console.warn('[IM WS] 自动已读上报失败', e)
|
||||||
|
|
@ -766,7 +775,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
if (!readMessageId || !websocketMessage.groupId) {
|
if (!readMessageId || !websocketMessage.groupId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void useMessageStore()
|
void useConversationStore()
|
||||||
.applyConversationReadList([
|
.applyConversationReadList([
|
||||||
{
|
{
|
||||||
id: readMessageId,
|
id: readMessageId,
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,17 @@ export interface ConversationDO extends Conversation {
|
||||||
clientConversationId: string // `${type}:${targetId}`
|
clientConversationId: string // `${type}:${targetId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConversationRead {
|
||||||
|
conversationType: number // 会话类型,对齐 ImConversationType
|
||||||
|
targetId: number // 会话目标编号
|
||||||
|
messageId: number // 当前用户已读到的最大消息编号
|
||||||
|
updateTime?: number // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationReadDO extends ConversationRead {
|
||||||
|
clientConversationId: string // `${conversationType}:${targetId}`
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageDO extends Omit<Message, 'uploadProgress' | '_localFile' | '_ackMerging'> {
|
export interface MessageDO extends Omit<Message, 'uploadProgress' | '_localFile' | '_ackMerging'> {
|
||||||
messageKey: string // `${conversationType}:${id}` 或 `client:${clientMessageId}`
|
messageKey: string // `${conversationType}:${id}` 或 `client:${clientMessageId}`
|
||||||
conversationType: number // 会话类型,对齐 ImConversationType
|
conversationType: number // 会话类型,对齐 ImConversationType
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import { getCurrentUserId } from '@/utils/auth'
|
||||||
import { ImConversationType } from './constants'
|
import { ImConversationType } from './constants'
|
||||||
import type { MessageDO, SettingDO } from '../home/types'
|
import type { MessageDO, SettingDO } from '../home/types'
|
||||||
|
|
||||||
export const DB_SCHEMA_VERSION = 1
|
export const DB_SCHEMA_VERSION = 2
|
||||||
|
|
||||||
export type DbStoreName =
|
export type DbStoreName =
|
||||||
| 'conversations'
|
| 'conversations'
|
||||||
|
| 'conversationReads'
|
||||||
| 'messages'
|
| 'messages'
|
||||||
| 'friends'
|
| 'friends'
|
||||||
| 'friendRequests'
|
| 'friendRequests'
|
||||||
|
|
@ -103,6 +104,12 @@ function upgradeSchema(db: IDBDatabase) {
|
||||||
const store = db.createObjectStore('conversations', { keyPath: 'clientConversationId' })
|
const store = db.createObjectStore('conversations', { keyPath: 'clientConversationId' })
|
||||||
createIndex(store, 'lastSendTime', 'lastSendTime')
|
createIndex(store, 'lastSendTime', 'lastSendTime')
|
||||||
}
|
}
|
||||||
|
if (!db.objectStoreNames.contains('conversationReads')) {
|
||||||
|
const store = db.createObjectStore('conversationReads', { keyPath: 'clientConversationId' })
|
||||||
|
createIndex(store, 'conversationType+targetId', ['conversationType', 'targetId'], {
|
||||||
|
unique: true
|
||||||
|
})
|
||||||
|
}
|
||||||
if (!db.objectStoreNames.contains('messages')) {
|
if (!db.objectStoreNames.contains('messages')) {
|
||||||
const store = db.createObjectStore('messages', { keyPath: 'messageKey' })
|
const store = db.createObjectStore('messages', { keyPath: 'messageKey' })
|
||||||
createIndex(store, 'clientConversationId', 'clientConversationId')
|
createIndex(store, 'clientConversationId', 'clientConversationId')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue