|
|
|
import { useEffect, useRef, useState, forwardRef, memo } from 'react';
|
|
|
|
import { MessageBox } from 'react-chat-elements';
|
|
|
|
import { App, Button } from 'antd';
|
|
|
|
import { DownOutlined, LoadingOutlined } from '@ant-design/icons';
|
|
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
|
|
import useConversationStore from '@/stores/ConversationStore';
|
|
|
|
import { groupBy, isEmpty, } from '@/utils/commons';
|
|
|
|
import ConversationsNewItem from './ConversationsNewItem';
|
|
|
|
|
|
|
|
const MessagesList = ({ messages, handlePreview, reference, longListLoading, getMoreMessages, shouldScrollBottom, loadNextPage, handleContactClick, ...props }) => {
|
|
|
|
|
|
|
|
const { message: appMessage } = App.useApp()
|
|
|
|
|
|
|
|
const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
|
|
|
|
|
|
|
|
// const messagesEndRef = useRef(null);
|
|
|
|
const messageRefs = useRef([]);
|
|
|
|
const [focusMsg, setFocusMsg] = useState('');
|
|
|
|
useEffect(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
setFocusMsg('');
|
|
|
|
}, 3500);
|
|
|
|
|
|
|
|
return () => '';
|
|
|
|
}, [focusMsg])
|
|
|
|
|
|
|
|
|
|
|
|
const scrollToBottom = (force = false) => {
|
|
|
|
if (reference.current && (shouldScrollBottom || force)) {
|
|
|
|
reference.current.scrollTop = reference.current.scrollHeight;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const scrollToMessage = (id, index) => {
|
|
|
|
const _i = index || messages.findIndex((msg) => msg.id === id);
|
|
|
|
if (_i >= 0) {
|
|
|
|
messageRefs.current[_i].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
setFocusMsg(id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(scrollToBottom, [messages]);
|
|
|
|
|
|
|
|
const RenderText = memo(function renderText({ str, className, template }) {
|
|
|
|
|
|
|
|
let headerObj, footerObj, buttonsArr;
|
|
|
|
if (!isEmpty(template) && !isEmpty(template.components)) {
|
|
|
|
const componentsObj = groupBy(template.components, (item) => item.type);
|
|
|
|
headerObj = componentsObj?.header?.[0];
|
|
|
|
footerObj = componentsObj?.footer?.[0];
|
|
|
|
buttonsArr = componentsObj?.buttons?.reduce((r, c) => r.concat(c.buttons), []);
|
|
|
|
}
|
|
|
|
|
|
|
|
const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji_Presentation}|\d{4,})/gmu).filter((s) => s !== '');
|
|
|
|
const links = str.match(/https?:\/\/[\S]+/gi) || [];
|
|
|
|
const numbers = str.match(/\d{4,}/g) || [];
|
|
|
|
const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || [];
|
|
|
|
const extraClass = isEmpty(emojis) ? '' : '';
|
|
|
|
const objArr = parts.reduce((prev, curr, index) => {
|
|
|
|
if (links.includes(curr)) {
|
|
|
|
prev.push({ type: 'link', key: curr });
|
|
|
|
} else if (numbers.includes(curr)) {
|
|
|
|
prev.push({ type: 'number', key: curr });
|
|
|
|
} else if (emojis.includes(curr)) {
|
|
|
|
prev.push({ type: 'emoji', key: curr });
|
|
|
|
} else {
|
|
|
|
prev.push({ type: 'text', key: curr });
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
}, []);
|
|
|
|
return (
|
|
|
|
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`} key={'msg-text'}>
|
|
|
|
{headerObj ? (
|
|
|
|
<div className='text-neutral-500 text-center'>
|
|
|
|
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
|
|
|
|
{'image' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <img src={headerObj.parameters[0].image.link} height={100}></img>}
|
|
|
|
{['document', 'video'].includes((headerObj?.parameters?.[0]?.type || '').toLowerCase()) && (
|
|
|
|
<a href={headerObj.parameters[0][headerObj.parameters[0].type].link} target='_blank' key={headerObj.format} rel='noreferrer' className='text-sm'>
|
|
|
|
[ {headerObj.parameters[0].type} ]
|
|
|
|
</a>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
{(objArr || []).map((part, index) => {
|
|
|
|
if (part.type === 'link') {
|
|
|
|
return (
|
|
|
|
<a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-sm'>
|
|
|
|
{part.key}
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
} else if (part.type === 'number') {
|
|
|
|
return (
|
|
|
|
<a key={`${part.key}${index}`} className='text-sm' onClick={() => {setNewChatModalVisible(true);
|
|
|
|
setNewChatFormValues(prev => ({...prev, phone_number: part.key, is_current_order: true, }))}}>
|
|
|
|
{part.key}
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// if (part.type === 'emoji')
|
|
|
|
return part.key;
|
|
|
|
}
|
|
|
|
})}
|
|
|
|
{footerObj ? <div className=' text-neutral-500'>{footerObj.text}</div> : null}
|
|
|
|
{buttonsArr && buttonsArr.length > 0 ? (
|
|
|
|
<div className='flex flex-row gap-1'>
|
|
|
|
{buttonsArr.map((btn, index) =>
|
|
|
|
btn.type.toLowerCase() === 'url' ? (
|
|
|
|
<Button className='text-blue-500' size={'small'} href={btn.url} target={'_blank'} key={btn.url} rel='noreferrer'>
|
|
|
|
{btn.text}
|
|
|
|
</Button>
|
|
|
|
) : btn.type.toLowerCase() === 'phone_number' ? (
|
|
|
|
<Button className='text-blue-500' size={'small'} key={btn.phone_number} rel='noreferrer'>
|
|
|
|
{btn.text} ({btn.phone_number})
|
|
|
|
</Button>
|
|
|
|
) : (
|
|
|
|
<Button className='text-blue-500' size={'small'} key={btn.type}>
|
|
|
|
{btn.text}
|
|
|
|
</Button>
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
const onLoadMore = async () => {
|
|
|
|
const newLen = await getMoreMessages();
|
|
|
|
};
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
|
|
const MessageBoxWithRef = forwardRef((props, ref) => (
|
|
|
|
<div ref={ref}>
|
|
|
|
<MessageBox {...props} />
|
|
|
|
</div>
|
|
|
|
));
|
|
|
|
|
|
|
|
const [newChatModalVisible, setNewChatModalVisible] = useState(false);
|
|
|
|
const [newChatFormValues, setNewChatFormValues] = useState({});
|
|
|
|
const handleNewChat = async (values) => {
|
|
|
|
const newContact = { wa_id: values.wa_id };
|
|
|
|
await handleContactClick([newContact]);
|
|
|
|
setNewChatModalVisible(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 && messages.length > 0 && (
|
|
|
|
<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)}
|
|
|
|
key={message.id}
|
|
|
|
{...message}
|
|
|
|
position={message.sender === 'me' ? 'right' : 'left'}
|
|
|
|
onReplyClick={() => setReferenceMsg(message)}
|
|
|
|
onReplyMessageClick={() => scrollToMessage(message.reply.id)}
|
|
|
|
onOpen={() => handlePreview(message)}
|
|
|
|
onTitleClick={() => handlePreview(message)}
|
|
|
|
text={<RenderText str={message?.text || ''} className={message.status === 'failed' ? 'line-through text-neutral-400' : ''} template={message.template} />}
|
|
|
|
replyButton={['text', 'document', 'image'].includes(message.whatsapp_msg_type)}
|
|
|
|
{...(message.sender === 'me'
|
|
|
|
? {
|
|
|
|
styles: { backgroundColor: '#ccd4ae' },
|
|
|
|
notchStyle: { fill: '#ccd4ae' },
|
|
|
|
replyButton: ['text', 'document', 'image'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false,
|
|
|
|
}
|
|
|
|
: {})}
|
|
|
|
className={[
|
|
|
|
'whitespace-pre-wrap',
|
|
|
|
message.whatsapp_msg_type === 'sticker' ? 'bg-transparent' : '',
|
|
|
|
message.sender === 'me' ? 'whatsappme-container' : '',
|
|
|
|
focusMsg === message.id ? 'message-box-focus' : '',
|
|
|
|
message.status === 'failed' ? 'failed-msg' : '',
|
|
|
|
].join(' ')}
|
|
|
|
{...(message.type === 'meetingLink'
|
|
|
|
? {
|
|
|
|
actionButtons: [
|
|
|
|
...(message.waBtn
|
|
|
|
? [
|
|
|
|
{
|
|
|
|
onClickButton: () => handleContactClick(message.data),
|
|
|
|
Component: () => <div key={'talk-now'}>发消息</div>,
|
|
|
|
},
|
|
|
|
]
|
|
|
|
: []),
|
|
|
|
{
|
|
|
|
onClickButton: () => {
|
|
|
|
navigator.clipboard.writeText(message.text);
|
|
|
|
appMessage.success('复制成功😀')
|
|
|
|
},
|
|
|
|
Component: () => <div key={'copy'}>复制</div>,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
: {})}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
<Button onClick={() => scrollToBottom(true)} ghost type={'dashed'} shape={'circle'} className=' absolute bottom-1 right-4' icon={<DownOutlined />} />
|
|
|
|
<ConversationsNewItem initialValues={newChatFormValues} open={newChatModalVisible} onCreate={handleNewChat} onCancel={() => setNewChatModalVisible(false)} />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default MessagesList;
|