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
+12 -6
View File
@@ -6,28 +6,34 @@ import { useDropzone } from 'react-dropzone'
type Props = {
title?: string
onChange?: (files: File[]) => void
onRemove?: (files: File[]) => void
}
const UploadButton: FC<Props> = (props) => {
const { t } = useTranslation()
const { title } = props
const { title, onChange, onRemove } = props
const [files, setFiles] = useState<File[]>([])
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles((prev) => [...prev, ...acceptedFiles])
}, [])
const updatedFiles = [...files, ...acceptedFiles]
setFiles(updatedFiles)
onChange?.(updatedFiles)
}, [files, onChange])
const { getRootProps, getInputProps } = useDropzone({ onDrop })
const handleRemove = (index: number) => {
const array = [...files]
array.splice(index, 1)
setFiles(array)
const updatedFiles = files.filter((_, i) => i !== index)
setFiles(updatedFiles)
onRemove?.(updatedFiles)
}
return (
<div className='flex flex-col gap-4 sm:flex-row'>
<div {...getRootProps()} className='h-10 bg-[#ECEEF5] rounded-full flex gap-2 items-center px-3 cursor-pointer'>
@@ -0,0 +1,69 @@
import { FC } from 'react'
import UploadButton from '@/components/UploadButton'
import { EmailAttachmentDto } from '@/pages/received/types/Types'
import { toast } from '@/components/Toast'
interface EmailAttachmentsProps {
attachments: EmailAttachmentDto[]
onAttachmentsChange: (attachments: EmailAttachmentDto[]) => void
}
const EmailAttachments: FC<EmailAttachmentsProps> = ({
attachments,
onAttachmentsChange
}) => {
const convertFileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const result = reader.result as string
const base64 = result.split(',')[1]
resolve(base64)
}
reader.onerror = reject
reader.readAsDataURL(file)
})
}
const handleFilesUpload = async (files: File[]) => {
try {
const newAttachments: EmailAttachmentDto[] = []
for (const file of files) {
const base64Content = await convertFileToBase64(file)
const attachment: EmailAttachmentDto = {
filename: file.name,
contentType: file.type,
encoding: 'base64',
contentDisposition: 'attachment',
content: base64Content
}
newAttachments.push(attachment)
}
onAttachmentsChange([...attachments, ...newAttachments])
} catch (error) {
console.error('Error converting files to base64:', error)
toast('خطا در آپلود فایل‌ها', 'error')
}
}
const handleRemoveFile = (files: File[]) => {
const currentFileNames = files.map(f => f.name)
const updatedAttachments = attachments.filter(att =>
currentFileNames.includes(att.filename || '')
)
onAttachmentsChange(updatedAttachments)
}
return (
<div>
<UploadButton
onChange={handleFilesUpload}
onRemove={handleRemoveFile}
/>
</div>
)
}
export default EmailAttachments
+39
View File
@@ -0,0 +1,39 @@
import { FC } from 'react'
import ReactQuill from 'react-quill-new'
interface EmailEditorProps {
value: string
onChange: (value: string) => void
placeholder?: string
}
const EmailEditor: FC<EmailEditorProps> = ({
value,
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}
placeholder={placeholder}
style={{ minHeight: '120px' }}
className='text-sm'
/>
)
}
export default EmailEditor
@@ -0,0 +1,60 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
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
}
const EmailFormActions: FC<EmailFormActionsProps> = ({
priority,
onPriorityChange,
onSend,
onSaveDraft,
isSending,
isSavingDraft
}) => {
const { t } = useTranslation('global')
const priorityOptions = [
{ value: 'high', label: 'بالا' },
{ value: 'medium', label: 'متوسط' },
{ value: 'low', label: 'پایین' },
]
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>
<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}
/>
<Button
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
label={t('new_message.send')}
onClick={onSend}
loading={isSending}
/>
</div>
</div>
)
}
export default EmailFormActions
+227
View File
@@ -0,0 +1,227 @@
import { FC, useState, useRef, useEffect } from 'react'
import { CloseCircle } from 'iconsax-react'
import { EmailSuggestion } from '@/services/EmailSuggestionsService'
interface EmailInputProps {
toEmails: string[]
onAddEmail: (email: string) => void
onRemoveEmail: (email: string) => void
suggestions: EmailSuggestion[]
onSearchSuggestions?: (query: string) => void
}
const EmailInput: FC<EmailInputProps> = ({
toEmails,
onAddEmail,
onRemoveEmail,
suggestions,
onSearchSuggestions
}) => {
const [to, setTo] = useState('')
const [filteredSuggestions, setFilteredSuggestions] = useState<EmailSuggestion[]>([])
const [showSuggestions, setShowSuggestions] = useState(false)
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1)
const typingTimeoutRef = useRef<number | null>(null)
const searchTimeoutRef = useRef<number | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const isValidEmail = (email: string) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email)
}
const filterSuggestions = (input: string) => {
if (!input.trim()) {
setFilteredSuggestions([])
setShowSuggestions(false)
return
}
// Always search via API for fresh results
if (onSearchSuggestions) {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
}
searchTimeoutRef.current = window.setTimeout(() => {
onSearchSuggestions(input)
}, 300)
}
setSelectedSuggestionIndex(-1)
}
// Update filtered suggestions when API suggestions change
useEffect(() => {
if (suggestions.length > 0) {
const filtered = suggestions
.filter(suggestion =>
!toEmails.includes(suggestion.address)
)
.slice(0, 5)
setFilteredSuggestions(filtered)
setShowSuggestions(filtered.length > 0 && to.trim().length > 0)
} else if (to.trim() === '') {
// Clear suggestions when input is empty
setFilteredSuggestions([])
setShowSuggestions(false)
}
}, [suggestions, to, toEmails])
const selectSuggestion = (suggestion: EmailSuggestion) => {
onAddEmail(suggestion.address)
setTo('')
setShowSuggestions(false)
setFilteredSuggestions([])
setSelectedSuggestionIndex(-1)
inputRef.current?.focus()
}
const addEmail = (email: string) => {
if (isValidEmail(email.trim())) {
onAddEmail(email.trim())
setTo('')
setShowSuggestions(false)
setFilteredSuggestions([])
}
}
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value
setTo(newValue)
filterSuggestions(newValue)
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
if (newValue.trim()) {
typingTimeoutRef.current = window.setTimeout(() => {
if (isValidEmail(newValue.trim())) {
addEmail(newValue.trim())
}
}, 1500)
}
}
const handleToBlur = () => {
setTimeout(() => {
setShowSuggestions(false)
setFilteredSuggestions([])
}, 200)
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
if (to.trim() && isValidEmail(to.trim())) {
addEmail(to.trim())
}
}
const handleToFocus = () => {
if (to.trim()) {
filterSuggestions(to)
}
}
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (showSuggestions && filteredSuggestions.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault()
setSelectedSuggestionIndex(prev =>
prev < filteredSuggestions.length - 1 ? prev + 1 : 0
)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setSelectedSuggestionIndex(prev =>
prev > 0 ? prev - 1 : filteredSuggestions.length - 1
)
} else if (e.key === 'Enter' && selectedSuggestionIndex >= 0) {
e.preventDefault()
selectSuggestion(filteredSuggestions[selectedSuggestionIndex])
return
} else if (e.key === 'Escape') {
setShowSuggestions(false)
setFilteredSuggestions([])
setSelectedSuggestionIndex(-1)
return
}
}
if (e.key === 'Enter' && to.trim()) {
e.preventDefault()
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
addEmail(to.trim())
}
}
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
}
}
}, [])
return (
<div className='w-full relative'>
<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">
{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={() => onRemoveEmail(email)}
/>
</div>
))}
<input
ref={inputRef}
value={to}
onChange={handleToChange}
onBlur={handleToBlur}
onFocus={handleToFocus}
onKeyDown={handleToKeyDown}
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
className="flex-1 min-w-32 bg-transparent outline-none"
/>
</div>
{showSuggestions && filteredSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg z-[10000] mt-1">
{filteredSuggestions.map((suggestion, index) => (
<div
key={suggestion.address}
className={`px-4 py-2 cursor-pointer text-sm hover:bg-gray-100 border-b border-gray-100 last:border-b-0 ${selectedSuggestionIndex === index ? 'bg-blue-50 text-blue-600' : 'text-gray-700'
}`}
onMouseDown={() => selectSuggestion(suggestion)}
onMouseEnter={() => setSelectedSuggestionIndex(index)}
>
<div className="flex flex-col">
<span className="font-medium">{suggestion.name || suggestion.address}</span>
{suggestion.name && (
<span className="text-xs text-gray-500">{suggestion.address}</span>
)}
</div>
</div>
))}
</div>
)}
</div>
)
}
export default EmailInput
+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>
</>
)}
</>
)
}
@@ -0,0 +1,40 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { EmailSuggestionsService } from "@/services/EmailSuggestionsService";
export const useEmailSuggestions = () => {
const [debouncedQuery, setDebouncedQuery] = useState("");
const { data: suggestionsData, isLoading, error } = useQuery({
queryKey: ["emailSuggestions", debouncedQuery],
queryFn: () =>
EmailSuggestionsService.getSuggestions({
query: debouncedQuery,
limit: 5,
}),
enabled: debouncedQuery.length >= 2,
staleTime: 1000 * 60 * 5, // 5 minutes
});
if (error) {
console.error("🔥 Query error:", error);
}
const searchSuggestions = (query: string) => {
// Debounce the actual search
const timeoutId = setTimeout(() => {
setDebouncedQuery(query);
}, 300);
return () => clearTimeout(timeoutId);
};
const suggestions = suggestionsData?.suggestions || [];
return {
suggestions,
searchSuggestions,
isLoading,
hasResults: suggestions.length > 0,
};
};
@@ -0,0 +1,262 @@
import { useState, useEffect } from "react";
import { useSharedStore } from "@/shared/store/sharedStore";
import { useAuthStore } from "@/pages/auth/store/AuthStore";
import {
useGetMessageDetail,
useSendEmail,
} from "@/pages/received/hooks/useEmailData";
import { SendEmailDto, EmailAttachmentDto } from "@/pages/received/types/Types";
import { useUpdateDraft, useSendDraft } from "@/pages/draft/hooks/useDraftData";
import { toast } from "@/components/Toast";
import { useEmailSuggestions } from "./useEmailSuggestions";
export const useNewMessage = () => {
const {
draftData,
setDraftData,
editingDraftId,
setEditingDraftId,
} = useSharedStore();
const { email: userEmail } = useAuthStore();
// Form state
const [toEmails, setToEmails] = useState<string[]>([]);
const [subject, setSubject] = useState("");
const [content, setContent] = useState("");
const [priority, setPriority] = useState("");
const [attachments, setAttachments] = useState<EmailAttachmentDto[]>([]);
// Loading states
const [isSending, setIsSending] = useState(false);
const [isSavingDraft, setIsSavingDraft] = useState(false);
// API hooks
const sendEmailMutation = useSendEmail();
const updateDraftMutation = useUpdateDraft();
const sendDraftMutation = useSendDraft();
// Email suggestions
const {
suggestions: emailSuggestions,
searchSuggestions,
} = useEmailSuggestions();
// Get draft detail if draftData exists
const { data: draftDetail } = useGetMessageDetail(
draftData?.id.toString() || "",
draftData?.mailbox || ""
);
// Load draft data into form
useEffect(() => {
if (draftDetail) {
const toEmailsList =
draftDetail.to?.map((recipient) => recipient.address) || [];
setToEmails(toEmailsList);
setSubject(draftDetail.subject || "");
const draftWithContent = (draftDetail as unknown) as {
html?: string;
text?: string;
intro: string;
};
const draftContent = ensureString(
draftWithContent.html || draftWithContent.text || draftWithContent.intro
);
setContent(draftContent);
setDraftData(null);
}
}, [draftDetail, setDraftData]);
const ensureString = (val: unknown): string => {
if (typeof val === "string") return val;
if (val === null || val === undefined) return "";
return String(val);
};
const isValidEmail = (email: string) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
};
// Function to search email suggestions
const searchEmailSuggestions = (query: string) => {
searchSuggestions(query);
};
const addEmail = (email: string) => {
if (!isValidEmail(email)) {
return;
}
if (toEmails.includes(email)) {
toast("این ایمیل قبلاً اضافه شده است", "error");
return;
}
setToEmails([...toEmails, email]);
};
const removeEmail = (emailToRemove: string) => {
setToEmails(toEmails.filter((email) => email !== emailToRemove));
};
const parseEmailAddresses = (emails: string[]) => {
return emails
.filter((email) => email.length > 0)
.map((email) => ({ address: email }));
};
const resetForm = () => {
setToEmails([]);
setSubject("");
setContent("");
setPriority("");
setAttachments([]);
};
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(content).trim() === ""
) {
toast("حداقل یکی از فیلدها باید پر باشد", "error");
return;
}
if (isDraft) {
setIsSavingDraft(true);
} else {
setIsSending(true);
}
try {
let messageId = "";
if (isEditingDraft) {
const draftUpdateData = {
to: parseEmailAddresses(toEmails),
subject,
html: ensureString(content),
text: ensureString(content).replace(/<[^>]*>/g, ""),
...(attachments.length > 0 && { attachments }),
};
if (isDraft) {
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 {
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");
}
resetForm();
setEditingDraftId(null);
return true;
} else {
const emailData: SendEmailDto = {
from: { address: userEmail || "sender@example.com" },
to: parseEmailAddresses(toEmails),
subject,
html: ensureString(content),
text: ensureString(content).replace(/<[^>]*>/g, ""),
...(attachments.length > 0 && { attachments }),
...(isDraft && { isDraft: true, uploadOnly: true }),
};
await sendEmailMutation.mutateAsync(emailData, {
onSuccess: (data) => {
toast(data?.data.message, "success");
},
});
resetForm();
return true;
}
} catch (error) {
console.error("Error in email action:", error);
toast("خطا در انجام عملیات", "error");
return false;
} finally {
setIsSending(false);
setIsSavingDraft(false);
}
};
const handleSend = async () => {
const result = await handleEmailAction(false);
return result || false;
};
const handleSaveDraft = async () => {
const result = await handleEmailAction(true);
return result || false;
};
return {
// Form state
toEmails,
subject,
content,
priority,
attachments,
emailSuggestions,
// Loading states
isSending,
isSavingDraft,
// Form handlers
addEmail,
removeEmail,
setSubject,
setContent,
setPriority,
setAttachments,
// Search functions
searchEmailSuggestions,
// Actions
handleSend,
handleSaveDraft,
resetForm,
};
};