Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89d237d75b | |||
| 7dbd2def27 | |||
| a0ea1e1fb5 | |||
| dbe28c3b5f | |||
| 2ccc096277 | |||
| 4a20073ad6 | |||
| 0be4a02dea | |||
| c76d9c788b | |||
| 58f054ccfa | |||
| a081c364fe |
+3
-5
@@ -2,17 +2,15 @@ FROM node:22-alpine AS builder
|
|||||||
|
|
||||||
# Install tzdata to support timezone settings
|
# Install tzdata to support timezone settings
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata
|
||||||
RUN npm install -g corepack@latest
|
|
||||||
RUN corepack enable && corepack prepare pnpm@10 --activate
|
|
||||||
|
|
||||||
# Set the timezone to Asia/Tehran
|
# Set the timezone to Asia/Tehran
|
||||||
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY package*.json pnpm-lock.yaml ./
|
COPY package*.json ./
|
||||||
RUN pnpm install --frozen-lockfile --loglevel info
|
RUN npm ci --loglevel info || npm install --loglevel info
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN pnpm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:stable-alpine AS production-stage
|
FROM nginx:stable-alpine AS production-stage
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -41,7 +41,6 @@
|
|||||||
"react-loading-skeleton": "^3.5.0",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-quill": "^2.0.0",
|
|
||||||
"react-quill-new": "^3.4.6",
|
"react-quill-new": "^3.4.6",
|
||||||
"react-router-dom": "^7.5.2",
|
"react-router-dom": "^7.5.2",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
@@ -73,6 +72,5 @@
|
|||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.26.1",
|
"typescript-eslint": "^8.26.1",
|
||||||
"vite": "^6.3.1"
|
"vite": "^6.3.1"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-8578
File diff suppressed because it is too large
Load Diff
+1365
-19
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 103 KiB |
@@ -8,6 +8,8 @@ interface EmailInputProps {
|
|||||||
onRemoveEmail: (email: string) => void
|
onRemoveEmail: (email: string) => void
|
||||||
suggestions: EmailSuggestion[]
|
suggestions: EmailSuggestion[]
|
||||||
onSearchSuggestions?: (query: string) => void
|
onSearchSuggestions?: (query: string) => void
|
||||||
|
showCcBccButton?: boolean
|
||||||
|
onCcBccClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmailInput: FC<EmailInputProps> = ({
|
const EmailInput: FC<EmailInputProps> = ({
|
||||||
@@ -15,7 +17,9 @@ const EmailInput: FC<EmailInputProps> = ({
|
|||||||
onAddEmail,
|
onAddEmail,
|
||||||
onRemoveEmail,
|
onRemoveEmail,
|
||||||
suggestions,
|
suggestions,
|
||||||
onSearchSuggestions
|
onSearchSuggestions,
|
||||||
|
showCcBccButton = false,
|
||||||
|
onCcBccClick
|
||||||
}) => {
|
}) => {
|
||||||
const [to, setTo] = useState('')
|
const [to, setTo] = useState('')
|
||||||
const [filteredSuggestions, setFilteredSuggestions] = useState<EmailSuggestion[]>([])
|
const [filteredSuggestions, setFilteredSuggestions] = useState<EmailSuggestion[]>([])
|
||||||
@@ -195,6 +199,15 @@ const EmailInput: FC<EmailInputProps> = ({
|
|||||||
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
|
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
|
||||||
className="flex-1 min-w-32 bg-transparent outline-none"
|
className="flex-1 min-w-32 bg-transparent outline-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showCcBccButton && (
|
||||||
|
<button
|
||||||
|
onClick={onCcBccClick}
|
||||||
|
className='text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors pt-[3px] cursor-pointer '
|
||||||
|
>
|
||||||
|
CC/BCC
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSuggestions && filteredSuggestions.length > 0 && (
|
{showSuggestions && filteredSuggestions.length > 0 && (
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ const NewMessage: FC = () => {
|
|||||||
const [isOpening, setIsOpening] = useState(false)
|
const [isOpening, setIsOpening] = useState(false)
|
||||||
const [isIntelligenseOpen, setIsIntelligenseOpen] = useState(false)
|
const [isIntelligenseOpen, setIsIntelligenseOpen] = useState(false)
|
||||||
const [prompt, setPrompt] = useState('')
|
const [prompt, setPrompt] = useState('')
|
||||||
|
const [showCcBcc, setShowCcBcc] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// Form state
|
// Form state
|
||||||
toEmails,
|
toEmails,
|
||||||
|
ccEmails,
|
||||||
|
bccEmails,
|
||||||
subject,
|
subject,
|
||||||
content,
|
content,
|
||||||
priority,
|
priority,
|
||||||
@@ -38,6 +41,10 @@ const NewMessage: FC = () => {
|
|||||||
// Form handlers
|
// Form handlers
|
||||||
addEmail,
|
addEmail,
|
||||||
removeEmail,
|
removeEmail,
|
||||||
|
addCcEmail,
|
||||||
|
removeCcEmail,
|
||||||
|
addBccEmail,
|
||||||
|
removeBccEmail,
|
||||||
setSubject,
|
setSubject,
|
||||||
setContent,
|
setContent,
|
||||||
setPriority,
|
setPriority,
|
||||||
@@ -76,6 +83,7 @@ const NewMessage: FC = () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setOpenNewMessage(false)
|
setOpenNewMessage(false)
|
||||||
resetForm()
|
resetForm()
|
||||||
|
setShowCcBcc(false)
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,23 +138,63 @@ const NewMessage: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className='space-y-6 md:space-y-8'>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<label className='text-sm'>
|
<label className='text-sm mb-1 block'>
|
||||||
{t('new_message.to')}
|
{t('new_message.to')}
|
||||||
</label>
|
</label>
|
||||||
<div className='mt-1'>
|
<div>
|
||||||
<EmailInput
|
<EmailInput
|
||||||
toEmails={toEmails}
|
toEmails={toEmails}
|
||||||
onAddEmail={addEmail}
|
onAddEmail={addEmail}
|
||||||
onRemoveEmail={removeEmail}
|
onRemoveEmail={removeEmail}
|
||||||
suggestions={emailSuggestions}
|
suggestions={emailSuggestions}
|
||||||
onSearchSuggestions={searchEmailSuggestions}
|
onSearchSuggestions={searchEmailSuggestions}
|
||||||
|
showCcBccButton={true}
|
||||||
|
onCcBccClick={() => setShowCcBcc(!showCcBcc)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2 items-end'>
|
{/* CC/BCC Accordion */}
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
'overflow-hidden transition-all duration-300 ease-in-out',
|
||||||
|
showCcBcc ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='mt-4'>
|
||||||
|
<label className='text-sm mb-1 block'>
|
||||||
|
CC
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<EmailInput
|
||||||
|
toEmails={ccEmails}
|
||||||
|
onAddEmail={addCcEmail}
|
||||||
|
onRemoveEmail={removeCcEmail}
|
||||||
|
suggestions={emailSuggestions}
|
||||||
|
onSearchSuggestions={searchEmailSuggestions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4'>
|
||||||
|
<label className='text-sm mb-1 block'>
|
||||||
|
BCC
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<EmailInput
|
||||||
|
toEmails={bccEmails}
|
||||||
|
onAddEmail={addBccEmail}
|
||||||
|
onRemoveEmail={removeBccEmail}
|
||||||
|
suggestions={emailSuggestions}
|
||||||
|
onSearchSuggestions={searchEmailSuggestions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2 items-end mt-4'>
|
||||||
<div className='flex lg:flex-row flex-col flex-1 items-end gap-2'>
|
<div className='flex lg:flex-row flex-col flex-1 items-end gap-2'>
|
||||||
<Input
|
<Input
|
||||||
label={t('new_message.subject')}
|
label={t('new_message.subject')}
|
||||||
@@ -175,28 +223,30 @@ const NewMessage: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4'>
|
||||||
|
|
||||||
<div>
|
|
||||||
<EmailEditor
|
<EmailEditor
|
||||||
value={content}
|
value={content}
|
||||||
onChange={setContent}
|
onChange={setContent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EmailAttachments
|
<div className='mt-4'>
|
||||||
attachments={attachments}
|
<EmailAttachments
|
||||||
onAttachmentsChange={setAttachments}
|
attachments={attachments}
|
||||||
/>
|
onAttachmentsChange={setAttachments}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<EmailFormActions
|
<div className='mt-4'>
|
||||||
priority={priority}
|
<EmailFormActions
|
||||||
onPriorityChange={setPriority}
|
priority={priority}
|
||||||
onSend={handleSendWithClose}
|
onPriorityChange={setPriority}
|
||||||
onSaveDraft={handleSaveDraftWithClose}
|
onSend={handleSendWithClose}
|
||||||
isSending={isSending}
|
onSaveDraft={handleSaveDraftWithClose}
|
||||||
isSavingDraft={isSavingDraft}
|
isSending={isSending}
|
||||||
/>
|
isSavingDraft={isSavingDraft}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AvatarImage from '@/assets/images/avatar.svg'
|
|||||||
import AnswerIcon from '@/assets/images/answer.svg'
|
import AnswerIcon from '@/assets/images/answer.svg'
|
||||||
import EmailEditor from './EmailEditor'
|
import EmailEditor from './EmailEditor'
|
||||||
import EmailFormActions from './EmailFormActions'
|
import EmailFormActions from './EmailFormActions'
|
||||||
|
import EmailAttachments from './EmailAttachments'
|
||||||
import { useReply } from './hooks/useReply'
|
import { useReply } from './hooks/useReply'
|
||||||
import { MessageDetail } from '@/pages/received/types/Types'
|
import { MessageDetail } from '@/pages/received/types/Types'
|
||||||
import { toast } from '@/components/Toast'
|
import { toast } from '@/components/Toast'
|
||||||
@@ -30,7 +31,9 @@ const Reply: FC<ReplyProps> = ({
|
|||||||
content,
|
content,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isSending,
|
isSending,
|
||||||
|
attachments,
|
||||||
setContent,
|
setContent,
|
||||||
|
setAttachments,
|
||||||
expandReply,
|
expandReply,
|
||||||
collapseReply,
|
collapseReply,
|
||||||
handleSendReply,
|
handleSendReply,
|
||||||
@@ -137,6 +140,13 @@ const Reply: FC<ReplyProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-4'>
|
||||||
|
<EmailAttachments
|
||||||
|
attachments={attachments}
|
||||||
|
onAttachmentsChange={setAttachments}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<EmailFormActions
|
<EmailFormActions
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export const useNewMessage = () => {
|
|||||||
const { email: userEmail } = useAuthStore();
|
const { email: userEmail } = useAuthStore();
|
||||||
|
|
||||||
const [toEmails, setToEmails] = useState<string[]>([]);
|
const [toEmails, setToEmails] = useState<string[]>([]);
|
||||||
|
const [ccEmails, setCcEmails] = useState<string[]>([]);
|
||||||
|
const [bccEmails, setBccEmails] = useState<string[]>([]);
|
||||||
const [subject, setSubject] = useState("");
|
const [subject, setSubject] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [priority, setPriority] = useState("");
|
const [priority, setPriority] = useState("");
|
||||||
@@ -46,8 +48,23 @@ export const useNewMessage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (draftDetail) {
|
if (draftDetail) {
|
||||||
const toEmailsList =
|
const toEmailsList =
|
||||||
draftDetail.to?.map((recipient) => recipient.address) || [];
|
draftDetail.to?.map(
|
||||||
|
(recipient: { address: string; name: string }) => recipient.address
|
||||||
|
) || [];
|
||||||
setToEmails(toEmailsList);
|
setToEmails(toEmailsList);
|
||||||
|
|
||||||
|
const ccEmailsList =
|
||||||
|
draftDetail.cc?.map(
|
||||||
|
(recipient: { address: string; name: string }) => recipient.address
|
||||||
|
) || [];
|
||||||
|
setCcEmails(ccEmailsList);
|
||||||
|
|
||||||
|
const bccEmailsList =
|
||||||
|
draftDetail.bcc?.map(
|
||||||
|
(recipient: { address: string; name: string }) => recipient.address
|
||||||
|
) || [];
|
||||||
|
setBccEmails(bccEmailsList);
|
||||||
|
|
||||||
setSubject(draftDetail.subject || "");
|
setSubject(draftDetail.subject || "");
|
||||||
|
|
||||||
const draftWithContent = (draftDetail as unknown) as {
|
const draftWithContent = (draftDetail as unknown) as {
|
||||||
@@ -96,6 +113,40 @@ export const useNewMessage = () => {
|
|||||||
setToEmails(toEmails.filter((email) => email !== emailToRemove));
|
setToEmails(toEmails.filter((email) => email !== emailToRemove));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addCcEmail = (email: string) => {
|
||||||
|
if (!isValidEmail(email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ccEmails.includes(email)) {
|
||||||
|
toast("این ایمیل قبلاً اضافه شده است", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCcEmails([...ccEmails, email]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCcEmail = (emailToRemove: string) => {
|
||||||
|
setCcEmails(ccEmails.filter((email) => email !== emailToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBccEmail = (email: string) => {
|
||||||
|
if (!isValidEmail(email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bccEmails.includes(email)) {
|
||||||
|
toast("این ایمیل قبلاً اضافه شده است", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBccEmails([...bccEmails, email]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBccEmail = (emailToRemove: string) => {
|
||||||
|
setBccEmails(bccEmails.filter((email) => email !== emailToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
const parseEmailAddresses = (emails: string[]) => {
|
const parseEmailAddresses = (emails: string[]) => {
|
||||||
return emails
|
return emails
|
||||||
.filter((email) => email.length > 0)
|
.filter((email) => email.length > 0)
|
||||||
@@ -104,6 +155,8 @@ export const useNewMessage = () => {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setToEmails([]);
|
setToEmails([]);
|
||||||
|
setCcEmails([]);
|
||||||
|
setBccEmails([]);
|
||||||
setSubject("");
|
setSubject("");
|
||||||
setContent("");
|
setContent("");
|
||||||
setPriority("");
|
setPriority("");
|
||||||
@@ -141,6 +194,8 @@ export const useNewMessage = () => {
|
|||||||
const textDirection = detectTextDirection(contentString);
|
const textDirection = detectTextDirection(contentString);
|
||||||
const draftUpdateData = {
|
const draftUpdateData = {
|
||||||
to: parseEmailAddresses(toEmails),
|
to: parseEmailAddresses(toEmails),
|
||||||
|
...(ccEmails.length > 0 && { cc: parseEmailAddresses(ccEmails) }),
|
||||||
|
...(bccEmails.length > 0 && { bcc: parseEmailAddresses(bccEmails) }),
|
||||||
subject,
|
subject,
|
||||||
html: `<div dir="${textDirection}" style="text-align: start; unicode-bidi: plaintext; font-family: 'Irancell', 'Vazirmatn', sans-serif;">${contentString}</div>`,
|
html: `<div dir="${textDirection}" style="text-align: start; unicode-bidi: plaintext; font-family: 'Irancell', 'Vazirmatn', sans-serif;">${contentString}</div>`,
|
||||||
text: contentString.replace(/<[^>]*>/g, ""),
|
text: contentString.replace(/<[^>]*>/g, ""),
|
||||||
@@ -193,6 +248,8 @@ export const useNewMessage = () => {
|
|||||||
const emailData: SendEmailDto = {
|
const emailData: SendEmailDto = {
|
||||||
from: { address: userEmail || "sender@example.com" },
|
from: { address: userEmail || "sender@example.com" },
|
||||||
to: parseEmailAddresses(toEmails),
|
to: parseEmailAddresses(toEmails),
|
||||||
|
...(ccEmails.length > 0 && { cc: parseEmailAddresses(ccEmails) }),
|
||||||
|
...(bccEmails.length > 0 && { bcc: parseEmailAddresses(bccEmails) }),
|
||||||
subject,
|
subject,
|
||||||
html: `<div dir="${textDirection}" style="text-align: start; unicode-bidi: plaintext; font-family: 'Irancell', 'Vazirmatn', sans-serif;">${contentString}</div>`,
|
html: `<div dir="${textDirection}" style="text-align: start; unicode-bidi: plaintext; font-family: 'Irancell', 'Vazirmatn', sans-serif;">${contentString}</div>`,
|
||||||
text: contentString.replace(/<[^>]*>/g, ""),
|
text: contentString.replace(/<[^>]*>/g, ""),
|
||||||
@@ -231,6 +288,8 @@ export const useNewMessage = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
toEmails,
|
toEmails,
|
||||||
|
ccEmails,
|
||||||
|
bccEmails,
|
||||||
subject,
|
subject,
|
||||||
content,
|
content,
|
||||||
priority,
|
priority,
|
||||||
@@ -242,6 +301,10 @@ export const useNewMessage = () => {
|
|||||||
|
|
||||||
addEmail,
|
addEmail,
|
||||||
removeEmail,
|
removeEmail,
|
||||||
|
addCcEmail,
|
||||||
|
removeCcEmail,
|
||||||
|
addBccEmail,
|
||||||
|
removeBccEmail,
|
||||||
setSubject,
|
setSubject,
|
||||||
setContent,
|
setContent,
|
||||||
setPriority,
|
setPriority,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAuthStore } from "@/pages/auth/store/AuthStore";
|
import { useAuthStore } from "@/pages/auth/store/AuthStore";
|
||||||
import { useSendEmail } from "@/pages/received/hooks/useEmailData";
|
import { useSendEmail } from "@/pages/received/hooks/useEmailData";
|
||||||
import { SendEmailDto } from "@/pages/received/types/Types";
|
import { SendEmailDto, EmailAttachmentDto } from "@/pages/received/types/Types";
|
||||||
import { MessageDetail } from "@/pages/received/types/Types";
|
import { MessageDetail } from "@/pages/received/types/Types";
|
||||||
import { toast } from "@/components/Toast";
|
import { toast } from "@/components/Toast";
|
||||||
|
|
||||||
@@ -19,12 +19,14 @@ export const useReply = ({
|
|||||||
const { email: userEmail } = useAuthStore();
|
const { email: userEmail } = useAuthStore();
|
||||||
const [content, setContent] = useState<string>("");
|
const [content, setContent] = useState<string>("");
|
||||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||||
|
const [attachments, setAttachments] = useState<EmailAttachmentDto[]>([]);
|
||||||
|
|
||||||
const sendEmailMutation = useSendEmail();
|
const sendEmailMutation = useSendEmail();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setContent("");
|
setContent("");
|
||||||
setIsExpanded(false);
|
setIsExpanded(false);
|
||||||
|
setAttachments([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendReply = async (html: string) => {
|
const handleSendReply = async (html: string) => {
|
||||||
@@ -97,6 +99,7 @@ ${
|
|||||||
originalMessage.html?.join("").replace(/<[^>]*>/g, "") ||
|
originalMessage.html?.join("").replace(/<[^>]*>/g, "") ||
|
||||||
""
|
""
|
||||||
}`,
|
}`,
|
||||||
|
attachments: attachments.length > 0 ? attachments : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -144,8 +147,10 @@ ${
|
|||||||
content,
|
content,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isSending: sendEmailMutation.isPending,
|
isSending: sendEmailMutation.isPending,
|
||||||
|
attachments,
|
||||||
|
|
||||||
setContent,
|
setContent,
|
||||||
|
setAttachments,
|
||||||
expandReply,
|
expandReply,
|
||||||
collapseReply,
|
collapseReply,
|
||||||
handleSendReply,
|
handleSendReply,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useGetFavoriteMessages } from './hooks/useFavoriteData';
|
import { useGetFavoriteMessages } from './hooks/useFavoriteData';
|
||||||
import { FavoriteMessage } from './types/FavoriteTypes';
|
import { FavoriteMessage } from './types/FavoriteTypes';
|
||||||
import { useEmailActions } from '@/hooks/useEmailActions';
|
import { useEmailActions } from '@/hooks/useEmailActions';
|
||||||
import Favorite from '../received/Components/Favorite';
|
import Favorite from '../received/components/Favorite';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getIconColor } from '@/utils/colorUtils';
|
import { getIconColor } from '@/utils/colorUtils';
|
||||||
|
|||||||
@@ -4,25 +4,28 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import AvatarImage from '@/assets/images/avatar.svg'
|
import AvatarImage from '@/assets/images/avatar.svg'
|
||||||
import { DocumentDownload } from 'iconsax-react'
|
import { DocumentDownload } from 'iconsax-react'
|
||||||
import Header from './Components/Header'
|
import Header from './components/Header'
|
||||||
import { useGetMessageDetail, useDownloadAttachment, useSendEmail } from './hooks/useEmailData'
|
import { useGetMessageDetail, useDownloadAttachment, useSendEmail } from './hooks/useEmailData'
|
||||||
import { toast } from '@/components/Toast'
|
import { toast } from '@/components/Toast'
|
||||||
import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func'
|
import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func'
|
||||||
import SkeletonDetail from './Components/SkeletonDetail'
|
import SkeletonDetail from './components/SkeletonDetail'
|
||||||
import { useEmailActions } from '@/hooks/useEmailActions'
|
import { useEmailActions } from '@/hooks/useEmailActions'
|
||||||
import { MailboxEnum } from './enum/Enum'
|
import { MailboxEnum } from './enum/Enum'
|
||||||
import Reply from '@/components/newMessage/Reply'
|
import Reply from '@/components/newMessage/Reply'
|
||||||
import Forward from '@/components/newMessage/Forward'
|
import Forward from '@/components/newMessage/Forward'
|
||||||
import EmojiReaction from '@/components/EmojiReaction'
|
import EmojiReaction from '@/components/EmojiReaction'
|
||||||
import { useAuthStore } from '@/pages/auth/store/AuthStore'
|
|
||||||
import { SendEmailDto } from './types/Types'
|
import { SendEmailDto } from './types/Types'
|
||||||
import { getIconColor } from '@/utils/colorUtils'
|
import { getIconColor } from '@/utils/colorUtils'
|
||||||
|
import CcRecipients from './components/CcRecipients'
|
||||||
|
import BccRecipients from './components/BccRecipients'
|
||||||
|
import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
|
||||||
|
|
||||||
const DetailEmail: FC = () => {
|
const DetailEmail: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const emailActions = useEmailActions()
|
const emailActions = useEmailActions()
|
||||||
const { email: userEmail } = useAuthStore()
|
const { data: profileData } = useGetProfile()
|
||||||
|
const userEmail = profileData?.data?.user?.emailAddress || ''
|
||||||
const { id, mailbox } = useParams<{ id: string, mailbox: string }>()
|
const { id, mailbox } = useParams<{ id: string, mailbox: string }>()
|
||||||
const [activeAction, setActiveAction] = useState<'reply' | 'forward' | null>(null)
|
const [activeAction, setActiveAction] = useState<'reply' | 'forward' | null>(null)
|
||||||
|
|
||||||
@@ -179,18 +182,25 @@ const DetailEmail: FC = () => {
|
|||||||
<img src={AvatarImage} className='size-10' />
|
<img src={AvatarImage} className='size-10' />
|
||||||
{
|
{
|
||||||
messageDetail.mailboxName === MailboxEnum.SENT ?
|
messageDetail.mailboxName === MailboxEnum.SENT ?
|
||||||
<div>
|
<div className='flex-1'>
|
||||||
<div className='text-sm'>{messageDetail.to[0].name || messageDetail.to[0].address}</div>
|
<div className='text-sm'>{messageDetail.to[0].name || messageDetail.to[0].address}</div>
|
||||||
<div className='text-xs text-description mt-0.5'>{messageDetail.to[0]?.address}</div>
|
<div className='text-xs text-description mt-0.5'>{messageDetail.to[0]?.address}</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div>
|
<div className='flex-1'>
|
||||||
<div className='text-sm'>{messageDetail.from.name || messageDetail.from.address}</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 className='text-xs text-description mt-0.5'>{messageDetail.from?.address}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CcRecipients recipients={messageDetail.cc || []} />
|
||||||
|
|
||||||
|
<BccRecipients
|
||||||
|
messageDetail={messageDetail}
|
||||||
|
userEmail={userEmail}
|
||||||
|
mailboxName={messageDetail.mailboxName as MailboxEnum}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='xl:mt-8 mt-3 text-[13px] leading-7 font-light'>
|
<div className='xl:mt-8 mt-3 text-[13px] leading-7 font-light'>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import { InboxMessage } from './types/Types';
|
|||||||
|
|
||||||
import MarkAsRead from '@/assets/images/mark_as_read.svg'
|
import MarkAsRead from '@/assets/images/mark_as_read.svg'
|
||||||
import { useEmailActions } from '@/hooks/useEmailActions';
|
import { useEmailActions } from '@/hooks/useEmailActions';
|
||||||
import Favorite from './Components/Favorite';
|
import Favorite from './components/Favorite';
|
||||||
import Summerize from './Components/Summerize';
|
import Summerize from './components/Summerize';
|
||||||
import Intelligense from '@/assets/images/intelligense.svg'
|
import Intelligense from '@/assets/images/intelligense.svg'
|
||||||
import SummarizeModal from './Components/SummarizeModal';
|
import SummarizeModal from './components/SummarizeModal';
|
||||||
import ModalConfrim from '@/components/ModalConfrim';
|
import ModalConfrim from '@/components/ModalConfrim';
|
||||||
import { ErrorType } from '@/helpers/types';
|
import { ErrorType } from '@/helpers/types';
|
||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { MessageDetail } from '../types/Types'
|
||||||
|
import { MailboxEnum } from '../enum/Enum'
|
||||||
|
|
||||||
|
interface BccRecipientsProps {
|
||||||
|
messageDetail: MessageDetail
|
||||||
|
userEmail: string | null
|
||||||
|
mailboxName: MailboxEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
const BccRecipients: FC<BccRecipientsProps> = ({ messageDetail, userEmail, mailboxName }) => {
|
||||||
|
// If userEmail is not available, we can't determine BCC status
|
||||||
|
if (!userEmail || userEmail.trim() === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current user was BCC recipient (for received messages)
|
||||||
|
const isInTo = messageDetail.to?.some(recipient =>
|
||||||
|
recipient.address.toLowerCase().trim() === userEmail.toLowerCase().trim()
|
||||||
|
) || false
|
||||||
|
const isInCc = messageDetail.cc?.some(recipient =>
|
||||||
|
recipient.address.toLowerCase().trim() === userEmail.toLowerCase().trim()
|
||||||
|
) || false
|
||||||
|
|
||||||
|
// Check if user is explicitly in BCC field OR implicitly (not in to/cc but received)
|
||||||
|
const isExplicitlyInBcc = messageDetail.bcc?.some(recipient =>
|
||||||
|
recipient.address.toLowerCase().trim() === userEmail.toLowerCase().trim()
|
||||||
|
) || false
|
||||||
|
const isInBcc = isExplicitlyInBcc || (!isInTo && !isInCc && mailboxName !== MailboxEnum.SENT)
|
||||||
|
|
||||||
|
// Show BCC notice for received messages
|
||||||
|
if (isInBcc) {
|
||||||
|
return (
|
||||||
|
<div className='mt-4 mr-14'>
|
||||||
|
<div className='bg-accent border border-border rounded-lg px-4 py-2 text-xs text-muted-foreground'>
|
||||||
|
شما به عنوان رونوشت مخفی (BCC) این پیام را دریافت کردهاید
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show BCC recipients for sent messages
|
||||||
|
if (mailboxName === MailboxEnum.SENT && messageDetail.envelope?.rcpt) {
|
||||||
|
const toAddresses = messageDetail.to?.map(r => r.address.toLowerCase()) || []
|
||||||
|
const ccAddresses = messageDetail.cc?.map(r => r.address.toLowerCase()) || []
|
||||||
|
|
||||||
|
const bccRecipients = messageDetail.envelope.rcpt.filter(rcpt => {
|
||||||
|
const address = rcpt.value.toLowerCase()
|
||||||
|
return !toAddresses.includes(address) && !ccAddresses.includes(address)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (bccRecipients.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4 mr-14'>
|
||||||
|
<div className='text-xs text-description mb-2'>رونوشت مخفی (BCC):</div>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{bccRecipients.map((recipient, index) => (
|
||||||
|
<div key={index} className='bg-secondary rounded-full px-3 py-1 text-xs'>
|
||||||
|
{recipient.formatted || recipient.value}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BccRecipients
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { type EmailRecipientDto } from '../types/Types'
|
||||||
|
|
||||||
|
interface CcRecipientsProps {
|
||||||
|
recipients: EmailRecipientDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const CcRecipients: FC<CcRecipientsProps> = ({ recipients }) => {
|
||||||
|
if (!recipients || recipients.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4 mr-14'>
|
||||||
|
<div className='text-xs text-description mb-2'>رونوشت (CC):</div>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{recipients.map((recipient, index) => (
|
||||||
|
<div key={index} className='bg-secondary rounded-full px-3 py-1 text-xs'>
|
||||||
|
{recipient.name || recipient.address} {recipient.name && `<${recipient.address}>`}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CcRecipients
|
||||||
|
|
||||||
@@ -198,6 +198,14 @@ export interface MessageDetailResponse {
|
|||||||
address: string;
|
address: string;
|
||||||
name: string;
|
name: string;
|
||||||
}>;
|
}>;
|
||||||
|
cc?: Array<{
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
bcc?: Array<{
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
subject: string;
|
subject: string;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
date: string;
|
date: string;
|
||||||
@@ -214,7 +222,7 @@ export interface MessageDetailResponse {
|
|||||||
attachments: EmailAttachmentDto[];
|
attachments: EmailAttachmentDto[];
|
||||||
references: string[];
|
references: string[];
|
||||||
metaData: Record<string, unknown>;
|
metaData: Record<string, unknown>;
|
||||||
verificationResults: {
|
verificationResults?: {
|
||||||
tls: {
|
tls: {
|
||||||
name: string;
|
name: string;
|
||||||
standardName: string;
|
standardName: string;
|
||||||
@@ -258,6 +266,14 @@ export interface MessageDetail {
|
|||||||
address: string;
|
address: string;
|
||||||
name: string;
|
name: string;
|
||||||
}>;
|
}>;
|
||||||
|
cc?: Array<{
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
bcc?: Array<{
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
subject: string;
|
subject: string;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
date: string;
|
date: string;
|
||||||
@@ -274,7 +290,7 @@ export interface MessageDetail {
|
|||||||
attachments: EmailAttachmentDto[];
|
attachments: EmailAttachmentDto[];
|
||||||
references: string[];
|
references: string[];
|
||||||
metaData: Record<string, unknown>;
|
metaData: Record<string, unknown>;
|
||||||
verificationResults: {
|
verificationResults?: {
|
||||||
tls: {
|
tls: {
|
||||||
name: string;
|
name: string;
|
||||||
standardName: string;
|
standardName: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user