| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, // æ¯å¦æ£å¨æ¹éå è½½ |
| | | 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)) |
| | | } |