feat: Add emoji reaction feature and improve reply formatting

- Add emoji-picker-react package
- Create EmojiReaction component for quick emoji responses
- Replace EmojiHappy icon with interactive EmojiReaction component
- Implement emoji selection as structured reply messages
- Improve reply HTML formatting with better styling and Persian date
- Add contextual information to both emoji reactions and text replies
- Update success messages to be more concise
This commit is contained in:
hamid zarghami
2025-07-22 13:15:42 +03:30
parent f844ff53d4
commit 013d4238c9
5 changed files with 176 additions and 10 deletions
+1
View File
@@ -22,6 +22,7 @@
"axios": "^1.10.0", "axios": "^1.10.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"emoji-picker-react": "^4.13.2",
"formik": "^2.4.6", "formik": "^2.4.6",
"i18next": "^25.0.1", "i18next": "^25.0.1",
"iconsax-react": "^0.0.8", "iconsax-react": "^0.0.8",
+19
View File
@@ -41,6 +41,9 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
emoji-picker-react:
specifier: ^4.13.2
version: 4.13.2(react@19.1.0)
formik: formik:
specifier: ^2.4.6 specifier: ^2.4.6
version: 2.4.6(react@19.1.0) version: 2.4.6(react@19.1.0)
@@ -1579,6 +1582,12 @@ packages:
electron-to-chromium@1.5.158: electron-to-chromium@1.5.158:
resolution: {integrity: sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ==} 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: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1776,6 +1785,9 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
flairup@1.0.0:
resolution: {integrity: sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==}
flat-cache@4.0.1: flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -4817,6 +4829,11 @@ snapshots:
electron-to-chromium@1.5.158: {} 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: {} emoji-regex@8.0.0: {}
engine.io-client@6.6.3: engine.io-client@6.6.3:
@@ -5118,6 +5135,8 @@ snapshots:
locate-path: 6.0.0 locate-path: 6.0.0
path-exists: 4.0.0 path-exists: 4.0.0
flairup@1.0.0: {}
flat-cache@4.0.1: flat-cache@4.0.1:
dependencies: dependencies:
flatted: 3.3.3 flatted: 3.3.3
+61
View File
@@ -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<EmojiReactionProps> = ({ 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 (
<div ref={containerRef} className={`relative ${className}`}>
<EmojiHappy
size={24}
color='black'
className='cursor-pointer hover:opacity-70 transition-opacity'
onClick={togglePicker}
/>
{isOpen && (
<div className='absolute bottom-10 right-0 z-50'>
<div className='bg-white rounded-lg shadow-lg border border-gray-200'>
<EmojiPicker
onEmojiClick={handleEmojiClick}
width={300}
height={400}
searchDisabled
skinTonesDisabled
previewConfig={{
defaultCaption: 'یک ایموجی انتخاب کنید',
defaultEmoji: '1f60a'
}}
/>
</div>
</div>
)}
</div>
)
}
export default EmojiReaction
+32 -7
View File
@@ -50,12 +50,25 @@ export const useReply = ({
minute: "2-digit", minute: "2-digit",
}).format(originalDate); }).format(originalDate);
const htmlContent = const htmlContent = `
content + <div dir="rtl" style="font-family: 'Irancell', Arial, sans-serif; line-height: 1.6;">
"<br /><br />" + <div style="margin-bottom: 10px; padding: 15px;">
`<div dir="rtl">در تاریخ ${persianDate} ${originalMessage.from.name} <${originalMessage.from.address}> نوشت:</div>` + ${content}
"<br /><br />" + </div>
`<div dir="rtl">${html}</div>`;
<div style="border-top: 2px solid #e9ecef; padding-top: 10px;">
<div style=" font-size: 12px; margin-bottom: 10px; padding: 8px; border-radius: 4px;">
در پاسخ به پیام ارسالی در تاریخ ${persianDate}
<br />
از: ${originalMessage.from.name} &lt;${originalMessage.from.address}&gt;
</div>
<div >
${html}
</div>
</div>
</div>
`;
const replyData: SendEmailDto = { const replyData: SendEmailDto = {
from: { address: userEmail || "sender@example.com" }, from: { address: userEmail || "sender@example.com" },
@@ -73,7 +86,19 @@ export const useReply = ({
? originalMessage.subject ? originalMessage.subject
: `Re: ${originalMessage.subject}`, : `Re: ${originalMessage.subject}`,
html: htmlContent, 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 { try {
+63 -3
View File
@@ -3,9 +3,9 @@ import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' 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, EmojiHappy } from 'iconsax-react' import { DocumentDownload } from 'iconsax-react'
import Header from './Components/Header' import Header from './Components/Header'
import { useGetMessageDetail, useDownloadAttachment } 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'
@@ -13,17 +13,22 @@ 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 { useAuthStore } from '@/pages/auth/store/AuthStore'
import { SendEmailDto } from './types/Types'
const DetailEmail: FC = () => { const DetailEmail: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const emailActions = useEmailActions() const emailActions = useEmailActions()
const { email: userEmail } = useAuthStore()
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)
// API hooks // API hooks
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '') const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '')
const downloadAttachmentMutation = useDownloadAttachment() const downloadAttachmentMutation = useDownloadAttachment()
const sendEmailMutation = useSendEmail()
const handleDownloadAttachment = async (attachmentId: string, filename: string) => { const handleDownloadAttachment = async (attachmentId: string, filename: string) => {
if (!id) { 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 = `
<div style="font-size: 24px; text-align: center; margin: 20px 0;">
${emoji}
</div>
<br />
<div dir="rtl" style="color: #666; font-size: 12px; border-top: 1px solid #ddd; padding-top: 10px;">
در پاسخ به پیام ${persianDate} از ${messageDetail.from.name} &lt;${messageDetail.from.address}&gt;
</div>
`
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 // Mark message as seen when it's loaded and not already seen
useEffect(() => { useEffect(() => {
if (messageDetail && !messageDetail.seen && id) { if (messageDetail && !messageDetail.seen && id) {
@@ -163,7 +223,7 @@ const DetailEmail: FC = () => {
{activeAction === null && ( {activeAction === null && (
<div className='flex mt-9 justify-end'> <div className='flex mt-9 justify-end'>
<div className='flex gap-5 text-sm'> <div className='flex gap-5 text-sm'>
<EmojiHappy size={24} color='black' /> <EmojiReaction onEmojiSelect={handleEmojiSelect} />
<Reply <Reply
originalMessage={messageDetail} originalMessage={messageDetail}