feat: 邮件回复: 附带原文

dev/email
Lei OT 1 year ago
parent 00b8f940dc
commit eea8457c36

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-break"><path d="M0 10.5a.5.5 0 0 1 .5-.5h15a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zM12 0H4a2 2 0 0 0-2 2v7h1V2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v7h1V2a2 2 0 0 0-2-2zm2 12h-1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-2H2v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2z"/></svg>

After

Width:  |  Height:  |  Size: 348 B

@ -17,6 +17,8 @@ import { AutoLinkNode, LinkNode } from "@lexical/link";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import {HorizontalRulePlugin} from '@lexical/react/LexicalHorizontalRulePlugin';
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
import { TRANSFORMERS } from "@lexical/markdown";
import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
@ -25,7 +27,7 @@ import AutoLinkPlugin from "./plugins/AutoLinkPlugin";
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot, $getSelection, } from 'lexical';
import { $generateHtmlFromNodes } from '@lexical/html';
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
import './styles.css';
@ -53,9 +55,37 @@ const editorConfig = {
TableCellNode,
TableRowNode,
AutoLinkNode,
LinkNode
LinkNode,
HorizontalRuleNode,
]
};
function LexicalDefaultValuePlugin({ value = "" }= {}) {
const [editor] = useLexicalComposerContext();
const updateHTML = (editor, value, clear) => {
const root = $getRoot();
const parser = new DOMParser();
const dom = parser.parseFromString(value, "text/html");
const nodes = $generateNodesFromDOM(editor, dom);
if (clear) {
root.clear();
}
// console.log(nodes);
root.append(...nodes.filter(n => n));
};
useEffect(() => {
if (editor && value) {
editor.update(() => {
updateHTML(editor, value, true);
});
}
}, [value]);
return null;
}
function MyOnChangePlugin({ onChange }) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
@ -80,7 +110,7 @@ function MyOnChangePlugin({ onChange }) {
}, [editor, onChange]);
return null;
}
export default function Editor({ isRichText, onChange, ...props }) {
export default function Editor({ isRichText, onChange, initialValue, ...props }) {
return (
<LexicalComposer initialConfig={editorConfig}>
<div className='editor-container'>
@ -94,6 +124,7 @@ export default function Editor({ isRichText, onChange, ...props }) {
)}
<HistoryPlugin />
{import.meta.env.DEV && <TreeViewPlugin />}
<LexicalDefaultValuePlugin value={initialValue} />
<AutoFocusPlugin />
<CodeHighlightPlugin />
<ListPlugin />
@ -101,6 +132,7 @@ export default function Editor({ isRichText, onChange, ...props }) {
<AutoLinkPlugin />
<ListMaxIndentLevelPlugin maxDepth={7} />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<HorizontalRulePlugin />
<MyOnChangePlugin onChange={onChange}/>
</div>
</div>

@ -39,6 +39,7 @@ import {
getDefaultCodeLanguage,
getCodeLanguages
} from "@lexical/code";
import { INSERT_HORIZONTAL_RULE_COMMAND, } from '@lexical/react/LexicalHorizontalRuleNode';
const LowPriority = 1;
@ -413,12 +414,12 @@ function BlockOptionsDropdownList({
</button>
<button className="item" onClick={formatLargeHeading}>
<span className="icon large-heading" />
<span className="text">Large Heading</span>
<span className="text">Heading 1</span>
{blockType === "h1" && <span className="active" />}
</button>
<button className="item" onClick={formatSmallHeading}>
<span className="icon small-heading" />
<span className="text">Small Heading</span>
<span className="text">Heading 2</span>
{blockType === "h2" && <span className="active" />}
</button>
<button className="item" onClick={formatSmallHeading3}>
@ -441,11 +442,11 @@ function BlockOptionsDropdownList({
<span className="text">Quote</span>
{blockType === "quote" && <span className="active" />}
</button>
<button className="item" onClick={formatCode}>
{/* <button className="item" onClick={formatCode}>
<span className="icon code" />
<span className="text">Code Block</span>
{blockType === "code" && <span className="active" />}
</button>
</button> */}
</div>
);
}
@ -571,6 +572,13 @@ export default function ToolbarPlugin() {
}
}, [editor, isLink]);
const insertHorizontalRule = useCallback(() => {
editor.dispatchCommand(
INSERT_HORIZONTAL_RULE_COMMAND,
undefined,
);
}, [editor]);
return (
<div className="toolbar" ref={toolbarRef}>
<button
@ -670,7 +678,7 @@ export default function ToolbarPlugin() {
>
<i className="format strikethrough" />
</button>
<button
{/* <button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
}}
@ -678,7 +686,7 @@ export default function ToolbarPlugin() {
aria-label="Insert Code"
>
<i className="format code" />
</button>
</button> */}
<button
onClick={insertLink}
className={"toolbar-item spaced " + (isLink ? "active" : "")}
@ -688,6 +696,16 @@ export default function ToolbarPlugin() {
</button>
{isLink &&
createPortal(<FloatingLinkEditor editor={editor} />, document.body)}
<button
onClick={() => {
editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined);
}}
className={"toolbar-item spaced "}
aria-label="Insert Horizontal Rule"
>
<i className="format icon horizontal-rule" />
{/* <span className="text">Horizontal Rule</span> */}
</button>
<Divider />
<button
onClick={() => {

@ -270,7 +270,7 @@ h1 {
margin: 0;
margin-top: 10px;
padding: 0;
text-transform: uppercase;
/* text-transform: uppercase; */
}
.editor-quote {
@ -773,6 +773,9 @@ i.code {
i.link {
background-image: url(/images/icons/link.svg);
}
i.horizontal-rule {
background-image: url(/images/icons/horizontal-rule.svg);
}
i.left-align {
background-image: url(/images/icons/text-left.svg);

@ -42,17 +42,17 @@ const ChatboxEmail = ({ onOpenEditor, onOpenEmail, ...message }) => {
// titleColor={message.sender !== 'me' ? '#4f46e5' : ''} // 600
notch={false}
position={message.sender === 'me' ? 'right' : 'left'}
onReplyClick={() => onOpenEditor(message.emailOrigin.replyToEmail)}
onReplyClick={() => onOpenEditor(message.emailOrigin)}
// 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={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:
// replyButton: ['text', 'document', 'image'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false, // todo:
}
: {})}
className={[

@ -21,17 +21,18 @@
"fromEmail": "ycc@hainatravel.com",
"toName": "LYT",
"toEmail": "lyt@hainatravel.com",
"cc": "lioyjun@gmail.com",
"cc": "lioyjun@gmail.com, LJ <lj@hainatravel.com>",
"bcc": "",
"subject": "反馈表低分提醒",
"body": "<p class=\"editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">We are offering US$50 voucher per person for our guests to join our Highlights Travel Family loyalty group on Facebook:&nbsp;</span><a href=\"https://www.facebook.com/groups/ghloyaltyclub\" class=\"editor-link\"><u><span class=\"editor-text-underline\" style=\"white-space: pre-wrap;\">https://www.facebook.com/groups/ghloyaltyclub</span></u></a><span style=\"white-space: pre-wrap;\">&nbsp;.&nbsp;&nbsp;If you two joined the group, you can save USD$ 100. &nbsp;The discount will be cut from the balance later.</span></p>",
"subject": "New booking: Sun 29.Sep '24 @ (TP-T78840699) Ext. booking ref: 1184791715",
"content": "The following booking was just created.<br>Booking ref.VIA-51242561<br>Product booking ref.TP-T78840699<br>Ext. booking ref1184791715<br>Product7137P273 - Qatar: Doha Hamad International Airport (DOH) Al Maha Lounge <br>SupplierTrippest Tours<br>Sold byViator.com <br>Booking channelViator.com <br>Customerhung, tak wai <br>Customer emailS-1a553e197d734ab1b19b3ac34c56beb7+1184791715-2efw857tc2u53@expmessaging.tripadvisor.com<br>Customer phone+66 2 030 4763<br>DateSun 29.Sep '24 <br>RateQatar: Doha Hamad International Airport (DOH) VIP Lounge Access <br>PAX1 Adult<br>Extras<br>CreatedWed, September 25 2024 @ 16:38<br>Notes--- Inclusions: --- <br>Unlimited buffet food & beverage, including alcoholic beverages <br>Halal and vegetarian food <br>Wi-Fi connection & flight monitor <br>International TV channels, newspapers and magazines <br>Disabled Access <br>Smoking Room <br><br>--- Questions and answers: --- <br>Departure Flight No : qr817 <br>Departure Airline : qatar <br>Pick up Location : hkg <br>Departure Time : 19:10 <br>Viator amount: USD 37.41 <br>",
"abstract": "阿坝州九寨岷江国际旅行社有限责任公司……",
"replyToEmail": "ycc@hainatravel.com",
"replyToEmail": "ycc@chinahighlights.com",
"replyToName": "YCC",
"senderEmail": "",
"senderName": "",
"bCopyEmail1": "",
"priority": "",
"sent": "2024-02-21T03:37:33.000Z",
"#": "#"
},
"date": "2024-02-21T03:37:33.000Z",

@ -24,7 +24,7 @@
"cc": "lioyjun@gmail.com",
"bcc": "",
"subject": "发送示例",
"body": "发送示例发送示例发送示例发送示例",
"content": "发送示例发送示例发送示例发送示例",
"abstract": "发送示例发送示例发送示例发送示例发送示例……",
"replyToEmail": "",
"replyToName": "",
@ -40,7 +40,7 @@
"from": "ycc@hainatravel.com",
"sender": "me",
"senderName": "me",
"replyButton": false,
"replyButton": true,
"status": "sent",
"dateString": "",
"statusCN": "",

@ -1,10 +1,7 @@
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 EmailEditorPopup from './EmailEditorPopup';
import EmailEditorPopup2 from './EmailEditorPopup1';
const EmailComposer = ({ ...props }) => {
const [open, setOpen] = useState(false);
@ -14,7 +11,8 @@ const EmailComposer = ({ ...props }) => {
setFromEmail(email_addr);
};
return (
<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'>
<Flex gap={8} className='p-2 bg-gray-200 justify-end items-center rounded rounded-b-none border-gray-300 border-solid border border-b-0 border-x-0'>
<span>新邮件:</span>
{[
{ email: 'lyt@hainatravel.com', name: 'LYT' },
{ email: 'lot@hainatravel.com', name: 'LOT' },

@ -114,7 +114,7 @@ const EmailDetail = ({ open, setOpen, emailDetail, ...props }) => {
</div> */}
<Divider className='my-2' />
{/* <div className='mt-2'>{emailOrigin.body}</div> */}
<div className='mt-2' dangerouslySetInnerHTML={{ __html: emailOrigin.body }}></div>
<div className='mt-2' dangerouslySetInnerHTML={{ __html: emailOrigin.content }}></div>
{/* <div className='mt-2'>{emailOrigin.attachments.map(attachment => <div key={attachment.name}>{attachment.name}</div>)}</div> */}
</div>
</div>

@ -6,7 +6,8 @@ import '@dckj/react-better-modal/dist/index.css';
import LexicalEditor from '@/components/LexicalEditor';
import { $getRoot, $getSelection, } from 'lexical';
import {$generateHtmlFromNodes} from '@lexical/html';
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import { isEmpty } from '@/utils/commons';
// import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
// import { LexicalComposer } from '@lexical/react/LexicalComposer';
@ -26,7 +27,7 @@ import {$generateHtmlFromNodes} from '@lexical/html';
// function onError(error) {
// console.error(error);
// }
const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, ...props }) => {
const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, quote={}, ...props }) => {
const [form] = Form.useForm();
// const [open, setOpen] = useState(false);
@ -43,6 +44,7 @@ const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, ...props }) =>
function onHandleCancel() {
console.log('onCancel callback');
form.resetFields();
setOpen(false);
}
function onStageChange({ state }) {
@ -73,10 +75,10 @@ const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, ...props }) =>
const onHandleSend = () => {
console.log('onSend callback', '\nisRichText', isRichText);
console.log(form.getFieldsValue());
// console.log(form.getFieldsValue());
const body = structuredClone(form.getFieldsValue());
body.content = isRichText ? htmlContent : textContent;
console.log('body', body);
// console.log('body', body);
form
.validateFields()
.then((values) => {})
@ -87,6 +89,58 @@ const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, ...props }) =>
// setOpen(false);
};
const [initialForm, setInitialForm] = useState({});
const [initialContent, setInitialContent] = useState('');
useEffect(() => {
if (isEmpty(quote)) {
return () => {};
}
setShowCc(!isEmpty(quote.cc));
const { fromEmail, replyToEmail, subject, content } = quote;
const preQuoteBody = `<p class="editor-paragraph" dir="ltr"><br></p><p class="editor-paragraph" dir="ltr"><br>
<hr>
<p class="editor-paragraph" dir="ltr">
<b>
<strong class="editor-text-bold" style="white-space: pre-wrap;">From: </strong>
</b>
<span style="white-space: pre-wrap;">${quote.fromName} &lt;${quote.fromEmail}&gt;</span>
</p>
<p class="editor-paragraph" dir="ltr">
<b>
<strong class="editor-text-bold" style="white-space: pre-wrap;">Sent: </strong>
</b>
<span style="white-space: pre-wrap;">${quote.sent}</span>
</p>
<p class="editor-paragraph" dir="ltr">
<b>
<strong class="editor-text-bold" style="white-space: pre-wrap;">To: </strong>
</b>
<span style="white-space: pre-wrap;">${quote.toName} &lt;${quote.toEmail}&gt;</span>
</p>
<p class="editor-paragraph" dir="ltr">
<b>
<strong class="editor-text-bold" style="white-space: pre-wrap;">Subject: </strong>
</b>
<span style="white-space: pre-wrap;">${subject}</span>
</p>
<p class="editor-paragraph" dir="ltr">${content}</p>
`;
// <blockquote class="editor-quote">
// </blockquote>
setInitialContent(preQuoteBody);
const _formValues = {
to: replyToEmail || fromEmail,
cc: quote.cc,
subject: `Re: ${subject}`,
};
form.setFieldsValue(_formValues);
setInitialForm(_formValues);
return () => {};
}, [quote])
return (
<>
<Modal
@ -107,7 +161,7 @@ const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, ...props }) =>
initialHeight={600}
initialTop={74}
initialLeft={window.innerWidth - 700}
title={`${reference ? '回复: ' : '写邮件: '} ${fromEmail || ''}`}
title={initialForm.subject || `${reference ? '回复: ' : '写邮件: '} ${fromEmail || ''}`}
minimizeButton={<></>}
onMove={onHandleMove}
onResize={onHandleResize}
@ -132,7 +186,7 @@ const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, ...props }) =>
name='conversation_filter_form'
size='small'
variant={'borderless'}
initialValues={{ }}
initialValues={{}}
// onFinish={() => {}}
className='*:mb-2 *:border-b *:border-t-0 *:border-x-0 *:border-indigo-100 *:border-solid '
requiredMark={false}
@ -179,7 +233,7 @@ const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, ...props }) =>
<Input />
</Form.Item>
</Form>
<LexicalEditor {...{isRichText}} onChange={handleEditorChange} />
<LexicalEditor {...{isRichText}} onChange={handleEditorChange} initialValue={initialContent} />
</Modal>
</>
);

@ -130,9 +130,11 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
const [openEmailEditor, setOpenEmailEditor] = useState(false);
const [fromEmail, setFromEmail] = useState('');
const [ReferEmailMsg, setReferEmailMsg] = useState('');
const onOpenEditor = (email_addr) => {
const onOpenEditor = (emailOrigin) => {
const { replyToEmail: email_addr, content } = emailOrigin;
setOpenEmailEditor(true);
setFromEmail(email_addr);
setReferEmailMsg(emailOrigin);
};
const [openEmailDetail, setOpenEmailDetail] = useState(false);
@ -191,7 +193,7 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
onCancel={() => setNewChatModalVisible(false)}
/>
{/* <EmailEditor open={openEmailEditor} setOpen={setOpenEmailEditor} reference={ReferEmailMsg} setRefernce={setReferEmailMsg} {...{ fromEmail, }} key={'email-editor-reply'} /> */}
<EmailEditorPopup open={openEmailEditor} setOpen={setOpenEmailEditor} fromEmail={fromEmail} key={'email-editor-reply-popup'}/>
<EmailEditorPopup open={openEmailEditor} setOpen={setOpenEmailEditor} fromEmail={fromEmail} quote={ReferEmailMsg} key={`email-editor-reply-popup_${ReferEmailMsg.id}`}/>
<EmailDetail open={openEmailDetail} setOpen={setOpenEmailDetail} emailDetail={emailDetail} />
</>
);

Loading…
Cancel
Save