Skip to content

Commit

Permalink
feat: add local message to avoid network error which occur repeat sen…
Browse files Browse the repository at this point in the history
…d same message
  • Loading branch information
moonrailgun committed Aug 2, 2023
1 parent c045475 commit 9bb931a
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 47 deletions.
11 changes: 11 additions & 0 deletions client/shared/model/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
87 changes: 54 additions & 33 deletions client/shared/redux/hooks/useConverseMessage.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,65 +11,86 @@ 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';
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: 当先输入文本再选中消息回复时,直接发送无法带上回复信息

/**
* 发送消息
*/
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;
}
Expand Down Expand Up @@ -190,7 +211,7 @@ export function useConverseMessage(context: ConverseContext) {
await _handleFetchMoreMessage();
});

const handleSendMessage = useHandleSendMessage(context);
const handleSendMessage = useHandleSendMessage();

return {
messages,
Expand Down
7 changes: 7 additions & 0 deletions client/shared/redux/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
59 changes: 55 additions & 4 deletions client/shared/redux/slices/chat.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
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';
import { isValidStr } from '../../utils/string-helper';
import type { InboxItem } from '../../model/inbox';

export interface ChatConverseState extends ChatConverseInfo {
messages: ChatMessage[];
messages: LocalChatMessage[];
hasFetchedHistory: boolean;
/**
* 判定是否还有更多的信息
Expand Down Expand Up @@ -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<string, unknown>,
isLocal: true,
};

const newMessages = _orderBy(
_uniqBy([...state.converses[converseId].messages, message], '_id'),
'_id',
'asc'
);

state.converses[converseId].messages = newMessages;
},

/**
* 初始化历史信息
*/
Expand Down Expand Up @@ -186,18 +230,25 @@ const chatSlice = createSlice({
updateMessageInfo(
state,
action: PayloadAction<{
message: ChatMessage;
messageId?: string;
message: Partial<LocalChatMessage>;
}>
) {
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],
Expand Down
33 changes: 25 additions & 8 deletions client/web/src/components/ChatBox/ChatMessageList/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -142,6 +143,13 @@ const NormalMessage: React.FC<ChatMessageItemProps> = React.memo((props) => {

<span>{getMessageRender(payload.content)}</span>

{payload.sendFailed === true && (
<Icon
className="inline-block ml-1"
icon="emojione:cross-mark-button"
/>
)}

{/* 解释器按钮 */}
{useRenderPluginMessageInterpreter(payload.content)}
</div>
Expand Down Expand Up @@ -235,7 +243,7 @@ SystemMessageWithNickname.displayName = 'SystemMessageWithNickname';

interface ChatMessageItemProps {
showAvatar: boolean;
payload: ChatMessage;
payload: LocalChatMessage;
}
const ChatMessageItem: React.FC<ChatMessageItemProps> = React.memo((props) => {
const payload = props.payload;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -305,12 +316,18 @@ export function buildMessageItemRow(messages: ChatMessage[], index: number) {
</Divider>
)}

<MessageAckContainer
converseId={message.converseId}
messageId={message._id}
>
<ChatMessageItem showAvatar={showAvatar} payload={message} />
</MessageAckContainer>
{message.isLocal === true ? (
<div className="opacity-50">
<ChatMessageItem showAvatar={showAvatar} payload={message} />
</div>
) : (
<MessageAckContainer
converseId={message.converseId}
messageId={message._id}
>
<ChatMessageItem showAvatar={showAvatar} payload={message} />
</MessageAckContainer>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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';

/**
* 消息已读回调容器
Expand All @@ -14,7 +14,7 @@ export const MessageAckContainer: React.FC<MessageAckContainerProps> =
React.memo((props) => {
const { updateConverseAck } = useConverseAck(props.converseId);

const handleIntersection = useMemoizedFn(() => {
const handleIntersection = useEvent(() => {
updateConverseAck(props.messageId);
});

Expand Down

0 comments on commit 9bb931a

Please sign in to comment.