解决链接之后上级组件re-render; 获取loginUser; 不含参数的模板; 会话列表和消息右键菜单; 主动重连

dev/mobile
Lei OT 2 years ago
parent 6344b2068e
commit ad7b43d752

@ -185,7 +185,8 @@ export const whatsappMsgTypeMapped = {
type: 'photo',
data: (msg) => ({
id: msg.wamid,
data: { id: msg.wamid, uri: msg.image.link, width: 200, height: 200, alt: '' },
text: msg.image.caption,
data: { id: msg.wamid, uri: msg.image.link, width: 200, height: 200, alt: msg.image.caption, },
onOpen: () => {
console.log('Open image', msg.image.link);
},

@ -28,12 +28,12 @@ const initialConversationState = {
// referenceMsg: {},
};
export const templatesSlice = (set) => ({
const templatesSlice = (set) => ({
templates: [],
setTemplates: (templates) => set({ templates }),
});
export const websocketSlice = (set, get) => ({
const websocketSlice = (set, get) => ({
websocket: null,
websocketOpened: null,
websocketRetrying: null,
@ -72,6 +72,13 @@ export const websocketSlice = (set, get) => ({
websocket.disconnect();
return set({ websocket: null });
},
reconnectWebsocket: (userId) => {
const {disconnectWebsocket, connectWebsocket} = get();
disconnectWebsocket();
setTimeout(() => {
connectWebsocket(userId);
}, 500);
},
handleMessage: (data) => {
console.log('handleMessage------------------');
console.log(data);
@ -113,12 +120,12 @@ export const websocketSlice = (set, get) => ({
},
});
export const referenceMsgSlice = (set) => ({
const referenceMsgSlice = (set) => ({
referenceMsg: {},
setReferenceMsg: (referenceMsg) => set({ referenceMsg }),
});
export const conversationSlice = (set, get) => ({
const conversationSlice = (set, get) => ({
conversationsList: [],
currentConversation: {},
@ -166,7 +173,7 @@ export const conversationSlice = (set, get) => ({
},
});
export const messageSlice = (set, get) => ({
const messageSlice = (set, get) => ({
msgListLoading: false,
activeConversations: {},
setMsgLoading: (msgListLoading) => set({ msgListLoading }),

@ -13,7 +13,6 @@ import 'dayjs/locale/zh-cn'
import 'react-chat-elements/dist/main.css'
import '@/assets/App.css'
import AppLogo from '@/assets/logo-gh.png'
import { isEmpty } from '@/utils/commons'
const { Header, Footer, Content } = Layout
const { Title } = Typography
@ -42,16 +41,16 @@ function AuthApp() {
token: { colorBgContainer },
} = theme.useToken()
const { connectWebsocket, disconnectWebsocket, fetchInitialData } = useConversationStore();
const { userId } = loginUser;
useEffect(() => {
if (loginUser && loginUser.userId) {
connectWebsocket(loginUser.userId);
fetchInitialData(loginUser.userId);
if (userId) {
useConversationStore.getState().connectWebsocket(userId);
useConversationStore.getState().fetchInitialData(userId);
}
return () => {
disconnectWebsocket();
useConversationStore.getState().disconnectWebsocket();
}
}, [loginUser.userId]);
}, [userId]);
return (
<ConfigProvider
@ -109,10 +108,10 @@ function AuthApp() {
trigger={['click']}
>
<a onClick={(e) => e.preventDefault()} style={{ color: colorPrimary }}>
<Space><Avatar
<Space><Avatar
style={{
backgroundColor: colorPrimary,
}}
}}
src={loginUser.avatarUrl}>{loginUser.username.substring(1)}</Avatar>{loginUser.username}<DownOutlined /></Space>
</a>
</Dropdown>

@ -18,16 +18,7 @@ const { Sider, Content, Header, Footer } = Layout;
*
*/
const ChatWindow = () => {
console.log('chat window;;;;;;;;;;;;;;;;;;;;;;;;');
// const { order_sn } = useParams();
// const { loginUser } = useAuthContext();
// const { currentConversation } = useConversationStore();
useEffect(() => {
console.log('chat window 222;;;;;;;;;;;;;;;;;;;;;;;;');
return () => {};
}, []);
const [collapsedLeft, setCollapsedLeft] = useState(false);
const [collapsedRight, setCollapsedRight] = useState(false);
@ -40,17 +31,18 @@ const ChatWindow = () => {
className='h-full overflow-y-auto'
style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}
collapsible={true}
breakpoint='xxl'
breakpoint='xl'
collapsedWidth={73}
collapsed={collapsedLeft}
onBreakpoint={(broken) => {
console.log('xxxxxxxxxxxxxxxxxxxxxx', broken);
setCollapsedLeft(broken)
setCollapsedRight(broken)
}}
trigger={null}>
<ConversationsList />
</Sider>
<Content style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<Content style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)', minWidth: '360px' }}>
<Layout className='h-full'>
<Header className='ant-layout-sider-light ant-card h-auto flex justify-between gap-1 items-center'>
<Button type='text' icon={collapsedLeft ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} onClick={() => setCollapsedLeft(!collapsedLeft)} className=' rounded-none rounded-l' />
@ -75,7 +67,7 @@ const ChatWindow = () => {
className=' overflow-y-auto'
style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}
collapsible={true}
breakpoint='xxl'
breakpoint='xl'
collapsedWidth={0}
trigger={null}
collapsed={collapsedRight}>

@ -2,77 +2,34 @@ import { useEffect, useState } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Button, Dropdown } from 'antd';
import { MoreOutlined } from '@ant-design/icons';
import { useAuthContext } from '@/stores/AuthContext';
import { fetchOrderConversationsList, fetchConversationItemClose, fetchMessages } from '@/actions/ConversationActions';
import { ChatList, } from 'react-chat-elements';
import { ChatList, ChatItem } from 'react-chat-elements';
import { isEmpty } from '@/utils/utils';
import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore'
const CDropdown = (props) => {
const { delConversationitem } = useConversationStore();
const handleConversationItemClose = async () => {
await fetchConversationItemClose({ conversationid: props.sn, opisn: props.opi_sn });
delConversationitem(props);
};
return (
<Dropdown
key={'more-action'}
trigger={'click'}
menu={{
items: [{ key: 'close', danger: true, label: '关闭会话' }],
onClick: (e) => {
e.domEvent.stopPropagation();
switch (e.key) {
case 'close':
return handleConversationItemClose();
default:
return;
}
},
}}>
<Button key={'More'} type='text' title='More' className=' rounded-none text-gray-400' icon={<MoreOutlined />} size={'middle'} onClick={(e) => e.stopPropagation()} />
</Dropdown>
);
}
/**
* []
*/
const Conversations = () => {
const { state: orderRow } = useLocation()
const { state: orderRow } = useLocation();
const { coli_guest_WhatsApp } = orderRow || {};
const { order_sn } = useParams();
const navigate = useNavigate();
const { loginUser } = useAuthContext();
const { loadUser } = useAuthStore();
const loginUser = loadUser();
const { userId } = loginUser;
const { initialState, activeConversations, currentConversation, conversationsList, addToConversationList, setCurrentConversation, receivedMessageList, setMsgLoading } = useConversationStore();
const [chatlist, setChatlist] = useState([]);
useEffect(() => {
setChatlist(
conversationsList.map((item) => ({
...item,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${item.whatsapp_name.trim() || item.whatsapp_phone_number}`,
id: item.sn,
alt: item.whatsapp_name,
title: item.whatsapp_name.trim() || item.whatsapp_phone_number,
// subtitle: item.whatsapp_phone_number,
// subtitle: item.lastMessage,
date: item.last_received_time, // last_send_time
unread: item.unread_msg_count,
// showMute: true,
// muted: false,
// showVideoCall: true,
// statusColor: '#ccd5ae',
// statusColorType: 'badge',
// statusText: 'online',
className: String(item.sn) === String(currentConversation.sn) ? '__active text-primary underline bg-whatsapp-me ' : '',
customStatusComponents: [() => CDropdown(item)],
}))
);
return () => {};
}, [conversationsList, currentConversation]);
const {
initialState,
activeConversations,
currentConversation,
conversationsList,
addToConversationList,
delConversationitem,
setCurrentConversation,
receivedMessageList,
setMsgLoading,
} = useConversationStore();
const [switchToC, setSwitchToC] = useState({});
const [shouldFetchCList, setShouldFetchCList] = useState(true);
@ -91,8 +48,8 @@ const Conversations = () => {
if (!isEmpty(data)) {
addToConversationList(data);
}
// const ifCurrent = data.findIndex((item) => item.sn === currentConversation.sn);
const ifCurrent = conversationsList.findIndex((item) => item.coli_sn === Number(colisn));
let ifCurrent = data.findIndex((item) => item.sn === currentConversation.sn);
ifCurrent = ifCurrent !== -1 ? ifCurrent : conversationsList.findIndex((item) => item.coli_sn === Number(colisn));
if (ifCurrent !== -1) {
switchConversation(conversationsList[ifCurrent === -1 ? 0 : ifCurrent]);
} else {
@ -118,7 +75,7 @@ const Conversations = () => {
};
const onSwitchConversation = (item) => {
if ( ! isEmpty(item.coli_sn)) {
if (!isEmpty(item.coli_sn)) {
setSwitchToC(item);
setShouldFetchCList(false);
navigate(`/order/chat/${item.coli_sn}`, { replace: true });
@ -128,13 +85,45 @@ const Conversations = () => {
switchConversation(item);
};
const handleConversationItemClose = async (item) => {
await fetchConversationItemClose({ conversationid: item.sn, opisn: item.opi_sn });
delConversationitem(item);
};
return (
<>
<ChatList
className=' overflow-x-hidden'
dataSource={chatlist}
onClick={(item) => onSwitchConversation(item)}
/>
<div className=' overflow-x-hidden'>
{conversationsList.map((item) => (
<Dropdown
key={item.sn}
menu={{
items: [{ label: '关闭会话', key: 'close', danger: true, }],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
switch (key) {
case 'close':
return handleConversationItemClose(item);
default:
return;
}
},
}}
trigger={['contextMenu']}>
<ChatItem
{...item}
key={item.sn}
id={item.sn}
avatar={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.whatsapp_name.trim() || item.whatsapp_phone_number}`}
alt={`${item.whatsapp_name.trim()}`}
title={item.whatsapp_name.trim() || item.whatsapp_phone_number}
date={item.last_received_time}
unread={item.unread_msg_count}
className={String(item.sn) === String(currentConversation.sn) ? '__active text-primary underline bg-whatsapp-me border-y-0 border-e-0 border-s-2 border-solid border-whatsapp-me ' : ''}
onClick={() => onSwitchConversation(item)}
/>
</Dropdown>
))}
</div>
</>
);
};

@ -0,0 +1,59 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { Upload, Button, message } from 'antd';
import {
SendOutlined,
MessageOutlined,
SmileOutlined,
PictureOutlined,
FileImageOutlined,
CommentOutlined,
UploadOutlined,
CloudUploadOutlined,
FolderAddOutlined,
FilePdfOutlined,
CloseCircleOutlined,
} from '@ant-design/icons';
import useConversationStore from '@/stores/ConversationStore';
const props = {
name: 'file',
action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188',
headers: {
authorization: 'authorization-text',
},
showUploadList: false,
};
const ImageUpload = ({ disabled, invokeSendMessage }) => {
const { currentConversation, referenceMsg, setReferenceMsg } = useConversationStore();
const [uploading, setUploading] = useState(false);
const handleSendImage = (src) => {
const msgObj = {
type: 'photo',
data: { uri: src, },
};
invokeSendMessage(msgObj);
};
return (
<Upload
{...props}
onChange={(info) => {
setUploading(info.file.status === 'uploading');
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
// message.success(`${info.file.name} file uploaded successfully`);
// test: src
// handleSendImage('blob:https://web.whatsapp.com/bbe878fc-7bde-447f-aa28-a4b929621a50');
// handleSendImage('https://images.chinahighlights.com//allpicture/2020/04/9330cd3c78a34c81afd3b1fb.jpg');
} else if (info.file.status === 'error') {
message.error(`图片添加失败`);
}
}}>
<Button key={'addPic'} type='text' loading={uploading} disabled={disabled} icon={<PictureOutlined />} size={'middle'} className='text-primary rounded-none' />
</Upload>
);
};
export default ImageUpload;

@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react';
import { App, Popover, Flex, Button, List, Input } from 'antd';
import { MessageOutlined, SendOutlined } from '@ant-design/icons';
import { useAuthContext } from '@/stores/AuthContext';
import useAuthStore from '@/stores/AuthStore'
import useConversationStore from '@/stores/ConversationStore';
import { cloneDeep, getNestedValue, objectMapper } from '@/utils/utils';
import { v4 as uuid } from 'uuid';
@ -24,7 +24,8 @@ const splitTemplate = (template) => {
const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
const searchInputRef = useRef(null);
const { notification } = App.useApp();
const { loginUser } = useAuthContext();
const { loadUser } = useAuthStore();
const loginUser = loadUser();
const { currentConversation, templates } = useConversationStore();
// : customer, agent
const valueMapped = { ...cloneDeep(currentConversation), ...objectMapper(loginUser, { username: [{ key: 'agent_name' }, { key: 'your_name' }] }) };
@ -52,12 +53,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
let valid = true;
const msgObj = {
type: 'whatsappTemplate',
to: currentConversation.whatsapp_phone_number,
id: `${currentConversation.sn}.${uuid()}`,
date: new Date(),
status: 'waiting',
// statusTitle: 'Ready to send',
sender: 'me',
template: {
name: fromTemplate.name,
language: { code: fromTemplate.language },
@ -99,7 +95,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
const renderForm = (tempItem) => {
const templateText = tempItem.components.body?.[0]?.text || '';
const tempArr = splitTemplate(templateText);
const keys = templateText.match(/{{(.*?)}}/g).map((key) => key.replace(/{{|}}/g, ''));
const keys = (templateText.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
const paramsVal = keys.reduce((r, k) => ({ ...r, [k]: getNestedValue(valueMapped, [k]) }), {});
return tempArr.map((ele) =>
@ -139,7 +135,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
placeholder='搜索名称'
/>
<List
className='w-96 h-4/6 overflow-y-auto text-slate-900'
className='h-4/6 overflow-y-auto text-slate-900' style={{width: '600px'}}
itemLayout='horizontal'
dataSource={dataSource}
rowKey={'name'}
@ -158,7 +154,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
}
description={
<>
<div className='divide-dashed divide-x-0 divide-y divide-gray-300'>
<div className=' max-h-40 overflow-y-auto divide-dashed divide-x-0 divide-y divide-gray-300'>
<div className='text-slate-500'>{renderForm(item)}</div>
{item.components?.footer?.[0] ? <div className=''>{item.components.footer[0].text || ''}</div> : null}
</div>

@ -1,17 +1,19 @@
import React, { useState } from 'react';
import { Input, Flex, Button, } from 'antd';
// import { Input } from 'react-chat-elements';
import { useAuthContext } from '@/stores/AuthContext';
import useAuthStore from '@/stores/AuthStore'
import useConversationStore from '@/stores/ConversationStore';
import { SendOutlined, MessageOutlined, SmileOutlined, PictureOutlined, CommentOutlined, UploadOutlined, CloudUploadOutlined, FolderAddOutlined, FilePdfOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { isEmpty } from '@/utils/utils';
import { v4 as uuid } from 'uuid';
import { sentMsgTypeMapped } from '@/lib/msgUtils';
import InputTemplate from './InputTemplate';
import InputTemplate from './Input/Template';
import InputImageUpload from './Input/ImageUpload';
import dayjs from 'dayjs';
const InputBox = () => {
const { loginUser } = useAuthContext();
const { loadUser } = useAuthStore();
const loginUser = loadUser();
const { userId } = loginUser;
const { websocket, websocketOpened, currentConversation, referenceMsg, setReferenceMsg, sentOrReceivedNewMessage } = useConversationStore();
const [textContent, setTextContent] = useState('');
@ -23,13 +25,25 @@ const InputBox = () => {
const textabled = (talkabled && !gt24h);
const invokeSendMessage = (msgObj) => {
console.log('sendMessage------------------', msgObj);
const contentToSend = sentMsgTypeMapped[msgObj.type].contentToSend(msgObj);
const msgObjMerge = {
id: `${currentConversation.sn}.${uuid()}`,
sender: 'me',
to: currentConversation.whatsapp_phone_number,
date: new Date(),
status: 'waiting',
...(referenceMsg.id ? { context: { message_id: referenceMsg.id }, message_origin: referenceMsg } : {}),
...msgObj,
};
console.log('sendMessage------------------', msgObjMerge);
const contentToSend = sentMsgTypeMapped[msgObjMerge.type].contentToSend(msgObjMerge);
console.log('content to send-------------------------------------', contentToSend);
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn });
const contentToRender = sentMsgTypeMapped[msgObj.type].contentToRender(msgObj);
const contentToRender = sentMsgTypeMapped[msgObjMerge.type].contentToRender(msgObjMerge);
console.log(contentToRender, 'contentToRender sendMessage------------------');
sentOrReceivedNewMessage(contentToRender.conversationid, contentToRender);
setTextContent('');
setReferenceMsg({});
};
const handleSendText = () => {
@ -37,35 +51,10 @@ const InputBox = () => {
const msgObj = {
type: 'text',
text: textContent,
sender: 'me',
to: currentConversation.whatsapp_phone_number,
id: `${currentConversation.sn}.${uuid()}`, // Date.now().toString(16),
date: new Date(),
status: 'waiting',
...(referenceMsg.id ? { context: { message_id: referenceMsg.id, }, message_origin: referenceMsg } : {}),
};
invokeSendMessage(msgObj);
setTextContent('');
setReferenceMsg({});
}
};
const handleSendImage = () => {
if (textContent.trim() !== '') {
const msgObj = {
type: 'photo',
data: { uri: textContent },
sender: 'me',
to: currentConversation.whatsapp_phone_number,
id: `${currentConversation.sn}.${uuid()}`, // Date.now().toString(16),
date: new Date(),
status: 'waiting',
...(referenceMsg.id ? { context: { message_id: referenceMsg.id }, message_origin: referenceMsg } : {}),
};
invokeSendMessage(msgObj);
setTextContent('');
setReferenceMsg({});
}
}
return (
<div>
@ -95,7 +84,7 @@ const InputBox = () => {
<Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'>
<Flex gap={4} className='divide-y-0 divide-x divide-solid divide-gray-500 *:text-primary *:rounded-none'>
<InputTemplate key='templates' disabled={!talkabled} invokeSendMessage={invokeSendMessage} />
<Button key={'addPic'} type='text' disabled={!textabled} className='' icon={<PictureOutlined />} size={'middle'} />
{/* <InputImageUpload key={'addNewPic'} disabled={!textabled} invokeSendMessage={invokeSendMessage} /> */}
{/* <Button type='text' className='' icon={<FolderAddOutlined />} size={'middle'} /> */}
{/* <Button type='text' className='' icon={<CloudUploadOutlined />} size={'middle'} /> */}
{/* <Button type='text' className='' icon={<FilePdfOutlined />} size={'middle'} /> */}

@ -53,23 +53,23 @@ const Messages = () => {
<div>
<Spin spinning={msgListLoading} tip={'正在读取...'} wrapperClassName='pt-8 '>
{messagesList.map((message, index) => (
// <Dropdown
// key={message.key}
// menu={{
// items: [{ label: '', key: 'reply' }],
// onClick: ({ key, domEvent }) => {
// domEvent.stopPropagation();
// switch (key) {
// case 'reply':
// return setReferenceMsg(message);
<Dropdown
key={message.key}
menu={{
items: [{ label: '回复', key: 'reply' }],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
switch (key) {
case 'reply':
return setReferenceMsg(message);
// default:
// return;
// }
// },
// }}
// trigger={['contextMenu']}
// >
default:
return;
}
},
}}
trigger={['contextMenu']}
>
<MessageBox
key={message.key}
{...message}
@ -77,7 +77,7 @@ const Messages = () => {
onOpen={() => handlePreview(message)}
{...(message.type === 'text' ? { text: <div dangerouslySetInnerHTML={{ __html: message.text }}></div> } : {})}
/>
// </Dropdown>
</Dropdown>
))}
<Image src={previewSrc} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />

@ -18,3 +18,16 @@
.chatwindow-wrapper .rce-mbox-text:after{
content: none;
}
.chatwindow-wrapper .rce-mbox-photo .rce-mbox-text{
padding-left: 8px;
}
.chatwindow-wrapper .rce-mbox-left-notch {
width: 10px;
height: 10px;
left: -9px;
}
.chatwindow-wrapper .rce-mbox-right-notch {
width: 10px;
height: 10px;
right: -9px;
}

Loading…
Cancel
Save