根据最后一条消息切换渠道; Email气泡

dev/email
Lei OT 1 year ago
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}&nbsp;&lt;{email.fromEmail}&gt;</div>}
<div><b>To: </b>{email.toName}&nbsp;&lt;{email.toEmail}&gt;</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}&nbsp;&lt;{message?.emailOrigin.fromEmail}&gt;
</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'>
[&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={() => 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;

@ -9,9 +9,9 @@ const ChannelLogo = ({channel}) => {
case 'wa':
return <WhatsAppOutlined key={channel} className='text-whatsapp' />;
case 'email':
return <MailOutlined key={channel} className='text-violet-500' />
return <MailOutlined key={channel} className='text-indigo-500' />
default:
return <MailOutlined key={'channel'} className='text-violet-500' />
return <MailOutlined key={'channel'} className='text-indigo-500' />
}
}
export default ChannelLogo;

@ -0,0 +1,49 @@
{
"conversationid": 2983,
"sn": 14201,
"msg_direction": "inbound",
"msgtime": "2024-02-21T11:37:33",
"msgtext_AsJOSN": {},
"msgtype": "email",
"template_AsJOSN": {},
"messageorigin_AsJOSN": [],
"messageorigin_direction": "inbound",
"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": "阿坝州九寨岷江国际旅行社有限责任公司,中华游240903-Leeky240602070反馈表有低分项Grand Hyatt Chengdu,: 3;;更多详情请到助手平台查看。",
"abstract": "阿坝州九寨岷江国际旅行社有限责任公司……",
"replyToEmail": "ycc@hainatravel.com",
"replyToName": "YCC",
"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": "example@test.com",
"senderName": "example@test.com",
"replyButton": true,
"status": "",
"dateString": "",
"statusCN": "",
"statusTitle": "",
"whatsapp_msg_type": ""
}

@ -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": ""
}

@ -1,139 +1,28 @@
import { createContext, useEffect, useState, useRef } from 'react';
import { Button, Flex, Modal } from 'antd';
import Draggable from 'react-draggable';
import { useState } from 'react';
import { Button, Flex } from 'antd';
import 'react-quill/dist/quill.snow.css';
import EmailEditor from './EmailEditor';
import { WABIcon } from '@/components/Icons';
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 }) => {
const EmailComposer = ({ ...props }) => {
const [open, setOpen] = useState(false);
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),
});
};
const [useAddr, setUseAddr] = useState('');
const [fromEmail, setFromEmail] = useState('');
const openEditor = (email_addr) => {
setOpen(true);
setUseAddr(email_addr);
setFromEmail(email_addr);
};
return (
<>
<Button
type='primary'
className='bg-violet-500 shadow shadow-violet-300 hover:!bg-violet-400 active:bg-violet-400 focus:bg-violet-400'
onClick={() => openEditor('lyt@hainatravel.com')}>
lyt@hainatravel.com
</Button>
<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
>
写邮件: {useAddr}
</div>
}
open={open}
mask={false}
maskClosable={false}
classNames={{content: '!border !border-solid !border-primary rounded !p-2'}}
style={{ bottom: 0, left: '18%' }}
width={mobile === undefined ? '800px' : '100%'}
zIndex={2}
// footer={false}
onCancel={() => setOpen(false)}
destroyOnClose
modalRender={(modal) => (
<Draggable disabled={dragDisabled} bounds={bounds} nodeRef={draggleRef} onStart={(event, uiData) => onStart(event, uiData)}>
<div ref={draggleRef}>{modal}</div>
</Draggable>
)}>
<LexicalEditor />
</Modal>
</>
);
};
const EmailComposer = ({ ...props }) => {
return (
<Flex gap={8} className='p-2 bg-gray-200 rounded rounded-b-none border-gray-300 border-solid border border-b-0 border-x-0'>
<EmailEditor />
<Flex gap={8} className='p-2 bg-gray-200 justify-end rounded rounded-b-none border-gray-300 border-solid border border-b-0 border-x-0'>
{[{ email: 'lyt@hainatravel.com', name: 'LYT' }].map(({ email, name }, i) => (
<Button
key={email}
type='primary'
className='bg-indigo-500 shadow shadow-indigo-300 hover:!bg-indigo-400 active:bg-indigo-400 focus:bg-indigo-400'
onClick={() => openEditor(email)}>
{name}&nbsp;&lt;{email}&gt;
</Button>
))}
<EmailEditor {...{ open, setOpen }} fromEmail={fromEmail} key={'email-editor'} />
</Flex>
);
};

@ -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;

@ -28,7 +28,13 @@ import { postUploadFileItem } from '@/actions/CommonActions';
import ExpireTimeClock from '../ExpireTimeClock';
import dayjs from 'dayjs';
const InputComposer = ({ mobile, isWABA }) => {
const ButtonStyleClsMapped =
{
'waba': 'bg-waba shadow shadow-waba-300 hover:!bg-waba-400 active:bg-waba-400 focus:bg-waba-400',
'whatsapp': 'bg-whatsapp shadow shadow-whatsapp-300 hover:!bg-whatsapp-400 active:bg-whatsapp-400 focus:bg-whatsapp-400',
};
const InputComposer = ({ mobile, isWABA, channel }) => {
const userId = useAuthStore((state) => state.loginUser.userId);
const websocket = useConversationStore((state) => state.websocket);
const websocketOpened = useConversationStore((state) => state.websocketOpened);
@ -271,7 +277,7 @@ const InputComposer = ({ mobile, isWABA }) => {
placeholder={
!talkabled
? '请先选择会话'
: (!textabled0 && isWABA)
: !textabled0 && isWABA
? '会话已超24h不活跃. 请发送打招呼消息激活对话💬.'
: mobile === undefined
? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'
@ -306,7 +312,15 @@ const InputComposer = ({ mobile, isWABA }) => {
{/* <ExpireTimeClock expireTime={currentConversation.conversation_expiretime} /> */}
<div>{textPlaceHolder}</div>
</div>
<Button key={'send-btn'} onClick={handleSendText} type='primary' size='middle' icon={<SendOutlined />} disabled={!textabled || pastedUploading}>
<Button
key={'send-btn'}
onClick={handleSendText}
type='primary'
size='middle'
icon={<SendOutlined />}
disabled={!textabled || pastedUploading}
className={ButtonStyleClsMapped[channel]
}>
Send
</Button>
</Flex>

@ -5,12 +5,10 @@ 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 BubbleEmail from './Components/BubbleEmail';
import BubbleIM from './Components/BubbleIM';
const MessagesList = ({ messages, handlePreview, reference, longListLoading, getMoreMessages, shouldScrollBottom, loadNextPage, handleContactClick, setNewChatModalVisible, setNewChatFormValues, ...props }) => {
const { message: appMessage } = App.useApp()
const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
const MessagesList = ({ messages, handlePreview, reference, longListLoading, getMoreMessages, shouldScrollBottom, loadNextPage, handleContactClick, setNewChatModalVisible, setNewChatFormValues, ...listProps }) => {
// const messagesEndRef = useRef(null);
const messageRefs = useRef([]);
@ -40,100 +38,15 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
useEffect(scrollToBottom, [messages]);
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'>
[&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={() => 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>
);
});
const onLoadMore = async () => {
const newLen = await getMoreMessages();
await getMoreMessages();
};
// eslint-disable-next-line react/display-name
const MessageBoxWithRef = forwardRef((props, ref) => (
<div ref={ref}>
<MessageBox {...props} />
{props.whatsapp_msg_type
&& <BubbleIM {...props} {...{ scrollToMessage, focusMsg, handleContactClick, setNewChatModalVisible, setNewChatFormValues, handlePreview }} />}
{props.type === 'email' && <BubbleEmail {...props} onOpenEditor={listProps.onOpenEditor} />}
</div>
));
@ -156,48 +69,6 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
ref={(el) => (messageRefs.current[index] = el)}
key={`${message.sn}.${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>

@ -7,6 +7,9 @@ import { fetchCleanUnreadMsgCount, fetchMessages, MESSAGE_PAGE_SIZE } from '@/ac
import useAuthStore from '@/stores/AuthStore';
import { useVisibilityState } from '@/hooks/useVisibilityState';
import ConversationNewItem from './ConversationsNewItem';
import emailItem from './Components/emailSent.json';
import emailReItem from './Components/emailRe.json';
import EmailEditor from './Input/EmailEditor';
const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
const userId = useAuthStore((state) => state.loginUser.userId);
@ -54,6 +57,11 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
const getFirstPageMessages = async (item) => {
setMsgLoading(true);
const data = await fetchMessages({ opisn: forceGetMessages ? (currentConversation.opi_sn || '') : userId, whatsappid: item.whatsapp_phone_number, lasttime: '' });
// test:
data.push(emailItem);
data.push(emailReItem);
setMsgLoading(false);
receivedMessageList(item.sn, data);
const thisLastTime = data.length > 0 ? data[0].orgmsgtime : '';
@ -115,27 +123,61 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
const handleContactClick = (data) => {
return data.length > 1 ? handleContactListClick(data) : handleContactItemClick(data[0]);
}
// EmailEditor
const [openEmailEditor, setOpenEmailEditor] = useState(false);
const [fromEmail, setFromEmail] = useState('');
const onOpenEditor = (email_addr) => {
setOpenEmailEditor(true);
setFromEmail(email_addr);
};
return (
<>
<MessagesList messages={longList} dataSourceLen={longList.length} {...{
reference, shouldScrollBottom,
handlePreview, handleContactClick,
setNewChatModalVisible, setNewChatFormValues,
longListLoading, setLongListLoading, getMoreMessages, loadNextPage: currentConversation?.loadNextPage ?? true
<MessagesList
messages={longList}
dataSourceLen={longList.length}
{...{
reference,
shouldScrollBottom,
handlePreview,
handleContactClick,
setNewChatModalVisible,
setNewChatFormValues,
longListLoading,
setLongListLoading,
getMoreMessages,
loadNextPage: currentConversation?.loadNextPage ?? true,
onOpenEditor,
}}
/>
<Image width={0} height={0} src={null} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose, fallback: 'https://hiana-crm.oss-accelerate.aliyuncs.com/WAMedia/afe412d4-3acf-4e79-a623-048aeb4d696a.png' }} />
<Modal title="联系人" closable open={contactsModalVisible} onOk={handleContactItemClick} onCancel={() => setContactsModalVisible(false)} footer={null} >
<Image
width={0}
height={0}
src={null}
preview={{
visible: previewVisible,
src: previewSrc,
onClose: onPreviewClose,
fallback: 'https://hiana-crm.oss-accelerate.aliyuncs.com/WAMedia/afe412d4-3acf-4e79-a623-048aeb4d696a.png',
}}
/>
<Modal title='联系人' closable open={contactsModalVisible} onOk={handleContactItemClick} onCancel={() => setContactsModalVisible(false)} footer={null}>
{contactListData.map((contact) => (
<Button onClick={() => handleContactItemClick(contact)} type='link' key={contact.id}>{contact.name}: <span>{contact.wa_id}</span></Button>
<Button onClick={() => handleContactItemClick(contact)} type='link' key={contact.id}>
{contact.name}: <span>{contact.wa_id}</span>
</Button>
))}
</Modal>
<ConversationNewItem
initialValues={{ ...newChatFormValues, is_current_order: false }}
open={newChatModalVisible}
onCreate={() => { setNewChatModalVisible(false); setContactsModalVisible(false);}}
onCreate={() => {
setNewChatModalVisible(false);
setContactsModalVisible(false);
}}
onCancel={() => setNewChatModalVisible(false)}
/>
/>
<EmailEditor open={openEmailEditor} setOpen={setOpenEmailEditor} {...{ fromEmail }} key={'email-editor-reply'} />
</>
);
};

@ -3,17 +3,37 @@ import { Tabs } from 'antd';
import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons';
import InputComposer from './Input/InputComposer';
import EmailComposer from './Input/EmailComposer';
import { WABIcon } from './../../../components/Icons';
import { WABIcon } from '@/components/Icons';
import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
const DEFAULT_CHANNEL = 'waba';
const ReplyWrapper = ({ ...props }) => {
const [activeChannel, setActiveChannel] = useState(DEFAULT_CHANNEL);
const onChannelTabsChange = (activeKey) => {
setActiveChannel(activeKey);
};
const activeMessages = useConversationStore(
useShallow((state) => (state.currentConversation.sn && state.activeConversations[state.currentConversation.sn] ? state.activeConversations[state.currentConversation.sn] : []))
);
useEffect(() => {
const len = activeMessages.length;
const thisLastChannel = activeMessages.length > 0 ? activeMessages[len - 1]?.type : DEFAULT_CHANNEL;
setActiveChannel(thisLastChannel);
return () => {};
}, [activeMessages]);
const replyTypes = [
{ key: 'WABA', label: 'WABA-Global Highlights', icon: <WABIcon />, children: <InputComposer isWABA /> },
{ key: 'Email', label: 'Email', icon: <MailOutlined className='text-violet-500' />, children: <EmailComposer /> },
{ key: 'WA', label: 'WhatsApp', icon: <WhatsAppOutlined className='text-whatsapp' />, children: <InputComposer /> },
{ key: 'waba', label: 'WABA-Global Highlights', icon: <WABIcon />, children: <InputComposer isWABA channel={'waba'} /> },
{ key: 'email', label: 'Email', icon: <MailOutlined className='text-indigo-500' />, children: <EmailComposer /> },
{ key: 'whatsapp', label: 'WhatsApp', icon: <WhatsAppOutlined className='text-whatsapp' />, children: <InputComposer channel={'whatsapp'} /> },
];
return (
<div className='reply-wrapper rounded rounded-b-none emoji bg-white'>
<Tabs type={'card'} size={'small'} tabPosition={'bottom'} className='bg-white *:m-0 ' items={replyTypes} />
<Tabs activeKey={activeChannel} onChange={onChannelTabsChange} type={'card'} size={'small'} tabPosition={'bottom'} className='bg-white *:m-0 ' items={replyTypes} />
</div>
);
};

@ -9,6 +9,8 @@ export default {
...colors,
'whatsapp': {
DEFAULT: '#25D366',
400: '#66e094',
300: '#92e9b3',
dark: '#075E54',
second: '#128c7e',
gossip: '#dcf8c6',
@ -16,6 +18,11 @@ export default {
bgdark: '#0b141a',
me: '#ccd5ae', // '#d9fdd3'
},
'waba': {
DEFAULT: '#2ba84a',
400: '#6bc280',
300: '#95d4a5',
},
'primary': '#1ba784',
},
extend: {

Loading…
Cancel
Save