diff --git a/client/shared/model/message.ts b/client/shared/model/message.ts index 5d66ecc8..3b379d56 100644 --- a/client/shared/model/message.ts +++ b/client/shared/model/message.ts @@ -3,6 +3,17 @@ import type { ChatMessageReaction, ChatMessage } from 'tailchat-types'; export { ChatMessageReaction, ChatMessage }; +export interface LocalChatMessage extends ChatMessage { + /** + * 本地添加消息的标识,用于标记该条消息尚未确定已经发送到服务端 + */ + isLocal?: boolean; + /** + * 判断是否发送失败 + */ + sendFailed?: boolean; +} + export interface SimpleMessagePayload { groupId?: string; converseId: string; diff --git a/client/shared/redux/hooks/useConverseMessage.ts b/client/shared/redux/hooks/useConverseMessage.ts index ed4b2142..dbe687b3 100644 --- a/client/shared/redux/hooks/useConverseMessage.ts +++ b/client/shared/redux/hooks/useConverseMessage.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { ensureDMConverse } from '../../helper/converse-helper'; import { useAsync } from '../../hooks/useAsync'; import { showErrorToasts } from '../../manager/ui'; @@ -11,12 +11,14 @@ import { chatActions } from '../slices'; import { useAppDispatch, useAppSelector } from './useAppSelector'; import _get from 'lodash/get'; import _isNil from 'lodash/isNil'; +import _uniqueId from 'lodash/uniqueId'; import { ChatConverseState, isValidStr, t, useAsyncRequest, useChatBoxContext, + useEvent, useMemoizedFn, } from '../..'; import { MessageHelper } from '../../utils/message-helper'; @@ -24,8 +26,10 @@ import { ChatConverseType } from '../../model/converse'; import { sharedEvent } from '../../event'; import { useUpdateRef } from '../../hooks/useUpdateRef'; -function useHandleSendMessage(context: ConverseContext) { - const { converseId } = context; +const genLocalMessageId = () => _uniqueId('localMessage_'); + +function useHandleSendMessage() { + const userId = useAppSelector((state) => state.user.info?._id); const dispatch = useAppDispatch(); const { hasContext, replyMsg, clearReplyMsg } = useChatBoxContext(); const replyMsgRef = useUpdateRef(replyMsg); // NOTICE: 这个是为了修复一个边界case: 当先输入文本再选中消息回复时,直接发送无法带上回复信息 @@ -33,43 +37,60 @@ function useHandleSendMessage(context: ConverseContext) { /** * 发送消息 */ - const handleSendMessage = useCallback( - async (payload: SendMessagePayload) => { - // 输入合法性检测 - if (payload.content === '') { - showErrorToasts(t('无法发送空消息')); - return; - } - - try { - if (hasContext === true) { - // 如果有上下文, 则组装payload - const msgHelper = new MessageHelper(payload); - if (!_isNil(replyMsgRef.current)) { - msgHelper.setReplyMsg(replyMsgRef.current); - clearReplyMsg(); - } + const handleSendMessage = useEvent((payload: SendMessagePayload) => { + // 输入合法性检测 + if (payload.content === '') { + showErrorToasts(t('无法发送空消息')); + return; + } - payload = msgHelper.generatePayload(); - } + if (hasContext === true) { + // 如果有上下文, 则组装payload + const msgHelper = new MessageHelper(payload); + if (!_isNil(replyMsgRef.current)) { + msgHelper.setReplyMsg(replyMsgRef.current); + clearReplyMsg(); + } - // TODO: 增加临时消息, 对网络环境不佳的状态进行优化 + payload = msgHelper.generatePayload(); + } - const message = await sendMessage(payload); + const localMessageId = genLocalMessageId(); + dispatch( + chatActions.appendLocalMessage({ + author: userId, + localMessageId, + payload, + }) + ); + + sendMessage(payload) + .then((message) => { dispatch( - chatActions.appendConverseMessage({ - converseId, - messages: [message], + chatActions.updateMessageInfo({ + messageId: localMessageId, + message: { + ...message, + isLocal: false, + sendFailed: false, + }, }) ); + sharedEvent.emit('sendMessage', payload); - } catch (err) { + }) + .catch((err) => { showErrorToasts(err); - throw err; - } - }, - [converseId, hasContext, clearReplyMsg] - ); + dispatch( + chatActions.updateMessageInfo({ + messageId: localMessageId, + message: { + sendFailed: true, + }, + }) + ); + }); + }); return handleSendMessage; } @@ -190,7 +211,7 @@ export function useConverseMessage(context: ConverseContext) { await _handleFetchMoreMessage(); }); - const handleSendMessage = useHandleSendMessage(context); + const handleSendMessage = useHandleSendMessage(); return { messages, diff --git a/client/shared/redux/setup.ts b/client/shared/redux/setup.ts index 4d2ebbe2..9cb0f794 100644 --- a/client/shared/redux/setup.ts +++ b/client/shared/redux/setup.ts @@ -166,9 +166,16 @@ function listenNotify(socket: AppSocket, store: AppStore) { // 处理接受到的消息 const converseId = message.converseId; const converse = store.getState().chat.converses[converseId]; + const userId = store.getState().user.info?._id; // 添加消息到会话中 const appendMessage = () => { + if (message.author === userId) { + // 如果是自己发送的消息,则忽略 + // 因为存在local状态的消息,应该由发送消息的地方处理 + return; + } + store.dispatch( chatActions.appendConverseMessage({ converseId, diff --git a/client/shared/redux/slices/chat.ts b/client/shared/redux/slices/chat.ts index 27443156..b1161b3b 100644 --- a/client/shared/redux/slices/chat.ts +++ b/client/shared/redux/slices/chat.ts @@ -1,6 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { ChatConverseInfo } from '../../model/converse'; -import type { ChatMessage, ChatMessageReaction } from '../../model/message'; +import type { + ChatMessage, + ChatMessageReaction, + LocalChatMessage, + SendMessagePayload, +} from '../../model/message'; import _uniqBy from 'lodash/uniqBy'; import _orderBy from 'lodash/orderBy'; import _last from 'lodash/last'; @@ -8,7 +13,7 @@ import { isValidStr } from '../../utils/string-helper'; import type { InboxItem } from '../../model/inbox'; export interface ChatConverseState extends ChatConverseInfo { - messages: ChatMessage[]; + messages: LocalChatMessage[]; hasFetchedHistory: boolean; /** * 判定是否还有更多的信息 @@ -96,6 +101,45 @@ const chatSlice = createSlice({ } }, + /** + * 追加本地消息消息 + */ + appendLocalMessage( + state, + action: PayloadAction<{ + author?: string; + localMessageId: string; + payload: SendMessagePayload; + }> + ) { + const { author, localMessageId, payload } = action.payload; + const { converseId, groupId, content, meta } = payload; + + if (!state.converses[converseId]) { + // 没有会话信息, 请先设置会话信息 + console.error('没有会话信息, 请先设置会话信息'); + return; + } + + const message: LocalChatMessage = { + _id: localMessageId, + author, + groupId, + converseId, + content, + meta: meta as Record, + isLocal: true, + }; + + const newMessages = _orderBy( + _uniqBy([...state.converses[converseId].messages, message], '_id'), + '_id', + 'asc' + ); + + state.converses[converseId].messages = newMessages; + }, + /** * 初始化历史信息 */ @@ -186,18 +230,25 @@ const chatSlice = createSlice({ updateMessageInfo( state, action: PayloadAction<{ - message: ChatMessage; + messageId?: string; + message: Partial; }> ) { const { message } = action.payload; + const messageId = action.payload.messageId ?? message._id; const converseId = message.converseId; + if (!converseId) { + console.warn('Not found converse id,', message); + return; + } + const converse = state.converses[converseId]; if (!converse) { console.warn('Not found converse,', converseId); return; } - const index = converse.messages.findIndex((m) => m._id === message._id); + const index = converse.messages.findIndex((m) => m._id === messageId); if (index >= 0) { converse.messages[index] = { ...converse.messages[index], diff --git a/client/web/src/components/ChatBox/ChatMessageList/Item.tsx b/client/web/src/components/ChatBox/ChatMessageList/Item.tsx index 6d081287..816051d6 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/Item.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/Item.tsx @@ -25,6 +25,7 @@ import { AutoFolder, Avatar, Icon } from 'tailchat-design'; import { MessageAckContainer } from './MessageAckContainer'; import { UserPopover } from '@/components/popover/UserPopover'; import _isEmpty from 'lodash/isEmpty'; +import type { LocalChatMessage } from 'tailchat-shared/model/message'; import './Item.less'; /** @@ -142,6 +143,13 @@ const NormalMessage: React.FC = React.memo((props) => { {getMessageRender(payload.content)} + {payload.sendFailed === true && ( + + )} + {/* 解释器按钮 */} {useRenderPluginMessageInterpreter(payload.content)} @@ -235,7 +243,7 @@ SystemMessageWithNickname.displayName = 'SystemMessageWithNickname'; interface ChatMessageItemProps { showAvatar: boolean; - payload: ChatMessage; + payload: LocalChatMessage; } const ChatMessageItem: React.FC = React.memo((props) => { const payload = props.payload; @@ -266,7 +274,10 @@ ChatMessageItem.displayName = 'ChatMessageItem'; /** * 构造聊天项 */ -export function buildMessageItemRow(messages: ChatMessage[], index: number) { +export function buildMessageItemRow( + messages: LocalChatMessage[], + index: number +) { const message = messages[index]; if (!message) { @@ -305,12 +316,18 @@ export function buildMessageItemRow(messages: ChatMessage[], index: number) { )} - - - + {message.isLocal === true ? ( +
+ +
+ ) : ( + + + + )} ); } diff --git a/client/web/src/components/ChatBox/ChatMessageList/MessageAckContainer.tsx b/client/web/src/components/ChatBox/ChatMessageList/MessageAckContainer.tsx index 68643abf..31029f33 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/MessageAckContainer.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/MessageAckContainer.tsx @@ -1,6 +1,6 @@ import { Intersection } from '@/components/Intersection'; import React from 'react'; -import { useConverseAck, useMemoizedFn } from 'tailchat-shared'; +import { useConverseAck, useEvent } from 'tailchat-shared'; /** * 消息已读回调容器 @@ -14,7 +14,7 @@ export const MessageAckContainer: React.FC = React.memo((props) => { const { updateConverseAck } = useConverseAck(props.converseId); - const handleIntersection = useMemoizedFn(() => { + const handleIntersection = useEvent(() => { updateConverseAck(props.messageId); });