From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块
---
src/views/im/home/store/conversationStore.ts | 910 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 910 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/store/conversationStore.ts b/src/views/im/home/store/conversationStore.ts
new file mode 100644
index 0000000..a799ffc
--- /dev/null
+++ b/src/views/im/home/store/conversationStore.ts
@@ -0,0 +1,910 @@
+import type {
+ Conversation,
+ ConversationDO,
+ ConversationRead,
+ ConversationReadDO,
+ MessageDO
+} from '../types'
+
+import type { ImConversationReadApi } from '#/api/im/conversation/read'
+
+import { acceptHMRUpdate, defineStore } from 'pinia'
+
+import { pullMyConversationReadList as apiPullMyConversationReadList } from '#/api/im/conversation/read'
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { CONVERSATION_RECENT_FORWARD_MAX } from '../../utils/config'
+import {
+ ImConversationType,
+ ImMessageReceiptStatus,
+ ImMessageStatus,
+ isNormalMessage
+} from '../../utils/constants'
+import { type DbTransaction, getClientConversationId, getDb, StorageKeys } from '../../utils/db'
+import { runIncrementalPull } from '../../utils/pull'
+import { useMessageStore } from './messageStore'
+
+const PERSIST_DRAFT_DEBOUNCE_MS = 500
+const pendingDraftConversations = new Set<Conversation>()
+
+/** 鍒涘缓浼氳瘽璇讳綅缃褰� */
+function createConversationRead(
+ type: number,
+ targetId: number,
+ messageId: number
+): ConversationRead {
+ return {
+ conversationType: type,
+ targetId,
+ messageId,
+ updateTime: Date.now()
+ }
+}
+
+/** 鍒涘缓鑽夌淇濆瓨闃叉姈鍑芥暟 */
+function createDraftDebounce(fn: () => void, wait: number) {
+ let timer: ReturnType<typeof setTimeout> | undefined
+
+ const run = () => {
+ if (timer) {
+ clearTimeout(timer)
+ timer = undefined
+ }
+ fn()
+ }
+ const debounced = () => {
+ if (timer) {
+ clearTimeout(timer)
+ }
+ timer = setTimeout(run, wait)
+ }
+ debounced.cancel = () => {
+ if (timer) {
+ clearTimeout(timer)
+ timer = undefined
+ }
+ }
+ debounced.flush = run
+ return debounced
+}
+
+/** 浼氳瘽杞� IndexedDB 璁板綍 */
+function toConversationDO(conversation: Conversation): ConversationDO {
+ const draft = conversation.draft
+ return {
+ targetId: conversation.targetId,
+ type: conversation.type,
+ name: conversation.name,
+ avatar: conversation.avatar,
+ unreadCount: conversation.unreadCount,
+ lastContent: conversation.lastContent,
+ lastSendTime: conversation.lastSendTime,
+ lastSenderId: conversation.lastSenderId,
+ lastMessageType: conversation.lastMessageType,
+ lastMessageId: conversation.lastMessageId,
+ lastClientMessageId: conversation.lastClientMessageId,
+ lastMessageStatus: conversation.lastMessageStatus,
+ lastReceiptStatus: conversation.lastReceiptStatus,
+ lastSelfSend: conversation.lastSelfSend,
+ lastSenderDisplayName: conversation.lastSenderDisplayName,
+ reportedReadMessageId: conversation.reportedReadMessageId,
+ deleted: conversation.deleted,
+ top: conversation.top,
+ silent: conversation.silent,
+ atMe: conversation.atMe,
+ atAll: conversation.atAll,
+ draft: draft ? { ...draft, reply: draft.reply ? { ...draft.reply } : undefined } : undefined,
+ clientConversationId: getClientConversationId(conversation.type, conversation.targetId)
+ }
+}
+
+/** IndexedDB 璁板綍杞細璇� */
+function fromConversationDO(conversation: ConversationDO): Conversation {
+ const {
+ clientConversationId: _clientConversationId,
+ ...rest
+ } = conversation
+ 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: ImConversationReadApi.ConversationReadRespVO): boolean {
+ return !!record.conversationType && !!record.targetId && !!record.messageId
+}
+
+/** 鑾峰彇瀵规柟鏅�氭秷鎭渶澶х紪鍙� */
+function getMaxIncomingNormalMessageId(
+ messages: Array<Pick<MessageDO, 'id' | 'selfSend' | 'status' | 'type'>>
+): number {
+ let maxMessageId = 0
+ for (const message of messages) {
+ if (
+ message.id &&
+ !message.selfSend &&
+ isNormalMessage(message.type) &&
+ message.status !== ImMessageStatus.RECALL &&
+ message.id > maxMessageId
+ ) {
+ maxMessageId = message.id
+ }
+ }
+ return maxMessageId
+}
+
+export const useConversationStore = defineStore('imConversationStore', {
+ state: () => ({
+ conversations: [] as Conversation[], // 鍏ㄩ噺浼氳瘽鍒楄〃锛堢鑱� + 缇よ亰 + 棰戦亾锛�
+ conversationReads: {} as Record<string, ConversationRead>, // 浼氳瘽璇讳綅缃�
+ activeConversation: null as Conversation | null, // 褰撳墠婵�娲荤殑浼氳瘽
+ loading: false, // 鏄惁姝e湪鎵归噺鍔犺浇
+ recentForwardConversationKeys: [] as string[] // 鏈�杩戣浆鍙戜細璇� key 鍒楄〃
+ }),
+
+ getters: {
+ /** 鎺掑簭鍚庣殑浼氳瘽鍒楄〃 */
+ getSortedConversationList(state): Conversation[] {
+ return state.conversations
+ .filter((conversation) => !conversation.deleted)
+ .toSorted((a, b) => {
+ const aTop = a.top ? 1 : 0
+ const bTop = b.top ? 1 : 0
+ if (aTop !== bTop) {
+ return bTop - aTop
+ }
+ return (b.lastSendTime || 0) - (a.lastSendTime || 0)
+ })
+ },
+
+ /** 鏈鎬绘暟 */
+ getTotalUnreadCount(state): number {
+ return state.conversations
+ .filter((conversation) => !conversation.deleted && !conversation.silent)
+ .reduce((sum, conversation) => sum + (conversation.unreadCount || 0), 0)
+ },
+
+ /** 鏌ユ壘浼氳瘽 */
+ getConversation:
+ (state) =>
+ (type: number, targetId: number): Conversation | undefined =>
+ state.conversations.find(
+ (conversation) => conversation.type === type && conversation.targetId === targetId
+ ),
+
+ /** 鏌ユ壘浼氳瘽璇讳綅缃� */
+ getConversationRead:
+ (state) =>
+ (type: number, targetId: number): ConversationRead | undefined =>
+ state.conversationReads[getClientConversationId(type, targetId)]
+ },
+
+ actions: {
+ /** 鍔犺浇浼氳瘽 */
+ async loadConversationList() {
+ // 1. 娓呯悊鏃ц处鍙峰唴瀛�
+ const userId = getCurrentUserId()
+ if (!userId) {
+ this.clear()
+ return
+ }
+ const previousActiveKey = this.activeConversation
+ ? getClientConversationId(this.activeConversation.type, this.activeConversation.targetId)
+ : null
+ this.clear()
+ // 2. 浠� IndexedDB 璇诲彇浼氳瘽鍜岃交閲忚缃�
+ const db = getDb()
+ const [conversations, conversationReads, recent] = await Promise.all([
+ db.getAll<ConversationDO>('conversations'),
+ db.getAll<ConversationReadDO>('conversationReads'),
+ db.getSetting<string[]>(StorageKeys.settings.recentForwardConversationKeys)
+ ])
+ const nextConversationReads: Record<string, ConversationRead> = {}
+ for (const record of conversationReads) {
+ const item = fromConversationReadDO(record)
+ nextConversationReads[getClientConversationId(item.conversationType, item.targetId)] = item
+ }
+ const nextConversations = conversations.map((conversation) => fromConversationDO(conversation))
+ this.conversationReads = nextConversationReads
+ await this.applyLocalConversationReads(nextConversations)
+ this.conversations = nextConversations
+ if (Array.isArray(recent)) {
+ this.recentForwardConversationKeys = recent.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
+ }
+ // 3. 鎭㈠褰撳墠婵�娲讳細璇�
+ if (previousActiveKey) {
+ this.activeConversation =
+ this.conversations.find(
+ (conversation) =>
+ !conversation.deleted &&
+ getClientConversationId(conversation.type, conversation.targetId) ===
+ previousActiveKey
+ ) ?? null
+ }
+ },
+
+ /** 娓呯┖浼氳瘽鍐呭瓨 */
+ clear() {
+ saveDraftConversationListDebounced.cancel()
+ pendingDraftConversations.clear()
+ this.conversations = []
+ this.conversationReads = {}
+ this.activeConversation = null
+ this.recentForwardConversationKeys = []
+ },
+
+ /** 鎸佷箙鍖栦細璇濊浣嶇疆 */
+ async saveConversationReadRecord(
+ target: ConversationRead | ConversationRead[] | null | undefined,
+ tx?: DbTransaction
+ ): Promise<void> {
+ let targets: ConversationRead[] = []
+ if (Array.isArray(target)) {
+ targets = target
+ } else if (target) {
+ targets = [target]
+ }
+ const records = targets.map((record) => toConversationReadDO(record))
+ 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, 'targetId' | 'type'>,
+ message?: null | { id?: number }
+ ): boolean {
+ if (!message?.id) {
+ return false
+ }
+ const record = this.getConversationRead(conversation.type, conversation.targetId)
+ return !!record && message.id <= record.messageId
+ },
+
+ /** 鍒ゆ柇浼氳瘽璇讳綅缃槸鍚﹁鐩栨秷鎭紪鍙� */
+ isReadPositionCovered(type: number, targetId: number, messageId?: number): boolean {
+ if (!messageId) {
+ return false
+ }
+ const record = this.getConversationRead(type, targetId)
+ 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?.reportedReadMessageId || 0) >= 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: ImConversationReadApi.ConversationReadRespVO[],
+ 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)
+ const conversation = this.getConversation(record.conversationType, record.targetId)
+ if (conversation && record.messageId > (conversation.reportedReadMessageId || 0)) {
+ conversation.reportedReadMessageId = record.messageId
+ changedConversations.set(clientConversationId, conversation)
+ }
+ 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)
+ }
+
+ 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(
+ target: Conversation | Conversation[] | null | undefined,
+ tx?: DbTransaction
+ ): Promise<void> {
+ const db = getDb()
+ const conversations = (Array.isArray(target) ? target : (target ? [target] : [])).map(
+ (conversation) => toConversationDO(conversation)
+ )
+ if (conversations.length === 0) {
+ return
+ }
+ if (tx) {
+ for (const conversation of conversations) {
+ await db.put('conversations', conversation, tx)
+ }
+ return
+ }
+ await db.transaction(['conversations'], 'readwrite', async (tx) => {
+ for (const conversation of conversations) {
+ await db.put('conversations', conversation, tx)
+ }
+ })
+ },
+
+ /** 鎸佷箙鍖栧崟涓細璇� */
+ saveConversation(conversation: Conversation | null | undefined, tx?: DbTransaction): void {
+ if (!conversation) {
+ return
+ }
+ void this.saveConversationRecord(conversation, tx).catch((error) =>
+ console.warn('[IM conversationStore] 浼氳瘽鍐欏叆澶辫触', error)
+ )
+ },
+
+ /** 鎸佷箙鍖栦細璇濆垪琛� */
+ saveConversationList(conversations?: Conversation[] | null, tx?: DbTransaction): void {
+ if (this.loading && !tx) {
+ return
+ }
+ void this.saveConversationRecord(conversations || this.conversations, tx).catch((error) =>
+ console.warn('[IM conversationStore] 浼氳瘽鍐欏叆澶辫触', error)
+ )
+ },
+
+ /** 纭繚浼氳瘽瀛樺湪 */
+ ensureConversation(info: {
+ avatar: string
+ name: string
+ silent?: boolean
+ targetId: number
+ type: number
+ }): Conversation {
+ // 1. 鍒涘缓涓嶅瓨鍦ㄧ殑浼氳瘽
+ let conversation = this.getConversation(info.type, info.targetId)
+ if (!conversation) {
+ conversation = this.createEmptyConversation(
+ info.type,
+ info.targetId,
+ info.name,
+ info.avatar,
+ info.silent
+ )
+ this.conversations.unshift(conversation)
+ } else if (conversation.deleted) {
+ // 2. 鎭㈠杞垹闄や細璇�
+ conversation.deleted = false
+ conversation.name = info.name || conversation.name
+ conversation.avatar = info.avatar || conversation.avatar
+ if (info.silent !== undefined) {
+ conversation.silent = info.silent
+ }
+ } else {
+ // 3. 鍚屾浼氳瘽灞曠ず鍏冩暟鎹�
+ if (info.name) {
+ conversation.name = info.name
+ }
+ if (info.avatar) {
+ conversation.avatar = info.avatar
+ }
+ if (info.silent !== undefined) {
+ conversation.silent = info.silent
+ }
+ }
+ return conversation
+ },
+
+ /** 鎵撳紑鎴栧垱寤轰細璇� */
+ openConversation(
+ targetId: number,
+ type: number,
+ name: string,
+ avatar: string,
+ options?: { silent?: boolean }
+ ): Conversation {
+ // 1. 纭繚浼氳瘽鍦ㄥ垪琛ㄤ腑
+ const conversation = this.ensureConversation({
+ type,
+ targetId,
+ name,
+ avatar,
+ silent: options?.silent
+ })
+ // 2. 婵�娲讳細璇濆苟淇濆瓨
+ this.setActiveConversation(conversation)
+ this.saveConversation(conversation)
+ return conversation
+ },
+
+ /** 璁剧疆褰撳墠浼氳瘽 */
+ setActiveConversation(conversation: Conversation | null) {
+ this.activeConversation = conversation
+ if (!conversation) {
+ return
+ }
+ // 鎳掑姞杞芥秷鎭苟淇濆瓨浼氳瘽鎽樿
+ void useMessageStore().ensureConversationMessageListLoaded(conversation)
+ this.saveConversation(conversation)
+ },
+
+ /** 鍒涘缓绌轰細璇� */
+ createEmptyConversation(
+ type: number,
+ targetId: number,
+ name: string,
+ avatar: string,
+ silent = false
+ ): Conversation {
+ return {
+ targetId,
+ type,
+ name,
+ avatar,
+ lastContent: '',
+ lastSendTime: 0,
+ unreadCount: 0,
+ deleted: false,
+ top: false,
+ silent,
+ atMe: false,
+ atAll: false
+ }
+ },
+
+ /** 璁剧疆缃《 */
+ setConversationTop(type: number, targetId: number, top: boolean) {
+ const conversation = this.getConversation(type, targetId)
+ if (!conversation) {
+ return
+ }
+ conversation.top = top
+ this.saveConversation(conversation)
+ },
+
+ /** 璁剧疆鍏嶆墦鎵� */
+ setConversationSilent(type: number, targetId: number, silent: boolean) {
+ const conversation = this.getConversation(type, targetId)
+ if (!conversation) {
+ return
+ }
+ conversation.silent = silent
+ this.saveConversation(conversation)
+ },
+
+ /** 鍒犻櫎浼氳瘽 */
+ removeConversation(type: number, targetId: number) {
+ // 1. 鏍囪浼氳瘽鍒犻櫎
+ const conversation = this.getConversation(type, targetId)
+ if (!conversation) {
+ return
+ }
+ if (this.activeConversation === conversation) {
+ this.activeConversation = null
+ }
+ conversation.deleted = true
+ // 2. 鍒犻櫎浼氳瘽鍏宠仈鐨勬秷鎭拰鑽夌
+ useMessageStore().deleteConversationMessageList(type, targetId)
+ this.clearConversationDraft(conversation)
+ this.saveConversation(conversation)
+ },
+
+ /** 鍒犻櫎绉佽亰浼氳瘽 */
+ removePrivateConversation(friendId: number) {
+ this.removeConversation(ImConversationType.PRIVATE, friendId)
+ },
+
+ /** 鍒犻櫎缇よ亰浼氳瘽 */
+ removeGroupConversation(groupId: number) {
+ this.removeConversation(ImConversationType.GROUP, groupId)
+ },
+
+ /** 鏍囪浼氳瘽宸茶 */
+ markConversationRead(type: number, targetId: number, messageId?: number): void {
+ const conversation = this.getConversation(type, targetId)
+ if (!conversation) {
+ return
+ }
+ 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
+ }
+ conversation.unreadCount = 0
+ conversation.atMe = false
+ conversation.atAll = false
+ if (readMessageIdAdvanced) {
+ const record = createConversationRead(type, targetId, messageId)
+ this.conversationReads[key] = record
+ void getDb()
+ .transaction(['conversations', 'conversationReads'], 'readwrite', async (tx) => {
+ await this.saveConversationRecord(conversation, tx)
+ await this.saveConversationReadRecord(record, tx)
+ })
+ .catch((error) =>
+ console.warn(
+ '[IM conversationStore] 浼氳瘽宸茶鍐欏叆澶辫触',
+ {
+ conversationType: type,
+ targetId,
+ messageId,
+ conversationKey: key
+ },
+ error
+ )
+ )
+ return
+ }
+ this.saveConversation(conversation)
+ },
+
+ /** 鏍囪浼氳瘽宸蹭笂鎶ユ湇鍔$璇讳綅缃� */
+ markConversationReadReported(type: number, targetId: number, messageId?: number): void {
+ if (!messageId) {
+ return
+ }
+ const conversation = this.getConversation(type, targetId)
+ if (!conversation || messageId <= (conversation.reportedReadMessageId || 0)) {
+ return
+ }
+ conversation.reportedReadMessageId = messageId
+ this.saveConversation(conversation)
+ },
+
+ // ==================== 鏈�杩戣浆鍙� ====================
+
+ /** 鎺ㄩ�佹渶杩戣浆鍙戜細璇� */
+ pushRecentForwardConversationKeyList(keys: string[]) {
+ if (!keys || keys.length === 0) {
+ return
+ }
+ const merged = [...keys, ...this.recentForwardConversationKeys]
+ this.recentForwardConversationKeys = [...new Set(merged)].slice(
+ 0,
+ CONVERSATION_RECENT_FORWARD_MAX
+ )
+ this.saveRecentForwardConversationKeyList()
+ },
+
+ /** 绉婚櫎鏈�杩戣浆鍙戜細璇� */
+ removeRecentForwardConversationKey(key: string) {
+ const index = this.recentForwardConversationKeys.indexOf(key)
+ if (index === -1) {
+ return
+ }
+ this.recentForwardConversationKeys.splice(index, 1)
+ this.saveRecentForwardConversationKeyList()
+ },
+
+ /** 淇濆瓨鏈�杩戣浆鍙戜細璇� */
+ saveRecentForwardConversationKeyList() {
+ void getDb()
+ .setSetting(
+ StorageKeys.settings.recentForwardConversationKeys,
+ this.recentForwardConversationKeys.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
+ )
+ .catch((error) => console.warn('[IM conversationStore] 鏈�杩戣浆鍙戝垪琛ㄥ啓鍏ュけ璐�', error))
+ },
+
+ // ==================== 浼氳瘽缁存姢 ====================
+
+ /** 閲嶆帓浼氳瘽 */
+ sortConversationList() {
+ this.conversations.sort((a, b) => (b.lastSendTime || 0) - (a.lastSendTime || 0))
+ this.saveConversationList(this.conversations)
+ },
+
+ /** 鍚屾浼氳瘽灞曠ず鍏冩暟鎹� */
+ updateConversation(
+ type: number,
+ targetId: number,
+ info: { avatar?: string; name?: string; silent?: boolean }
+ ) {
+ const conversation = this.getConversation(type, targetId)
+ if (!conversation) {
+ return
+ }
+ let changed = false
+ if (info.name && conversation.name !== info.name) {
+ conversation.name = info.name
+ changed = true
+ }
+ if (info.avatar !== undefined && conversation.avatar !== info.avatar) {
+ conversation.avatar = info.avatar || ''
+ changed = true
+ }
+ if (info.silent !== undefined && conversation.silent !== info.silent) {
+ conversation.silent = info.silent
+ changed = true
+ }
+ if (changed) {
+ this.saveConversation(conversation)
+ }
+ },
+
+ // ==================== 鑽夌 ====================
+
+ /** 鑾峰彇鑽夌 */
+ getConversationDraft(conversation: { targetId: number; type: number; }): Conversation['draft'] | undefined {
+ return this.getConversation(conversation.type, conversation.targetId)?.draft
+ },
+
+ /** 璁剧疆鑽夌 */
+ setConversationDraft(
+ conversation: { targetId: number; type: number; },
+ snapshot: NonNullable<Conversation['draft']>
+ ): void {
+ if (!snapshot.plain.trim() && !snapshot.reply) {
+ this.clearConversationDraft(conversation)
+ return
+ }
+ const target = this.getConversation(conversation.type, conversation.targetId)
+ if (!target) {
+ return
+ }
+ target.draft = snapshot
+ this.scheduleConversationDraftSave(target)
+ },
+
+ /** 娓呴櫎鑽夌 */
+ clearConversationDraft(conversation: { targetId: number; type: number; }): void {
+ const target = this.getConversation(conversation.type, conversation.targetId)
+ if (!target?.draft) {
+ return
+ }
+ target.draft = undefined
+ this.scheduleConversationDraftSave(target)
+ },
+
+ /** 璁剧疆鍥炲鑽夌 */
+ setConversationReplyDraft(
+ conversation: { targetId: number; type: number; },
+ quote: NonNullable<Conversation['draft']>['reply']
+ ) {
+ if (!quote) {
+ return
+ }
+ const existing = this.getConversationDraft(conversation)
+ this.setConversationDraft(conversation, {
+ html: existing?.html ?? '',
+ plain: existing?.plain ?? '',
+ reply: quote
+ })
+ },
+
+ /** 娓呴櫎鍥炲鑽夌 */
+ clearConversationReplyDraft(conversation: { targetId: number; type: number; }): void {
+ const existing = this.getConversationDraft(conversation)
+ if (!existing?.reply) {
+ return
+ }
+ this.setConversationDraft(conversation, { ...existing, reply: undefined })
+ },
+
+ /** 璋冨害鑽夌淇濆瓨 */
+ scheduleConversationDraftSave(conversation: Conversation): void {
+ pendingDraftConversations.add(conversation)
+ saveDraftConversationListDebounced()
+ },
+
+ /** 绔嬪嵆淇濆瓨鑽夌 */
+ flushConversationDraftSave(): void {
+ saveDraftConversationListDebounced.flush()
+ }
+ }
+})
+
+export const useConversationStoreWithOut = () => useConversationStore()
+
+/** 鍚堝苟鑽夌鍐欏叆 */
+const saveDraftConversationListDebounced = createDraftDebounce(() => {
+ const conversations = [...pendingDraftConversations]
+ pendingDraftConversations.clear()
+ if (conversations.length === 0) {
+ return
+ }
+ void useConversationStoreWithOut()
+ .saveConversationRecord(conversations)
+ .catch((error) => console.warn('[IM conversationStore] 鑽夌鍐欏叆澶辫触', error))
+}, PERSIST_DRAFT_DEBOUNCE_MS)
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useConversationStore, import.meta.hot))
+}
--
Gitblit v1.9.3