| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { Conversation, Message } from '../types' |
| | | |
| | | import { readChannelMessages as apiReadChannelMessages } from '#/api/im/message/channel' |
| | | import { |
| | | readGroupMessages as apiReadGroupMessages, |
| | | recallGroupMessage as apiRecallGroupMessage, |
| | | sendGroupMessage as apiSendGroupMessage |
| | | } from '#/api/im/message/group' |
| | | import { |
| | | getPrivateMaxReadMessageId as apiGetPrivateMaxReadMessageId, |
| | | readPrivateMessages as apiReadPrivateMessages, |
| | | recallPrivateMessage as apiRecallPrivateMessage, |
| | | sendPrivateMessage as apiSendPrivateMessage |
| | | } from '#/api/im/message/private' |
| | | import { getCurrentUserId } from '#/views/im/utils/auth' |
| | | |
| | | import { MESSAGE_GROUP_READ_ENABLED, MESSAGE_PRIVATE_READ_ENABLED } from '../../utils/config' |
| | | import { ImContentType, ImConversationType, ImMessageStatus } from '../../utils/constants' |
| | | import { getClientConversationId } from '../../utils/db' |
| | | import { |
| | | generateClientMessageId, |
| | | type QuoteMessage, |
| | | serializeMessage, |
| | | type TextMessage, |
| | | withQuotePayload |
| | | } from '../../utils/message' |
| | | import { useConversationStore } from '../store/conversationStore' |
| | | import { useMessageStore } from '../store/messageStore' |
| | | |
| | | /** éææ¬æ¶æ¯çæ©å±é项ï¼éç¨ï¼ */ |
| | | interface SendExtOptions { |
| | | atUserIds?: number[] // 群è @ çç¨æ·ç¼å·å表 |
| | | receipt?: boolean // æ¯å¦éè¦ç¾¤åæ§ï¼é»è®¤ falseï¼ |
| | | targetId?: number // è¦çé»è®¤ç targetId |
| | | /** |
| | | * æ¾å¼æå®ç®æ ä¼è¯ï¼è½¬å / åçæ¨èåºæ¯ï¼ |
| | | * |
| | | * ä¸ä¼ æ¶é»è®¤å conversationStore.activeConversationï¼ä¼ å
¥æ¶ææ¬å¼åé + ä¹è§æ´æ°å°å¯¹åºä¼è¯ï¼ |
| | | * ä¸è¦æ±è¯¥ä¼è¯å½åæ¯æ¿æ´»ç¶æï¼éååç»ãéå½åä¼è¯ãçå¤ä¸ªç®æ ï¼ |
| | | */ |
| | | conversation?: Conversation |
| | | /** 被å¼ç¨æ¶æ¯ï¼å¯éï¼ï¼åè¿ content.quote ç¨äºä¹è§æ¸²æï¼æå¡ç«¯æ quote.messageId 忥éç®è¦ç */ |
| | | quote?: QuoteMessage |
| | | /** |
| | | * å¤ç¨å·²åå¨çæ¬å°å 使¶æ¯ clientMessageIdï¼åªä½ä¸ä¼ åºæ¯ï¼ |
| | | * |
| | | * åªä½ä¸ä¼ é¾è·¯å¨è¯·æ±æå¡ç«¯åå·²ç» insertMessage äºå ä½ï¼å¸¦ blob URL + è¿åº¦æ¡ï¼ï¼ |
| | | * è¿éè·³è¿ buildLocalMessage / insertMessageï¼ç´æ¥æ¿è¿ä¸ª id èµ° ackMessage æ¶å°¾ï¼é¿å
é夿å
¥ä¸¤æ¡ |
| | | */ |
| | | existingClientMessageId?: string |
| | | } |
| | | |
| | | /** |
| | | * æ¶æ¯åé / æ¤å / 已读 ç»åå¼é»è¾ |
| | | * |
| | | * 设计è¦ç¹ï¼ |
| | | * 1. ç§è / ç¾¤èæ¥å£ç¾åå¯¹ç§°ï¼æ conversation.type 忝è°åº¦ï¼å·®å¼å¨åæ¯å
鍿¶å |
| | | * 2. åéèµ°ãä¹è§æ´æ°ãï¼å
insertMessage åå
¥ SENDING å ä½ï¼è¯·æ±æå ackMessage æ´æ°ä¸º NORMALï¼å¤±è´¥æ´æ°ä¸º FAILED |
| | | * 3. æ¤åä¸åä¹è§æ´æ°ï¼æå¡ç«¯éè¿ WebSocket RECALL äºä»¶åä¼ ï¼ç± websocketStore ç»ä¸æ´æ°ç¶æï¼é¿å
ç½ç»å¤±è´¥åä¸å¯åé |
| | | * 4. å·²è¯»ä¸æ¥ï¼æ¬ç«¯ç«å»æ¸
æªè¯»æ°å¹¶è®°å½æ¬å°è¯»ä½ç½®ï¼æ¥å£å¤±è´¥ä»
è®°å½æ¥å¿ |
| | | */ |
| | | export const useMessageSender = () => { |
| | | const conversationStore = useConversationStore() |
| | | const messageStore = useMessageStore() |
| | | |
| | | /** æé æ¬å°ä¹è§æ¶æ¯å¯¹è±¡ */ |
| | | const buildLocalMessage = (opts: { |
| | | atUserIds?: number[] |
| | | clientMessageId: string |
| | | content: string |
| | | targetId: number |
| | | type: number |
| | | }): Message => { |
| | | return { |
| | | clientMessageId: opts.clientMessageId, |
| | | type: opts.type, |
| | | content: opts.content, |
| | | status: ImMessageStatus.SENDING, |
| | | sendTime: Date.now(), |
| | | senderId: getCurrentUserId(), |
| | | targetId: opts.targetId, |
| | | selfSend: true, |
| | | atUserIds: opts.atUserIds |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * åéä»»æç±»åçæ¶æ¯ï¼åºå±å®ç°ï¼ |
| | | * 1. ææ¬ãå¾çãæä»¶ãè¯é³çé½èµ°è¿é |
| | | * 2. type / content ç±è°ç¨æ¹æé |
| | | * 3. è¿åå¼ï¼æå true / 失败 falseï¼å¤±è´¥æ¶æ¬å°å ä½å·²æ FAILEDï¼ï¼åæ°ç¼ºå¤±çæ æ³åéçåºæ¯ä¹è¿ false |
| | | * 转å / åçæ¨èçåºæ¯æè¿åå¼å³å®æ¯å¦ç»§ç»åç»å¨ä½ï¼å¦æçè¨æ¶ä»
å¨åçæååååçè¨ï¼ |
| | | */ |
| | | const sendRaw = async ( |
| | | type: number, |
| | | content: string, |
| | | options?: SendExtOptions |
| | | ): Promise<boolean> => { |
| | | // 1. åæ°æ ¡éªï¼ä¼å
ç¨æ¾å¼ä¼ å
¥ç conversationï¼è½¬ååºæ¯ï¼ï¼å¦ååæ¿æ´»ä¼è¯ |
| | | const conversation = options?.conversation ?? conversationStore.activeConversation |
| | | if (!conversation) { |
| | | return false |
| | | } |
| | | const realTarget = options?.targetId || conversation.targetId |
| | | if (!realTarget) { |
| | | return false |
| | | } |
| | | |
| | | // 2. åå¤ clientMessageIdï¼åªä½ä¸ä¼ é¾è·¯å¨ step 1 å·²ç» insertMessage å ä½ï¼è¿éç´æ¥å¤ç¨ idï¼å
¶ä½åºæ¯èµ°é»è®¤ä¹è§æå
¥ |
| | | let clientMessageId: string |
| | | if (options?.existingClientMessageId) { |
| | | clientMessageId = options.existingClientMessageId |
| | | // å ä½è¥å·²è¢«å é¤ï¼ä¸ä¼ æé´ç¨æ·å³é®å é¤ / æ¤å / removeMessage çï¼åæ¾å¼åéï¼ |
| | | // å¦å sendRaw ä»ä¼ææ¶æ¯æ¨å°æå¡ç«¯ï¼å¯¼è´"æ¬å°æ æ°æ³¡ / 对æ¹å´æ¶å°ä¸æ¡" |
| | | const stillExists = messageStore |
| | | .getMessageList(conversation.type, realTarget) |
| | | .some((message) => message.clientMessageId === clientMessageId && !message._ackMerging) |
| | | if (!stillExists) { |
| | | return false |
| | | } |
| | | } else { |
| | | clientMessageId = generateClientMessageId() |
| | | const message = buildLocalMessage({ |
| | | clientMessageId, |
| | | content, |
| | | targetId: realTarget, |
| | | type, |
| | | atUserIds: options?.atUserIds |
| | | }) |
| | | const conversationInfo = { |
| | | type: conversation.type, |
| | | targetId: realTarget, |
| | | name: conversation.name || String(realTarget), |
| | | avatar: conversation.avatar || '' |
| | | } |
| | | void messageStore.insertMessage(conversationInfo, message).catch(() => undefined) |
| | | } |
| | | |
| | | // 3. åé请æ±ï¼æä¼è¯ç±»åååå°ä¸åæ¥å£ï¼æåå ackMessage æ´æ°ä¸º NORMALï¼å¤±è´¥æ´æ°ä¸º FAILED |
| | | try { |
| | | if (conversation.type === ImConversationType.PRIVATE) { |
| | | const data = await apiSendPrivateMessage({ |
| | | clientMessageId, |
| | | receiverId: realTarget, |
| | | type, |
| | | content |
| | | }) |
| | | void messageStore |
| | | .ackMessage(conversation.type, realTarget, clientMessageId, { |
| | | id: data.id, |
| | | sendTime: new Date(data.sendTime).getTime(), |
| | | status: data.status, |
| | | receiptStatus: data.receiptStatus, |
| | | content: data.content |
| | | }) |
| | | .catch(() => undefined) |
| | | } else if (conversation.type === ImConversationType.GROUP) { |
| | | const data = await apiSendGroupMessage({ |
| | | clientMessageId, |
| | | groupId: realTarget, |
| | | type, |
| | | content, |
| | | atUserIds: options?.atUserIds, |
| | | receipt: options?.receipt |
| | | }) |
| | | void messageStore |
| | | .ackMessage(conversation.type, realTarget, clientMessageId, { |
| | | id: data.id, |
| | | sendTime: new Date(data.sendTime).getTime(), |
| | | status: data.status, |
| | | receiptStatus: data.receiptStatus, |
| | | readCount: data.readCount, |
| | | content: data.content |
| | | }) |
| | | .catch(() => undefined) |
| | | } |
| | | return true |
| | | } catch (error) { |
| | | console.error('[IM] æ¶æ¯åé失败', { type, realTarget, clientMessageId }, error) |
| | | void messageStore |
| | | .ackMessage(conversation.type, realTarget, clientMessageId, { |
| | | status: ImMessageStatus.FAILED |
| | | }) |
| | | .catch(() => undefined) |
| | | return false |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * åéææ¬æ¶æ¯ï¼æå¸¸ç¨çå¿«æ·å
¥å£ï¼ï¼message-input.vue ææ¬å车走è¿é |
| | | * è¿åå¼ï¼æå true / 失败 false / ç©ºææ¬ falseï¼ä¸ sendRaw 对é½ï¼è½¬ååºæ¯æè¿åå¼å¤æï¼ |
| | | */ |
| | | const send = async (text: string, options?: SendExtOptions): Promise<boolean> => { |
| | | if (!text.trim()) { |
| | | return false |
| | | } |
| | | const payload = withQuotePayload<TextMessage>({ content: text }, options?.quote) |
| | | return sendRaw(ImContentType.TEXT, serializeMessage(payload), options) |
| | | } |
| | | |
| | | /** |
| | | * æ¤åææ¡æ¶æ¯ |
| | | * 1. æå¡ç«¯ä¼éè¿ WebSocket RECALL äºä»¶åä¼ ï¼æ¬ç«¯ UI ç± websocketStore ç»ä¸æ´æ° |
| | | * 2. æ¤å¤ä¸åä¹è§æ¤åï¼é¿å
ç½ç»å¤±è´¥åç¶æä¸å¯åé |
| | | */ |
| | | const recall = async (message: Message) => { |
| | | // åæ°æ ¡éªï¼æ¬å°å 使¶æ¯ä¸è½æ¤å |
| | | if (!message.id) { |
| | | return |
| | | } |
| | | const conversation = conversationStore.activeConversation |
| | | if (!conversation) { |
| | | return |
| | | } |
| | | // ç§è / ç¾¤èæ¥å£ç¾åä¸è´ï¼æä¼è¯ç±»ååå |
| | | const isPrivate = conversation.type === ImConversationType.PRIVATE |
| | | try { |
| | | await (isPrivate ? apiRecallPrivateMessage(message.id) : apiRecallGroupMessage(message.id)) |
| | | } catch (error) { |
| | | console.error('[IM] æ¤å失败', { messageId: message.id, type: conversation.type }, error) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 触åå½åä¼è¯çå·²è¯»ä¸æ¥ï¼åä¼è¯ / è¿å
¥é¡µé¢æ¶è°ç¨ï¼ |
| | | * 1. æ¬ç«¯ç«å»æ¸
æªè¯»æ°å¹¶æ¨è¿è¯»ä½ç½® |
| | | * 2. 已读ä½ç½®åå·²å è½½æ¶æ¯åä¼è¯æ«æ¡æ¶æ¯çæå¤§æå¡ç«¯ id |
| | | */ |
| | | const readActive = async () => { |
| | | const conversation = conversationStore.activeConversation |
| | | if (!conversation) { |
| | | return |
| | | } |
| | | let loadedMaxMessageId = 0 |
| | | for (const message of messageStore.getMessages( |
| | | getClientConversationId(conversation.type, conversation.targetId) |
| | | )) { |
| | | if (message.id && message.id > loadedMaxMessageId) { |
| | | loadedMaxMessageId = message.id |
| | | } |
| | | } |
| | | const maxMessageId = Math.max(loadedMaxMessageId, conversation.lastMessageId || 0) |
| | | const readReported = conversationStore.isReportedReadPositionCovered( |
| | | conversation.type, |
| | | conversation.targetId, |
| | | maxMessageId |
| | | ) |
| | | if (readReported) { |
| | | conversationStore.markConversationRead(conversation.type, conversation.targetId) |
| | | return |
| | | } |
| | | const isPrivate = conversation.type === ImConversationType.PRIVATE |
| | | const isGroup = conversation.type === ImConversationType.GROUP |
| | | const isChannel = conversation.type === ImConversationType.CHANNEL |
| | | // æ¬å°æ è®°å·²è¯»ï¼æªè¯»æ°æ¸
é¶ï¼UI ç«å»ååºï¼ |
| | | conversationStore.markConversationRead(conversation.type, conversation.targetId, maxMessageId) |
| | | if (!maxMessageId) { |
| | | return |
| | | } |
| | | // æ¥å£è°ç¨ï¼æä¼è¯ç±»åååï¼å¹¶æå¯¹åºå·²è¯»å¼å
³æ§å¶ |
| | | if (!isPrivate && !isGroup && !isChannel) { |
| | | return |
| | | } |
| | | if (isPrivate && !MESSAGE_PRIVATE_READ_ENABLED) { |
| | | return |
| | | } |
| | | if (isGroup && !MESSAGE_GROUP_READ_ENABLED) { |
| | | return |
| | | } |
| | | try { |
| | | if (isPrivate) { |
| | | await apiReadPrivateMessages(conversation.targetId, maxMessageId) |
| | | } else if (isGroup) { |
| | | await apiReadGroupMessages(conversation.targetId, maxMessageId) |
| | | } else { |
| | | await apiReadChannelMessages(conversation.targetId, maxMessageId) |
| | | } |
| | | conversationStore.markConversationReadReported( |
| | | conversation.type, |
| | | conversation.targetId, |
| | | maxMessageId |
| | | ) |
| | | } catch (error) { |
| | | console.error( |
| | | '[IM] æ 记已读失败', |
| | | { type: conversation.type, targetId: conversation.targetId, maxMessageId }, |
| | | error |
| | | ) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æåã对æ¹å·²è¯»å°æåªæ¡æ¶æ¯ãå¹¶è¡¥é½æ¬å°ç¶æ |
| | | * |
| | | * 1. 弥补离线 / å¤ç«¯æé´éè¿ç RECEIPT æ¨éï¼è¿å
¥ç§èä¼è¯ææçº¿éè¿åè°ä¸æ¬¡ï¼ |
| | | * æå¯¹æ¹ maxReadId 忥尿¬å°æ¶æ¯ statusï¼é¿å
å¯¹æ¹ææè¯»äºãæ¬ç«¯å´ä»æ¾ç¤ºæªè¯» |
| | | * 2. ä»
ç§è使ç¨ï¼ç¾¤è已读ä½ç½®å¨æ¯æ¡æ¶æ¯ç readCount / receiptStatus åæ®µï¼ç¦»çº¿æåèªå¸¦å |
| | | */ |
| | | const syncPrivateReadStatus = async (peerId: number) => { |
| | | if (!peerId) { |
| | | return |
| | | } |
| | | // ç§è已读å
³éï¼è·³è¿å¯¹æ¹å·²è¯»ä½ç½®åæ¥ï¼é¿å
æ è°æ¥å£è°ç¨ |
| | | if (!MESSAGE_PRIVATE_READ_ENABLED) { |
| | | return |
| | | } |
| | | const cachedMaxReadId = messageStore.getPrivateReadMaxId(peerId) |
| | | if (cachedMaxReadId !== undefined) { |
| | | if (cachedMaxReadId > 0) { |
| | | messageStore.applyMessageReadReceipt({ |
| | | conversationType: ImConversationType.PRIVATE, |
| | | targetId: peerId, |
| | | privateReadMaxId: cachedMaxReadId |
| | | }) |
| | | } |
| | | return |
| | | } |
| | | try { |
| | | // æå对æ¹å·²è¯»å°çæå¤§æ¶æ¯ id |
| | | const maxReadId = await apiGetPrivateMaxReadMessageId(peerId) |
| | | messageStore.updatePrivateReadMaxId(peerId, maxReadId) |
| | | if (!maxReadId) { |
| | | return |
| | | } |
| | | // applyMessageReadReceipt å
鍿 ⤠maxReadId çæ¬ç«¯æ¶æ¯åæ§æ´æ°ä¸º DONE |
| | | messageStore.applyMessageReadReceipt({ |
| | | conversationType: ImConversationType.PRIVATE, |
| | | targetId: peerId, |
| | | privateReadMaxId: maxReadId |
| | | }) |
| | | } catch (error) { |
| | | console.warn('[IM] æå对æ¹å·²è¯»ä½ç½®å¤±è´¥', { peerId }, error) |
| | | } |
| | | } |
| | | |
| | | return { send, sendRaw, recall, readActive, syncPrivateReadStatus } |
| | | } |