Merge branch 'main' of github.com:hainatravel/global-sales

# Conflicts:
#	src/views/OrderFollow.jsx
dev/mobile
Jimmy Liow 2 years ago
commit fae69a3dbe

@ -1,4 +1,4 @@
import { fetchJSON } from '@/utils/request';
import { fetchJSON, postForm, } from '@/utils/request';
import { API_HOST } from '@/config';
/**
@ -8,3 +8,22 @@ export const fetchSalesAgent = async (q) => {
const { errcode, result } = await fetchJSON(`https://p9axztuwd7x8a7.mycht.cn/service-Analyse2/GetOperatorInfo`, { q });
return errcode !== 0 ? [] : result.map((ele) => ({ ...ele, label: ele.cn_name, value: ele.op_id }));
};
/**
* 客人列表
*/
export const fetchCustomerList = async (q) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/GetWhatsappidList`, { search: q });
return errcode !== 0 ? [] : result.map((ele) => ({ ...ele, label: ele.whatsapp_name, value: ele.whatsapp_phone_number }));
};
/**
* 上传单个文件
* @returns {object} { errcode, result: { file_url } }
*/
export const postUploadFileItem = async (fileObj, rename) => {
const formData = new FormData();
formData.append('file', fileObj, rename);
const { errcode, result } = await postForm(`${API_HOST}/WAFileUpload`, formData);
return errcode !== 0 ? {} : result;
};

@ -36,15 +36,9 @@ export const fetchMessages = async (params) => {
pagesize: MESSAGE_PAGE_SIZE,
};
const { errcode, result } = await fetchJSON(`${API_HOST}/getcusmessages`, {...defaultParams, ...params});
return errcode !== 0 ? [] : parseRenderMessageList(result || []);
return errcode !== 0 ? [] : parseRenderMessageList(result.reverse() || []);
}
export const fetchCustomerProfile = async (colisn) => {
const { result } = await fetchJSON(`${API_HOST}/getorderinfo`, { colisn });
const data = result?.[0] || {};
return data;
};
/**
*
* @param {object} params { opisn, whatsappid, colisn }
@ -72,3 +66,8 @@ export const fetchCleanUnreadMsgCount = async (params) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/clean_unread_msg_count`, params);
return errcode !== 0 ? {} : result;
};
/**
* ------------------------------------------------------------------------------------------------
* 历史记录
*/

@ -149,7 +149,7 @@ const conversationSlice = (set, get) => ({
const newConversationsMapped = newConversations.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
const newListIds = newList.map((chatItem) => `${chatItem.sn}`);
const withoutNew = conversationsList.filter(item => !newListIds.includes(`${item.sn}`));
const withoutNew = conversationsList.filter((item) => !newListIds.includes(`${item.sn}`));
return set((state) => ({
conversationsList: [...newList, ...withoutNew],
@ -188,6 +188,19 @@ const conversationSlice = (set, get) => ({
conversationsList: [...conversationsList],
}));
},
updateCurrentConversation: (conversation) => set((state) => ({ currentConversation: { ...state.currentConversation, ...conversation } })),
updateConversationItem: (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],
...conversation,
})
: null;
return set({ conversationsList: [...conversationsList] });
},
});
const messageSlice = (set, get) => ({
@ -204,7 +217,7 @@ const messageSlice = (set, get) => ({
// msgUpdate
console.log('UPDATE_SENT_MESSAGE_ITEM-----------------------------------------------------------------');
// 更新会话中的消息
const { activeConversations } = get();
const { activeConversations, conversationsList } = get();
const targetId = message.conversationid;
const targetMsgs = (activeConversations[String(targetId)] || []).map((ele) => {
// 更新状态
@ -224,17 +237,17 @@ const messageSlice = (set, get) => ({
}
// 更新列表的时间
// 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,
// });
// }
if (message.status === 'received') { // 'delivered'
const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
conversationsList.splice(targetIndex, 1, {
...conversationsList[targetIndex],
last_received_time: message.deliverTime, // todo: 需要+8 hours
});
}
return set({
activeConversations: { ...activeConversations, [String(targetId)]: targetMsgs },
// conversationsList: [...conversationsList],
conversationsList: [...conversationsList],
});
},
sentOrReceivedNewMessage: (targetId, message) => {

@ -1,15 +1,12 @@
import { useNavigate } from 'react-router-dom';
import { memo, useCallback, useEffect, useRef, useState, forwardRef } from 'react';
import { Row, Col, Divider, Table, Card, Button, Input, Flex, Layout, Space, Empty, Radio, Select, DatePicker, Form, List, Avatar, Spin, Image } from 'antd';
import { StarFilled, ZoomInOutlined, StarOutlined, SearchOutlined } from '@ant-design/icons';
import { ChatList, ChatItem, MessageBox } from 'react-chat-elements';
import { Divider, Button, Input, Layout, Select, DatePicker, Form, List, Spin } from 'antd';
import { ChatItem, MessageBox } from 'react-chat-elements';
import { fetchConversationsList, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions';
import { isEmpty } from '@/utils/utils';
import useFormStore from '@/stores/FormStore';
import { useShallow } from 'zustand/react/shallow';
import { fetchSalesAgent } from '@/actions/CommonActions';
// import SearchInput from '@/components/SearchInput2';
import { fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions';
import SearchInput from '@/components/SearchInput';
const { Sider, Content, Header, Footer } = Layout;
@ -22,7 +19,11 @@ const { RangePicker } = DatePicker;
const SearchForm = memo(function ({ initialValues, onSubmit }) {
const [form] = Form.useForm();
function handleSubmit(values) {
onSubmit?.({...values, travel: values?.agent?.value || -1});
onSubmit?.({
...values,
travel: values?.agent?.value || '',
customer_name: values?.customer?.label || '',
});
}
return (
<Form
@ -36,57 +37,19 @@ const SearchForm = memo(function ({ initialValues, onSubmit }) {
<Form.Item label='顾问' name='agent' style={{ width: '200px' }}>
<SearchInput placeholder='搜索顾问' fetchOptions={fetchSalesAgent} />
</Form.Item>
<Form.Item label='客人' name='orderLabel' style={{ width: '200px' }}>
<Form.Item label='客人' name='customer' style={{ width: '200px' }}>
<SearchInput placeholder='搜索客人' fetchOptions={fetchCustomerList} />
</Form.Item>
{/* <Form.Item label='H' name='orderLabel' style={{ width: '200px' }}>
<Select
showSearch
placeholder='请选择'
optionFilterProp='children'
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
options={[
{
value: 'Denise',
label: 'Denise',
},
{
value: 'Kennedy',
label: 'Kennedy',
},
{
value: 'Harsh',
label: 'Harsh',
},
{
value: 'SMLiew',
label: 'SMLiew',
},
{
value: 'See Kok Ching',
label: 'See Kok Ching',
},
{
value: 'Jonathan Michael Lilley',
label: 'Jonathan Michael Lilley',
},
{
value: 'Loan',
label: 'Loan',
},
{
value: 'Leah Belle',
label: 'Leah Belle',
},
{
value: 'Christelle Narvasa',
label: 'Christelle Narvasa',
},
{
value: 'Mai Schaefer',
label: 'Mai Schaefer',
},
]}
options={[]}
/>
</Form.Item>
<Form.Item label='关键词' name='orderNumber'>
</Form.Item> */}
<Form.Item label='关键词' name='search'>
<Input placeholder='关键词' allowClear />
</Form.Item>
<Form.Item label='日期' name='startDate'>
@ -197,7 +160,7 @@ function ChatHistory() {
const onLoadMore = () => {
getMessages(selectedConversation);
window.dispatchEvent(new Event('resize'));
// window.dispatchEvent(new Event('resize'));
};
const loadMore = !loading && selectedConversation.loadNextPage ? (
<div className='text-center pt-3 mb-3 h-8 leading-8 border-dotted border-0 border-t border-slate-300'>

@ -2,7 +2,7 @@ import { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Button, Dropdown, Input } from 'antd';
import { MoreOutlined } from '@ant-design/icons';
import { fetchOrderConversationsList, fetchConversationItemClose, fetchMessages, fetchCleanUnreadMsgCount } from '@/actions/ConversationActions';
import { fetchOrderConversationsList, fetchConversationItemClose, fetchMessages, MESSAGE_PAGE_SIZE, fetchCleanUnreadMsgCount } from '@/actions/ConversationActions';
import { ChatList, ChatItem } from 'react-chat-elements';
import { isEmpty } from '@/utils/utils';
import useConversationStore from '@/stores/ConversationStore';
@ -19,7 +19,7 @@ const Conversations = () => {
const userId = useAuthStore((state) => state.loginUser.userId);
const initialState = useConversationStore((state) => state.initialState);
const activeConversations = useConversationStore((state) => state.activeConversations);
const [currentConversation, setCurrentConversation] = useConversationStore((state) => [state.currentConversation, state.setCurrentConversation]);
const [currentConversation, setCurrentConversation, updateCurrentConversation] = useConversationStore((state) => [state.currentConversation, state.setCurrentConversation, state.updateCurrentConversation]);
const conversationsList = useConversationStore((state) => state.conversationsList);
const addToConversationList = useConversationStore((state) => state.addToConversationList);
const delConversationitem = useConversationStore((state) => state.delConversationitem);
@ -76,6 +76,9 @@ const Conversations = () => {
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number, lasttime: '2024-01-01T00:25:30' });
setMsgLoading(false);
receivedMessageList(item.sn, data);
const thisLastTime = data.length > 0 ? data[data.length - 1].orgmsgtime : '';
const loadNextPage = !(data.length === 0 || data.length < MESSAGE_PAGE_SIZE);
updateCurrentConversation({ lasttime: thisLastTime, loadNextPage });
};
const switchConversation = async (item) => {
setCurrentConversation(item);

@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react';
import { useState, useRef, useEffect, memo } from 'react';
import { App, Popover, Flex, Button, List, Input } from 'antd';
import { MessageOutlined, SendOutlined } from '@ant-design/icons';
import useAuthStore from '@/stores/AuthStore'
@ -92,7 +92,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
});
};
const renderForm = (tempItem) => {
const RenderForm = memo(function renderForm({tempItem}) {
const templateText = tempItem.components.body?.[0]?.text || '';
const tempArr = splitTemplate(templateText);
const keys = (templateText.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
@ -116,11 +116,11 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
/>
)
);
};
});
return (
<>
<Popover overlayClassName='w-3/5'
fresh
fresh forceRender destroyTooltipOnHide={true}
content={
<>
<Input.Search
@ -155,7 +155,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
description={
<>
<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>
<div className='text-slate-500'><RenderForm tempItem={item}/></div>
{item.components?.footer?.[0] ? <div className=''>{item.components.footer[0].text || ''}</div> : null}
</div>
{/* <div className='text-right px-2'>

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { Input, Flex, Button, Image } from 'antd';
import { App, Input, Flex, Button, Image, } from 'antd';
// import { Input } from 'react-chat-elements';
import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore';
@ -23,17 +23,36 @@ import { sentMsgTypeMapped } from '@/lib/msgUtils';
import InputTemplate from './Input/Template';
import InputEmoji from './Input/Emoji';
import InputMediaUpload from './Input/MediaUpload';
import { postUploadFileItem } from '@/actions/CommonActions';
import dayjs from 'dayjs';
const aliOSSHost = `https://haina-sale-system.oss-cn-shenzhen.aliyuncs.com/WAMedia/`;
/**
* image
* ext: ani;bmp;gif;ico;jpe;jpeg;jpg;pcx;png;psd;tga;tif;tiff;wmf
*
* audio
* ext: aac;ac3;aif;aifc;aiff;au;cda;dts;fla;flac;it;m1a;m2a;m3u;m4a;mid;midi;mka;mod;mp2;mp3;mpa;ogg;ra;rmi;spc;rmi;snd;umx;voc;wav;wma;xm
*
* video
* ext: 3g2;3gp;3gp2;3gpp;amr;amv;asf;avi;bdmv;bik;d2v;divx;drc;dsa;dsm;dss;dsv;evo;f4v;flc;fli;flic;flv;hdmov;ifo;ivf;m1v;m2p;m2t;m2ts;m2v;m4b;m4p;m4v;mkv;mp2v;mp4;mp4v;mpe;mpeg;mpg;mpls;mpv2;mpv4;mov;mts;ogm;ogv;pss;pva;qt;ram;ratdvd;rm;rmm;rmvb;roq;rpm;smil;smk;swf;tp;tpr;ts;vob;vp6;webm;wm;wmp;wmv
*
*/
const fileTypesExt = {
sticker: ['webp'],
photo: ['jpeg', 'jpg', 'png'],
video: ['gif', 'mp4', '3gp'],
document: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv'],
audio: ['aac', 'mp4', 'm4a', 'mp3', 'amr', 'ogg'],
};
const InputComposer = () => {
const userId = useAuthStore(state => state.loginUser.userId);
const websocket = useConversationStore(state => state.websocket);
const websocketOpened = useConversationStore(state => state.websocketOpened);
const currentConversation = useConversationStore(state => state.currentConversation);
const referenceMsg = useConversationStore(state => state.referenceMsg);
const setReferenceMsg = useConversationStore(state => state.setReferenceMsg);
const complexMsg = useConversationStore(state => state.complexMsg);
const setComplexMsg = useConversationStore(state => state.setComplexMsg);
const [referenceMsg, setReferenceMsg] = useConversationStore(state => [state.referenceMsg, state.setReferenceMsg]);
const [complexMsg, setComplexMsg] = useConversationStore(state => [state.complexMsg, state.setComplexMsg]);
const sentOrReceivedNewMessage = useConversationStore(state => state.sentOrReceivedNewMessage);
const talkabled = !isEmpty(currentConversation.sn) && websocketOpened;
@ -103,6 +122,69 @@ const InputComposer = () => {
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn });
}
const { message } = App.useApp();
const [pastedUploading, setPastedUploading] = useState(false);
const readPasted = async (file, rename = false) => {
// 使 FileReader
const reader = new FileReader();
const suffix = file.name.slice(file.name.lastIndexOf('.')+1);
const newName = rename ? `${uuid()}.${suffix}` : file.name;
const type = Object.keys(fileTypesExt).find((type) => fileTypesExt[type].includes(suffix));
const dataUri = aliOSSHost + newName;
const msgObj = {
type: type,
name: newName,
uploadStatus: 'loading',
data: { dataUri: dataUri, link: dataUri, width: '100%', height: 150, loading: 0.01 },
id: uuid(),
};
//
reader.onload = (event) => {
const previewSrc = event.target.result;
msgObj.data.uri = previewSrc;
};
file.newName = newName;
file.msgData = msgObj;
// dataURL
reader.readAsDataURL(file);
return file;
};
const handlePaste = async (event) => {
const items = (event.clipboardData || window.clipboardData).items;
let tmpfile = null;
if (!items || items.length === 0) {
//
message.warning('当前浏览器不支持本地');
return;
}
let isNotFile = true;
for (let i = 0; i < items.length; i++) {
// if (items[i].type.indexOf("image") !== -1) {
if (items[i].kind.indexOf("file") !== -1) {
isNotFile = false;
tmpfile = items[i].getAsFile();
break;
}
}
if (isNotFile) {
//
return;
}
if (!tmpfile) {
message.warning('没有读取到粘贴内容');
return;
}
const shouldRename = tmpfile.type.indexOf('image') !== -1;
const _tmpFile = await readPasted(tmpfile, shouldRename);
setComplexMsg(_tmpFile.msgData);
setPastedUploading(true);
const { file_url } = await postUploadFileItem(tmpfile, _tmpFile.newName);
setPastedUploading(false);
setComplexMsg({..._tmpFile.msgData, uploadStatus: file_url ? 'done' : 'error'});
return;
}
const focusInput = () => {
textInputRef.current.focus({ cursor: 'end', preventScroll: true, });
};
@ -144,17 +226,27 @@ const InputComposer = () => {
{complexMsg.id && (
<Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'>
<div className='pl-2 pr-1 py-1'>
{(complexMsg.type === 'photo' && complexMsg.data.uri) && <Image width={100} src={complexMsg.data.uri} />}
{complexMsg.type === 'video' && <FileOutlined className=' text-red-400' />}
{complexMsg.type !== 'photo' && <span className='px-1'>{complexMsg.name}</span>}
{complexMsg.status === 'loading' && <LoadingOutlined className='px-1' />}
{complexMsg.status === 'done' && <CheckCircleOutlined className='px-1 text-primary' />}
{complexMsg.status === 'error' && <><CloseCircleOutlined className='px-1 text-red-400' /> <span>添加失败</span> </>}
{['photo', 'sticker'].includes(complexMsg.type) && complexMsg.data.uri ? (
<Image width={100} src={complexMsg.data.uri} />
) : (
<>
<FileOutlined className=' text-red-400' />
<span className='px-1'>{complexMsg.name}</span>
</>
)}
{complexMsg.uploadStatus === 'loading' && <LoadingOutlined className='px-1' />}
{/* {complexMsg.uploadStatus === 'done' && <CheckCircleOutlined className='px-1 text-primary' />} */}
{complexMsg.uploadStatus === 'error' && (
<>
<CloseCircleOutlined className='px-1 text-red-400' /> <span>添加失败</span>{' '}
</>
)}
</div>
<Button type='text' title='删除' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => setComplexMsg({})} />
</Flex>
)}
<Input.TextArea
onPaste={handlePaste}
ref={textInputRef}
size='large'
placeholder={gt24h ? 'This session has expired. Please send a template message to activate the session' : 'Enter for Send, Shift+Enter for new line'}
@ -175,14 +267,14 @@ const InputComposer = () => {
<Flex gap={4} className='*:text-primary *:rounded-none'>
<InputTemplate key='templates' disabled={!talkabled || textabled} invokeSendMessage={invokeSendMessage} />
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} />
<InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{invokeUploadFileMessage, invokeSendUploadMessage}} />
<InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} />
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />
<Button type='text' className='' icon={<AudioOutlined />} size={'middle'} />
<Button type='text' className='' icon={<FolderAddOutlined />} size={'middle'} />
<Button type='text' className='' icon={<CloudUploadOutlined />} size={'middle'} />
<Button type='text' className='' icon={<FilePdfOutlined />} size={'middle'} /> */}
</Flex>
<Button key={'send-btn'} onClick={handleSendText} type='primary' size='middle' icon={<SendOutlined />} disabled={!textabled}>
<Button key={'send-btn'} onClick={handleSendText} type='primary' size='middle' icon={<SendOutlined />} disabled={!textabled || pastedUploading}>
Send
</Button>
</Flex>

@ -1,37 +1,20 @@
import { useEffect, useRef, useState, forwardRef, memo } from 'react';
import { MessageBox } from 'react-chat-elements';
import { Button } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import { DownOutlined, LoadingOutlined } from '@ant-design/icons';
import { useShallow } from 'zustand/react/shallow';
import useConversationStore from '@/stores/ConversationStore';
import { isEmpty, olog } from '@/utils/utils';
import { isEmpty, } from '@/utils/utils';
const MessagesList = ({ messages, handlePreview, reference }) => {
const MessagesList = ({ messages, handlePreview, reference, longListLoading, getMoreMessages, shouldScrollBottom, loadNextPage, ...props }) => {
const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
// const messagesEndRef = useRef(null);
const messageRefs = useRef([]);
const [page, setPage] = useState(1);
let timeout = null;
const prevProps = useRef(props)
const fetchNextPage = async () => {
olog('fetchNextPage')
setPage(page + 1);
// Fetch next page of messages here
};
const handleScroll = (e) => {
const { scrollTop } = e.target;
const delay = 1000; // 1 second
if (scrollTop === 0) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(fetchNextPage, delay);
}
};
const scrollToBottom = () => {
if (reference.current) {
const scrollToBottom = (force = false) => {
if (reference.current && (shouldScrollBottom || force)) {
reference.current.scrollTop = reference.current.scrollHeight;
}
};
@ -45,18 +28,6 @@ const MessagesList = ({ messages, handlePreview, reference }) => {
useEffect(scrollToBottom, [messages]);
useEffect(() => {
const messageList = reference.current;
if (messageList) {
messageList.addEventListener('scroll', handleScroll);
}
return () => {
if (messageList) {
messageList.removeEventListener('scroll', handleScroll);
}
};
}, []);
const RenderText = memo(function renderText({ str }) {
const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji_Presentation})/gmu).filter((s) => s !== '');
const links = str.match(/https?:\/\/[\S]+/gi) || [];
@ -90,6 +61,9 @@ const MessagesList = ({ messages, handlePreview, reference }) => {
);
});
const onLoadMore = async () => {
const newLen = await getMoreMessages();
};
// eslint-disable-next-line react/display-name
const MessageBoxWithRef = forwardRef((props, ref) => (
<div ref={ref}>
@ -100,6 +74,11 @@ const MessagesList = ({ messages, handlePreview, reference }) => {
return (
<div className='relative h-full overflow-y-auto overflow-x-hidden flex flex-1'>
<div ref={reference} className='relative overflow-y-auto overflow-x-hidden block flex-1'>
{loadNextPage && (
<div className='text-center pt-3 mb-3 h-8 leading-8 '>
{!longListLoading ? <Button onClick={onLoadMore} type={'dashed'}>loading more</Button> : <LoadingOutlined className='text-primary' />}
</div>
)}
{messages.map((message, index) => (
<MessageBoxWithRef
ref={(el) => (messageRefs.current[index] = el)}
@ -137,7 +116,7 @@ const MessagesList = ({ messages, handlePreview, reference }) => {
/>
))}
</div>
<Button onClick={scrollToBottom} ghost type={'dashed'} shape={'circle'} className=' absolute bottom-1 right-4' icon={<DownOutlined />} />
<Button onClick={() => scrollToBottom(true)} ghost type={'dashed'} shape={'circle'} className=' absolute bottom-1 right-4' icon={<DownOutlined />} />
</div>
);
};

@ -3,20 +3,36 @@ import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
import { Image, } from 'antd';
import MessagesList from './MessagesList';
import { fetchCleanUnreadMsgCount } from '@/actions/ConversationActions';
import { fetchCleanUnreadMsgCount, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions';
const MessagesWrapper = () => {
const currentConversation = useConversationStore(useShallow((state) => state.currentConversation));
const [currentConversation, updateCurrentConversation] = useConversationStore(useShallow((state) => [state.currentConversation, state.updateCurrentConversation]));
const activeMessages = useConversationStore(useShallow((state) => (state.currentConversation.sn && state.activeConversations[state.currentConversation.sn] ? state.activeConversations[state.currentConversation.sn]: [])));
const [longList, setLongList] = useState([]);
const [longListLoading, setLongListLoading] = useState(false);
const [shouldScrollBottom, setShouldScrollBottom] = useState(true);
useEffect(() => {
setLongList(activeMessages);
setShouldScrollBottom(true);
if (currentConversation.opi_sn && currentConversation.whatsapp_phone_number && activeMessages.length > 0) {
fetchCleanUnreadMsgCount({ opisn: currentConversation.opi_sn, whatsappid: currentConversation.whatsapp_phone_number });
}
return () => {};
}, [activeMessages]);
}, [activeMessages, currentConversation.sn]);
const getMoreMessages = async () => {
setShouldScrollBottom(false);
setLongListLoading(true);
const data = await fetchMessages({ opisn: currentConversation.opi_sn, whatsappid: currentConversation.whatsapp_phone_number, lasttime: currentConversation?.lasttime || '2024-01-01T00:00:00' });
setLongListLoading(false);
setLongList(prevValue => data.concat(prevValue));
const thisLastTime = data.length > 0 ? data[data.length - 1].orgmsgtime : '';
const loadNextPage = !(data.length === 0 || data.length < MESSAGE_PAGE_SIZE);
updateCurrentConversation({ lasttime: thisLastTime, loadNextPage });
return data.length;
};
const reference = useRef(null);
@ -43,7 +59,7 @@ const MessagesWrapper = () => {
};
return (
<>
<MessagesList messages={activeMessages} {...{ reference, handlePreview }} />
<MessagesList messages={longList} dataSourceLen={longList.length} {...{ reference, handlePreview, longListLoading, setLongListLoading, getMoreMessages, shouldScrollBottom, loadNextPage: currentConversation?.loadNextPage ?? true }} />
<Image width={0} height={0} src={null} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />
</>
);

@ -120,8 +120,8 @@
.chatwindow-wrapper .rce-mbox-reply-message,
.chatwindow-wrapper .emoji
{
font-family: 'Noto Sans',"Noto Color Emoji", 'Apple Color Emoji', 'Twemoji Mozilla', 'Segoe UI Emoji', 'Segoe UI Symbol', 'EmojiOne Color', 'Android Emoji', Arial, sans-serif;
font-weight: 500;
font-family: 'Open Sans', 'Noto Sans',"Noto Color Emoji", 'Apple Color Emoji', 'Twemoji Mozilla', 'Segoe UI Emoji', 'Segoe UI Symbol', 'EmojiOne Color', 'Android Emoji', Arial, sans-serif;
font-weight: 400;
}
.chatwindow-wrapper .rce-mbox-text a{
color: #4f81a1;

@ -9,9 +9,9 @@ import dayjs from 'dayjs'
import { Conditional } from '@/components/Conditional'
import useOrderStore from '@/stores/OrderStore'
import useAuthStore from '@/stores/AuthStore'
import { copy, isEmpty } from '@/utils/commons'
import { copy, isNotEmpty } from '@/utils/commons'
import useFormStore from '@/stores/FormStore';
import { useShallow } from 'zustand/react/shallow';
import { useShallow } from 'zustand/react/shallow'
const { RangePicker } = DatePicker
@ -166,11 +166,12 @@ function OrderGroupTable({ formValues }) {
render: (text, record) => {
let regularText = ''
if (record.buytime > 0) regularText = '(R' + record.buytime + ')'
const whatsAppIcon = isEmpty(record.coli_guest_WhatsApp) ? null : <WhatsAppOutlined />
return (
<Space>
{whatsAppIcon}
<Link to={`/order/chat/${record.COLI_SN}`} state={record} title={record.coli_guest_WhatsApp}>{text + regularText}</Link>
{isNotEmpty(record.coli_guest_WhatsApp) && <WhatsAppOutlined className={['pl-1', record.last_received_time ? 'text-whatsapp' : 'text-neutral-500']} />}
<Link to={`/order/chat/${record.COLI_SN}`} state={record} title={record.coli_guest_WhatsApp}>
{text + regularText}
</Link>
<Badge
count={record.unread_msg}
style={{
@ -178,7 +179,7 @@ function OrderGroupTable({ formValues }) {
}}
/>
</Space>
)
);
}
},
{

Loading…
Cancel
Save