attachment + download attach + suggestion

This commit is contained in:
hamid zarghami
2025-07-21 16:31:38 +03:30
parent 60f459d33b
commit b31b64a7f9
14 changed files with 879 additions and 331 deletions
+64 -318
View File
@@ -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>
</>
)}
</>
)
}