forward and reply
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { FC } from 'react'
|
||||
import { EmojiHappy } from 'iconsax-react'
|
||||
import Reply from './Reply'
|
||||
import Forward from './Forward'
|
||||
import { MessageDetail } from '@/pages/received/types/Types'
|
||||
|
||||
interface EmailActionsProps {
|
||||
messageDetail: MessageDetail
|
||||
className?: string
|
||||
showReactions?: boolean
|
||||
}
|
||||
|
||||
const EmailActions: FC<EmailActionsProps> = ({
|
||||
messageDetail,
|
||||
className = '',
|
||||
showReactions = true
|
||||
}) => {
|
||||
return (
|
||||
<div className={`flex mt-9 justify-end ${className}`}>
|
||||
<div className='flex gap-5 text-sm items-center'>
|
||||
{showReactions && (
|
||||
<EmojiHappy
|
||||
size={24}
|
||||
color='black'
|
||||
className='cursor-pointer hover:opacity-70 transition-opacity'
|
||||
/>
|
||||
)}
|
||||
|
||||
<Reply
|
||||
originalMessage={messageDetail}
|
||||
className='border-r border-border pr-5'
|
||||
onClose={() => { }}
|
||||
/>
|
||||
|
||||
<Forward
|
||||
originalMessage={messageDetail}
|
||||
className='border-r border-border pr-5'
|
||||
onClose={() => { }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmailActions
|
||||
@@ -12,20 +12,10 @@ const EmailEditor: FC<EmailEditorProps> = ({
|
||||
onChange,
|
||||
placeholder = ''
|
||||
}) => {
|
||||
const modules = {
|
||||
toolbar: [
|
||||
[{ header: '1' }, { header: '2' }, { font: [] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['link', 'image'],
|
||||
[{ align: [] }],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<ReactQuill
|
||||
modules={modules}
|
||||
theme="snow"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -4,21 +4,23 @@ import Button from '@/components/Button'
|
||||
import Select from '@/components/Select'
|
||||
|
||||
interface EmailFormActionsProps {
|
||||
priority: string
|
||||
onPriorityChange: (priority: string) => void
|
||||
onSend: () => void
|
||||
onSaveDraft: () => void
|
||||
isSending: boolean
|
||||
isSavingDraft: boolean
|
||||
onClose?: () => void
|
||||
onSaveDraft?: () => void
|
||||
isSavingDraft?: boolean
|
||||
priority?: string
|
||||
onPriorityChange?: (priority: string) => void
|
||||
}
|
||||
|
||||
const EmailFormActions: FC<EmailFormActionsProps> = ({
|
||||
priority,
|
||||
onPriorityChange,
|
||||
onSend,
|
||||
onSaveDraft,
|
||||
isSending,
|
||||
isSavingDraft
|
||||
onClose,
|
||||
onSaveDraft,
|
||||
isSavingDraft = false,
|
||||
priority,
|
||||
onPriorityChange
|
||||
}) => {
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
@@ -30,22 +32,40 @@ const EmailFormActions: FC<EmailFormActionsProps> = ({
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
||||
<div>
|
||||
<Select
|
||||
items={priorityOptions}
|
||||
placeholder={t('new_message.select_priority')}
|
||||
className='xl:w-[190px] min-w-[130px]'
|
||||
value={priority}
|
||||
onChange={(e) => onPriorityChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Priority Selector */}
|
||||
{priority !== undefined && onPriorityChange && (
|
||||
<div>
|
||||
<Select
|
||||
items={priorityOptions}
|
||||
placeholder={t('new_message.select_priority')}
|
||||
className='xl:w-[190px] min-w-[130px]'
|
||||
value={priority}
|
||||
onChange={(e) => onPriorityChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex-1 justify-end flex gap-3'>
|
||||
<Button
|
||||
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
|
||||
label={t('new_message.draft')}
|
||||
onClick={onSaveDraft}
|
||||
loading={isSavingDraft}
|
||||
/>
|
||||
{/* Save Draft Button */}
|
||||
{onSaveDraft && (
|
||||
<Button
|
||||
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
|
||||
label={t('new_message.draft')}
|
||||
onClick={onSaveDraft}
|
||||
loading={isSavingDraft}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Close Button */}
|
||||
{onClose && !onSaveDraft && (
|
||||
<Button
|
||||
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
|
||||
label={t('common.close')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Send Button */}
|
||||
<Button
|
||||
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
||||
label={t('new_message.send')}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDown2, CloseSquare } from 'iconsax-react'
|
||||
import AvatarImage from '@/assets/images/avatar.svg'
|
||||
import ForwardIcon from '@/assets/images/forward.svg'
|
||||
import EmailEditor from './EmailEditor'
|
||||
import EmailFormActions from './EmailFormActions'
|
||||
import EmailInput from './EmailInput'
|
||||
import { MessageDetail } from '@/pages/received/types/Types'
|
||||
import { useAuthStore } from '@/pages/auth/store/AuthStore'
|
||||
import { useSendEmail } from '@/pages/received/hooks/useEmailData'
|
||||
import { SendEmailDto } from '@/pages/received/types/Types'
|
||||
import { toast } from '@/components/Toast'
|
||||
|
||||
interface ForwardProps {
|
||||
originalMessage: MessageDetail
|
||||
onClose: () => void
|
||||
onExpand?: () => void
|
||||
className?: string
|
||||
showInitialButton?: boolean
|
||||
}
|
||||
|
||||
const Forward: FC<ForwardProps> = ({
|
||||
originalMessage,
|
||||
onClose,
|
||||
onExpand,
|
||||
className = '',
|
||||
showInitialButton = true
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { email: userEmail } = useAuthStore()
|
||||
const sendEmailMutation = useSendEmail()
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false)
|
||||
const [toEmails, setToEmails] = useState<string[]>([])
|
||||
const [content, setContent] = useState<string>('')
|
||||
|
||||
const expandForward = () => {
|
||||
setIsExpanded(true)
|
||||
// Pre-fill content with original message
|
||||
const forwardContent = `
|
||||
<br/><br/>
|
||||
<div dir="rtl">
|
||||
---------- پیام ارسالی ----------<br/>
|
||||
<strong>از:</strong> ${originalMessage.from.name || originalMessage.from.address} <${originalMessage.from.address}><br/>
|
||||
<strong>تاریخ:</strong> ${originalMessage.date}<br/>
|
||||
<strong>موضوع:</strong> ${originalMessage.subject}<br/>
|
||||
<strong>به:</strong> ${originalMessage.to.map(recipient => recipient.address).join(', ')}<br/>
|
||||
<br/>
|
||||
${originalMessage.html ? originalMessage.html.join('') : originalMessage.text}
|
||||
</div>
|
||||
`
|
||||
setContent(forwardContent)
|
||||
onExpand?.()
|
||||
}
|
||||
|
||||
const handleClose = async () => {
|
||||
// اگر محتوا یا ایمیل وارد شده، پیشنویس ذخیره کن
|
||||
if (content.trim() || toEmails.length > 0) {
|
||||
// TODO: Implement save draft functionality
|
||||
toast('پیشنویس ذخیره شد', 'success')
|
||||
}
|
||||
|
||||
// بسته کردن و برگشت به حالت قبل
|
||||
collapseForward()
|
||||
onClose() // این خط مهمه تا parent component بفهمه
|
||||
}
|
||||
|
||||
const collapseForward = () => {
|
||||
setIsExpanded(false)
|
||||
resetForm()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setToEmails([])
|
||||
setContent('')
|
||||
setIsExpanded(false)
|
||||
}
|
||||
|
||||
const handleSendForward = async () => {
|
||||
if (toEmails.length === 0) {
|
||||
toast('لطفا حداقل یک گیرنده وارد کنید', 'error')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!content.trim()) {
|
||||
toast('لطفا متن پیام را وارد کنید', 'error')
|
||||
return false
|
||||
}
|
||||
|
||||
// Format date to Persian
|
||||
const originalDate = new Date(originalMessage.date)
|
||||
const persianDate = new Intl.DateTimeFormat('fa-IR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(originalDate)
|
||||
|
||||
const htmlContent =
|
||||
content +
|
||||
"<br /><br />" +
|
||||
"<div dir='rtl'>---------- پیام ارسالی ----------<br/>" +
|
||||
`<strong>از:</strong> ${originalMessage.from.name || originalMessage.from.address} <${originalMessage.from.address}><br/>` +
|
||||
`<strong>تاریخ:</strong> ${persianDate}<br/>` +
|
||||
`<strong>موضوع:</strong> ${originalMessage.subject}<br/>` +
|
||||
`<strong>به:</strong> ${originalMessage.to.map(recipient => recipient.address).join(', ')}<br/>` +
|
||||
"<br/>" +
|
||||
(originalMessage?.html?.[0] || originalMessage?.text || '') + "</div>"
|
||||
|
||||
const forwardData: SendEmailDto = {
|
||||
from: { address: userEmail || 'sender@example.com' },
|
||||
to: toEmails.map(email => ({ address: email })),
|
||||
subject: originalMessage.subject.startsWith('Fwd:')
|
||||
? originalMessage.subject
|
||||
: `Fwd: ${originalMessage.subject}`,
|
||||
html: htmlContent,
|
||||
text: content.replace(/<[^>]*>/g, ''),
|
||||
isForward: true,
|
||||
}
|
||||
|
||||
try {
|
||||
await sendEmailMutation.mutateAsync(forwardData)
|
||||
toast('پیام با موفقیت ارسال شد', 'success')
|
||||
resetForm()
|
||||
onClose?.()
|
||||
return true
|
||||
} catch {
|
||||
const errorMessage = 'خطا در ارسال پیام'
|
||||
toast(errorMessage, 'error')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// اگر showInitialButton true باشه و هنوز expand نشده، فقط دکمه نشون بده
|
||||
if (showInitialButton && !isExpanded) {
|
||||
return (
|
||||
<div className={`flex gap-5 text-sm ${className}`}>
|
||||
<div
|
||||
onClick={expandForward}
|
||||
className='flex gap-2 cursor-pointer items-center border-r border-border pr-5 hover:text-primary transition-colors'
|
||||
>
|
||||
<img src={ForwardIcon} className='w-[17px]' />
|
||||
<div>{t('mail.forward')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// در غیر این صورت editor رو نشون بده
|
||||
// (یا showInitialButton false هست یا isExpanded true هست)
|
||||
|
||||
return (
|
||||
<div className={`mt-6 w-full ${className}`}>
|
||||
{/* Forward Header */}
|
||||
<div className='flex items-center justify-between mb-4 p-3 bg-gray-50 rounded-lg border'>
|
||||
<div className='flex items-center gap-2 text-sm text-gray-600'>
|
||||
<img src={ForwardIcon} className='w-4 h-4' />
|
||||
<span>ارسال به دیگران</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<button
|
||||
onClick={collapseForward}
|
||||
className='p-1 hover:bg-gray-200 rounded transition-colors'
|
||||
title='بستن'
|
||||
>
|
||||
<ArrowDown2 size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
collapseForward()
|
||||
onClose?.()
|
||||
}}
|
||||
className='p-1 hover:bg-gray-200 rounded transition-colors'
|
||||
title='لغو'
|
||||
>
|
||||
<CloseSquare size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Forward Content */}
|
||||
<div className='flex gap-4'>
|
||||
<img src={AvatarImage} className='size-10 flex-shrink-0' />
|
||||
<div className='w-full'>
|
||||
{/* To Field */}
|
||||
<div className='mb-4'>
|
||||
<EmailInput
|
||||
toEmails={toEmails}
|
||||
onAddEmail={(email) => {
|
||||
if (!toEmails.includes(email)) {
|
||||
setToEmails([...toEmails, email])
|
||||
}
|
||||
}}
|
||||
onRemoveEmail={(email) => {
|
||||
setToEmails(toEmails.filter(e => e !== email))
|
||||
}}
|
||||
suggestions={[]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subject Field */}
|
||||
<div className='mb-4 text-sm'>
|
||||
<div className='flex items-center gap-2 p-2 bg-gray-50 rounded border'>
|
||||
<span className='text-gray-600 min-w-fit'>موضوع:</span>
|
||||
<span className='text-gray-800'>
|
||||
{originalMessage.subject.startsWith('Fwd:')
|
||||
? originalMessage.subject
|
||||
: `Fwd: ${originalMessage.subject}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className='mb-4'>
|
||||
<EmailEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
placeholder='پیام خود را اضافه کنید...'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<EmailFormActions
|
||||
onSend={handleSendForward}
|
||||
isSending={sendEmailMutation.isPending}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Forward
|
||||
@@ -95,7 +95,7 @@ const NewMessage: FC = () => {
|
||||
}`}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<div className={`fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
|
||||
<div className={`fixed left-2 bottom-2 md:left-4 md:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
|
||||
}`}>
|
||||
{/* Header */}
|
||||
<div className='flex justify-between items-center mb-6 md:mb-8'>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDown2, CloseSquare } from 'iconsax-react'
|
||||
import AvatarImage from '@/assets/images/avatar.svg'
|
||||
import AnswerIcon from '@/assets/images/answer.svg'
|
||||
import EmailEditor from './EmailEditor'
|
||||
import EmailFormActions from './EmailFormActions'
|
||||
import { useReply } from './hooks/useReply'
|
||||
import { MessageDetail } from '@/pages/received/types/Types'
|
||||
import { toast } from '@/components/Toast'
|
||||
|
||||
interface ReplyProps {
|
||||
originalMessage: MessageDetail
|
||||
onClose?: () => void
|
||||
onExpand?: () => void
|
||||
className?: string
|
||||
showInitialButton?: boolean
|
||||
}
|
||||
|
||||
const Reply: FC<ReplyProps> = ({
|
||||
originalMessage,
|
||||
onClose,
|
||||
onExpand,
|
||||
className = '',
|
||||
showInitialButton = true
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
content,
|
||||
isExpanded,
|
||||
isSending,
|
||||
setContent,
|
||||
expandReply,
|
||||
collapseReply,
|
||||
handleSendReply,
|
||||
handleSaveDraft
|
||||
} = useReply({
|
||||
originalMessage,
|
||||
onSuccess: () => {
|
||||
onClose?.()
|
||||
}
|
||||
})
|
||||
|
||||
const handleExpand = () => {
|
||||
expandReply()
|
||||
onExpand?.()
|
||||
}
|
||||
|
||||
|
||||
const handleClose = async () => {
|
||||
// اگر محتوایی وارد شده، پیشنویس ذخیره کن
|
||||
if (content.trim()) {
|
||||
await handleSaveDraft()
|
||||
toast('پیشنویس ذخیره شد', 'success')
|
||||
}
|
||||
|
||||
// بسته کردن و برگشت به حالت قبل
|
||||
collapseReply()
|
||||
onClose?.() // این خط مهمه تا parent component بفهمه
|
||||
}
|
||||
|
||||
// اگر showInitialButton true باشه و هنوز expand نشده، فقط دکمه نشون بده
|
||||
if (showInitialButton && !isExpanded) {
|
||||
return (
|
||||
<div className={`flex justify-end ${className}`}>
|
||||
<div className='flex gap-5 text-sm'>
|
||||
<div
|
||||
onClick={handleExpand}
|
||||
className='flex gap-2 cursor-pointer items-center hover:text-primary transition-colors'
|
||||
>
|
||||
<img src={AnswerIcon} className='w-[17px]' />
|
||||
<div>{t('mail.answer')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
handleSendReply(originalMessage?.html[0])
|
||||
collapseReply()
|
||||
}
|
||||
|
||||
// در غیر این صورت editor رو نشون بده
|
||||
// (یا showInitialButton false هست یا isExpanded true هست)
|
||||
|
||||
return (
|
||||
<div className={`mt-6 w-full ${className}`}>
|
||||
{/* Reply Header */}
|
||||
<div className='flex items-center justify-between mb-4 p-3 bg-gray-50 rounded-lg border'>
|
||||
<div className='flex items-center gap-2 text-sm text-gray-600'>
|
||||
<img src={AnswerIcon} className='w-4 h-4' />
|
||||
<span>پاسخ به {originalMessage.from.name || originalMessage.from.address}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<button
|
||||
onClick={collapseReply}
|
||||
className='p-1 hover:bg-gray-200 rounded transition-colors'
|
||||
title='بستن'
|
||||
>
|
||||
<ArrowDown2 size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
collapseReply()
|
||||
onClose?.()
|
||||
}}
|
||||
className='p-1 hover:bg-gray-200 rounded transition-colors'
|
||||
title='لغو'
|
||||
>
|
||||
<CloseSquare size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reply Content */}
|
||||
<div className='flex gap-4'>
|
||||
<img src={AvatarImage} className='size-10 flex-shrink-0' />
|
||||
<div className='w-full'>
|
||||
{/* To Field */}
|
||||
<div className='mb-4 text-sm'>
|
||||
<div className='flex items-center gap-2 p-2 bg-gray-50 rounded border'>
|
||||
<span className='text-gray-600 min-w-fit'>به:</span>
|
||||
<span className='text-gray-800'>{originalMessage.from.address}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subject Field */}
|
||||
<div className='mb-4 text-sm'>
|
||||
<div className='flex items-center gap-2 p-2 bg-gray-50 rounded border'>
|
||||
<span className='text-gray-600 min-w-fit'>موضوع:</span>
|
||||
<span className='text-gray-800'>
|
||||
{originalMessage.subject.startsWith('Re:')
|
||||
? originalMessage.subject
|
||||
: `Re: ${originalMessage.subject}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className='mb-4'>
|
||||
<EmailEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
placeholder='پیام خود را بنویسید...'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<EmailFormActions
|
||||
onSend={handleSend}
|
||||
onClose={handleClose}
|
||||
isSending={isSending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Reply
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react";
|
||||
import { useAuthStore } from "@/pages/auth/store/AuthStore";
|
||||
import { useSendEmail } from "@/pages/received/hooks/useEmailData";
|
||||
import { SendEmailDto } from "@/pages/received/types/Types";
|
||||
import { MessageDetail } from "@/pages/received/types/Types";
|
||||
import { toast } from "@/components/Toast";
|
||||
|
||||
interface UseReplyProps {
|
||||
originalMessage: MessageDetail;
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
export const useReply = ({
|
||||
originalMessage,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: UseReplyProps) => {
|
||||
const { email: userEmail } = useAuthStore();
|
||||
const [content, setContent] = useState<string>("");
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
|
||||
// API hooks
|
||||
const sendEmailMutation = useSendEmail();
|
||||
|
||||
const resetForm = () => {
|
||||
setContent("");
|
||||
setIsExpanded(false);
|
||||
};
|
||||
|
||||
const handleSendReply = async (html: string) => {
|
||||
if (!content.trim()) {
|
||||
toast("لطفا متن پیام را وارد کنید", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!originalMessage) {
|
||||
toast("اطلاعات پیام یافت نشد", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format date to Persian
|
||||
const originalDate = new Date(originalMessage.date);
|
||||
const persianDate = new Intl.DateTimeFormat("fa-IR", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(originalDate);
|
||||
|
||||
const htmlContent =
|
||||
content +
|
||||
"<br /><br />" +
|
||||
`<div dir="rtl">در تاریخ ${persianDate} ${originalMessage.from.name} <${originalMessage.from.address}> نوشت:</div>` +
|
||||
"<br /><br />" +
|
||||
`<div dir="rtl">${html}</div>`;
|
||||
|
||||
const replyData: SendEmailDto = {
|
||||
from: { address: userEmail || "sender@example.com" },
|
||||
to: [
|
||||
{
|
||||
address: originalMessage.from.address,
|
||||
name: originalMessage.from.name,
|
||||
},
|
||||
],
|
||||
replyTo: {
|
||||
name: originalMessage.from.name,
|
||||
address: originalMessage.from.address,
|
||||
},
|
||||
subject: originalMessage.subject.startsWith("Re:")
|
||||
? originalMessage.subject
|
||||
: `Re: ${originalMessage.subject}`,
|
||||
html: htmlContent,
|
||||
text: content.replace(/<[^>]*>/g, ""),
|
||||
};
|
||||
|
||||
try {
|
||||
await sendEmailMutation.mutateAsync(replyData);
|
||||
toast("پاسخ با موفقیت ارسال شد", "success");
|
||||
resetForm();
|
||||
onSuccess?.();
|
||||
return true;
|
||||
} catch {
|
||||
const errorMessage = "خطا در ارسال پاسخ";
|
||||
toast(errorMessage, "error");
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveDraft = async () => {
|
||||
if (!content.trim()) {
|
||||
toast("متن پیام خالی است", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement save draft functionality
|
||||
toast("پیشنویس ذخیره شد", "success");
|
||||
return true;
|
||||
} catch {
|
||||
const errorMessage = "خطا در ذخیره پیشنویس";
|
||||
toast(errorMessage, "error");
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const expandReply = () => {
|
||||
setIsExpanded(true);
|
||||
};
|
||||
|
||||
const collapseReply = () => {
|
||||
setIsExpanded(false);
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
content,
|
||||
isExpanded,
|
||||
isSending: sendEmailMutation.isPending,
|
||||
|
||||
// Handlers
|
||||
setContent,
|
||||
expandReply,
|
||||
collapseReply,
|
||||
handleSendReply,
|
||||
handleSaveDraft,
|
||||
resetForm,
|
||||
|
||||
// Original message data
|
||||
originalMessage,
|
||||
};
|
||||
};
|
||||
@@ -348,3 +348,6 @@ textarea::placeholder {
|
||||
|
||||
backdrop-filter: blur(44px);
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
+5
-1
@@ -23,6 +23,9 @@
|
||||
"required": "این فیلد اجباری است",
|
||||
"invalid_email": "فرمت ایمیل نامعتبر است"
|
||||
},
|
||||
"common": {
|
||||
"close": "بستن"
|
||||
},
|
||||
"received": {
|
||||
"title": "دریافتی ها",
|
||||
"from_date": "از تاریخ",
|
||||
@@ -140,7 +143,8 @@
|
||||
"FAVORITE": "نشانشدهها",
|
||||
"Junk": "هرزنامهها",
|
||||
"SENT": "ارسال شدهها",
|
||||
"TRASH": "سطل زباله"
|
||||
"TRASH": "سطل زباله",
|
||||
"Sent Mail": "ارسال شده ها"
|
||||
},
|
||||
"auth": {
|
||||
"welcome": "خوش آمدید",
|
||||
|
||||
+41
-158
@@ -4,112 +4,26 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import AvatarImage from '@/assets/images/avatar.svg'
|
||||
import { DocumentDownload, EmojiHappy } from 'iconsax-react'
|
||||
import ForwardIcon from '@/assets/images/forward.svg'
|
||||
import AnswerIcon from '@/assets/images/answer.svg'
|
||||
import Header from './Components/Header'
|
||||
import ReactQuill from 'react-quill-new';
|
||||
import Select from '@/components/Select'
|
||||
import { useGetMessageDetail, useSendEmail, useDownloadAttachment } from './hooks/useEmailData'
|
||||
import { useAuthStore } from '../auth/store/AuthStore'
|
||||
import { SendEmailDto } from './types/Types'
|
||||
import { useGetMessageDetail, useDownloadAttachment } from './hooks/useEmailData'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func'
|
||||
import SkeletonDetail from './Components/SkeletonDetail'
|
||||
import { useEmailActions } from '@/hooks/useEmailActions'
|
||||
import { MailboxEnum } from './enum/Enum'
|
||||
import Reply from '@/components/newMessage/Reply'
|
||||
import Forward from '@/components/newMessage/Forward'
|
||||
|
||||
const DetailEmail: FC = () => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const emailActions = useEmailActions()
|
||||
const { id, mailbox } = useParams<{ id: string, mailbox: string }>()
|
||||
const { email: userEmail } = useAuthStore()
|
||||
const [showAnswer, setShowAnswer] = useState<boolean>(false)
|
||||
const [value, setValue] = useState<string>('')
|
||||
const [priority, setPriority] = useState<string>('')
|
||||
const [activeAction, setActiveAction] = useState<'reply' | 'forward' | null>(null)
|
||||
|
||||
// API hooks
|
||||
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '')
|
||||
const sendEmailMutation = useSendEmail()
|
||||
const downloadAttachmentMutation = useDownloadAttachment()
|
||||
// const saveDraftMutation = useSaveDraft()
|
||||
|
||||
const priorityOptions = [
|
||||
{ value: 'high', label: 'بالا' },
|
||||
{ value: 'medium', label: 'متوسط' },
|
||||
{ value: 'low', label: 'پایین' },
|
||||
]
|
||||
|
||||
const handleSendReply = async () => {
|
||||
if (!value.trim()) {
|
||||
toast('لطفا متن پیام را وارد کنید', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!messageDetail) {
|
||||
toast('اطلاعات پیام یافت نشد', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const replyData: SendEmailDto = {
|
||||
from: { address: userEmail || 'sender@example.com' },
|
||||
to: [{ address: messageDetail.from.address, name: messageDetail.from.name }],
|
||||
subject: `Re: ${messageDetail.subject}`,
|
||||
html: value,
|
||||
text: value.replace(/<[^>]*>/g, ''),
|
||||
reference: {
|
||||
inReplyTo: messageDetail.messageId,
|
||||
originalMessageId: messageDetail.id.toString()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await sendEmailMutation.mutateAsync(replyData)
|
||||
toast('پاسخ با موفقیت ارسال شد', 'success')
|
||||
setShowAnswer(false)
|
||||
setValue('')
|
||||
setPriority('')
|
||||
} catch {
|
||||
toast('خطا در ارسال پاسخ', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveDraft = async () => {
|
||||
if (!value.trim()) {
|
||||
toast('متن پیام خالی است', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!messageDetail) {
|
||||
toast('اطلاعات پیام یافت نشد', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// const draftData: SendEmailDto = {
|
||||
// from: { address: userEmail || 'sender@example.com' },
|
||||
// to: [{ address: messageDetail.from.address, name: messageDetail.from.name }],
|
||||
// subject: `Re: ${messageDetail.subject}`,
|
||||
// html: value,
|
||||
// text: value.replace(/<[^>]*>/g, ''),
|
||||
// isDraft: true,
|
||||
// reference: {
|
||||
// inReplyTo: messageDetail.messageId,
|
||||
// originalMessageId: messageDetail.id.toString()
|
||||
// }
|
||||
// }
|
||||
|
||||
try {
|
||||
// await saveDraftMutation.mutateAsync(draftData)
|
||||
toast('پیشنویس ذخیره شد', 'success')
|
||||
} catch {
|
||||
toast('خطا در ذخیره پیشنویس', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleForward = () => {
|
||||
// TODO: Implement forward functionality
|
||||
toast('قابلیت ارسال به دیگران به زودی اضافه خواهد شد', 'info')
|
||||
}
|
||||
|
||||
const handleDownloadAttachment = async (attachmentId: string, filename: string) => {
|
||||
if (!id) {
|
||||
@@ -245,78 +159,47 @@ const DetailEmail: FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{
|
||||
!showAnswer ?
|
||||
<div className='flex mt-9 justify-end'>
|
||||
<div className='flex gap-5 text-sm'>
|
||||
<EmojiHappy size={24} color='black' />
|
||||
{/* Email Actions */}
|
||||
{activeAction === null && (
|
||||
<div className='flex mt-9 justify-end'>
|
||||
<div className='flex gap-5 text-sm'>
|
||||
<EmojiHappy size={24} color='black' />
|
||||
|
||||
<div onClick={() => setShowAnswer(true)} className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
|
||||
<img src={AnswerIcon} className='w-[17px]' />
|
||||
<div>{t('mail.answer')}</div>
|
||||
</div>
|
||||
<Reply
|
||||
originalMessage={messageDetail}
|
||||
className='border-r border-border pr-5'
|
||||
showInitialButton={true}
|
||||
onClose={() => setActiveAction(null)}
|
||||
onExpand={() => setActiveAction('reply')}
|
||||
/>
|
||||
|
||||
<div onClick={handleForward} className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
|
||||
<img src={ForwardIcon} className='w-[17px]' />
|
||||
<div>{t('mail.forward')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Forward
|
||||
originalMessage={messageDetail}
|
||||
showInitialButton={true}
|
||||
onClose={() => setActiveAction(null)}
|
||||
onExpand={() => setActiveAction('forward')}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
<div className='mt-9 flex gap-4'>
|
||||
<img src={AvatarImage} className='size-10' />
|
||||
<div className='w-full'>
|
||||
<div className='mb-2 text-sm'>
|
||||
{t('mail.message')}
|
||||
</div>
|
||||
<ReactQuill
|
||||
modules={{
|
||||
toolbar: [
|
||||
[{ header: '1' }, { header: '2' }, { font: [] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['link', 'image'],
|
||||
[{ align: [] }],
|
||||
['clean']
|
||||
]
|
||||
}}
|
||||
theme="snow"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
style={{ minHeight: '120px', width: '100%' }}
|
||||
className='text-sm'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
||||
<div>
|
||||
<Select
|
||||
items={priorityOptions}
|
||||
placeholder={t('new_message.select_priority')}
|
||||
className='xl:w-[190px]'
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-1 justify-end flex gap-3'>
|
||||
<Button
|
||||
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
|
||||
label={t('new_message.draft')}
|
||||
onClick={handleSaveDraft}
|
||||
// loading={saveDraftMutation.isPending}
|
||||
/>
|
||||
<Button
|
||||
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
||||
label={t('new_message.send')}
|
||||
onClick={handleSendReply}
|
||||
loading={sendEmailMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{/* Full width Reply */}
|
||||
{activeAction === 'reply' && (
|
||||
<Reply
|
||||
originalMessage={messageDetail}
|
||||
showInitialButton={false}
|
||||
onClose={() => setActiveAction(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Full width Forward */}
|
||||
{activeAction === 'forward' && (
|
||||
<Forward
|
||||
originalMessage={messageDetail}
|
||||
showInitialButton={false}
|
||||
onClose={() => setActiveAction(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface SendEmailDto {
|
||||
sendTime?: string;
|
||||
uploadOnly?: boolean;
|
||||
envelope?: EmailEnvelopeDto;
|
||||
isForward?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageListQueryDto {
|
||||
|
||||
Reference in New Issue
Block a user