状态管理: zustand

dev/chat
Lei OT 2 years ago
parent a7ccf7a9b0
commit 0b0bf329aa

@ -19,7 +19,8 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.21.1", "react-router-dom": "^6.21.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"uuid": "^9.0.1" "uuid": "^9.0.1",
"zustand": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.15", "@types/react": "^18.2.15",

@ -0,0 +1,262 @@
import { create } from 'zustand';
import { RealTimeAPI } from '@/lib/realTimeAPI';
import { olog, isEmpty } from '@/utils/utils';
import { receivedMsgTypeMapped } from '@/lib/msgUtils';
import { fetchConversationsList, fetchTemplates } from '@/actions/ConversationActions';
// const WS_URL = 'ws://202.103.68.144:8888/whatever/';
// const WS_URL = 'ws://120.79.9.217:10022/whatever/';
const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_callback'; // prod:
const initialConversationState = {
// websocket: null,
// websocketOpened: null,
// websocketRetrying: null,
// websocketRetrytimes: null,
errors: [], // 错误信息
// templates: [],
// conversationsList: [], // 对话列表
// currentConversation: {}, // 当前对话
// activeConversations: {}, // 激活的对话的消息列表: { [conversationId]: <messageItem>[] }
// referenceMsg: {},
};
olog('initialConversationState');
export const templatesSlice = (set) => ({
templates: [],
setTemplates: (templates) => set({ templates }),
});
export const websocketSlice = (set, get) => ({
websocket: null,
websocketOpened: null,
websocketRetrying: null,
websocketRetrytimes: null,
setWebsocket: (websocket) => set({ websocket }),
setWebsocketOpened: (opened) => set({ websocketOpened: opened }),
setWebsocketRetrying: (retrying) => set({ websocketRetrying: retrying }),
setWebsocketRetrytimes: (retrytimes) => set({ websocketRetrytimes: retrytimes, websocketRetrying: retrytimes > 0 }),
connectWebsocket: (userId) => {
const { setWebsocket, setWebsocketOpened, setWebsocketRetrytimes, addError, handleMessage } = get();
const realtimeAPI = new RealTimeAPI(
{
url: `${WS_URL}?opisn=${userId || ''}&_spam=${Date.now().toString()}`,
protocol: 'WhatsApp',
},
() => {
setWebsocketOpened(true);
setWebsocketRetrytimes(0);
},
() => setWebsocketOpened(false),
(n) => setWebsocketRetrytimes(n)
);
realtimeAPI.onError(() => addError('Error'));
realtimeAPI.onMessage(handleMessage);
realtimeAPI.onCompletion(() => addError('Connection broken'));
setWebsocket(realtimeAPI);
},
disconnectWebsocket: () => {
const { websocket } = get();
websocket.disconnect();
return set({ websocket: null });
},
handleMessage: (data) => {
console.log('handleMessage------------------');
console.log(data);
const { updateMessageItem, sentOrReceivedNewMessage } = get();
const { errcode, errmsg, result } = data;
if (!result) {
return false;
}
let resultType = result?.action || result.type;
if (errcode !== 0) {
// addError('Error Connecting to Server');
resultType = 'error';
}
console.log(resultType, 'result.type');
const msgObj = receivedMsgTypeMapped[resultType].getMsg(result);
const msgRender = receivedMsgTypeMapped[resultType].contentToRender(msgObj);
const msgUpdate = receivedMsgTypeMapped[resultType].contentToUpdate(msgObj);
console.log('msgRender msgUpdate', msgRender, msgUpdate);
if (['whatsapp.message.updated', 'message', 'error'].includes(resultType)) {
updateMessageItem(msgUpdate);
// return false;
}
if (!isEmpty(msgRender)) {
sentOrReceivedNewMessage(msgRender.conversationid, msgRender);
window.Notification.requestPermission().then(function (permission) {
if (permission === 'granted') {
const notification = new Notification(`${msgRender.senderName}`, {
body: msgRender?.text || `[ ${msgRender.type} ]`,
...(msgRender.type === 'photo' ? { image: msgRender.data.uri } : {}),
});
notification.onclick = function () {
window.parent.parent.focus();
};
}
});
}
console.log('handleMessage*******************');
},
});
export const referenceMsgSlice = (set) => ({
referenceMsg: [],
setReferenceMsg: (referenceMsg) => set({ referenceMsg }),
});
export const conversationSlice = (set, get) => ({
conversationsList: [],
currentConversation: {},
setConversationsList: (conversationsList) => {
const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return set({ conversationsList, activeConversations: conversationsMapped });
},
addToConversationList: (newList) => {
const { activeConversations } = get();
const conversationsIds = Object.keys(activeConversations);
const newConversations = newList.filter((conversation) => !conversationsIds.includes(`${conversation.sn}`));
const newConversationsMapped = newConversations.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return set((state) => ({
conversationsList: [...newConversations, ...state.conversationsList],
activeConversations: { ...activeConversations, ...newConversationsMapped }
}));
},
delConversationitem: (conversation) => {
const { conversationsList, activeConversations } = get();
const targetId = conversation.sn;
const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
conversationsList.splice(targetIndex, 1);
return set((state) => ({
conversationsList: [...conversationsList],
activeConversations: { ...activeConversations, [`${targetId}`]: [] },
currentConversation: {},
}));
},
setCurrentConversation: (conversation) => {
// 清空未读
const { conversationsList } = get();
const targetId = conversation.sn;
const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
targetIndex !== -1 ? conversationsList.splice(targetIndex, 1, {
...conversationsList[targetIndex],
unread_msg_count: 0,
}) : null;
return set({ currentConversation: conversation, referenceMsg: {}, conversationsList: [...conversationsList] });
},
});
export const messageSlice = (set, get) => ({
activeConversations: {},
receivedMessageList: (conversationid, msgList) => set((state) => ({
activeConversations: { ...state.activeConversations, [String(conversationid)]: msgList }
})),
updateMessageItem: (message) => { // msgUpdate
console.log('UPDATE_SENT_MESSAGE_ITEM-----------------------------------------------------------------');
// 更新会话中的消息
const { activeConversations, conversationsList } = get();
const targetId = message.conversationid;
const targetMsgs = (activeConversations[String(targetId)] || []).map((ele) => {
// 更新状态
// * 已读的不再更新状态, 有时候投递结果在已读之后返回
if (ele.id === ele.actionId && ele.actionId === message.actionId) {
return { ...ele, id: message.id, status: ele.status === 'read' ? ele.status : message.status, dateString: message.dateString };
} else if (ele.id === message.id) {
return { ...ele, id: message.id, status: ele.status === 'read' ? ele.status : message.status, dateString: message.dateString };
}
return ele;
});
// 显示会话中其他客户端发送的消息
const targetMsgsIds = targetMsgs.map((ele) => ele.id);
if (!targetMsgsIds.includes(message.id)) {
targetMsgs.push(message);
}
// 更新列表的时间
// if (message.type !== 'error') {
// const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
// conversationsList.splice(targetIndex, 1, {
// ...conversationsList[targetIndex],
// last_received_time: message.date,
// });
// }
return set((state) => ({
activeConversations: { ...activeConversations, [String(targetId)]: targetMsgs },
conversationsList: [...conversationsList],
}));
},
sentOrReceivedNewMessage: (targetId, message) => { // msgRender:
const { activeConversations, conversationsList, currentConversation } = get();
const targetMsgs = activeConversations[String(targetId)] || [];
const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
const newConversation =
targetId !== -1
? {
...conversationsList[targetIndex],
last_received_time: message.type !== 'system' && message.sender !== 'me' ? message.date : conversationsList[targetIndex].last_received_time,
unread_msg_count:
String(targetId) !== String(currentConversation.sn) && message.sender !== 'me'
? conversationsList[targetIndex].unread_msg_count + 1
: conversationsList[targetIndex].unread_msg_count,
}
: {
...message,
sn: targetId,
last_received_time: message.date,
unread_msg_count: message.sender === 'me' ? 0 : 1,
};
conversationsList.splice(targetIndex, 1);
conversationsList.unshift(newConversation);
return set((state) => ({
activeConversations: { ...activeConversations, [String(targetId)]: [...targetMsgs, message] },
conversationsList: [...conversationsList],
currentConversation: {
...currentConversation,
last_received_time: String(targetId) === String(currentConversation.sn) ? message.date : currentConversation.last_received_time,
},
}));
},
});
export const useConversationStore = create((set, get) => ({
...initialConversationState,
...websocketSlice(set, get),
...conversationSlice(set, get),
...templatesSlice(set, get),
...messageSlice(set, get),
...referenceMsgSlice(set, get),
// state actions
addError: (error) => set((state) => ({ errors: [...state.errors, error] })),
// side effects
fetchInitialData: async (userId) => {
olog('fetch init');
const { setConversationsList, setTemplates } = get();
const conversationsList = await fetchConversationsList({ opisn: userId });
setConversationsList(conversationsList);
const templates = await fetchTemplates();
setTemplates(templates);
},
}));
// window.store = useConversationStore; // debug:
export default useConversationStore;

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { Outlet, Link, useHref, NavLink } from 'react-router-dom' import { Outlet, Link, useHref, NavLink } from 'react-router-dom'
import { Layout, Menu, ConfigProvider, theme, Empty, Row, Col, Avatar, Dropdown, Space, Typography, App as AntApp } from 'antd' import { Layout, Menu, ConfigProvider, theme, Empty, Row, Col, Avatar, Dropdown, Space, Typography, App as AntApp } from 'antd'
import { DownOutlined } from '@ant-design/icons' import { DownOutlined } from '@ant-design/icons'
@ -5,6 +6,7 @@ import ErrorBoundary from '@/components/ErrorBoundary'
import zhLocale from 'antd/locale/zh_CN' import zhLocale from 'antd/locale/zh_CN'
import { useThemeContext } from '@/stores/ThemeContext' import { useThemeContext } from '@/stores/ThemeContext'
import { useAuthContext } from '@/stores/AuthContext' import { useAuthContext } from '@/stores/AuthContext'
import useConversationStore from '@/stores/ConversationStore';
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import 'react-chat-elements/dist/main.css' import 'react-chat-elements/dist/main.css'
@ -33,6 +35,17 @@ function AuthApp() {
token: { colorBgContainer }, token: { colorBgContainer },
} = theme.useToken() } = theme.useToken()
const { connectWebsocket, disconnectWebsocket, fetchInitialData } = useConversationStore();
useEffect(() => {
if (loginUser && loginUser.userId) {
connectWebsocket(loginUser.userId);
fetchInitialData(loginUser.userId);
}
return () => {
disconnectWebsocket();
}
}, [loginUser, connectWebsocket, disconnectWebsocket, fetchInitialData]);
return ( return (
<ConfigProvider <ConfigProvider
theme={{ theme={{

@ -6,7 +6,8 @@ import Messages from './Components/Messages';
import InputComposer from './Components/InputComposer'; import InputComposer from './Components/InputComposer';
import ConversationsList from './Components/ConversationsList'; import ConversationsList from './Components/ConversationsList';
import CustomerProfile from './Components/CustomerProfile'; import CustomerProfile from './Components/CustomerProfile';
import { useConversationState, useConversationDispatch } from '@/stores/ConversationContext'; import { useAuthContext } from '@/stores/AuthContext';
import useConversationStore from '@/stores/ConversationStore';
import './Conversations.css'; import './Conversations.css';
@ -18,7 +19,8 @@ const { Sider, Content, Header, Footer } = Layout;
const ChatWindow = () => { const ChatWindow = () => {
console.log('chat window;;;;;;;;;;;;;;;;;;;;;;;;'); console.log('chat window;;;;;;;;;;;;;;;;;;;;;;;;');
const { order_sn } = useParams(); const { order_sn } = useParams();
const { currentConversation } = useConversationState(); const { loginUser } = useAuthContext();
const { currentConversation } = useConversationStore();
useEffect(() => { useEffect(() => {
console.log('chat window 222;;;;;;;;;;;;;;;;;;;;;;;;'); console.log('chat window 222;;;;;;;;;;;;;;;;;;;;;;;;');

@ -1,26 +1,23 @@
import { useRef, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { List, Avatar, Flex, Popconfirm, Button, Dropdown } from 'antd'; import { Button, Dropdown } from 'antd';
import { SendOutlined, MessageOutlined, SmileOutlined, PictureOutlined, CommentOutlined, UploadOutlined, CloudUploadOutlined, FolderAddOutlined, FilePdfOutlined, CloseCircleOutlined, CloseCircleFilled, MoreOutlined } from '@ant-design/icons'; import { MoreOutlined } from '@ant-design/icons';
import { useAuthContext } from '@/stores/AuthContext'; import { useAuthContext } from '@/stores/AuthContext';
import { useConversationState, useConversationDispatch } from '@/stores/ConversationContext';
import { import {
fetchConversationsList, fetchConversationsList,
setCurrentConversation,
addConversationList, delConversationitem,
fetchConversationItemClose, fetchConversationItemClose,
fetchMessages, receivedMessageList, fetchMessages,
} from '@/actions/ConversationActions'; } from '@/actions/ConversationActions';
import { ChatList, } from 'react-chat-elements'; import { ChatList, } from 'react-chat-elements';
import { isEmpty, pick } from '@/utils/utils'; import { isEmpty } from '@/utils/utils';
import { v4 as uuid } from 'uuid'; import useConversationStore from '@/stores/ConversationStore';
const CDropdown = (props) => { const CDropdown = (props) => {
const dispatch = useConversationDispatch(); const { delConversationitem } = useConversationStore();
const handleConversationItemClose = async () => { const handleConversationItemClose = async () => {
await fetchConversationItemClose({ conversationid: props.sn, opisn: props.opi_sn }); await fetchConversationItemClose({ conversationid: props.sn, opisn: props.opi_sn });
dispatch(delConversationitem(props)); delConversationitem(props);
}; };
return ( return (
<Dropdown <Dropdown
@ -51,8 +48,7 @@ const Conversations = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { loginUser } = useAuthContext(); const { loginUser } = useAuthContext();
const { userId } = loginUser; const { userId } = loginUser;
const { conversationsList, activeConversations, currentConversation } = useConversationState(); const { activeConversations, currentConversation, conversationsList, addToConversationList, setCurrentConversation, receivedMessageList, } = useConversationStore();
const dispatch = useConversationDispatch();
const [chatlist, setChatlist] = useState([]); const [chatlist, setChatlist] = useState([]);
useEffect(() => { useEffect(() => {
setChatlist( setChatlist(
@ -78,11 +74,13 @@ const Conversations = () => {
); );
return () => {}; return () => {};
}, [conversationsList]); }, [conversationsList, currentConversation]);
useEffect(() => { useEffect(() => {
if (order_sn) { if (order_sn) {
getOrderConversationList(order_sn); // getOrderConversationList(order_sn);
// debug: reset chat window
setCurrentConversation({ sn: '', customer_name: '', coli_sn: order_sn });
} }
return () => {}; return () => {};
@ -91,26 +89,24 @@ const Conversations = () => {
const getOrderConversationList = async (colisn) => { const getOrderConversationList = async (colisn) => {
const data = await fetchConversationsList({ opisn: userId, colisn }); const data = await fetchConversationsList({ opisn: userId, colisn });
if (!isEmpty(data)) { if (!isEmpty(data)) {
dispatch(addConversationList(data)); addToConversationList(data);
switchConversation(data[0]); switchConversation(data[0]);
// dispatch(setCurrentConversation(data[0]));
} else { } else {
// reset chat window // reset chat window
dispatch(setCurrentConversation({ sn: '', customer_name: '', coli_sn: order_sn })); setCurrentConversation({ sn: '', customer_name: '', coli_sn: order_sn });
return false; return false;
} }
}; };
const switchConversation = async (item) => { const switchConversation = async (item) => {
console.log('invoke switch');
const messagesList = activeConversations[`${item.sn}`] || []; const messagesList = activeConversations[`${item.sn}`] || [];
if (isEmpty(messagesList)) { if (isEmpty(messagesList)) {
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number }); const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number });
dispatch(receivedMessageList(item.sn, data)); receivedMessageList(item.sn, data);
} }
if (String(item.sn) === String(currentConversation.sn)) { if (String(item.sn) === String(currentConversation.sn)) {
return false; return false;
} }
dispatch(setCurrentConversation(item)); setCurrentConversation(item);
}; };
const onSwitchConversation = (item) => { const onSwitchConversation = (item) => {

@ -1,9 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Input, Flex, Button, } from 'antd'; import { Input, Flex, Button, } from 'antd';
// import { Input } from 'react-chat-elements'; // import { Input } from 'react-chat-elements';
import { useConversationState, useConversationDispatch } from '@/stores/ConversationContext';
import { useAuthContext } from '@/stores/AuthContext'; import { useAuthContext } from '@/stores/AuthContext';
import { sentNewMessage, setReplyTo } from '@/actions/ConversationActions'; import useConversationStore from '@/stores/ConversationStore';
import { SendOutlined, MessageOutlined, SmileOutlined, PictureOutlined, CommentOutlined, UploadOutlined, CloudUploadOutlined, FolderAddOutlined, FilePdfOutlined, CloseCircleOutlined } from '@ant-design/icons'; import { SendOutlined, MessageOutlined, SmileOutlined, PictureOutlined, CommentOutlined, UploadOutlined, CloudUploadOutlined, FolderAddOutlined, FilePdfOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { isEmpty } from '@/utils/utils'; import { isEmpty } from '@/utils/utils';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -14,15 +13,14 @@ import dayjs from 'dayjs';
const InputBox = () => { const InputBox = () => {
const { loginUser } = useAuthContext(); const { loginUser } = useAuthContext();
const { userId } = loginUser; const { userId } = loginUser;
const { websocket, websocketOpened, currentConversation, referenceMsg } = useConversationState(); const { websocket, websocketOpened, currentConversation, referenceMsg, setReferenceMsg, sentOrReceivedNewMessage } = useConversationStore();
const dispatch = useConversationDispatch();
const [textContent, setTextContent] = useState(''); const [textContent, setTextContent] = useState('');
const talkabled = !isEmpty(currentConversation.sn) && websocketOpened; const talkabled = !isEmpty(currentConversation.sn) && websocketOpened;
const gt24h = currentConversation.last_received_time ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true; const gt24h = currentConversation.last_received_time ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true;
const textabled = talkabled && !gt24h; const textabled = (talkabled && !gt24h);
const invokeSendMessage = (msgObj) => { const invokeSendMessage = (msgObj) => {
console.log('sendMessage------------------', msgObj); console.log('sendMessage------------------', msgObj);
@ -31,7 +29,7 @@ const InputBox = () => {
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn }); websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn });
const contentToRender = sentMsgTypeMapped[msgObj.type].contentToRender(msgObj); const contentToRender = sentMsgTypeMapped[msgObj.type].contentToRender(msgObj);
console.log(contentToRender, 'contentToRender sendMessage------------------'); console.log(contentToRender, 'contentToRender sendMessage------------------');
dispatch(sentNewMessage(contentToRender)); sentOrReceivedNewMessage(contentToRender.conversationid, contentToRender);
}; };
const handleSendText = () => { const handleSendText = () => {
@ -48,7 +46,7 @@ const InputBox = () => {
}; };
invokeSendMessage(msgObj); invokeSendMessage(msgObj);
setTextContent(''); setTextContent('');
dispatch(setReplyTo({})); setReferenceMsg({});
} }
}; };
const handleSendImage = () => { const handleSendImage = () => {
@ -65,7 +63,7 @@ const InputBox = () => {
}; };
invokeSendMessage(msgObj); invokeSendMessage(msgObj);
setTextContent(''); setTextContent('');
dispatch(setReplyTo({})); setReferenceMsg({});
} }
} }
@ -74,7 +72,7 @@ const InputBox = () => {
{referenceMsg.id && ( {referenceMsg.id && (
<Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'> <Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'>
<div className=' border-l-3 border-y-0 border-r-0 border-slate-300 border-solid pl-2 pr-1 py-1'>{referenceMsg.text}</div> <div className=' border-l-3 border-y-0 border-r-0 border-slate-300 border-solid pl-2 pr-1 py-1'>{referenceMsg.text}</div>
<Button type='text' title='取消引用' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => dispatch(setReplyTo({}))} /> <Button type='text' title='取消引用' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => (setReferenceMsg({}))} />
</Flex> </Flex>
)} )}
<Input.TextArea <Input.TextArea

@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react';
import { App, Popover, Flex, Button, List, Input } from 'antd'; import { App, Popover, Flex, Button, List, Input } from 'antd';
import { MessageOutlined, SendOutlined } from '@ant-design/icons'; import { MessageOutlined, SendOutlined } from '@ant-design/icons';
import { useAuthContext } from '@/stores/AuthContext'; import { useAuthContext } from '@/stores/AuthContext';
import { useConversationState } from '@/stores/ConversationContext'; import useConversationStore from '@/stores/ConversationStore';
import { cloneDeep, getNestedValue, objectMapper } from '@/utils/utils'; import { cloneDeep, getNestedValue, objectMapper } from '@/utils/utils';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { replaceTemplateString } from '@/lib/msgUtils'; import { replaceTemplateString } from '@/lib/msgUtils';
@ -25,7 +25,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const { notification } = App.useApp(); const { notification } = App.useApp();
const { loginUser } = useAuthContext(); const { loginUser } = useAuthContext();
const { currentConversation, templates } = useConversationState(); const { currentConversation, templates } = useConversationStore();
// : customer, agent // : customer, agent
const valueMapped = { ...cloneDeep(currentConversation), ...objectMapper(loginUser, { username: [{ key: 'agent_name' }, { key: 'your_name' }] }) }; const valueMapped = { ...cloneDeep(currentConversation), ...objectMapper(loginUser, { username: [{ key: 'agent_name' }, { key: 'your_name' }] }) };
useEffect(() => { useEffect(() => {

@ -1,20 +1,33 @@
import { useEffect, useState, useRef, useMemo } from 'react'; import { useEffect, useState, useRef, useMemo } from 'react';
import { Image, Alert } from 'antd'; import { Image, Alert } from 'antd';
import { MessageBox } from 'react-chat-elements'; import { MessageBox } from 'react-chat-elements';
import { useConversationState, useConversationDispatch } from '@/stores/ConversationContext'; import useConversationStore from '@/stores/ConversationStore';
import { setReplyTo } from '@/actions/ConversationActions';
const Messages = () => { const Messages = () => {
const { activeConversations, currentConversation } = useConversationState(); const { activeConversations, currentConversation, setReferenceMsg } = useConversationStore();
const dispatch = useConversationDispatch();
const messagesList = useMemo(() => activeConversations[currentConversation.sn] || [], [activeConversations, currentConversation.sn]); const messagesList = useMemo(() => (activeConversations[currentConversation.sn] || []).map(message => ({
...message,
key: message.id,
position: message.sender === 'me' ? 'right' : 'left',
onOpen: () => handlePreview(message),
...(message.sender === 'me' ? {
styles: { backgroundColor: '#ccd4ae' } ,
notchStyle: { fill: '#ccd4ae'},
replyButton: ['text'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false,
onReplyClick: () => (setReferenceMsg(message)),
className: 'whatsappme-container whitespace-pre-wrap',
} : {
replyButton: ['text'].includes(message.whatsapp_msg_type) ? true : false,
onReplyClick: () => (setReferenceMsg(message)),
}),
})), [activeConversations, currentConversation.sn]);
console.log('messagesList----------------------------------------------------', messagesList); console.log('messagesList----------------------------------------------------', messagesList);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messagesList, currentConversation.last_received_time]); }, [currentConversation.last_received_time]);
const [previewVisible, setPreviewVisible] = useState(false); const [previewVisible, setPreviewVisible] = useState(false);
const [previewSrc, setPreviewSrc] = useState(); const [previewSrc, setPreviewSrc] = useState();
@ -33,31 +46,8 @@ const Messages = () => {
return ( return (
<div> <div>
{messagesList.map((message, index) => ( {messagesList.map((message, index) => (
<MessageBox <MessageBox key={message.key}
// className={message.sender === 'me' ? 'whatsappme-container' : ''}
key={message.id}
position={message.sender === 'me' ? 'right' : 'left'}
{...message} {...message}
onOpen={() => handlePreview(message)}
// letterItem={{ id: 1, letter: 'AS' }}
// 'waiting'| 'sent' | 'received' | 'read'
styles={{ backgroundColor: message.sender === 'me' ? '#ccd5ae' : '' }}
{...(message.sender === 'me'
? {
style: { backgroundColor: '#ccd5ae' },
notchStyle: { fill: '#ccd5ae' },
// forwarded: false,
// todo: reaction: emoji
replyButton: ['text'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false,
onReplyClick: () => dispatch(setReplyTo(message)),
className: 'whatsappme-container whitespace-pre-wrap',
}
: {
replyButton: ['text'].includes(message.whatsapp_msg_type) ? true : false,
onReplyClick: () => dispatch(setReplyTo(message)),
})}
// notchStyle={{fill: '#ccd5ae'}}
// copiableDate={false}
/> />
))} ))}
<Image src={previewSrc} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} /> <Image src={previewSrc} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />

@ -1,12 +1,12 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { useConversationState } from '@/stores/ConversationContext'; import useConversationStore from '@/stores/ConversationStore';
import { Flex, Typography, Avatar, Alert } from 'antd'; import { Flex, Typography, Avatar, Alert } from 'antd';
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import LocalTimeClock from './LocalTimeClock'; import LocalTimeClock from './LocalTimeClock';
const MessagesHeader = () => { const MessagesHeader = () => {
const { websocketOpened, websocketRetrying, websocketRetrytimes, currentConversation } = useConversationState(); const {websocketOpened, websocketRetrying, websocketRetrytimes, currentConversation} = useConversationStore();
return ( return (
<> <>
{websocketOpened===false && <Alert type='error' message='断开连接' banner />} {websocketOpened===false && <Alert type='error' message='断开连接' banner />}

@ -33,37 +33,37 @@ const ConversationProvider = ({ children }) => {
console.log('ConversationProvider', state, dispatch); console.log('ConversationProvider', state, dispatch);
useEffect(() => { // useEffect(() => {
console.log('invoke provider'); // console.log('invoke provider');
const realtimeAPI = new RealTimeAPI( // const realtimeAPI = new RealTimeAPI(
{ // {
url: `${WS_URL}?opisn=${userId || ''}&_spam=${Date.now().toString()}`, // url: `${WS_URL}?opisn=${userId || ''}&_spam=${Date.now().toString()}`,
protocol: 'WhatsApp', // protocol: 'WhatsApp',
}, // },
() => {dispatch(updateWebsocketState(true)); dispatch(updateWebsocketRetrytimes(0));}, // () => {dispatch(updateWebsocketState(true)); dispatch(updateWebsocketRetrytimes(0));},
() => dispatch(updateWebsocketState(false)), // () => dispatch(updateWebsocketState(false)),
(n) => dispatch(updateWebsocketRetrytimes(n)) // (n) => dispatch(updateWebsocketRetrytimes(n))
); // );
realtimeAPI.onError(() => dispatch(addError('Error'))); // realtimeAPI.onError(() => dispatch(addError('Error')));
realtimeAPI.onMessage(handleMessage); // realtimeAPI.onMessage(handleMessage);
realtimeAPI.onCompletion(() => dispatch(addError('Connection broken'))); // realtimeAPI.onCompletion(() => dispatch(addError('Connection broken')));
dispatch(initWebsocket(realtimeAPI)); // dispatch(initWebsocket(realtimeAPI));
return () => { // return () => {
realtimeAPI.disconnect(); // realtimeAPI.disconnect();
}; // };
}, []); // }, []);
useEffect(() => { // useEffect(() => {
fetchConversationsList({ opisn: userId }).then((data) => { // fetchConversationsList({ opisn: userId }).then((data) => {
dispatch(receivedConversationList(data)); // dispatch(receivedConversationList(data));
}); // });
fetchTemplates().then((data) => dispatch(receivedTemplates(data))); // fetchTemplates().then((data) => dispatch(receivedTemplates(data)));
return () => {}; // return () => {};
}, []); // }, []);
const handleMessage = (data) => { const handleMessage = (data) => {
console.log('handleMessage------------------'); console.log('handleMessage------------------');
console.log(data); console.log(data);

Loading…
Cancel
Save