diff --git a/package.json b/package.json index 2668b14..ea107b8 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "emoji-picker-react": "^4.13.2", "formik": "^2.4.6", "i18next": "^25.0.1", "iconsax-react": "^0.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6f5635..2db1eb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + emoji-picker-react: + specifier: ^4.13.2 + version: 4.13.2(react@19.1.0) formik: specifier: ^2.4.6 version: 2.4.6(react@19.1.0) @@ -1579,6 +1582,12 @@ packages: electron-to-chromium@1.5.158: resolution: {integrity: sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ==} + emoji-picker-react@4.13.2: + resolution: {integrity: sha512-azaJQLTshEOZVhksgU136izJWJyZ4Clx6xQ6Vctzk1gOdPPAUbTa/JYDwZJ8rh97QxnjpyeftXl99eRlYr3vNA==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1776,6 +1785,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + flairup@1.0.0: + resolution: {integrity: sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -4817,6 +4829,11 @@ snapshots: electron-to-chromium@1.5.158: {} + emoji-picker-react@4.13.2(react@19.1.0): + dependencies: + flairup: 1.0.0 + react: 19.1.0 + emoji-regex@8.0.0: {} engine.io-client@6.6.3: @@ -5118,6 +5135,8 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + flairup@1.0.0: {} + flat-cache@4.0.1: dependencies: flatted: 3.3.3 diff --git a/src/components/EmojiReaction.tsx b/src/components/EmojiReaction.tsx new file mode 100644 index 0000000..4503ed2 --- /dev/null +++ b/src/components/EmojiReaction.tsx @@ -0,0 +1,61 @@ +import { FC, useState } from 'react' +import EmojiPicker, { EmojiClickData } from 'emoji-picker-react' +import { EmojiHappy } from 'iconsax-react' +import { useOutsideClick } from '@/hooks/useOutSideClick' + +interface EmojiReactionProps { + onEmojiSelect: (emoji: string) => void + className?: string +} + +const EmojiReaction: FC = ({ onEmojiSelect, className = '' }) => { + const [isOpen, setIsOpen] = useState(false) + + // استفاده از hook موجود برای تشخیص کلیک خارج از المان + const containerRef = useOutsideClick(() => { + if (isOpen) { + setIsOpen(false) + } + }) + + const handleEmojiClick = (emojiData: EmojiClickData) => { + onEmojiSelect(emojiData.emoji) + setIsOpen(false) + } + + const togglePicker = () => { + setIsOpen(!isOpen) + } + + return ( +
+ + + {isOpen && ( +
+
+ +
+
+ )} +
+ ) +} + +export default EmojiReaction \ No newline at end of file diff --git a/src/components/newMessage/hooks/useReply.ts b/src/components/newMessage/hooks/useReply.ts index 60620b3..a25db4b 100644 --- a/src/components/newMessage/hooks/useReply.ts +++ b/src/components/newMessage/hooks/useReply.ts @@ -50,12 +50,25 @@ export const useReply = ({ minute: "2-digit", }).format(originalDate); - const htmlContent = - content + - "

" + - `
در تاریخ ${persianDate} ${originalMessage.from.name} <${originalMessage.from.address}> نوشت:
` + - "

" + - `
${html}
`; + const htmlContent = ` +
+
+ ${content} +
+ +
+
+ در پاسخ به پیام ارسالی در تاریخ ${persianDate} +
+ از: ${originalMessage.from.name} <${originalMessage.from.address}> +
+ +
+ ${html} +
+
+
+ `; const replyData: SendEmailDto = { from: { address: userEmail || "sender@example.com" }, @@ -73,7 +86,19 @@ export const useReply = ({ ? originalMessage.subject : `Re: ${originalMessage.subject}`, html: htmlContent, - text: content.replace(/<[^>]*>/g, ""), + text: `${content.replace(/<[^>]*>/g, "")} + +═══════════════════════════════════════════════════════ + +📩 در پاسخ به پیام ارسالی در تاریخ ${persianDate} +📧 از: ${originalMessage.from.name} <${originalMessage.from.address}> + +متن پیام اصلی: +${ + originalMessage.text || + originalMessage.html?.join("").replace(/<[^>]*>/g, "") || + "" +}`, }; try { diff --git a/src/pages/received/Detail.tsx b/src/pages/received/Detail.tsx index 93cd6a5..a7aa514 100644 --- a/src/pages/received/Detail.tsx +++ b/src/pages/received/Detail.tsx @@ -3,9 +3,9 @@ import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' import AvatarImage from '@/assets/images/avatar.svg' -import { DocumentDownload, EmojiHappy } from 'iconsax-react' +import { DocumentDownload } from 'iconsax-react' import Header from './Components/Header' -import { useGetMessageDetail, useDownloadAttachment } from './hooks/useEmailData' +import { useGetMessageDetail, useDownloadAttachment, useSendEmail } from './hooks/useEmailData' import { toast } from '@/components/Toast' import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func' import SkeletonDetail from './Components/SkeletonDetail' @@ -13,17 +13,22 @@ import { useEmailActions } from '@/hooks/useEmailActions' import { MailboxEnum } from './enum/Enum' import Reply from '@/components/newMessage/Reply' import Forward from '@/components/newMessage/Forward' +import EmojiReaction from '@/components/EmojiReaction' +import { useAuthStore } from '@/pages/auth/store/AuthStore' +import { SendEmailDto } from './types/Types' const DetailEmail: FC = () => { const { t } = useTranslation() const emailActions = useEmailActions() + const { email: userEmail } = useAuthStore() const { id, mailbox } = useParams<{ id: string, mailbox: string }>() const [activeAction, setActiveAction] = useState<'reply' | 'forward' | null>(null) // API hooks const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '') const downloadAttachmentMutation = useDownloadAttachment() + const sendEmailMutation = useSendEmail() const handleDownloadAttachment = async (attachmentId: string, filename: string) => { if (!id) { @@ -55,6 +60,61 @@ const DetailEmail: FC = () => { } } + const handleEmojiSelect = async (emoji: string) => { + if (!messageDetail) { + toast('اطلاعات پیام یافت نشد', 'error') + return + } + + // Format date to Persian for reply context + const originalDate = new Date(messageDetail.date) + const persianDate = new Intl.DateTimeFormat('fa-IR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(originalDate) + + const reactionHtml = ` +
+ ${emoji} +
+
+
+ در پاسخ به پیام ${persianDate} از ${messageDetail.from.name} <${messageDetail.from.address}> +
+ ` + + const replyData: SendEmailDto = { + from: { address: userEmail || 'sender@example.com' }, + to: [ + { + address: messageDetail.from.address, + name: messageDetail.from.name, + }, + ], + replyTo: { + name: messageDetail.from.name, + address: messageDetail.from.address, + }, + subject: messageDetail.subject.startsWith('Re:') + ? messageDetail.subject + : `Re: ${messageDetail.subject}`, + html: reactionHtml, + text: `واکنش: ${emoji}\n\nدر پاسخ به پیام ${persianDate} از ${messageDetail.from.name}`, + } + + try { + await sendEmailMutation.mutateAsync(replyData) + toast('واکنش نشان داد', 'success') + } catch (error) { + console.error('Send error:', error) + toast('خطا در نشان دادن واکنش', 'error') + } + } + // Mark message as seen when it's loaded and not already seen useEffect(() => { if (messageDetail && !messageDetail.seen && id) { @@ -163,7 +223,7 @@ const DetailEmail: FC = () => { {activeAction === null && (
- +