attachment + download attach + suggestion
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export const EmailWebSocketProvider: React.FC<EmailWebSocketProviderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useEmailWebSocketContext = () => {
|
||||
const context = useContext(EmailWebSocketContext);
|
||||
if (!context) {
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 } from './hooks/useEmailData'
|
||||
import { useGetMessageDetail, useSendEmail, useDownloadAttachment } from './hooks/useEmailData'
|
||||
import { useAuthStore } from '../auth/store/AuthStore'
|
||||
import { SendEmailDto } from './types/Types'
|
||||
import { toast } from '@/components/Toast'
|
||||
@@ -31,6 +31,7 @@ const DetailEmail: FC = () => {
|
||||
// API hooks
|
||||
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '')
|
||||
const sendEmailMutation = useSendEmail()
|
||||
const downloadAttachmentMutation = useDownloadAttachment()
|
||||
// const saveDraftMutation = useSaveDraft()
|
||||
|
||||
const priorityOptions = [
|
||||
@@ -110,6 +111,36 @@ const DetailEmail: FC = () => {
|
||||
toast('قابلیت ارسال به دیگران به زودی اضافه خواهد شد', 'info')
|
||||
}
|
||||
|
||||
const handleDownloadAttachment = async (attachmentId: string, filename: string) => {
|
||||
if (!id) {
|
||||
toast('شناسه پیام یافت نشد', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await downloadAttachmentMutation.mutateAsync({
|
||||
messageId: id,
|
||||
attachmentId: attachmentId,
|
||||
mailbox: mailbox || ''
|
||||
})
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename || `attachment-${attachmentId}`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
toast('فایل با موفقیت دانلود شد', 'success')
|
||||
} catch (error) {
|
||||
console.error('Download error:', error)
|
||||
toast('خطا در دانلود فایل', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Mark message as seen when it's loaded and not already seen
|
||||
useEffect(() => {
|
||||
if (messageDetail && !messageDetail.seen && id) {
|
||||
@@ -161,9 +192,14 @@ const DetailEmail: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex gap-4 items-center'>
|
||||
<img src={AvatarImage} className='size-10' />
|
||||
<div className='text-sm'>{messageDetail.from.name || messageDetail.from.address}</div>
|
||||
<div>
|
||||
<div className='mt-6 flex gap-4 items-center'>
|
||||
<img src={AvatarImage} className='size-10' />
|
||||
<div>
|
||||
<div className='text-sm'>{messageDetail.from.name || messageDetail.from.address}</div>
|
||||
<div className='text-xs text-description mt-0.5'>{messageDetail.from?.address}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='xl:mt-8 mt-3 text-[13px] leading-7 font-light'>
|
||||
@@ -191,13 +227,20 @@ const DetailEmail: FC = () => {
|
||||
messageDetail.attachments.map((attachment, index) => (
|
||||
<div key={index} className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
|
||||
<span>{attachment.filename || `attachment-${index + 1}`}</span>
|
||||
<DocumentDownload size={20} color='#0038FF' className='cursor-pointer' />
|
||||
<DocumentDownload
|
||||
size={20}
|
||||
color='#0038FF'
|
||||
className='cursor-pointer hover:opacity-70 transition-opacity'
|
||||
onClick={() => handleDownloadAttachment(
|
||||
attachment.id || `ATT${String(index + 1).padStart(5, '0')}`,
|
||||
attachment.filename || `attachment-${index + 1}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
|
||||
<span>Lorem Ipsum.pdf</span>
|
||||
<DocumentDownload size={20} color='#0038FF' />
|
||||
<span>فایل ضمیمهای وجود ندارد</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -112,3 +112,10 @@ export const useRestoreMessage = () => {
|
||||
mutationFn: ({ messageId, mailbox }: SingleActionRequest) => api.restoreMessage(messageId, mailbox),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDownloadAttachment = () => {
|
||||
return useMutation({
|
||||
mutationFn: ({ messageId, attachmentId, mailbox }: { messageId: string; attachmentId: string; mailbox: string }) =>
|
||||
api.downloadAttachment(messageId, attachmentId, mailbox),
|
||||
});
|
||||
};
|
||||
@@ -145,3 +145,17 @@ export const markAsUnread = async (
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const downloadAttachment = async (
|
||||
messageId: string,
|
||||
attachmentId: string,
|
||||
mailbox: string
|
||||
): Promise<Blob> => {
|
||||
const response = await axios.get(
|
||||
`/email/messages/${messageId}/attachment?attachmentId=${attachmentId}&mailbox=${mailbox}`,
|
||||
{
|
||||
responseType: "blob",
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface EmailHeaderDto {
|
||||
}
|
||||
|
||||
export interface EmailAttachmentDto {
|
||||
id?: string;
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
encoding?: string;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import axiosInstance from "@/config/axios";
|
||||
|
||||
export interface EmailSuggestion {
|
||||
address: string;
|
||||
name: string;
|
||||
frequency: number;
|
||||
lastUsed: string;
|
||||
}
|
||||
|
||||
export interface EmailSuggestionsResponse {
|
||||
suggestions: EmailSuggestion[];
|
||||
total: number;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface GetEmailSuggestionsParams {
|
||||
query?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export const EmailSuggestionsService = {
|
||||
async getSuggestions(
|
||||
params?: GetEmailSuggestionsParams
|
||||
): Promise<EmailSuggestionsResponse> {
|
||||
const response = await axiosInstance.get("/email/suggestions", {
|
||||
params: {
|
||||
query: params?.query || "",
|
||||
limit: params?.limit || 5,
|
||||
},
|
||||
});
|
||||
return response.data.data; // API returns wrapped response
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user