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/messageStore.ts | 953 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 953 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/store/messageStore.ts b/src/views/im/home/store/messageStore.ts
new file mode 100644
index 0000000..fa17b67
--- /dev/null
+++ b/src/views/im/home/store/messageStore.ts
@@ -0,0 +1,953 @@
+import type { Conversation, Message, MessageDO } from '../types'
+
+import { acceptHMRUpdate, defineStore } from 'pinia'
+
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import {
+ IM_AT_ALL_USER_ID,
+ ImContentType,
+ ImConversationType,
+ ImMessageReceiptStatus,
+ ImMessageStatus,
+ isGroupNotification,
+ isNormalMessage
+} from '../../utils/constants'
+import { resolveConversationLastContent } from '../../utils/conversation'
+import {
+ type DbTransaction,
+ getClientConversationId,
+ getClientMessageKey,
+ getDb,
+ getServerMessageKey,
+ parseClientConversationId,
+ setMessageMaxId,
+ StorageKeys
+} from '../../utils/db'
+import {
+ generateClientMessageId,
+ parseRecallMessageId,
+ revokeBlobUrlsInContent
+} from '../../utils/message'
+import { isGroupQuit, tryGetSenderDisplayName } from '../../utils/user'
+import { useConversationStore } from './conversationStore'
+import { useGroupStore } from './groupStore'
+
+const MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT = 5
+const MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT = MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT + 1
+const ackMergingPromises = new Map<string, Promise<void>>()
+
+interface MessageConversationInfo {
+ type: number
+ targetId: number
+ name: string
+ avatar: string
+ silent?: boolean
+}
+
+interface PersistMessageRecordOptions {
+ mergeClientRecord?: boolean
+}
+
+/** 鎷夊彇娑堟伅鎵归噺澶勭悊椤� */
+export type PulledMessage =
+ | {
+ conversationInfo: MessageConversationInfo
+ kind: 'insert'
+ message: Message
+ }
+ | {
+ conversationType: number
+ kind: 'recall'
+ recallSignalContent: string
+ targetId: number
+ }
+
+/** 鑾峰彇浼氳瘽鐨勬秷鎭紦瀛� key */
+function getMessageCacheKey(type: number, targetId: number): string {
+ return getClientConversationId(type, targetId)
+}
+
+/** 鐢熸垚娑堟伅鏈湴涓婚敭 */
+function getMessageKey(
+ message: Pick<Message, 'clientMessageId' | 'id'>,
+ conversationType: number
+): string {
+ return message.id
+ ? getServerMessageKey(conversationType, message.id)
+ : getClientMessageKey(message.clientMessageId)
+}
+
+/** 琛ラ綈瀹㈡埛绔秷鎭紪鍙� */
+function ensureClientMessageId(message: Message): Message {
+ if (!message.clientMessageId) {
+ message.clientMessageId = generateClientMessageId()
+ }
+ if (!message.id) {
+ message.id = undefined
+ }
+ return message
+}
+
+/** 杞崲涓� IndexedDB 娑堟伅璁板綍 */
+function buildMessageDO(message: Message, conversationType: number): MessageDO {
+ return {
+ id: message.id,
+ clientMessageId: message.clientMessageId,
+ type: message.type,
+ content: message.content,
+ status: message.status,
+ sendTime: message.sendTime,
+ senderId: message.senderId,
+ atUserIds: message.atUserIds ? [...message.atUserIds] : undefined,
+ receiverUserIds: message.receiverUserIds ? [...message.receiverUserIds] : undefined,
+ receiptStatus: message.receiptStatus,
+ readCount: message.readCount,
+ materialId: message.materialId,
+ targetId: message.targetId,
+ selfSend: message.selfSend,
+ messageKey: getMessageKey(message, conversationType),
+ conversationType,
+ clientConversationId: getClientConversationId(conversationType, message.targetId)
+ }
+}
+
+/** IndexedDB 娑堟伅璁板綍杞墠绔秷鎭� */
+function buildMessageFromDO(message: MessageDO): Message {
+ const {
+ messageKey: _messageKey,
+ conversationType: _conversationType,
+ clientConversationId: _clientConversationId,
+ ...rest
+ } = message
+ return rest
+}
+
+/** 绠楀嚭鏈潯娑堟伅鐨勫彂閫佷汉蹇収 */
+function deriveLastSenderDisplayName(
+ conversation: Conversation,
+ senderId: number
+): string | undefined {
+ // 1. 浼樺厛浣跨敤褰撳墠鍐呭瓨涓殑濂藉弸 / 缇ゆ垚鍛樹俊鎭�
+ const liveSenderName = tryGetSenderDisplayName(senderId, conversation.type, conversation.targetId)
+ if (liveSenderName) {
+ return liveSenderName
+ }
+ // 2. 缇ゆ垚鍛樼紦瀛樼己澶辨椂寮傛琛ラ綈
+ if (conversation.type === ImConversationType.GROUP) {
+ const groupStore = useGroupStore()
+ const group = groupStore.getGroup(conversation.targetId)
+ if (!group || isGroupQuit(group)) {
+ return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined
+ }
+ const fetchPromise =
+ group?.membersLoaded && !group.membersExpired
+ ? groupStore.fetchGroupMember(conversation.targetId, senderId)
+ : groupStore.fetchGroupMemberList(conversation.targetId)
+ fetchPromise.catch((error) =>
+ console.warn(
+ '[IM messageStore] 鍏滃簳鎷夌兢鎴愬憳澶辫触',
+ { groupId: conversation.targetId, senderId, fullFetch: !group?.membersLoaded },
+ error
+ )
+ )
+ }
+ return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined
+}
+
+/** 鎸夋秷鎭洿鏂颁細璇濇憳瑕� */
+function applyConversationSummary(conversation: Conversation, message: Message): void {
+ const senderDisplayName = deriveLastSenderDisplayName(conversation, message.senderId)
+ conversation.lastContent = resolveConversationLastContent(
+ message,
+ conversation.type,
+ conversation.targetId,
+ senderDisplayName
+ )
+ conversation.lastSendTime = message.sendTime || Date.now()
+ conversation.lastSenderId = message.senderId
+ conversation.lastMessageType = message.type
+ conversation.lastMessageId = message.id
+ conversation.lastClientMessageId = message.clientMessageId
+ conversation.lastMessageStatus = message.status
+ conversation.lastReceiptStatus = message.receiptStatus
+ conversation.lastSelfSend = message.selfSend
+ conversation.lastSenderDisplayName = senderDisplayName
+}
+
+/** 鎸夋湯鏉℃秷鎭噸绠椾細璇濇憳瑕� */
+function recomputeConversationLast(conversation: Conversation, messages: Message[]): void {
+ const last = messages[messages.length - 1]
+ if (last) {
+ applyConversationSummary(conversation, last)
+ return
+ }
+ conversation.lastContent = ''
+ conversation.lastSendTime = 0
+ conversation.lastSenderId = undefined
+ conversation.lastMessageType = undefined
+ conversation.lastMessageId = undefined
+ conversation.lastClientMessageId = undefined
+ conversation.lastMessageStatus = undefined
+ conversation.lastReceiptStatus = undefined
+ conversation.lastSelfSend = undefined
+ conversation.lastSenderDisplayName = undefined
+}
+
+/** 鍚屾缇� @ 鐘舵�� */
+function syncConversationAtFlags(conversation: Conversation, message: Message): void {
+ if (
+ message.selfSend ||
+ conversation.type !== ImConversationType.GROUP ||
+ !message.atUserIds ||
+ message.atUserIds.length === 0
+ ) {
+ return
+ }
+ const currentUserId = getCurrentUserId()
+ if (currentUserId && message.atUserIds.includes(currentUserId)) {
+ conversation.atMe = true
+ }
+ if (message.atUserIds.includes(IM_AT_ALL_USER_ID)) {
+ conversation.atAll = true
+ }
+}
+
+/** 搴旂敤鏈嶅姟绔秷鎭洿鏂� */
+function applyServerMessageUpdate(message: Message, updates: Partial<Message>): void {
+ if (updates.content && updates.content !== message.content) {
+ revokeBlobUrlsInContent(message.content)
+ }
+ Object.assign(message, updates)
+ if (updates.id === 0) {
+ message.id = undefined
+ }
+ if (updates.status !== undefined && updates.status !== ImMessageStatus.SENDING) {
+ message.uploadProgress = undefined
+ if (updates.status !== ImMessageStatus.FAILED) {
+ message._localFile = undefined
+ }
+ }
+}
+
+/** 鍒ゆ柇鏄惁涓哄悓涓�鏉℃秷鎭� */
+function isSameMessage(left: Message, right: Message): boolean {
+ if (left.id && right.id && left.id === right.id) {
+ return true
+ }
+ return !!left.clientMessageId && left.clientMessageId === right.clientMessageId
+}
+
+export const useMessageStore = defineStore('imMessageStore', {
+ state: () => ({
+ messagesByConversation: {} as Record<string, Message[]>,
+ loadedConversationKeys: [] as string[],
+ privateReadMaxIds: {} as Partial<Record<number, number>>,
+ privateMessageMaxId: 0,
+ groupMessageMaxId: 0,
+ channelMessageMaxId: 0
+ }),
+
+ getters: {
+ /** 鑾峰彇浼氳瘽宸插姞杞芥秷鎭� */
+ getMessages:
+ (state) =>
+ (clientConversationId: string): Message[] =>
+ state.messagesByConversation[clientConversationId] || []
+ },
+
+ actions: {
+ /** 娓呯┖娑堟伅鍐呭瓨 */
+ clear() {
+ Object.values(this.messagesByConversation).forEach((messages) => {
+ messages.forEach((message) => {
+ revokeBlobUrlsInContent(message.content)
+ message._localFile = undefined
+ })
+ })
+ this.messagesByConversation = {}
+ this.loadedConversationKeys = []
+ this.privateReadMaxIds = {}
+ this.privateMessageMaxId = 0
+ this.groupMessageMaxId = 0
+ this.channelMessageMaxId = 0
+ ackMergingPromises.clear()
+ },
+
+ /** 浠� settings 鍔犺浇娑堟伅娓告爣 */
+ async loadMessageCursorList() {
+ const db = getDb()
+ const [privateMaxId, groupMaxId, channelMaxId] = await Promise.all([
+ db.getSetting<number>(StorageKeys.settings.privateMessageMaxId),
+ db.getSetting<number>(StorageKeys.settings.groupMessageMaxId),
+ db.getSetting<number>(StorageKeys.settings.channelMessageMaxId)
+ ])
+ this.privateMessageMaxId = privateMaxId || 0
+ this.groupMessageMaxId = groupMaxId || 0
+ this.channelMessageMaxId = channelMaxId || 0
+ },
+
+ /** 鏇存柊鍐呭瓨娓告爣 */
+ updateMessageCursor(conversationType: number, messageId?: number) {
+ if (!messageId) {
+ return
+ }
+ if (conversationType === ImConversationType.PRIVATE && messageId > this.privateMessageMaxId) {
+ this.privateMessageMaxId = messageId
+ } else if (
+ conversationType === ImConversationType.GROUP &&
+ messageId > this.groupMessageMaxId
+ ) {
+ this.groupMessageMaxId = messageId
+ } else if (
+ conversationType === ImConversationType.CHANNEL &&
+ messageId > this.channelMessageMaxId
+ ) {
+ this.channelMessageMaxId = messageId
+ }
+ },
+
+ /** 鑾峰彇绉佽亰瀵规柟宸茶浣嶇疆缂撳瓨 */
+ getPrivateReadMaxId(peerId: number): number | undefined {
+ return this.privateReadMaxIds[peerId]
+ },
+
+ /** 鏇存柊绉佽亰瀵规柟宸茶浣嶇疆缂撳瓨 */
+ updatePrivateReadMaxId(peerId: number, maxReadId: null | number = 0): number {
+ if (!peerId) {
+ return 0
+ }
+ const nextMaxReadId = maxReadId || 0
+ const current = this.getPrivateReadMaxId(peerId)
+ if (current !== undefined && nextMaxReadId <= current) {
+ return current
+ }
+ this.privateReadMaxIds = { ...this.privateReadMaxIds, [peerId]: nextMaxReadId }
+ return nextMaxReadId
+ },
+
+ /** 娓呯┖绉佽亰瀵规柟宸茶浣嶇疆缂撳瓨 */
+ clearPrivateReadMaxIdCache(): void {
+ this.privateReadMaxIds = {}
+ },
+
+ /** 鏍囪浼氳瘽杩戞湡浣跨敤 */
+ touchConversationMessageCache(clientConversationId: string) {
+ this.loadedConversationKeys = [
+ clientConversationId,
+ ...this.loadedConversationKeys.filter((key) => key !== clientConversationId)
+ ]
+ // 淇濈暀褰撳墠娲昏穬浼氳瘽 + 鏈�杩戞墦寮�杩囩殑浼氳瘽
+ const retained = this.loadedConversationKeys.slice(0, MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT)
+ const removed = this.loadedConversationKeys.slice(MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT)
+ this.loadedConversationKeys = retained
+ removed.forEach((key) => {
+ Reflect.deleteProperty(this.messagesByConversation, key)
+ })
+ },
+
+ /** 鍔犺浇褰撳墠浼氳瘽鏈�杩戞秷鎭� */
+ async loadMoreMessageList(
+ clientConversationId: string,
+ beforeSendTime?: number,
+ limit = 50
+ ): Promise<Message[]> {
+ // 1. 浠� IndexedDB 鍊掑簭璇诲彇涓�椤碉紝杩斿洖鍓嶅凡鎸夋椂闂村崌搴忔帓鍒�
+ const list = await getDb().getMessageListByConversation(clientConversationId, {
+ beforeSendTime,
+ limit
+ })
+ // 2. 鍚堝苟鍒板唴瀛樼紦瀛橈紝杩囨护宸插瓨鍦ㄧ殑娑堟伅
+ const parsed = parseClientConversationId(clientConversationId)
+ if (!parsed) {
+ return []
+ }
+ const messages = list.map((message) => buildMessageFromDO(message))
+ const existing = this.messagesByConversation[clientConversationId] || []
+ const existingKeys = new Set(existing.map((message) => getMessageKey(message, parsed.type)))
+ const fresh = messages.filter(
+ (message) => !existingKeys.has(getMessageKey(message, parsed.type))
+ )
+ this.messagesByConversation[clientConversationId] = [...fresh, ...existing].toSorted(
+ (messageA, messageB) => (messageA.sendTime || 0) - (messageB.sendTime || 0)
+ )
+ this.touchConversationMessageCache(clientConversationId)
+ return fresh
+ },
+
+ /** 纭繚浼氳瘽娑堟伅宸插姞杞� */
+ async ensureConversationMessageListLoaded(conversation: Conversation) {
+ const key = getMessageCacheKey(conversation.type, conversation.targetId)
+ if (this.messagesByConversation[key]) {
+ this.touchConversationMessageCache(key)
+ return
+ }
+ await this.loadMoreMessageList(key)
+ },
+
+ /** 鑾峰彇鍐呭瓨娑堟伅鏁扮粍 */
+ getMessageList(conversationType: number, targetId: number): Message[] {
+ const key = getMessageCacheKey(conversationType, targetId)
+ if (!this.messagesByConversation[key]) {
+ this.messagesByConversation[key] = []
+ }
+ this.touchConversationMessageCache(key)
+ return this.messagesByConversation[key]
+ },
+
+ /** 鎸佷箙鍖栨秷鎭褰� */
+ async saveMessageRecord(
+ message: Message,
+ conversationType: number,
+ tx?: DbTransaction,
+ options?: PersistMessageRecordOptions
+ ) {
+ const db = getDb()
+ const next = buildMessageDO(message, conversationType)
+ // 鏈嶅姟绔� key 鏇挎崲 client key
+ if (options?.mergeClientRecord && message.id && message.clientMessageId) {
+ const existing = await db.getByIndex<MessageDO>(
+ 'messages',
+ 'clientMessageId',
+ message.clientMessageId,
+ tx
+ )
+ if (existing && existing.messageKey !== next.messageKey) {
+ await db.delete('messages', existing.messageKey, tx)
+ }
+ }
+ await db.put('messages', next, tx)
+ },
+
+ /** 淇濆瓨娑堟伅娓告爣 */
+ async saveMessageCursor(conversationType: number, messageId?: number, tx?: DbTransaction) {
+ await setMessageMaxId(conversationType, messageId, tx)
+ this.updateMessageCursor(conversationType, messageId)
+ },
+
+ /** 搴旂敤鎾ゅ洖鍒板唴瀛� */
+ applyRecallMessageInMemory(
+ conversationType: number,
+ targetId: number,
+ recallSignalContent: string
+ ) {
+ // 1. 瀹氫綅琚挙鍥炵殑鍘熸秷鎭�
+ const messageId = parseRecallMessageId(recallSignalContent)
+ if (!messageId) {
+ return null
+ }
+ const conversationStore = useConversationStore()
+ const conversation = conversationStore.getConversation(conversationType, targetId)
+ if (!conversation) {
+ return null
+ }
+ const messages = this.getMessageList(conversationType, targetId)
+ const message = messages.find((item) => item.id === messageId)
+ if (!message) {
+ return null
+ }
+ // 2. 鏇存柊娑堟伅鍜屼細璇濇憳瑕�
+ message.type = ImContentType.RECALL
+ message.status = ImMessageStatus.RECALL
+ message.content = ''
+ if (messages[messages.length - 1]?.id === messageId) {
+ recomputeConversationLast(conversation, messages)
+ }
+ return { conversation, message }
+ },
+
+ /** 鎵归噺鍐欏叆鎷夊彇娑堟伅 */
+ async applyPulledMessageList(
+ pulledMessages: PulledMessage[],
+ conversationType: number,
+ maxMessageId?: number
+ ) {
+ if (pulledMessages.length === 0) {
+ // 1. 绌烘壒娆″彧鎺ㄨ繘娓告爣
+ await this.saveMessageCursor(conversationType, maxMessageId)
+ return
+ }
+ const conversationStore = useConversationStore()
+ const persistedMessages = new Map<
+ string,
+ { conversationType: number; mergeClientRecord?: boolean; message: Message; }
+ >()
+ const changedConversations = new Map<string, Conversation>()
+
+ const addChanged = (
+ conversation: Conversation,
+ message: Message,
+ options?: PersistMessageRecordOptions
+ ) => {
+ const clientConversationId = getClientConversationId(
+ conversation.type,
+ conversation.targetId
+ )
+ changedConversations.set(clientConversationId, conversation)
+ persistedMessages.set(getMessageKey(message, conversation.type), {
+ message,
+ conversationType: conversation.type,
+ mergeClientRecord: options?.mergeClientRecord
+ })
+ }
+
+ // 1. 鍏堟洿鏂板唴瀛橈紝鏀堕泦闇�瑕佹寔涔呭寲鐨勬秷鎭拰浼氳瘽
+ for (const pulledMessage of pulledMessages) {
+ if (pulledMessage.kind === 'recall') {
+ // 1.1 鎾ゅ洖淇″彿鏇存柊鍘熸秷鎭�
+ const changed = this.applyRecallMessageInMemory(
+ pulledMessage.conversationType,
+ pulledMessage.targetId,
+ pulledMessage.recallSignalContent
+ )
+ if (changed) {
+ addChanged(changed.conversation, changed.message)
+ }
+ continue
+ }
+
+ const { conversationInfo } = pulledMessage
+ const hasServerClientMessageId = !!pulledMessage.message.clientMessageId
+ const message = ensureClientMessageId(pulledMessage.message)
+ // 1.2 纭繚浼氳瘽鍜屾秷鎭紦瀛樺瓨鍦�
+ const conversation = conversationStore.ensureConversation(conversationInfo)
+ const messages = this.getMessageList(conversationInfo.type, conversationInfo.targetId)
+ const existingIndex = messages.findIndex((existing) => isSameMessage(existing, message))
+ if (existingIndex !== -1) {
+ const existing = messages[existingIndex]
+ if (!existing) {
+ continue
+ }
+ // 1.3 宸插瓨鍦ㄦ秷鎭悎骞舵湇鍔$鐘舵��
+ applyServerMessageUpdate(existing, message)
+ if (existingIndex === messages.length - 1) {
+ recomputeConversationLast(conversation, messages)
+ syncConversationAtFlags(conversation, message)
+ }
+ addChanged(conversation, existing, {
+ mergeClientRecord: hasServerClientMessageId
+ })
+ continue
+ }
+
+ // 1.4 鏂版秷鎭洿鏂颁細璇濇憳瑕佸拰鏈鐘舵��
+ applyConversationSummary(conversation, message)
+ syncConversationAtFlags(conversation, message)
+ const isActive =
+ conversationStore.activeConversation?.type === conversationInfo.type &&
+ conversationStore.activeConversation?.targetId === conversationInfo.targetId
+ if (
+ !message.selfSend &&
+ !isActive &&
+ !conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
+ isNormalMessage(message.type) &&
+ message.status !== ImMessageStatus.RECALL
+ ) {
+ conversation.unreadCount++
+ }
+
+ // 1.5 鏂版秷鎭寜鏈嶅姟绔� id 鎻掑叆鍐呭瓨鍒楄〃
+ let insertIndex = messages.length
+ if (message.id) {
+ for (const [index, existing] of messages.entries()) {
+ if (existing.id && message.id < existing.id) {
+ insertIndex = index
+ break
+ }
+ }
+ }
+ messages.splice(insertIndex, 0, message)
+ addChanged(conversation, message, {
+ mergeClientRecord: hasServerClientMessageId && !!message.id
+ })
+ }
+
+ // 2. 鍗曚簨鍔″啓鍏ユ秷鎭�佷細璇濇憳瑕佸拰娓告爣
+ await getDb().transaction(
+ ['messages', 'conversations', 'settings'],
+ 'readwrite',
+ async (tx) => {
+ // 2.1 鍐欏叆鏈壒鍙樻洿娑堟伅
+ for (const item of persistedMessages.values()) {
+ await this.saveMessageRecord(item.message, item.conversationType, tx, {
+ mergeClientRecord: item.mergeClientRecord
+ })
+ }
+ // 2.2 鍐欏叆鏈壒鍙樻洿浼氳瘽
+ await conversationStore.saveConversationRecord([...changedConversations.values()], tx)
+ // 2.3 鍐欏叆鏈壒娓告爣
+ await setMessageMaxId(conversationType, maxMessageId, tx)
+ }
+ )
+ // 3. 鎸佷箙鍖栨垚鍔熷悗鎺ㄨ繘鍐呭瓨娓告爣
+ this.updateMessageCursor(conversationType, maxMessageId)
+ for (const item of persistedMessages.values()) {
+ this.updateMessageCursor(item.conversationType, item.message.id)
+ }
+ },
+
+ /** 鎻掑叆娑堟伅 */
+ insertMessage(
+ conversationInfo: MessageConversationInfo,
+ messageInfo: Message,
+ options?: { saveMaxId?: boolean }
+ ): Promise<void> {
+ const conversationStore = useConversationStore()
+ const hasIncomingClientMessageId = !!messageInfo.clientMessageId
+ const message = ensureClientMessageId(messageInfo)
+ // 1. 鍏堝鐞嗘秷鎭甫鏉ョ殑缇よ祫鏂欏彉鏇�
+ if (conversationInfo.type === ImConversationType.GROUP && isGroupNotification(message.type)) {
+ useGroupStore().applyGroupNotification(
+ conversationInfo.targetId,
+ message.type,
+ message.content
+ )
+ }
+
+ // 2. 纭繚浼氳瘽鍜屾秷鎭紦瀛樺瓨鍦�
+ const conversation = conversationStore.ensureConversation(conversationInfo)
+ const messages = this.getMessageList(conversationInfo.type, conversationInfo.targetId)
+ const existingIndex = messages.findIndex((item) => isSameMessage(item, message))
+ // 3. 宸插瓨鍦ㄦ秷鎭蛋瑕嗙洊鏇存柊
+ if (existingIndex !== -1) {
+ const existing = messages[existingIndex]
+ if (!existing) {
+ return Promise.resolve()
+ }
+ applyServerMessageUpdate(existing, message)
+ if (existingIndex === messages.length - 1) {
+ recomputeConversationLast(conversation, messages)
+ syncConversationAtFlags(conversation, message)
+ }
+ return getDb()
+ .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
+ await this.saveMessageRecord(existing, conversationInfo.type, tx, {
+ mergeClientRecord: hasIncomingClientMessageId
+ })
+ await conversationStore.saveConversationRecord(conversation, tx)
+ if (options?.saveMaxId !== false) {
+ await setMessageMaxId(conversationInfo.type, message.id, tx)
+ }
+ })
+ .catch((error) => {
+ console.error('[IM messageStore] 娑堟伅鍐欏叆澶辫触', error)
+ throw error
+ })
+ .then(() => {
+ this.updateMessageCursor(conversationInfo.type, message.id)
+ })
+ }
+
+ // 4. 鏂版秷鎭洿鏂颁細璇濇憳瑕佸拰鏈鐘舵��
+ applyConversationSummary(conversation, message)
+ syncConversationAtFlags(conversation, message)
+
+ const isActive =
+ conversationStore.activeConversation?.type === conversationInfo.type &&
+ conversationStore.activeConversation?.targetId === conversationInfo.targetId
+ if (
+ !message.selfSend &&
+ !isActive &&
+ !conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
+ isNormalMessage(message.type) &&
+ message.status !== ImMessageStatus.RECALL
+ ) {
+ conversation.unreadCount++
+ }
+
+ // 5. 鏂版秷鎭寜 id 鎻掑叆鍒板唴瀛樻暟缁�
+ let insertIndex = messages.length
+ if (message.id) {
+ for (const [index, existing] of messages.entries()) {
+ if (existing.id && message.id < existing.id) {
+ insertIndex = index
+ break
+ }
+ }
+ }
+ messages.splice(insertIndex, 0, message)
+ // 6. 鍗曚簨鍔″啓鍏ユ秷鎭�佷細璇濇憳瑕佸拰娓告爣
+ return getDb()
+ .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
+ await this.saveMessageRecord(message, conversationInfo.type, tx, {
+ mergeClientRecord: hasIncomingClientMessageId && !!message.id
+ })
+ await conversationStore.saveConversationRecord(conversation, tx)
+ if (options?.saveMaxId !== false) {
+ await setMessageMaxId(conversationInfo.type, message.id, tx)
+ }
+ })
+ .catch((error) => {
+ console.error('[IM messageStore] 娑堟伅鍐欏叆澶辫触', error)
+ throw error
+ })
+ .then(() => {
+ this.updateMessageCursor(conversationInfo.type, message.id)
+ })
+ },
+
+ /** ack 鍚堝苟 */
+ ackMessage(
+ conversationType: number,
+ targetId: number,
+ clientMessageId: string,
+ updates: Partial<Message>
+ ) {
+ const mergeKey = `${conversationType}:${targetId}:${clientMessageId}`
+ const existingPromise = ackMergingPromises.get(mergeKey)
+ if (existingPromise) {
+ return existingPromise
+ }
+ const promise = this.doAckMessage(
+ conversationType,
+ targetId,
+ clientMessageId,
+ updates
+ ).finally(() => {
+ ackMergingPromises.delete(mergeKey)
+ })
+ ackMergingPromises.set(mergeKey, promise)
+ return promise
+ },
+
+ /** 鎵ц ack 鍚堝苟 */
+ async doAckMessage(
+ conversationType: number,
+ targetId: number,
+ clientMessageId: string,
+ updates: Partial<Message>
+ ) {
+ // 1. 瀹氫綅寰呭悎骞舵秷鎭�
+ const conversationStore = useConversationStore()
+ const conversation = conversationStore.getConversation(conversationType, targetId)
+ if (!conversation) {
+ return
+ }
+ const messages = this.getMessageList(conversationType, targetId)
+ const message = messages.find((item) => item.clientMessageId === clientMessageId)
+ if (!message) {
+ return
+ }
+ message._ackMerging = true
+ try {
+ // 2. 鍚堝苟鏈嶅姟绔� ack 鍒板唴瀛�
+ applyServerMessageUpdate(message, updates)
+ if (messages[messages.length - 1] === message) {
+ recomputeConversationLast(conversation, messages)
+ }
+ // 3. 鍗曚簨鍔″啓鍏ユ秷鎭�佷細璇濇憳瑕佸拰娓告爣
+ await getDb()
+ .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
+ await this.saveMessageRecord(message, conversationType, tx, {
+ mergeClientRecord: true
+ })
+ await conversationStore.saveConversationRecord(conversation, tx)
+ await setMessageMaxId(conversationType, message.id, tx)
+ })
+ .catch((error) => {
+ console.error('[IM messageStore] ack 鍐欏叆澶辫触', error)
+ throw error
+ })
+ this.updateMessageCursor(conversationType, message.id)
+ } finally {
+ // 4. 娓呯悊鍚堝苟鏍囪
+ message._ackMerging = false
+ }
+ },
+
+ /** 灞�閮ㄦ洿鏂版秷鎭� */
+ patchMessage(
+ conversationType: number,
+ targetId: number,
+ clientMessageId: string,
+ patch: Partial<Message>
+ ) {
+ const message = this.getMessageList(conversationType, targetId).find(
+ (item) => item.clientMessageId === clientMessageId
+ )
+ if (!message) {
+ return
+ }
+ let changed = false
+ for (const key in patch) {
+ if (
+ Object.prototype.hasOwnProperty.call(patch, key) &&
+ (patch as Record<string, unknown>)[key] !==
+ (message as unknown as Record<string, unknown>)[key]
+ ) {
+ changed = true
+ break
+ }
+ }
+ if (changed) {
+ applyServerMessageUpdate(message, patch)
+ }
+ },
+
+ /** 鎾ゅ洖娑堟伅 */
+ async recallMessage(
+ conversationType: number,
+ targetId: number,
+ recallSignalContent: string
+ ): Promise<void> {
+ const conversationStore = useConversationStore()
+ const changed = this.applyRecallMessageInMemory(
+ conversationType,
+ targetId,
+ recallSignalContent
+ )
+ if (!changed) {
+ return
+ }
+ await getDb()
+ .transaction(['messages', 'conversations'], 'readwrite', async (tx) => {
+ await this.saveMessageRecord(changed.message, conversationType, tx)
+ await conversationStore.saveConversationRecord(changed.conversation, tx)
+ })
+ .catch((error) => {
+ console.error('[IM messageStore] 鎾ゅ洖娑堟伅鍐欏叆澶辫触', error)
+ throw error
+ })
+ },
+
+ /** 搴旂敤宸茶鍥炴墽 */
+ applyMessageReadReceipt(options: {
+ conversationType: number
+ groupMessageId?: number
+ privateReadMaxId?: number
+ readCount?: number
+ receiptStatus?: number
+ targetId: number
+ }) {
+ const messages = this.getMessageList(options.conversationType, options.targetId)
+ const changed: Message[] = []
+ // 1. 绉佽亰鍥炴墽鎵归噺鏇存柊鑷繁鍙戦�佺殑娑堟伅
+ if (options.conversationType === ImConversationType.PRIVATE && options.privateReadMaxId) {
+ this.updatePrivateReadMaxId(options.targetId, options.privateReadMaxId)
+ const privateReadMaxId = options.privateReadMaxId
+ messages.forEach((message) => {
+ if (
+ message.selfSend &&
+ message.id &&
+ message.id <= privateReadMaxId &&
+ message.receiptStatus === ImMessageReceiptStatus.PENDING
+ ) {
+ message.receiptStatus = ImMessageReceiptStatus.DONE
+ changed.push(message)
+ }
+ })
+ } else if (options.conversationType === ImConversationType.GROUP && options.groupMessageId) {
+ // 2. 缇よ亰鍥炴墽鏇存柊鍗曟潯娑堟伅
+ const message = messages.find((item) => item.id === options.groupMessageId)
+ if (message) {
+ if (options.readCount !== undefined) {
+ message.readCount = options.readCount
+ }
+ if (options.receiptStatus !== undefined) {
+ message.receiptStatus = options.receiptStatus
+ }
+ changed.push(message)
+ }
+ }
+ if (changed.length === 0) {
+ return
+ }
+ // 3. 鍗曚簨鍔″啓鍏ュ彉鏇存秷鎭�
+ void getDb()
+ .transaction(['messages'], 'readwrite', async (tx) => {
+ for (const message of changed) {
+ await this.saveMessageRecord(message, options.conversationType, tx)
+ }
+ })
+ .catch((error) => console.warn('[IM messageStore] 鍥炴墽鍐欏叆澶辫触', error))
+ },
+
+ /** 鍓嶇疆鍘嗗彶娑堟伅 */
+ prependMessageList(conversationType: number, targetId: number, earlierMessages: Message[]) {
+ if (earlierMessages.length === 0) {
+ return
+ }
+ const messages = this.getMessageList(conversationType, targetId)
+ const existingIds = new Set(messages.map((message) => message.id).filter(Boolean))
+ const fresh = earlierMessages
+ .map((message) => ensureClientMessageId(message))
+ .filter((message) => message.id && !existingIds.has(message.id))
+ .toSorted((messageA, messageB) => (messageA.id || 0) - (messageB.id || 0))
+ if (fresh.length === 0) {
+ return
+ }
+ const key = getMessageCacheKey(conversationType, targetId)
+ this.messagesByConversation[key] = [...fresh, ...messages]
+ void getDb()
+ .transaction(['messages'], 'readwrite', async (tx) => {
+ for (const message of fresh) {
+ await this.saveMessageRecord(message, conversationType, tx)
+ }
+ })
+ .catch((error) => console.warn('[IM messageStore] 鍘嗗彶娑堟伅鍐欏叆澶辫触', error))
+ },
+
+ /** 鍒犻櫎鍗曟潯娑堟伅 */
+ removeMessage(
+ conversationType: number,
+ targetId: number,
+ key: { clientMessageId?: string; id?: number; }
+ ) {
+ // 1. 瀹氫綅浼氳瘽鍜屾秷鎭�
+ const conversationStore = useConversationStore()
+ const conversation = conversationStore.getConversation(conversationType, targetId)
+ if (!conversation) {
+ return
+ }
+ const messages = this.getMessageList(conversationType, targetId)
+ const index = messages.findIndex((message) => {
+ if (key.id && message.id && message.id === key.id) {
+ return true
+ }
+ return !!key.clientMessageId && message.clientMessageId === key.clientMessageId
+ })
+ if (index === -1) {
+ return
+ }
+ // 2. 浠庡唴瀛樼Щ闄ゆ秷鎭�
+ const [removed] = messages.splice(index, 1)
+ if (!removed) {
+ return
+ }
+ revokeBlobUrlsInContent(removed.content)
+ if (index === messages.length) {
+ recomputeConversationLast(conversation, messages)
+ }
+ // 3. 鍒犻櫎鏈湴璁板綍骞朵繚瀛樹細璇濇憳瑕�
+ getDb()
+ .delete('messages', getMessageKey(removed, conversationType))
+ .catch((error) => console.warn('[IM messageStore] 娑堟伅鍒犻櫎澶辫触', error))
+ conversationStore.saveConversation(conversation)
+ },
+
+ /** 鍒犻櫎浼氳瘽鍏ㄩ儴娑堟伅 */
+ deleteConversationMessageList(conversationType: number, targetId: number) {
+ // 1. 娓呯悊鍐呭瓨娑堟伅鍜屽獟浣撹祫婧�
+ const clientConversationId = getClientConversationId(conversationType, targetId)
+ const messages = this.messagesByConversation[clientConversationId] || []
+ messages.forEach((message) => {
+ revokeBlobUrlsInContent(message.content)
+ message._localFile = undefined
+ })
+ Reflect.deleteProperty(this.messagesByConversation, clientConversationId)
+ this.loadedConversationKeys = this.loadedConversationKeys.filter(
+ (key) => key !== clientConversationId
+ )
+ // 2. 鍒犻櫎 IndexedDB 娑堟伅
+ getDb()
+ .deleteByIndex('messages', 'clientConversationId', clientConversationId)
+ .catch((error) => console.warn('[IM messageStore] 浼氳瘽娑堟伅鍒犻櫎澶辫触', error))
+ }
+ }
+})
+
+export const useMessageStoreWithOut = () => useMessageStore()
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useMessageStore, import.meta.hot))
+}
--
Gitblit v1.9.3