attachment + download attach + suggestion
This commit is contained in:
@@ -1,77 +1,56 @@
|
||||
import { FC, useState, useEffect, useRef } from 'react'
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { CloseCircle } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSharedStore } from '@/shared/store/sharedStore'
|
||||
import { useAuthStore } from '../../pages/auth/store/AuthStore'
|
||||
import { useGetMessageDetail, useSendEmail } from '../../pages/received/hooks/useEmailData'
|
||||
import { SendEmailDto } from '../../pages/received/types/Types'
|
||||
import { useUpdateDraft, useSendDraft } from '../../pages/draft/hooks/useDraftData'
|
||||
import Input from '../Input'
|
||||
import ReactQuill from 'react-quill-new';
|
||||
import Button from '../Button'
|
||||
import Select from '../Select'
|
||||
import UploadButton from '../UploadButton'
|
||||
import { toast } from '../Toast'
|
||||
|
||||
|
||||
import Input from '@/components/Input'
|
||||
import EmailInput from './EmailInput'
|
||||
import EmailEditor from './EmailEditor'
|
||||
import EmailAttachments from './EmailAttachments'
|
||||
import EmailFormActions from './EmailFormActions'
|
||||
import { useNewMessage } from './hooks/useNewMessage'
|
||||
|
||||
const NewMessage: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const [value, setValue] = useState<string>('');
|
||||
const { setOpenNewMessage, openNewMessage, draftData, setDraftData, editingDraftId, setEditingDraftId } = useSharedStore()
|
||||
const { email: userEmail } = useAuthStore()
|
||||
const { openNewMessage, setOpenNewMessage } = useSharedStore()
|
||||
const [isClosing, setIsClosing] = useState(false)
|
||||
const [shouldRender, setShouldRender] = useState(false)
|
||||
const [isOpening, setIsOpening] = useState(false)
|
||||
|
||||
// Form state
|
||||
const [to, setTo] = useState('')
|
||||
const [toEmails, setToEmails] = useState<string[]>([])
|
||||
const [subject, setSubject] = useState('')
|
||||
const [priority, setPriority] = useState('')
|
||||
const {
|
||||
// Form state
|
||||
toEmails,
|
||||
subject,
|
||||
content,
|
||||
priority,
|
||||
attachments,
|
||||
emailSuggestions,
|
||||
|
||||
// Loading states
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
const [isSavingDraft, setIsSavingDraft] = useState(false)
|
||||
// Loading states
|
||||
isSending,
|
||||
isSavingDraft,
|
||||
|
||||
// Auto-add email functionality
|
||||
const typingTimeoutRef = useRef<number | null>(null)
|
||||
// Form handlers
|
||||
addEmail,
|
||||
removeEmail,
|
||||
setSubject,
|
||||
setContent,
|
||||
setPriority,
|
||||
setAttachments,
|
||||
|
||||
// API hooks
|
||||
const sendEmailMutation = useSendEmail()
|
||||
const updateDraftMutation = useUpdateDraft()
|
||||
const sendDraftMutation = useSendDraft()
|
||||
// Search functions
|
||||
searchEmailSuggestions,
|
||||
|
||||
// Get draft detail if draftData exists
|
||||
const { data: draftDetail } = useGetMessageDetail(
|
||||
draftData?.id.toString() || '',
|
||||
draftData?.mailbox || '',
|
||||
)
|
||||
|
||||
// Load draft data into form when draft detail is available
|
||||
useEffect(() => {
|
||||
if (draftDetail) {
|
||||
// Extract emails from to array
|
||||
const toEmailsList = draftDetail.to?.map(recipient => recipient.address) || []
|
||||
setToEmails(toEmailsList)
|
||||
setSubject(draftDetail.subject || '')
|
||||
|
||||
// Try to load HTML content if available, fallback to text or intro
|
||||
const draftWithContent = draftDetail as unknown as { html?: string; text?: string; intro: string }
|
||||
const content = ensureString(draftWithContent.html || draftWithContent.text || draftWithContent.intro)
|
||||
setValue(content)
|
||||
|
||||
// Clear draft data after loading
|
||||
setDraftData(null)
|
||||
}
|
||||
}, [draftDetail, setDraftData])
|
||||
// Actions
|
||||
handleSend,
|
||||
handleSaveDraft,
|
||||
resetForm
|
||||
} = useNewMessage()
|
||||
|
||||
useEffect(() => {
|
||||
if (openNewMessage) {
|
||||
setShouldRender(true)
|
||||
setIsClosing(false)
|
||||
setIsOpening(true)
|
||||
// شروع انیمیشن باز شدن بعد از رندر
|
||||
setTimeout(() => {
|
||||
setIsOpening(false)
|
||||
}, 50)
|
||||
@@ -90,207 +69,25 @@ const NewMessage: FC = () => {
|
||||
setTimeout(() => {
|
||||
setOpenNewMessage(false)
|
||||
resetForm()
|
||||
setDraftData(null)
|
||||
setEditingDraftId(null)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const parseEmailAddresses = (emails: string[]) => {
|
||||
return emails
|
||||
.filter(email => email.length > 0)
|
||||
.map(email => ({ address: email }))
|
||||
}
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
const ensureString = (val: unknown): string => {
|
||||
if (typeof val === 'string') return val
|
||||
if (val === null || val === undefined) return ''
|
||||
return String(val)
|
||||
}
|
||||
|
||||
const addEmailToList = (email: string) => {
|
||||
if (!isValidEmail(email)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (toEmails.includes(email)) {
|
||||
toast('این ایمیل قبلاً اضافه شده است', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
setToEmails([...toEmails, email])
|
||||
setTo('')
|
||||
}
|
||||
|
||||
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value
|
||||
setTo(newValue)
|
||||
|
||||
// Clear existing timeout
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Set new timeout to auto-add email after user stops typing
|
||||
if (newValue.trim()) {
|
||||
typingTimeoutRef.current = window.setTimeout(() => {
|
||||
if (isValidEmail(newValue.trim())) {
|
||||
addEmailToList(newValue.trim())
|
||||
}
|
||||
}, 1500) // Wait 1.5 seconds after user stops typing
|
||||
const handleSendWithClose = async () => {
|
||||
const success = await handleSend()
|
||||
if (success) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleToBlur = () => {
|
||||
// Clear timeout when input loses focus
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Auto-add email if valid when input loses focus
|
||||
if (to.trim() && isValidEmail(to.trim())) {
|
||||
addEmailToList(to.trim())
|
||||
const handleSaveDraftWithClose = async () => {
|
||||
const success = await handleSaveDraft()
|
||||
if (success) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && to.trim()) {
|
||||
e.preventDefault()
|
||||
|
||||
// Clear timeout when Enter is pressed
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current)
|
||||
}
|
||||
|
||||
addEmailToList(to.trim())
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up timeout on component unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const removeEmail = (emailToRemove: string) => {
|
||||
setToEmails(toEmails.filter(email => email !== emailToRemove))
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setTo('')
|
||||
setToEmails([])
|
||||
setSubject('')
|
||||
setValue('')
|
||||
setPriority('')
|
||||
}
|
||||
|
||||
const handleEmailAction = async (isDraft: boolean = false) => {
|
||||
const isEditingDraft = !!editingDraftId
|
||||
|
||||
if (!isDraft && (toEmails.length === 0 || !subject)) {
|
||||
toast('لطفا تمام فیلدهای مورد نیاز را پر کنید', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (isDraft && toEmails.length === 0 && !subject && ensureString(value).trim() === '') {
|
||||
toast('حداقل یکی از فیلدها باید پر باشد', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// Set appropriate loading state
|
||||
if (isDraft) {
|
||||
setIsSavingDraft(true)
|
||||
} else {
|
||||
setIsSending(true)
|
||||
}
|
||||
|
||||
try {
|
||||
let messageId = ''
|
||||
if (isEditingDraft) {
|
||||
// We're editing an existing draft
|
||||
const draftUpdateData = {
|
||||
to: parseEmailAddresses(toEmails),
|
||||
subject,
|
||||
html: ensureString(value),
|
||||
text: ensureString(value).replace(/<[^>]*>/g, ''),
|
||||
}
|
||||
|
||||
if (isDraft) {
|
||||
// Just update the draft
|
||||
await updateDraftMutation.mutateAsync({
|
||||
messageId: editingDraftId.toString(),
|
||||
draftData: draftUpdateData
|
||||
}, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onSuccess: (data: any) => {
|
||||
messageId = data.data?.messageId
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Update first, then send
|
||||
await updateDraftMutation.mutateAsync({
|
||||
messageId: editingDraftId.toString(),
|
||||
draftData: {
|
||||
...draftUpdateData,
|
||||
isDraft: true,
|
||||
uploadOnly: true
|
||||
}
|
||||
}, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onSuccess: (data: any) => {
|
||||
messageId = data.data?.messageId
|
||||
}
|
||||
})
|
||||
await sendDraftMutation.mutateAsync(messageId || editingDraftId.toString())
|
||||
toast('پیام با موفقیت ارسال شد', 'success')
|
||||
}
|
||||
handleClose()
|
||||
} else {
|
||||
// Creating new email/draft
|
||||
const emailData: SendEmailDto = {
|
||||
from: { address: userEmail || 'sender@example.com' },
|
||||
to: parseEmailAddresses(toEmails),
|
||||
subject,
|
||||
html: ensureString(value),
|
||||
text: ensureString(value).replace(/<[^>]*>/g, ''),
|
||||
...(isDraft && { isDraft: true, uploadOnly: true }),
|
||||
}
|
||||
|
||||
await sendEmailMutation.mutateAsync(emailData, {
|
||||
onSuccess: (data) => {
|
||||
toast(data.message, 'success')
|
||||
handleClose()
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in email action:', error)
|
||||
toast('خطا در انجام عملیات', 'error')
|
||||
} finally {
|
||||
// Reset loading states
|
||||
setIsSending(false)
|
||||
setIsSavingDraft(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = () => handleEmailAction(false)
|
||||
const handleSaveDraft = () => handleEmailAction(true)
|
||||
|
||||
const priorityOptions = [
|
||||
{ value: 'high', label: 'بالا' },
|
||||
{ value: 'medium', label: 'متوسط' },
|
||||
{ value: 'low', label: 'پایین' },
|
||||
]
|
||||
return (
|
||||
<>
|
||||
|
||||
{shouldRender && (
|
||||
<>
|
||||
<div
|
||||
@@ -316,34 +113,14 @@ const NewMessage: FC = () => {
|
||||
<label className='text-sm'>
|
||||
{t('new_message.to')}
|
||||
</label>
|
||||
<div className='w-full relative mt-1'>
|
||||
<div className="w-full bg-white min-h-10 text-black flex flex-wrap items-center gap-1 px-4 py-2 text-xs rounded-xl border border-border">
|
||||
{/* Email Tags */}
|
||||
{toEmails.map((email, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1 bg-[#EBEDF5] text-xs h-7 rounded-full px-3"
|
||||
>
|
||||
<span>{email}</span>
|
||||
<CloseCircle
|
||||
size={16}
|
||||
color='#D52903'
|
||||
className='cursor-pointer'
|
||||
onClick={() => removeEmail(email)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Input Field */}
|
||||
<input
|
||||
value={to}
|
||||
onChange={handleToChange}
|
||||
onBlur={handleToBlur}
|
||||
onKeyDown={handleToKeyDown}
|
||||
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
|
||||
className="flex-1 min-w-32 bg-transparent outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-1'>
|
||||
<EmailInput
|
||||
toEmails={toEmails}
|
||||
onAddEmail={addEmail}
|
||||
onRemoveEmail={removeEmail}
|
||||
suggestions={emailSuggestions}
|
||||
onSearchSuggestions={searchEmailSuggestions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -354,60 +131,29 @@ const NewMessage: FC = () => {
|
||||
/>
|
||||
|
||||
<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' }}
|
||||
className='text-sm'
|
||||
<EmailEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className=''>
|
||||
<UploadButton />
|
||||
</div>
|
||||
<EmailAttachments
|
||||
attachments={attachments}
|
||||
onAttachmentsChange={setAttachments}
|
||||
/>
|
||||
|
||||
<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) => 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={isSavingDraft}
|
||||
/>
|
||||
<Button
|
||||
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
||||
label={t('new_message.send')}
|
||||
onClick={handleSend}
|
||||
loading={isSending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EmailFormActions
|
||||
priority={priority}
|
||||
onPriorityChange={setPriority}
|
||||
onSend={handleSendWithClose}
|
||||
onSaveDraft={handleSaveDraftWithClose}
|
||||
isSending={isSending}
|
||||
isSavingDraft={isSavingDraft}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user