根据最后一条消息切换渠道; Email气泡
parent
dd6d3e686e
commit
ae5c210c28
@ -0,0 +1,66 @@
|
||||
import { createContext, useEffect, useState, memo } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons';
|
||||
import { MessageBox } from 'react-chat-elements';
|
||||
import { groupBy, isEmpty, } from '@/utils/commons';
|
||||
|
||||
const ChatboxEmail = ({ onOpenEditor, ...message }) => {
|
||||
|
||||
const RenderText = memo(function renderText({ className, email, sender }) {
|
||||
return (
|
||||
<div onClick={() => handlePreview(message)} className={`text-sm leading-5 emoji-text whitespace-pre-wrap cursor-pointer ${className}`} key={'msg-text'}>
|
||||
{sender === 'me' && <div><b>From: </b>{email.fromName} <{email.fromEmail}></div>}
|
||||
<div><b>To: </b>{email.toName} <{email.toEmail}></div>
|
||||
<div ><b>Subject: </b>{email.subject}</div>
|
||||
<hr className='border-0 border-solid border-b border-neutral-400'/>
|
||||
<div className='line-clamp-2 text-neutral-600'>{email.abstract}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const handlePreview = (message) => {
|
||||
console.log('handlePreview');
|
||||
|
||||
alert('on click: open email')
|
||||
}
|
||||
return (
|
||||
<MessageBox
|
||||
{...message}
|
||||
key={`${message.sn}.${message.id}`}
|
||||
type='text'
|
||||
title={ message.sender !== 'me' &&
|
||||
<>
|
||||
<MailOutlined className='text-indigo-600' />
|
||||
<span className={`pl-2 ${message.sender === 'me' ? '' : 'text-indigo-600'}`}>
|
||||
<b>From: </b>
|
||||
<span>
|
||||
{message?.emailOrigin?.fromName} <{message?.emailOrigin.fromEmail}>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
// titleColor={message.sender !== 'me' ? '#4f46e5' : ''} // 600
|
||||
notch={false}
|
||||
position={message.sender === 'me' ? 'right' : 'left'}
|
||||
onReplyClick={() => onOpenEditor(message.emailOrigin.replyToEmail)}
|
||||
// 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' : ''} email={message.emailOrigin} sender={message.sender} />}
|
||||
replyButton={message.sender !== 'me'}
|
||||
// replyButton={['text', 'document', 'image'].includes(message.whatsapp_msg_type)}
|
||||
{...(message.sender === 'me'
|
||||
? {
|
||||
styles: { backgroundColor: '#e0e7ff', boxShadow: 'none', border: '1px solid #818cf8' }, // 100 400
|
||||
replyButton: ['text', 'document', 'image'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false, // todo: 仅对接收的显示
|
||||
}
|
||||
: {})}
|
||||
className={[
|
||||
'whitespace-pre-wrap',
|
||||
message.sender === 'me' ? 'whatsappme-container' : '',
|
||||
// focusMsg === message.id ? 'message-box-focus' : '',
|
||||
message.status === 'failed' ? 'failed-msg' : '',
|
||||
].join(' ')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ChatboxEmail;
|
@ -0,0 +1,146 @@
|
||||
import { createContext, useEffect, useState, memo } from 'react';
|
||||
import { App, Button } from 'antd';
|
||||
import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons';
|
||||
import { MessageBox } from 'react-chat-elements';
|
||||
import { groupBy, isEmpty } from '@/utils/commons';
|
||||
import useConversationStore from '@/stores/ConversationStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, setNewChatFormValues, scrollToMessage, focusMsg, ...message }) => {
|
||||
const { message: appMessage } = App.useApp();
|
||||
const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
|
||||
|
||||
const openNewChatModal = ({ wa_id, wa_name }) => {
|
||||
setNewChatModalVisible(true);
|
||||
setNewChatFormValues((prev) => ({ ...prev, phone_number: wa_id, name: wa_name }));
|
||||
};
|
||||
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={() => openNewChatModal({ wa_id: part.key, wa_name: part.key })}>
|
||||
{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>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<MessageBox
|
||||
{...message}
|
||||
key={`${message.sn}.${message.id}`}
|
||||
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>,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default BubbleIM;
|
@ -0,0 +1,49 @@
|
||||
{
|
||||
"conversationid": 2983,
|
||||
"sn": 14201,
|
||||
"msg_direction": "outbound",
|
||||
"msgtime": "2024-02-21T11:37:33",
|
||||
"msgtext_AsJOSN": {},
|
||||
"msgtype": "email",
|
||||
"template_AsJOSN": {},
|
||||
"messageorigin_AsJOSN": [],
|
||||
"messageorigin_direction": "outbound",
|
||||
"orgmsgtime": "2024-02-22T00:41:30",
|
||||
"msgOrigin": {},
|
||||
"id": "emailid.qjMVpfPuxd8cwKs9o3bGIgYL6SWinB5vHRyQX1ZTU4OmeEAtDk07zaF=",
|
||||
"text": "",
|
||||
"title": "",
|
||||
"type": "email",
|
||||
"emailOrigin": {
|
||||
"id": "emailid.qjMVpfPuxd8cwKs9o3bGIgYL6SWinB5vHRyQX1ZTU4OmeEAtDk07zaF=",
|
||||
"status": "read",
|
||||
"fromName": "YCC",
|
||||
"fromEmail": "ycc@hainatravel.com",
|
||||
"toName": "LYT",
|
||||
"toEmail": "lyt@hainatravel.com",
|
||||
"cc": "lioyjun@gmail.com",
|
||||
"bcc": "",
|
||||
"subject": "发送示例",
|
||||
"body": "发送示例发送示例发送示例发送示例",
|
||||
"abstract": "发送示例发送示例发送示例发送示例发送示例……",
|
||||
"replyToEmail": "",
|
||||
"replyToName": "",
|
||||
"senderEmail": "",
|
||||
"senderName": "",
|
||||
"bCopyEmail1": "",
|
||||
"priority": "",
|
||||
"#": "#"
|
||||
},
|
||||
"date": "2024-02-21T03:37:33.000Z",
|
||||
"dateText": "02-21 11:37",
|
||||
"localDate": "2024-02-21 11:37:33",
|
||||
"from": "ycc@hainatravel.com",
|
||||
"sender": "me",
|
||||
"senderName": "me",
|
||||
"replyButton": false,
|
||||
"status": "sent",
|
||||
"dateString": "",
|
||||
"statusCN": "",
|
||||
"statusTitle": "",
|
||||
"whatsapp_msg_type": ""
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
import { createContext, useEffect, useState, useRef } from 'react';
|
||||
import { Button, Flex, Modal } from 'antd';
|
||||
import Draggable from 'react-draggable';
|
||||
import 'react-quill/dist/quill.snow.css';
|
||||
|
||||
import LexicalEditor from '@/components/LexicalEditor';
|
||||
|
||||
import { $getRoot, $getSelection } from 'lexical';
|
||||
|
||||
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||
|
||||
const theme = {
|
||||
// Theme styling goes here
|
||||
//...
|
||||
};
|
||||
|
||||
// Catch any errors that occur during Lexical updates and log them
|
||||
// or throw them as needed. If you don't throw them, Lexical will
|
||||
// try to recover gracefully without losing user data.
|
||||
function onError(error) {
|
||||
console.error(error);
|
||||
}
|
||||
const LexicalEditor1 = () => {
|
||||
const initialConfig = {
|
||||
namespace: 'MyEditor',
|
||||
theme,
|
||||
onError,
|
||||
};
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<RichTextPlugin contentEditable={<ContentEditable />} placeholder={<div>Enter some text...</div>} ErrorBoundary={LexicalErrorBoundary} />
|
||||
<HistoryPlugin />
|
||||
<AutoFocusPlugin />
|
||||
</LexicalComposer>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailEditor = ({ mobile, open, setOpen, fromEmail, ...props }) => {
|
||||
|
||||
const [dragDisabled, setDragDisabled] = useState(true);
|
||||
const [bounds, setBounds] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
});
|
||||
const draggleRef = useRef(null);
|
||||
const onStart = (_event, uiData) => {
|
||||
const { clientWidth, clientHeight } = window.document.documentElement;
|
||||
const targetRect = draggleRef.current?.getBoundingClientRect();
|
||||
if (!targetRect) {
|
||||
return;
|
||||
}
|
||||
setBounds({
|
||||
left: -targetRect.left + uiData.x,
|
||||
right: clientWidth - (targetRect.right - uiData.x),
|
||||
top: -targetRect.top + uiData.y,
|
||||
bottom: clientHeight - (targetRect.bottom - uiData.y),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
cursor: 'move',
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
if (dragDisabled) {
|
||||
setDragDisabled(false);
|
||||
}
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setDragDisabled(true);
|
||||
}}
|
||||
// fix eslintjsx-a11y/mouse-events-have-key-events
|
||||
// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/mouse-events-have-key-events.md
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
// end
|
||||
>
|
||||
写邮件: {fromEmail}
|
||||
</div>
|
||||
}
|
||||
open={open}
|
||||
mask={false}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
classNames={{ content: '!border !border-solid !border-indigo-500 rounded !p-2' }}
|
||||
okButtonProps={{ className: 'bg-indigo-500 shadow shadow-indigo-300 hover:!bg-indigo-400 active:bg-indigo-400 focus:bg-indigo-400' }}
|
||||
cancelButtonProps={{ className: 'hover:!text-indigo-500 hover:!border-indigo-400 active:border-indigo-400 focus:border-indigo-400' }}
|
||||
style={{ bottom: 0, left: '18%' }}
|
||||
width={mobile === undefined ? '800px' : '100%'}
|
||||
zIndex={2}
|
||||
// footer={false}
|
||||
onCancel={() => setOpen(false)}
|
||||
destroyOnClose={false} // todo:
|
||||
modalRender={(modal) => (
|
||||
<Draggable disabled={dragDisabled} bounds={bounds} nodeRef={draggleRef} onStart={(event, uiData) => onStart(event, uiData)}>
|
||||
<div ref={draggleRef}>{modal}</div>
|
||||
</Draggable>
|
||||
)}>
|
||||
<LexicalEditor />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailEditor;
|
Loading…
Reference in New Issue