forward and reply
This commit is contained in:
@@ -6,8 +6,8 @@ VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
|
|||||||
VITE_SOCKET_URL = 'wss://dmail-api.danakcorp.com/email'
|
VITE_SOCKET_URL = 'wss://dmail-api.danakcorp.com/email'
|
||||||
# VITE_SOCKET_URL = 'ws://192.168.1.118:4000/email'
|
# VITE_SOCKET_URL = 'ws://192.168.1.118:4000/email'
|
||||||
|
|
||||||
VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
|
# VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
|
||||||
# VITE_BASE_URL = 'http://192.168.1.118:4000'
|
VITE_BASE_URL = 'http://192.168.1.118:4000'
|
||||||
|
|
||||||
|
|
||||||
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
||||||
|
|||||||
@@ -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,
|
onChange,
|
||||||
placeholder = ''
|
placeholder = ''
|
||||||
}) => {
|
}) => {
|
||||||
const modules = {
|
|
||||||
toolbar: [
|
|
||||||
[{ header: '1' }, { header: '2' }, { font: [] }],
|
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
||||||
['bold', 'italic', 'underline'],
|
|
||||||
['link', 'image'],
|
|
||||||
[{ align: [] }],
|
|
||||||
['clean']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactQuill
|
<ReactQuill
|
||||||
modules={modules}
|
|
||||||
theme="snow"
|
theme="snow"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@@ -4,21 +4,23 @@ import Button from '@/components/Button'
|
|||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
|
|
||||||
interface EmailFormActionsProps {
|
interface EmailFormActionsProps {
|
||||||
priority: string
|
|
||||||
onPriorityChange: (priority: string) => void
|
|
||||||
onSend: () => void
|
onSend: () => void
|
||||||
onSaveDraft: () => void
|
|
||||||
isSending: boolean
|
isSending: boolean
|
||||||
isSavingDraft: boolean
|
onClose?: () => void
|
||||||
|
onSaveDraft?: () => void
|
||||||
|
isSavingDraft?: boolean
|
||||||
|
priority?: string
|
||||||
|
onPriorityChange?: (priority: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmailFormActions: FC<EmailFormActionsProps> = ({
|
const EmailFormActions: FC<EmailFormActionsProps> = ({
|
||||||
priority,
|
|
||||||
onPriorityChange,
|
|
||||||
onSend,
|
onSend,
|
||||||
onSaveDraft,
|
|
||||||
isSending,
|
isSending,
|
||||||
isSavingDraft
|
onClose,
|
||||||
|
onSaveDraft,
|
||||||
|
isSavingDraft = false,
|
||||||
|
priority,
|
||||||
|
onPriorityChange
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
|
||||||
@@ -30,6 +32,8 @@ const EmailFormActions: FC<EmailFormActionsProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
||||||
|
{/* Priority Selector */}
|
||||||
|
{priority !== undefined && onPriorityChange && (
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
items={priorityOptions}
|
items={priorityOptions}
|
||||||
@@ -39,13 +43,29 @@ const EmailFormActions: FC<EmailFormActionsProps> = ({
|
|||||||
onChange={(e) => onPriorityChange(e.target.value)}
|
onChange={(e) => onPriorityChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='flex-1 justify-end flex gap-3'>
|
<div className='flex-1 justify-end flex gap-3'>
|
||||||
|
{/* Save Draft Button */}
|
||||||
|
{onSaveDraft && (
|
||||||
<Button
|
<Button
|
||||||
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
|
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')}
|
label={t('new_message.draft')}
|
||||||
onClick={onSaveDraft}
|
onClick={onSaveDraft}
|
||||||
loading={isSavingDraft}
|
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
|
<Button
|
||||||
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
||||||
label={t('new_message.send')}
|
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}
|
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 */}
|
{/* Header */}
|
||||||
<div className='flex justify-between items-center mb-6 md:mb-8'>
|
<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);
|
backdrop-filter: blur(44px);
|
||||||
}
|
}
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|||||||
+5
-1
@@ -23,6 +23,9 @@
|
|||||||
"required": "این فیلد اجباری است",
|
"required": "این فیلد اجباری است",
|
||||||
"invalid_email": "فرمت ایمیل نامعتبر است"
|
"invalid_email": "فرمت ایمیل نامعتبر است"
|
||||||
},
|
},
|
||||||
|
"common": {
|
||||||
|
"close": "بستن"
|
||||||
|
},
|
||||||
"received": {
|
"received": {
|
||||||
"title": "دریافتی ها",
|
"title": "دریافتی ها",
|
||||||
"from_date": "از تاریخ",
|
"from_date": "از تاریخ",
|
||||||
@@ -140,7 +143,8 @@
|
|||||||
"FAVORITE": "نشانشدهها",
|
"FAVORITE": "نشانشدهها",
|
||||||
"Junk": "هرزنامهها",
|
"Junk": "هرزنامهها",
|
||||||
"SENT": "ارسال شدهها",
|
"SENT": "ارسال شدهها",
|
||||||
"TRASH": "سطل زباله"
|
"TRASH": "سطل زباله",
|
||||||
|
"Sent Mail": "ارسال شده ها"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"welcome": "خوش آمدید",
|
"welcome": "خوش آمدید",
|
||||||
|
|||||||
+36
-153
@@ -4,112 +4,26 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import AvatarImage from '@/assets/images/avatar.svg'
|
import AvatarImage from '@/assets/images/avatar.svg'
|
||||||
import { DocumentDownload, EmojiHappy } from 'iconsax-react'
|
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 Header from './Components/Header'
|
||||||
import ReactQuill from 'react-quill-new';
|
import { useGetMessageDetail, useDownloadAttachment } from './hooks/useEmailData'
|
||||||
import Select from '@/components/Select'
|
|
||||||
import { useGetMessageDetail, useSendEmail, useDownloadAttachment } from './hooks/useEmailData'
|
|
||||||
import { useAuthStore } from '../auth/store/AuthStore'
|
|
||||||
import { SendEmailDto } from './types/Types'
|
|
||||||
import { toast } from '@/components/Toast'
|
import { toast } from '@/components/Toast'
|
||||||
import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func'
|
import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func'
|
||||||
import SkeletonDetail from './Components/SkeletonDetail'
|
import SkeletonDetail from './Components/SkeletonDetail'
|
||||||
import { useEmailActions } from '@/hooks/useEmailActions'
|
import { useEmailActions } from '@/hooks/useEmailActions'
|
||||||
import { MailboxEnum } from './enum/Enum'
|
import { MailboxEnum } from './enum/Enum'
|
||||||
|
import Reply from '@/components/newMessage/Reply'
|
||||||
|
import Forward from '@/components/newMessage/Forward'
|
||||||
|
|
||||||
const DetailEmail: FC = () => {
|
const DetailEmail: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const emailActions = useEmailActions()
|
const emailActions = useEmailActions()
|
||||||
const { id, mailbox } = useParams<{ id: string, mailbox: string }>()
|
const { id, mailbox } = useParams<{ id: string, mailbox: string }>()
|
||||||
const { email: userEmail } = useAuthStore()
|
const [activeAction, setActiveAction] = useState<'reply' | 'forward' | null>(null)
|
||||||
const [showAnswer, setShowAnswer] = useState<boolean>(false)
|
|
||||||
const [value, setValue] = useState<string>('')
|
|
||||||
const [priority, setPriority] = useState<string>('')
|
|
||||||
|
|
||||||
// API hooks
|
// API hooks
|
||||||
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '')
|
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '')
|
||||||
const sendEmailMutation = useSendEmail()
|
|
||||||
const downloadAttachmentMutation = useDownloadAttachment()
|
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) => {
|
const handleDownloadAttachment = async (attachmentId: string, filename: string) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -245,78 +159,47 @@ const DetailEmail: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{/* Email Actions */}
|
||||||
!showAnswer ?
|
{activeAction === null && (
|
||||||
<div className='flex mt-9 justify-end'>
|
<div className='flex mt-9 justify-end'>
|
||||||
<div className='flex gap-5 text-sm'>
|
<div className='flex gap-5 text-sm'>
|
||||||
<EmojiHappy size={24} color='black' />
|
<EmojiHappy size={24} color='black' />
|
||||||
|
|
||||||
<div onClick={() => setShowAnswer(true)} className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
|
<Reply
|
||||||
<img src={AnswerIcon} className='w-[17px]' />
|
originalMessage={messageDetail}
|
||||||
<div>{t('mail.answer')}</div>
|
className='border-r border-border pr-5'
|
||||||
</div>
|
showInitialButton={true}
|
||||||
|
onClose={() => setActiveAction(null)}
|
||||||
<div onClick={handleForward} className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
|
onExpand={() => setActiveAction('reply')}
|
||||||
<img src={ForwardIcon} className='w-[17px]' />
|
|
||||||
<div>{t('mail.forward')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
<Forward
|
||||||
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
originalMessage={messageDetail}
|
||||||
<div>
|
showInitialButton={true}
|
||||||
<Select
|
onClose={() => setActiveAction(null)}
|
||||||
items={priorityOptions}
|
onExpand={() => setActiveAction('forward')}
|
||||||
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>
|
)}
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export interface SendEmailDto {
|
|||||||
sendTime?: string;
|
sendTime?: string;
|
||||||
uploadOnly?: boolean;
|
uploadOnly?: boolean;
|
||||||
envelope?: EmailEnvelopeDto;
|
envelope?: EmailEnvelopeDto;
|
||||||
|
isForward?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageListQueryDto {
|
export interface MessageListQueryDto {
|
||||||
|
|||||||
Reference in New Issue
Block a user