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:
@@ -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",
|
||||
|
||||
Generated
+19
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -50,12 +50,25 @@ export const useReply = ({
|
||||
minute: "2-digit",
|
||||
}).format(originalDate);
|
||||
|
||||
const htmlContent =
|
||||
content +
|
||||
"<br /><br />" +
|
||||
`<div dir="rtl">در تاریخ ${persianDate} ${originalMessage.from.name} <${originalMessage.from.address}> نوشت:</div>` +
|
||||
"<br /><br />" +
|
||||
`<div dir="rtl">${html}</div>`;
|
||||
const htmlContent = `
|
||||
<div dir="rtl" style="font-family: 'Irancell', Arial, sans-serif; line-height: 1.6;">
|
||||
<div style="margin-bottom: 10px; padding: 15px;">
|
||||
${content}
|
||||
</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} <${originalMessage.from.address}>
|
||||
</div>
|
||||
|
||||
<div >
|
||||
${html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 = `
|
||||
<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} <${messageDetail.from.address}>
|
||||
</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
|
||||
useEffect(() => {
|
||||
if (messageDetail && !messageDetail.seen && id) {
|
||||
@@ -163,7 +223,7 @@ const DetailEmail: FC = () => {
|
||||
{activeAction === null && (
|
||||
<div className='flex mt-9 justify-end'>
|
||||
<div className='flex gap-5 text-sm'>
|
||||
<EmojiHappy size={24} color='black' />
|
||||
<EmojiReaction onEmojiSelect={handleEmojiSelect} />
|
||||
|
||||
<Reply
|
||||
originalMessage={messageDetail}
|
||||
|
||||
Reference in New Issue
Block a user