You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Global-sales/src/views/Conversations/Online/MessagesList.jsx

217 lines
9.2 KiB
React

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';
2 years ago
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'>
[&nbsp;{headerObj.parameters[0].type}&nbsp;]
</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),
2 years ago
Component: () => <div key={'talk-now'}>发消息</div>,
},
]
: []),
{
onClickButton: () => {
navigator.clipboard.writeText(message.text);
appMessage.success('复制成功😀')
},
2 years ago
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;