fix bug message
This commit is contained in:
@@ -31,7 +31,7 @@ const ShopInformation = ({ product }: ShopInformationProps) => {
|
|||||||
|
|
||||||
const handleChatWithSeller = () => {
|
const handleChatWithSeller = () => {
|
||||||
// Navigate to chat page with product and shop context
|
// Navigate to chat page with product and shop context
|
||||||
router.push(`/profile/chat?productId=${product._id}&shopId=${shop._id}`)
|
router.push(`/profile/chat?productId=${product._id}&shopId=${shop.owner}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState, useRef, useEffect, useMemo } from 'react'
|
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Trash, More, EmojiHappy, AttachCircle, Send, Shop, Call, VideoPlay } from 'iconsax-react'
|
import { Send, Shop } from 'iconsax-react'
|
||||||
import { useSocket } from '../hooks/useSocket'
|
import { useSocket } from '../hooks/useSocket'
|
||||||
import { Message as SocketMessage } from '../service/SocketService'
|
import { Message as SocketMessage } from '../service/SocketService'
|
||||||
import { useChatDetail, useMarkChatAsRead } from '../../hooks/useChatData'
|
import { useChatDetail, useMarkChatAsRead } from '../../hooks/useChatData'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { isAuthenticated } from '@/config/func'
|
import { isAuthenticated } from '@/config/func'
|
||||||
import { ChatMessage as APIMessage } from '../../types/ProfileTypes'
|
import { ChatMessage as APIMessage } from '../../types/ProfileTypes'
|
||||||
|
|
||||||
@@ -17,17 +18,16 @@ interface Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ChatMessagesProps {
|
interface ChatMessagesProps {
|
||||||
productId?: string | null
|
|
||||||
shopId?: string | null
|
|
||||||
chatId?: string
|
chatId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatMessages({ productId, shopId, chatId }: ChatMessagesProps) {
|
export default function ChatMessages({ chatId }: ChatMessagesProps) {
|
||||||
const [messageText, setMessageText] = useState('')
|
const [messageText, setMessageText] = useState('')
|
||||||
const [isTyping, setIsTyping] = useState(false)
|
const [isTyping, setIsTyping] = useState(false)
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const markedAsReadRef = useRef<Set<string>>(new Set())
|
const markedAsReadRef = useRef<Set<string>>(new Set())
|
||||||
@@ -51,37 +51,42 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
connect
|
connect
|
||||||
} = useSocket()
|
} = useSocket()
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { data: chatData, isLoading, error } = useChatDetail(chatId)
|
const { data: chatData, isLoading, error } = useChatDetail(chatId)
|
||||||
|
|
||||||
const markAsRead = useMarkChatAsRead()
|
const markAsRead = useMarkChatAsRead()
|
||||||
|
|
||||||
// Convert API messages to component format
|
// Convert API messages to component format
|
||||||
const messages: Message[] = useMemo(() =>
|
const messages: Message[] = useMemo(() => {
|
||||||
chatData?.results?.messages?.map((msg: APIMessage) => ({
|
const apiMessages = chatData?.results?.messages
|
||||||
|
if (Array.isArray(apiMessages)) {
|
||||||
|
return apiMessages.map((msg: APIMessage) => ({
|
||||||
id: msg._id,
|
id: msg._id,
|
||||||
text: msg.content,
|
text: msg.content,
|
||||||
time: new Date(msg.createdAt).toLocaleTimeString('fa-IR', {
|
time: new Date(msg.createdAt).toLocaleTimeString('fa-IR', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
}),
|
}),
|
||||||
sender: msg.sender === 'customer' ? 'user' : 'admin',
|
sender: msg.senderRef === 'User' ? 'user' : 'admin',
|
||||||
status: msg.status
|
status: msg.status
|
||||||
})) || []
|
}))
|
||||||
, [chatData?.results?.messages])
|
}
|
||||||
|
return []
|
||||||
|
}, [chatData?.results?.messages])
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
if (messagesContainerRef.current) {
|
||||||
|
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
// اتصال به چت وقتی chatId تنظیم شده و کاربر لاگین کرده
|
// اتصال به سوکت وقتی chatId تنظیم شده و کاربر لاگین کرده
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatId && chatId !== '' && isUserAuthenticated === true) {
|
if (chatId && chatId !== '' && isUserAuthenticated === true && !isConnected) {
|
||||||
// اتصال به سوکت اگر متصل نیست
|
|
||||||
if (!isConnected) {
|
|
||||||
console.log('🔌 اتصال به سوکت برای چت...')
|
console.log('🔌 اتصال به سوکت برای چت...')
|
||||||
connect().catch((error) => {
|
connect().catch((error) => {
|
||||||
console.error('❌ خطا در اتصال سوکت:', error.message)
|
console.error('❌ خطا در اتصال سوکت:', error.message)
|
||||||
@@ -92,17 +97,18 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}, [chatId, isUserAuthenticated, isConnected, connect])
|
||||||
|
|
||||||
// اتصال به چت
|
// اتصال به چت وقتی سوکت متصل شد
|
||||||
if (isConnected) {
|
useEffect(() => {
|
||||||
|
if (chatId && chatId !== '' && isConnected) {
|
||||||
console.log('🔗 اتصال به چت:', chatId)
|
console.log('🔗 اتصال به چت:', chatId)
|
||||||
joinChat(chatId)
|
joinChat(chatId)
|
||||||
return () => {
|
return () => {
|
||||||
leaveChat(chatId)
|
leaveChat(chatId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [chatId, isConnected, joinChat, leaveChat])
|
||||||
}, [chatId, isConnected, joinChat, leaveChat, connect, isUserAuthenticated])
|
|
||||||
|
|
||||||
// Mark messages as read - فقط یک بار برای هر chatId
|
// Mark messages as read - فقط یک بار برای هر chatId
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -123,9 +129,12 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
const handleNewMessage = (socketMessage: unknown) => {
|
const handleNewMessage = (socketMessage: unknown) => {
|
||||||
const message = socketMessage as SocketMessage
|
const message = socketMessage as SocketMessage
|
||||||
if (message.chatId === chatId) {
|
if (message.chatId === chatId) {
|
||||||
// The message will be added via React Query invalidation
|
|
||||||
// or we can update local state if needed
|
|
||||||
console.log('New message received:', message)
|
console.log('New message received:', message)
|
||||||
|
// Invalidate chat detail query to refetch messages
|
||||||
|
// This will handle both sent and received messages
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['chat', 'detail', chatId] })
|
||||||
|
// Scroll to bottom after a short delay to ensure new message is rendered
|
||||||
|
setTimeout(scrollToBottom, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +153,7 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
return () => {
|
return () => {
|
||||||
// Cleanup will be handled by the hook
|
// Cleanup will be handled by the hook
|
||||||
}
|
}
|
||||||
}, [chatId, onNewMessage, onError])
|
}, [chatId, onNewMessage, onError, queryClient])
|
||||||
|
|
||||||
const handleTyping = () => {
|
const handleTyping = () => {
|
||||||
if (!isTyping) {
|
if (!isTyping) {
|
||||||
@@ -173,9 +182,9 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAttachClick = () => {
|
// const handleAttachClick = () => {
|
||||||
fileInputRef.current?.click()
|
// fileInputRef.current?.click()
|
||||||
}
|
// }
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if ((messageText.trim() || selectedFile) && chatId) {
|
if ((messageText.trim() || selectedFile) && chatId) {
|
||||||
@@ -192,7 +201,17 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
// For now, just send text message
|
// For now, just send text message
|
||||||
// TODO: Implement file upload when server supports it
|
// TODO: Implement file upload when server supports it
|
||||||
if (content) {
|
if (content) {
|
||||||
sendMessage(chatId, content)
|
const receiver = chat?.seller
|
||||||
|
if (receiver) {
|
||||||
|
sendMessage(chatId, content, receiver)
|
||||||
|
// Invalidate query to show sent message immediately after socket event
|
||||||
|
// Note: This might cause duplicate if socket event also invalidates
|
||||||
|
setTimeout(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['chat', 'detail', chatId] })
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
console.error('❌ receiver یافت نشد - نمیتوان پیام ارسال کرد')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to bottom after sending
|
// Scroll to bottom after sending
|
||||||
@@ -232,7 +251,7 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Shop size={18} className="text-primary" variant="Bold" />
|
<Shop size={18} color="#1a73e8" variant="Bold" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* وضعیت آنلاین در این API موجود نیست */}
|
{/* وضعیت آنلاین در این API موجود نیست */}
|
||||||
@@ -245,30 +264,6 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
|
||||||
<Call size={18} />
|
|
||||||
</button>
|
|
||||||
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
|
||||||
<VideoPlay size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
// TODO: Implement delete chat functionality
|
|
||||||
console.log('🗑️ درخواست حذف چت:', chatId)
|
|
||||||
if (confirm('آیا از حذف این گفتگو مطمئن هستید؟')) {
|
|
||||||
// deleteChatMutation.mutate(chatId)
|
|
||||||
alert('این قابلیت هنوز پیادهسازی نشده است.')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<Trash size={18} />
|
|
||||||
</button>
|
|
||||||
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
|
||||||
<More size={18} className="rotate-90" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-gray-500">
|
<div className="text-center text-gray-500">
|
||||||
@@ -278,7 +273,7 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages Area */}
|
{/* Messages Area */}
|
||||||
<div className="flex-1 overflow-y-auto bg-gray-50/30">
|
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto bg-gray-50/30">
|
||||||
{/* Socket Status Alert */}
|
{/* Socket Status Alert */}
|
||||||
{!isConnected && (
|
{!isConnected && (
|
||||||
<div className="px-6 py-3">
|
<div className="px-6 py-3">
|
||||||
@@ -286,7 +281,6 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
<div className="text-xs text-blue-800 leading-relaxed">
|
<div className="text-xs text-blue-800 leading-relaxed">
|
||||||
<div className="font-semibold mb-2">ℹ️ اتصال به چت</div>
|
<div className="font-semibold mb-2">ℹ️ اتصال به چت</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div>• در حال اتصال به سرور چت...</div>
|
|
||||||
<div>• لطفاً چند لحظه صبر کنید</div>
|
<div>• لطفاً چند لحظه صبر کنید</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +305,7 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
<div key={i} className={`flex ${i % 2 === 0 ? 'justify-start' : 'justify-end'}`}>
|
<div key={i} className={`flex ${i % 2 === 0 ? 'justify-start' : 'justify-end'}`}>
|
||||||
<div className="max-w-[70%]">
|
<div className="max-w-[70%]">
|
||||||
<div className="w-16 h-4 bg-gray-200 rounded animate-pulse mb-2"></div>
|
<div className="w-16 h-4 bg-gray-200 rounded animate-pulse mb-2"></div>
|
||||||
<div className={`rounded-2xl px-4 py-3 ${i % 2 === 0 ? 'bg-white' : 'bg-primary'} animate-pulse`}>
|
<div className="rounded-2xl px-4 py-3 bg-gray-200 animate-pulse">
|
||||||
<div className="w-32 h-4 bg-gray-100 rounded"></div>
|
<div className="w-32 h-4 bg-gray-100 rounded"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,17 +319,17 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
) : messages.length > 0 ? (
|
) : messages.length > 0 ? (
|
||||||
messages.map((message, index) => {
|
messages.map((message, index) => {
|
||||||
const isFirstInGroup = index === 0 || messages[index - 1].sender !== message.sender
|
const isFirstInGroup = index === 0 || messages[index - 1].sender !== message.sender
|
||||||
const senderName = message.sender === 'admin' ?
|
const senderName = message.sender === 'user' ? 'شما' :
|
||||||
(chat?.sellerRef || 'فروشگاه') : 'شما'
|
(typeof chat?.sellerRef === 'string' ? chat.sellerRef : 'فروشگاه')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={`flex ${message.sender === 'admin' ? 'justify-start' : 'justify-end'}`}
|
className={`flex ${message.sender === 'user' ? 'justify-start' : 'justify-end'}`}
|
||||||
>
|
>
|
||||||
<div className={`max-w-[70%] ${message.sender === 'admin' ? 'mr-0' : 'ml-0'}`}>
|
<div className={`max-w-[70%] ${message.sender === 'user' ? 'mr-0' : 'ml-0'}`}>
|
||||||
{isFirstInGroup && (
|
{isFirstInGroup && (
|
||||||
<div className={`flex items-center gap-2 mb-2 ${message.sender === 'admin' ? 'justify-start' : 'justify-end'}`}>
|
<div className={`flex items-center gap-2 mb-2 ${message.sender === 'user' ? 'justify-start' : 'justify-end'}`}>
|
||||||
<span className="text-xs text-gray-500 font-medium">
|
<span className="text-xs text-gray-500 font-medium">
|
||||||
{senderName}
|
{senderName}
|
||||||
</span>
|
</span>
|
||||||
@@ -344,16 +338,16 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`rounded-2xl px-4 py-3 shadow-sm ${message.sender === 'admin'
|
className={`rounded-2xl px-4 py-3 shadow-sm ${message.sender === 'user'
|
||||||
? 'bg-white text-gray-800 border border-gray-100'
|
? 'bg-white text-gray-800 border border-gray-100'
|
||||||
: 'bg-primary text-primary-foreground'
|
: 'bg-primary text-primary-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm leading-relaxed">{message.text}</p>
|
<p className="text-sm leading-relaxed">{typeof message.text === 'string' ? message.text : 'پیام نامعتبر'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message.sender === 'user' && (
|
{message.sender === 'user' && (
|
||||||
<div className="flex justify-end mt-1">
|
<div className="flex justify-start mt-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-xs text-gray-400">{message.time}</span>
|
<span className="text-xs text-gray-400">{message.time}</span>
|
||||||
{message.status === 'read' && (
|
{message.status === 'read' && (
|
||||||
@@ -381,21 +375,21 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
|
|
||||||
{/* Typing Indicator */}
|
{/* Typing Indicator */}
|
||||||
{isTyping && (
|
{isTyping && (
|
||||||
<div className="flex justify-start mb-4">
|
<div className="flex justify-end mb-4">
|
||||||
<div className="max-w-[70%]">
|
<div className="max-w-[70%]">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2 justify-end">
|
||||||
<span className="text-xs text-gray-500 font-medium">
|
<span className="text-xs text-gray-500 font-medium">
|
||||||
{chat?.sellerRef || 'فروشگاه'}
|
{chat?.sellerRef || 'فروشگاه'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white border border-gray-100 rounded-2xl px-4 py-3 shadow-sm">
|
<div className="bg-primary text-primary-foreground rounded-2xl px-4 py-3 shadow-sm">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
<div className="w-2 h-2 bg-white rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
||||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
<div className="w-2 h-2 bg-white rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
||||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
<div className="w-2 h-2 bg-white rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500 ml-2">در حال تایپ...</span>
|
<span className="text-xs text-white ml-2">در حال تایپ...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -406,15 +400,16 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input Area */}
|
{/* Input Area - Only show when chat is selected */}
|
||||||
|
{chatId && chatId !== '' && (
|
||||||
<div className="border-t border-gray-100 px-6 py-4 bg-white">
|
<div className="border-t border-gray-100 px-6 py-4 bg-white">
|
||||||
<div className="flex items-end gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
{/* <button
|
||||||
onClick={handleAttachClick}
|
onClick={handleAttachClick}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex-shrink-0"
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex-shrink-0"
|
||||||
>
|
>
|
||||||
<AttachCircle size={20} />
|
<AttachCircle color='#8C90A3' size={20} />
|
||||||
</button>
|
</button> */}
|
||||||
|
|
||||||
{/* Hidden file input */}
|
{/* Hidden file input */}
|
||||||
<input
|
<input
|
||||||
@@ -461,23 +456,20 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
}}
|
}}
|
||||||
placeholder={selectedFile ? "توضیحات فایل را بنویسید..." : "پیام خود را بنویسید..."}
|
placeholder={selectedFile ? "توضیحات فایل را بنویسید..." : "پیام خود را بنویسید..."}
|
||||||
rows={1}
|
rows={1}
|
||||||
className="w-full resize-none bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent max-h-32"
|
className="w-full resize-none bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent max-h-32"
|
||||||
style={{ minHeight: '44px' }}
|
style={{ minHeight: '44px' }}
|
||||||
/>
|
/>
|
||||||
<button className="absolute left-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-200 rounded-lg transition-colors">
|
|
||||||
<EmojiHappy size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={(!messageText.trim() && !selectedFile) || !isConnected}
|
disabled={(!messageText.trim() && !selectedFile) || !isConnected}
|
||||||
className={`p-3 rounded-xl transition-all flex-shrink-0 ${(messageText.trim() || selectedFile) && isConnected
|
className={`p-3 -mt-1 rounded-xl transition-all flex-shrink-0 ${(messageText.trim() || selectedFile) && isConnected
|
||||||
? 'bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg'
|
? 'bg-primary hover:bg-primary/90 text-primary-foreground '
|
||||||
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Send size={18} className="rotate-180" variant="Bold" />
|
<Send color='#fff' size={18} className="rotate-180" variant="Bold" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -485,6 +477,7 @@ export default function ChatMessages({ productId, shopId, chatId }: ChatMessages
|
|||||||
پیام شما بعد از ارسال قابل حذف نیست
|
پیام شما بعد از ارسال قابل حذف نیست
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Shop, SearchNormal1 } from 'iconsax-react'
|
import { Shop, SearchNormal1 } from 'iconsax-react'
|
||||||
|
import Image from 'next/image'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useSocket } from '../hooks/useSocket'
|
import { useSocket } from '../hooks/useSocket'
|
||||||
import { useChatList } from '../../hooks/useChatData'
|
import { useChatList } from '../../hooks/useChatData'
|
||||||
@@ -16,9 +17,9 @@ export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSideba
|
|||||||
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
||||||
const [isConnecting, setIsConnecting] = useState(false)
|
const [isConnecting, setIsConnecting] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { onUserOnline, onUserOffline, connect, isConnected, isServerSupported } = useSocket()
|
const { onUserOnline, onUserOffline, connect, isConnected } = useSocket()
|
||||||
const { data: chatData, isLoading, error } = useChatList({
|
const { data: chatData, isLoading, error } = useChatList({
|
||||||
enabled: isUserAuthenticated === true // Only fetch if user is logged in
|
enabled: isUserAuthenticated === true && isConnected // Only fetch if user is logged in AND socket is connected
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check authentication on mount
|
// Check authentication on mount
|
||||||
@@ -33,17 +34,11 @@ export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSideba
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر سرور پشتیبانی نمیکند، اصلاً تلاش نکن
|
// اتصال به سوکت اگر متصل نیست
|
||||||
if (!isServerSupported) {
|
|
||||||
console.log('🚫 سرور Socket.IO پشتیبانی نمیکند، اتصال متوقف شد')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// فقط اگر قبلاً اتصال برقرار نشده، connect کنیم
|
|
||||||
if (!isConnected && !isConnecting) {
|
if (!isConnected && !isConnecting) {
|
||||||
console.log('🔌 شروع اتصال به سوکت...')
|
console.log('🔌 شروع اتصال به سوکت...')
|
||||||
setIsConnecting(true)
|
setIsConnecting(true)
|
||||||
connect('customer')
|
connect()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('✅ اتصال سوکت موفق')
|
console.log('✅ اتصال سوکت موفق')
|
||||||
setIsConnecting(false)
|
setIsConnecting(false)
|
||||||
@@ -51,6 +46,12 @@ export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSideba
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log('❌ اتصال سوکت شکست خورد:', error.message)
|
console.log('❌ اتصال سوکت شکست خورد:', error.message)
|
||||||
setIsConnecting(false)
|
setIsConnecting(false)
|
||||||
|
|
||||||
|
// اگر توکن expired است، redirect به login
|
||||||
|
if (error.message === 'توکن منقضی شده' || error.message === 'توکن یافت نشد') {
|
||||||
|
console.log('🔐 توکن نامعتبر، redirect به صفحه ورود')
|
||||||
|
router.push('/auth')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else if (isConnected) {
|
} else if (isConnected) {
|
||||||
console.log('ℹ️ اتصال سوکت قبلاً برقرار شده')
|
console.log('ℹ️ اتصال سوکت قبلاً برقرار شده')
|
||||||
@@ -58,7 +59,7 @@ export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSideba
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkAuth()
|
checkAuth()
|
||||||
}, [connect, router, isConnected, isConnecting, isServerSupported])
|
}, [connect, router, isConnected, isConnecting])
|
||||||
|
|
||||||
// Listen for user online/offline events
|
// Listen for user online/offline events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,11 +84,15 @@ export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSideba
|
|||||||
}
|
}
|
||||||
}, [onUserOnline, onUserOffline])
|
}, [onUserOnline, onUserOffline])
|
||||||
|
|
||||||
const chats = chatData?.results?.chats || []
|
const chats = Array.isArray(chatData?.results?.chats) ? chatData.results.chats : []
|
||||||
|
|
||||||
const filteredChats = chats.filter(chat =>
|
const filteredChats = chats.filter(chat => {
|
||||||
chat.participants.seller.shopName.toLowerCase().includes(searchQuery.toLowerCase())
|
if (!chat || typeof chat !== 'object') return false
|
||||||
)
|
if (!chat.seller || typeof chat.seller !== 'object') return false
|
||||||
|
const fullName = chat.seller.fullName
|
||||||
|
if (typeof fullName !== 'string') return false
|
||||||
|
return fullName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
})
|
||||||
|
|
||||||
const handleChatClick = (chatId: string) => {
|
const handleChatClick = (chatId: string) => {
|
||||||
if (onChatSelect) {
|
if (onChatSelect) {
|
||||||
@@ -117,8 +122,27 @@ export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSideba
|
|||||||
{/* Chat List */}
|
{/* Chat List */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="p-8 text-center text-gray-400">
|
<div className="divide-y divide-gray-50">
|
||||||
<p className="text-sm">در حال بارگذاری...</p>
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<div key={i} className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{/* Avatar Skeleton */}
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<div className="w-12 h-12 bg-gray-200 rounded-full animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Skeleton */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="h-4 bg-gray-200 rounded animate-pulse w-24"></div>
|
||||||
|
<div className="h-3 bg-gray-200 rounded animate-pulse w-12"></div>
|
||||||
|
</div>
|
||||||
|
<div className="h-3 bg-gray-200 rounded animate-pulse w-32 mb-2"></div>
|
||||||
|
<div className="h-3 bg-gray-200 rounded animate-pulse w-20"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="p-8 text-center text-red-400">
|
<div className="p-8 text-center text-red-400">
|
||||||
@@ -129,55 +153,41 @@ export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSideba
|
|||||||
{filteredChats.map((chat) => (
|
{filteredChats.map((chat) => (
|
||||||
<div
|
<div
|
||||||
key={chat._id}
|
key={chat._id}
|
||||||
onClick={() => handleChatClick(chat.chatId)}
|
onClick={() => handleChatClick(chat._id)}
|
||||||
className={`p-4 cursor-pointer transition-all hover:bg-gray-50 ${selectedChatId === chat.chatId ? 'bg-primary/5 border-r-2 border-r-primary' : ''
|
className={`p-4 cursor-pointer transition-all hover:bg-gray-50 ${selectedChatId === chat._id ? 'bg-primary/5 border-r-2 border-r-primary' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className="relative flex-shrink-0">
|
<div className="relative flex-shrink-0">
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-primary/10 to-primary/20 rounded-full flex items-center justify-center overflow-hidden">
|
<div className="w-12 h-12 bg-gradient-to-br from-primary/10 to-primary/20 rounded-full flex items-center justify-center overflow-hidden">
|
||||||
{chat.participants.seller.logo ? (
|
{chat.shopLogo?.logo ? (
|
||||||
<img
|
<Image
|
||||||
src={chat.participants.seller.logo}
|
src={chat.shopLogo.logo}
|
||||||
alt={chat.participants.seller.shopName}
|
alt={chat.seller?.fullName || 'فروشنده'}
|
||||||
className="w-full h-full object-cover"
|
fill
|
||||||
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Shop size={20} className="text-primary" variant="Bold" />
|
<Shop size={20} className="text-primary" variant="Bold" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{chat.isOnline && (
|
|
||||||
<div className="absolute -bottom-0.5 -right-0.5 w-4 h-4 bg-green-500 border-2 border-white rounded-full"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h3 className="font-semibold text-gray-900 text-sm truncate">
|
<h3 className="font-semibold text-gray-900 text-sm truncate">
|
||||||
{chat.participants.seller.shopName}
|
{typeof chat.seller?.fullName === 'string' ? chat.seller.fullName : 'فروشنده'}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-xs text-gray-400 flex-shrink-0">
|
<span className="text-xs text-gray-400 flex-shrink-0">
|
||||||
{chat.lastMessage ?
|
{new Date(chat.updatedAt).toLocaleDateString('fa-IR')}
|
||||||
new Date(chat.lastMessage.createdAt).toLocaleTimeString('fa-IR', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}) :
|
|
||||||
new Date(chat.updatedAt).toLocaleDateString('fa-IR')
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 truncate mb-2">
|
<p className="text-xs text-gray-500 truncate mb-2">
|
||||||
{chat.lastMessage?.content || 'گفتگوی جدید'}
|
{typeof chat.lastMessage === 'string' ? chat.lastMessage : 'گفتگوی جدید'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{chat.unreadCount > 0 && (
|
|
||||||
<div className="inline-flex items-center px-2 py-0.5 bg-primary text-primary-foreground text-xs font-medium rounded-full">
|
|
||||||
{chat.unreadCount}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,88 +7,84 @@ import {
|
|||||||
|
|
||||||
export interface UseSocketReturn {
|
export interface UseSocketReturn {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isConnecting: boolean;
|
connect: () => Promise<void>;
|
||||||
isServerSupported: boolean;
|
|
||||||
connect: (type?: "customer" | "seller") => Promise<void>;
|
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
joinChat: (chatId: string) => void;
|
joinChat: (chatId: string) => void;
|
||||||
leaveChat: (chatId: string) => void;
|
leaveChat: (chatId: string) => void;
|
||||||
sendMessage: (chatId: string, content: string) => void;
|
sendMessage: (chatId: string, content: string, receiver: string) => void;
|
||||||
onUserOnline: (callback: (userId: unknown) => void) => void;
|
onNewMessage: (callback: (message: Message) => void) => void;
|
||||||
onUserOffline: (callback: (userId: unknown) => void) => void;
|
onUnreadMessages: (callback: (messages: UnreadMessage[]) => void) => void;
|
||||||
onUnreadMessages: (callback: (messages: unknown) => void) => void;
|
onUserOnline: (callback: (userId: string) => void) => void;
|
||||||
onNewMessage: (callback: (message: unknown) => void) => void;
|
onUserOffline: (callback: (userId: string) => void) => void;
|
||||||
onError: (callback: (error: unknown) => void) => void;
|
onError: (callback: (error: unknown) => void) => void;
|
||||||
offUserOnline: (callback: (userId: unknown) => void) => void;
|
offNewMessage: (callback: (message: Message) => void) => void;
|
||||||
offUserOffline: (callback: (userId: unknown) => void) => void;
|
offUnreadMessages: (callback: (messages: UnreadMessage[]) => void) => void;
|
||||||
offUnreadMessages: (callback: (messages: unknown) => void) => void;
|
offUserOnline: (callback: (userId: string) => void) => void;
|
||||||
offNewMessage: (callback: (message: unknown) => void) => void;
|
offUserOffline: (callback: (userId: string) => void) => void;
|
||||||
offError: (callback: (error: unknown) => void) => void;
|
offError: (callback: (error: unknown) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSocket = (): UseSocketReturn => {
|
export const useSocket = (): UseSocketReturn => {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
|
|
||||||
// Connect to socket
|
// اتصال به سوکت
|
||||||
const connect = useCallback(
|
const connect = useCallback(async () => {
|
||||||
async (type: "customer" | "seller" = "customer") => {
|
if (isConnected) return;
|
||||||
if (isConnected || isConnecting) return;
|
|
||||||
|
|
||||||
setIsConnecting(true);
|
|
||||||
try {
|
try {
|
||||||
await socketService.connect(type);
|
console.log("🔌 شروع اتصال به سوکت...");
|
||||||
|
await socketService.connect("User");
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
|
console.log("✅ سوکت متصل شد");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("خطا در اتصال سوکت:", error);
|
console.error("❌ خطا در اتصال سوکت:", error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
}
|
||||||
},
|
}, [isConnected]);
|
||||||
[isConnected, isConnecting]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Disconnect socket
|
// قطع اتصال سوکت
|
||||||
const disconnect = useCallback(() => {
|
const disconnect = useCallback(() => {
|
||||||
|
console.log("🔌 قطع اتصال سوکت");
|
||||||
socketService.disconnect();
|
socketService.disconnect();
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Join chat room
|
// ورود به چت
|
||||||
const joinChat = useCallback((chatId: string) => {
|
const joinChat = useCallback((chatId: string) => {
|
||||||
socketService.joinChat(chatId);
|
socketService.joinChat(chatId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Leave chat room
|
// خروج از چت
|
||||||
const leaveChat = useCallback((chatId: string) => {
|
const leaveChat = useCallback((chatId: string) => {
|
||||||
socketService.leaveChat(chatId);
|
socketService.leaveChat(chatId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Send message
|
// ارسال پیام
|
||||||
const sendMessage = useCallback((chatId: string, content: string) => {
|
const sendMessage = useCallback(
|
||||||
socketService.sendMessage(chatId, content);
|
(chatId: string, content: string, receiver: string) => {
|
||||||
}, []);
|
socketService.sendMessage(chatId, content, receiver);
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
const onUserOnline = useCallback((callback: (userId: unknown) => void) => {
|
|
||||||
socketService.on("userOnline", callback);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onUserOffline = useCallback((callback: (userId: unknown) => void) => {
|
|
||||||
socketService.on("userOffline", callback);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onUnreadMessages = useCallback(
|
|
||||||
(callback: (messages: unknown) => void) => {
|
|
||||||
socketService.on("unreadMessages", callback);
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onNewMessage = useCallback((callback: (message: unknown) => void) => {
|
// Event listeners
|
||||||
socketService.on("newMessage", callback);
|
const onNewMessage = useCallback((callback: (message: Message) => void) => {
|
||||||
|
socketService.on("newMessage", callback as (data: unknown) => void);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onUnreadMessages = useCallback(
|
||||||
|
(callback: (messages: UnreadMessage[]) => void) => {
|
||||||
|
socketService.on("unreadMessages", callback as (data: unknown) => void);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onUserOnline = useCallback((callback: (userId: string) => void) => {
|
||||||
|
socketService.on("userOnline", callback as (data: unknown) => void);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onUserOffline = useCallback((callback: (userId: string) => void) => {
|
||||||
|
socketService.on("userOffline", callback as (data: unknown) => void);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onError = useCallback((callback: (error: unknown) => void) => {
|
const onError = useCallback((callback: (error: unknown) => void) => {
|
||||||
@@ -96,38 +92,45 @@ export const useSocket = (): UseSocketReturn => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Remove event listeners
|
// Remove event listeners
|
||||||
const offUserOnline = useCallback((callback: (userId: unknown) => void) => {
|
const offNewMessage = useCallback((callback: (message: Message) => void) => {
|
||||||
socketService.off("userOnline", callback);
|
socketService.off("newMessage", callback as (data: unknown) => void);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const offUserOffline = useCallback((callback: (userId: unknown) => void) => {
|
|
||||||
socketService.off("userOffline", callback);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const offUnreadMessages = useCallback(
|
const offUnreadMessages = useCallback(
|
||||||
(callback: (messages: unknown) => void) => {
|
(callback: (messages: UnreadMessage[]) => void) => {
|
||||||
socketService.off("unreadMessages", callback);
|
socketService.off("unreadMessages", callback as (data: unknown) => void);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const offNewMessage = useCallback((callback: (message: unknown) => void) => {
|
const offUserOnline = useCallback((callback: (userId: string) => void) => {
|
||||||
socketService.off("newMessage", callback);
|
socketService.off("userOnline", callback as (data: unknown) => void);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const offUserOffline = useCallback((callback: (userId: string) => void) => {
|
||||||
|
socketService.off("userOffline", callback as (data: unknown) => void);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const offError = useCallback((callback: (error: unknown) => void) => {
|
const offError = useCallback((callback: (error: unknown) => void) => {
|
||||||
socketService.off("error", callback);
|
socketService.off("error", callback);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Setup connection status listener
|
// تنظیم وضعیت اتصال
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleConnect = () => setIsConnected(true);
|
const handleConnect = () => {
|
||||||
const handleDisconnect = () => setIsConnected(false);
|
console.log("📡 سوکت متصل شد");
|
||||||
|
setIsConnected(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
console.log("📡 سوکت قطع شد");
|
||||||
|
setIsConnected(false);
|
||||||
|
};
|
||||||
|
|
||||||
socketService.on("connect", handleConnect);
|
socketService.on("connect", handleConnect);
|
||||||
socketService.on("disconnect", handleDisconnect);
|
socketService.on("disconnect", handleDisconnect);
|
||||||
|
|
||||||
// Check initial connection status
|
// بررسی وضعیت اولیه
|
||||||
setIsConnected(socketService.isConnected());
|
setIsConnected(socketService.isConnected());
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -136,31 +139,22 @@ export const useSocket = (): UseSocketReturn => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
// Don't disconnect on unmount, let the service manage its own lifecycle
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isConnected,
|
isConnected,
|
||||||
isConnecting,
|
|
||||||
isServerSupported: socketService.getServerSupported(),
|
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
joinChat,
|
joinChat,
|
||||||
leaveChat,
|
leaveChat,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
onNewMessage,
|
||||||
|
onUnreadMessages,
|
||||||
onUserOnline,
|
onUserOnline,
|
||||||
onUserOffline,
|
onUserOffline,
|
||||||
onUnreadMessages,
|
|
||||||
onNewMessage,
|
|
||||||
onError,
|
onError,
|
||||||
|
offNewMessage,
|
||||||
|
offUnreadMessages,
|
||||||
offUserOnline,
|
offUserOnline,
|
||||||
offUserOffline,
|
offUserOffline,
|
||||||
offUnreadMessages,
|
|
||||||
offNewMessage,
|
|
||||||
offError,
|
offError,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ import { useState, useEffect, Suspense } from "react"
|
|||||||
import Menu from "../components/Menu"
|
import Menu from "../components/Menu"
|
||||||
import ChatSidebar from "./components/ChatSidebar"
|
import ChatSidebar from "./components/ChatSidebar"
|
||||||
import ChatMessages from "./components/ChatMessages"
|
import ChatMessages from "./components/ChatMessages"
|
||||||
import ChatProductCard from "./components/ChatProductCard"
|
|
||||||
import { isAuthenticated } from "@/config/func"
|
import { isAuthenticated } from "@/config/func"
|
||||||
|
import { useCreateChat } from "../hooks/useChatData"
|
||||||
|
|
||||||
const ChatContent: React.FC = () => {
|
const ChatContent: React.FC = () => {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const productId = searchParams.get('productId')
|
|
||||||
const shopId = searchParams.get('shopId')
|
const shopId = searchParams.get('shopId')
|
||||||
|
const createChat = useCreateChat()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const [selectedChatId, setSelectedChatId] = useState<string>('')
|
const [selectedChatId, setSelectedChatId] = useState<string>('')
|
||||||
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
|
|
||||||
// Check authentication on mount
|
// Check authentication on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,7 +30,13 @@ const ChatContent: React.FC = () => {
|
|||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
console.log('🔐 کاربر login نکرده یا توکن expire شده، redirect به صفحه ورود')
|
console.log('🔐 کاربر login نکرده یا توکن expire شده، redirect به صفحه ورود')
|
||||||
router.push('/auth')
|
router.push('/auth')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// کمی صبر کن تا کامپوننتها آماده شوند
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsInitialized(true)
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAuth()
|
checkAuth()
|
||||||
@@ -39,6 +46,14 @@ const ChatContent: React.FC = () => {
|
|||||||
setSelectedChatId(chatId)
|
setSelectedChatId(chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (shopId) {
|
||||||
|
createChat.mutate({ sellerId: shopId })
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [shopId, createChat])
|
||||||
|
|
||||||
// Show loading while checking authentication
|
// Show loading while checking authentication
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -73,36 +88,46 @@ const ChatContent: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="p-3 sm:p-4 md:p-6">
|
<div className="p-3 sm:p-4 md:p-6">
|
||||||
<Menu pageActive='chat' />
|
<Menu pageActive='chat' />
|
||||||
|
|
||||||
<div className="mt-6 flex gap-8 h-[calc(100vh-200px)] min-h-[600px]">
|
<div className="mt-6 h-[calc(100vh-200px)] min-h-[600px]">
|
||||||
{/* Chat Section - Sidebar + Messages */}
|
{/* Chat Section - Sidebar + Messages */}
|
||||||
<div className="flex-1 flex border border-gray-200 shadow-sm rounded-xl bg-white">
|
<div className="flex border border-gray-200 shadow-sm rounded-xl bg-white h-full">
|
||||||
{/* Chat Sidebar - Left Side */}
|
{/* Chat Sidebar - Left Side */}
|
||||||
<div className="w-80 border-r border-gray-200">
|
<div className="w-80 border-r border-gray-200">
|
||||||
|
{isInitialized ? (
|
||||||
<ChatSidebar
|
<ChatSidebar
|
||||||
selectedChatId={selectedChatId}
|
selectedChatId={selectedChatId}
|
||||||
onChatSelect={handleChatSelect}
|
onChatSelect={handleChatSelect}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-sm text-gray-500">در حال بارگذاری چت...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat Messages - Right Side */}
|
{/* Chat Messages - Right Side */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
{isInitialized ? (
|
||||||
<ChatMessages
|
<ChatMessages
|
||||||
productId={productId}
|
|
||||||
shopId={shopId}
|
|
||||||
chatId={selectedChatId}
|
chatId={selectedChatId}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-sm text-gray-500">در حال بارگذاری پیامها...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Product Card - Right Side */}
|
|
||||||
<div className="w-80 flex-shrink-0">
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm">
|
|
||||||
<ChatProductCard productId={productId} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface Message {
|
|||||||
id: string;
|
id: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
|
receiver: string;
|
||||||
content: string;
|
content: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -12,18 +13,13 @@ export interface Message {
|
|||||||
export interface UnreadMessage {
|
export interface UnreadMessage {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
|
receiver: string;
|
||||||
content: string;
|
content: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SocketService {
|
class SocketService {
|
||||||
private socket: Socket | null = null;
|
private socket: Socket | null = null;
|
||||||
private reconnectAttempts = 0;
|
|
||||||
private maxReconnectAttempts = 1; // کاهش تعداد تلاشها
|
|
||||||
private reconnectDelay = 2000; // افزایش delay
|
|
||||||
private isServerSupported = false; // آیا سرور پشتیبانی میکند
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
private eventListeners: { [key: string]: ((data: unknown) => void)[] } = {};
|
private eventListeners: { [key: string]: ((data: unknown) => void)[] } = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -32,81 +28,87 @@ class SocketService {
|
|||||||
|
|
||||||
private initializeEventListeners() {
|
private initializeEventListeners() {
|
||||||
this.eventListeners = {
|
this.eventListeners = {
|
||||||
userOnline: [],
|
|
||||||
userOffline: [],
|
|
||||||
unreadMessages: [],
|
|
||||||
newMessage: [],
|
|
||||||
error: [],
|
|
||||||
unauthorized: [],
|
|
||||||
connect: [],
|
connect: [],
|
||||||
disconnect: [],
|
disconnect: [],
|
||||||
|
newMessage: [],
|
||||||
|
unreadMessages: [],
|
||||||
|
userOnline: [],
|
||||||
|
userOffline: [],
|
||||||
|
error: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to socket server
|
// اتصال ساده به سوکت شبیه کد تست
|
||||||
connect(type: "customer" | "seller" = "customer"): Promise<void> {
|
connect(type: "User" | "Seller" = "User"): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// Get token from localStorage
|
// دریافت توکن از localStorage با بررسی دقیق
|
||||||
const token = localStorage.getItem(TOKEN_NAME);
|
const token = localStorage.getItem(TOKEN_NAME);
|
||||||
console.log("🔑 توکن سوکت:", token ? "موجود است" : "پیدا نشد");
|
console.log(
|
||||||
if (!token) {
|
"🔑 توکن سوکت:",
|
||||||
console.log("❌ اتصال رد شد: توکن موجود نیست");
|
token ? `${token.substring(0, 20)}...` : "پیدا نشد"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!token || token.trim() === "") {
|
||||||
|
console.log("❌ اتصال رد شد: توکن موجود نیست یا خالی است");
|
||||||
reject(new Error("توکن یافت نشد"));
|
reject(new Error("توکن یافت نشد"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر قبلاً تشخیص دادهایم سرور پشتیبانی نمیکند
|
// بررسی اعتبار توکن (ساده)
|
||||||
if (!this.isServerSupported && this.reconnectAttempts > 0) {
|
if (token.length < 10) {
|
||||||
console.log("🚫 اتصال رد شد: سرور Socket.IO پشتیبانی نمیکند");
|
console.log("❌ اتصال رد شد: توکن نامعتبر است (خیلی کوتاه)");
|
||||||
reject(new Error("سرور چت پشتیبانی نمیشود"));
|
reject(new Error("توکن نامعتبر"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔌 شروع اتصال سوکت به:", `${BASE_URL}/ws-chat`);
|
// بررسی اینکه آیا توکن JWT است و expired نشده
|
||||||
console.log("📝 پارامترهای اتصال:", {
|
try {
|
||||||
token: token.substring(0, 20) + "...",
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||||
type,
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
});
|
if (payload.exp && payload.exp < currentTime) {
|
||||||
|
console.log("❌ اتصال رد شد: توکن expired است");
|
||||||
|
reject(new Error("توکن منقضی شده"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.log("⚠️ نمیتوان اعتبار توکن را بررسی کرد، ادامه...");
|
||||||
|
}
|
||||||
|
|
||||||
// Create socket connection
|
console.log("🔌 شروع اتصال سوکت به:", `${BASE_URL}/ws-chat`);
|
||||||
|
|
||||||
|
// ایجاد اتصال سوکت دقیقاً مثل کد تست
|
||||||
this.socket = io(BASE_URL, {
|
this.socket = io(BASE_URL, {
|
||||||
path: "/ws-chat",
|
path: "/ws-chat",
|
||||||
query: {
|
query: {
|
||||||
token,
|
token: token.trim(), // حذف فضای خالی
|
||||||
type,
|
type: type,
|
||||||
},
|
},
|
||||||
transports: ["websocket"],
|
transports: ["websocket"],
|
||||||
timeout: 10000, // 10 seconds timeout
|
timeout: 10000, // 10 ثانیه timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup event listeners
|
// تنظیم event listeners
|
||||||
this.setupSocketListeners();
|
this.setupSocketListeners();
|
||||||
|
|
||||||
// Handle connection
|
// مدیریت اتصال موفق
|
||||||
this.socket.on("connect", () => {
|
this.socket.on("connect", () => {
|
||||||
console.log("✅ اتصال سوکت برقرار شد");
|
console.log("✅ اتصال سوکت برقرار شد، socket id:", this.socket?.id);
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.isServerSupported = true; // سرور پشتیبانی میکند
|
|
||||||
this.emit("connect", null);
|
this.emit("connect", null);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("connect_error", (error) => {
|
// مدیریت خطای اتصال
|
||||||
console.error("❌ خطا در اتصال سوکت:", error.message);
|
this.socket.on("connect_error", (err) => {
|
||||||
console.log("⚠️ احتمالاً سرور Socket.IO پیادهسازی نشده است");
|
console.error("❌ خطا در اتصال سوکت:", err.message);
|
||||||
|
this.emit("error", err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
// اگر چندین بار شکست خورد، فرض میکنیم سرور پشتیبانی نمیکند
|
// مدیریت قطع اتصال
|
||||||
if (this.reconnectAttempts >= 1) {
|
this.socket.on("disconnect", () => {
|
||||||
console.log(
|
console.log("❌ اتصال سوکت قطع شد");
|
||||||
"🚫 سرور Socket.IO پشتیبانی نمیشود، reconnect متوقف شد"
|
this.emit("disconnect", null);
|
||||||
);
|
|
||||||
this.isServerSupported = false;
|
|
||||||
reject(new Error("سرور چت پشتیبانی نمیشود"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reject(error);
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("خطا در ایجاد اتصال سوکت:", error);
|
console.error("خطا در ایجاد اتصال سوکت:", error);
|
||||||
@@ -115,122 +117,95 @@ class SocketService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup socket event listeners
|
// تنظیم event listeners برای پیامها
|
||||||
private setupSocketListeners() {
|
private setupSocketListeners() {
|
||||||
if (!this.socket) return;
|
if (!this.socket) return;
|
||||||
|
|
||||||
// User online/offline events
|
// پیام جدید
|
||||||
this.socket.on("userOnline", (userId: string) => {
|
this.socket.on("newMessage", (msg) => {
|
||||||
console.log("کاربر آنلاین شد:", userId);
|
console.log("💬 پیام جدید دریافت شد:", msg);
|
||||||
this.emit("userOnline", userId);
|
this.emit("newMessage", msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("userOffline", (userId: string) => {
|
// پیامهای خوانده نشده
|
||||||
console.log("کاربر آفلاین شد:", userId);
|
this.socket.on("unreadMessages", (messages) => {
|
||||||
this.emit("userOffline", userId);
|
console.log("📨 پیامهای خوانده نشده:", messages);
|
||||||
});
|
|
||||||
|
|
||||||
// Unread messages
|
|
||||||
this.socket.on("unreadMessages", (messages: UnreadMessage[]) => {
|
|
||||||
console.log("پیامهای خوانده نشده:", messages);
|
|
||||||
this.emit("unreadMessages", messages);
|
this.emit("unreadMessages", messages);
|
||||||
});
|
});
|
||||||
|
|
||||||
// New message
|
// کاربر آنلاین
|
||||||
this.socket.on("newMessage", (message: Message) => {
|
this.socket.on("userOnline", (userId) => {
|
||||||
console.log("پیام جدید:", message);
|
console.log("🟢 کاربر آنلاین شد:", userId);
|
||||||
this.emit("newMessage", message);
|
this.emit("userOnline", userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error handling
|
// کاربر آفلاین
|
||||||
this.socket.on("error", (error: { message: string }) => {
|
this.socket.on("userOffline", (userId) => {
|
||||||
console.error("خطای سوکت:", error.message);
|
console.log("🔴 کاربر آفلاین شد:", userId);
|
||||||
this.emit("error", error);
|
this.emit("userOffline", userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("unauthorized", (data: { message: string }) => {
|
// خطا
|
||||||
console.error("دسترسی غیرمجاز:", data.message);
|
this.socket.on("error", (data) => {
|
||||||
this.emit("unauthorized", data);
|
console.error("⚠️ خطای سوکت:", data);
|
||||||
});
|
this.emit("error", data);
|
||||||
|
|
||||||
// Connection events
|
|
||||||
this.socket.on("disconnect", (reason: string) => {
|
|
||||||
console.log("اتصال سوکت قطع شد:", reason);
|
|
||||||
this.emit("disconnect", reason);
|
|
||||||
|
|
||||||
// Auto reconnect if not manual disconnect
|
|
||||||
if (
|
|
||||||
reason !== "io client disconnect" &&
|
|
||||||
this.reconnectAttempts < this.maxReconnectAttempts
|
|
||||||
) {
|
|
||||||
this.scheduleReconnect();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule reconnection
|
// ورود به چت
|
||||||
private scheduleReconnect() {
|
|
||||||
// اگر سرور پشتیبانی نمیکند، reconnect نکن
|
|
||||||
if (!this.isServerSupported) {
|
|
||||||
console.log("🚫 reconnect متوقف شد چون سرور پشتیبانی نمیکند");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
console.log(
|
|
||||||
`🔄 تلاش برای اتصال مجدد (${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = localStorage.getItem(TOKEN_NAME);
|
|
||||||
if (token && this.socket) {
|
|
||||||
this.socket.connect();
|
|
||||||
} else {
|
|
||||||
console.log("❌ توکن یا سوکت موجود نیست، reconnect متوقف شد");
|
|
||||||
}
|
|
||||||
}, this.reconnectDelay * this.reconnectAttempts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join a chat room
|
|
||||||
joinChat(chatId: string): void {
|
joinChat(chatId: string): void {
|
||||||
if (this.socket && this.socket.connected) {
|
if (this.socket && this.socket.connected) {
|
||||||
|
console.log("🔗 ورود به چت:", chatId);
|
||||||
this.socket.emit("joinChat", chatId);
|
this.socket.emit("joinChat", chatId);
|
||||||
console.log("ورود به چت:", chatId);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("سوکت متصل نیست");
|
console.error("سوکت متصل نیست - نمیتوان به چت وارد شد");
|
||||||
this.emit("error", { message: "اتصال سوکت برقرار نیست" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave a chat room
|
// خروج از چت
|
||||||
leaveChat(chatId: string): void {
|
leaveChat(chatId: string): void {
|
||||||
if (this.socket && this.socket.connected) {
|
if (this.socket && this.socket.connected) {
|
||||||
|
console.log("🚪 خروج از چت:", chatId);
|
||||||
this.socket.emit("leaveChat", chatId);
|
this.socket.emit("leaveChat", chatId);
|
||||||
console.log("خروج از چت:", chatId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a message
|
// ارسال پیام
|
||||||
sendMessage(chatId: string, content: string): void {
|
sendMessage(chatId: string, content: string, receiver: string): void {
|
||||||
if (this.socket && this.socket.connected) {
|
if (this.socket && this.socket.connected) {
|
||||||
|
console.log("📤 ارسال پیام:", { chatId, content, receiver });
|
||||||
this.socket.emit("newMessage", {
|
this.socket.emit("newMessage", {
|
||||||
chatId,
|
chatId,
|
||||||
content,
|
content,
|
||||||
|
receiver,
|
||||||
});
|
});
|
||||||
console.log("ارسال پیام:", { chatId, content });
|
|
||||||
} else {
|
} else {
|
||||||
console.error("سوکت متصل نیست");
|
console.error("سوکت متصل نیست - نمیتوان پیام ارسال کرد");
|
||||||
this.emit("error", { message: "اتصال سوکت برقرار نیست" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event system
|
// قطع اتصال
|
||||||
on(event: string, callback: (data: unknown) => void): void {
|
disconnect(): void {
|
||||||
if (this.eventListeners[event]) {
|
if (this.socket) {
|
||||||
this.eventListeners[event].push(callback);
|
console.log("🔌 قطع اتصال سوکت");
|
||||||
|
this.socket.disconnect();
|
||||||
|
this.socket = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// بررسی اتصال
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.socket?.connected || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// سیستم event
|
||||||
|
on(event: string, callback: (data: unknown) => void): void {
|
||||||
|
if (!this.eventListeners[event]) {
|
||||||
|
this.eventListeners[event] = [];
|
||||||
|
}
|
||||||
|
this.eventListeners[event].push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
off(event: string, callback: (data: unknown) => void): void {
|
off(event: string, callback: (data: unknown) => void): void {
|
||||||
if (this.eventListeners[event]) {
|
if (this.eventListeners[event]) {
|
||||||
this.eventListeners[event] = this.eventListeners[event].filter(
|
this.eventListeners[event] = this.eventListeners[event].filter(
|
||||||
@@ -244,31 +219,7 @@ class SocketService {
|
|||||||
this.eventListeners[event].forEach((callback) => callback(data));
|
this.eventListeners[event].forEach((callback) => callback(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect socket
|
|
||||||
disconnect(): void {
|
|
||||||
if (this.socket) {
|
|
||||||
this.socket.disconnect();
|
|
||||||
this.socket = null;
|
|
||||||
}
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if socket is connected
|
// ایجاد instance singleton
|
||||||
isConnected(): boolean {
|
|
||||||
return this.socket?.connected || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if server supports socket
|
|
||||||
getServerSupported(): boolean {
|
|
||||||
return this.isServerSupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get socket instance (for advanced usage)
|
|
||||||
getSocket(): Socket | null {
|
|
||||||
return this.socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create singleton instance
|
|
||||||
export const socketService = new SocketService();
|
export const socketService = new SocketService();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
getChatList,
|
getChatList,
|
||||||
getChatMessages,
|
|
||||||
createChat,
|
createChat,
|
||||||
markChatAsRead,
|
markChatAsRead,
|
||||||
|
getChatDetail,
|
||||||
} from "../service/Service";
|
} from "../service/Service";
|
||||||
import { CreateChatRequest } from "../types/ProfileTypes";
|
import { CreateChatRequest } from "../types/ProfileTypes";
|
||||||
|
|
||||||
@@ -17,19 +17,6 @@ export const useChatList = (options?: { enabled?: boolean }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get chat messages
|
|
||||||
export const useChatMessages = (
|
|
||||||
chatId: string,
|
|
||||||
options?: { enabled?: boolean }
|
|
||||||
) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["chat", "messages", chatId],
|
|
||||||
queryFn: () => getChatMessages(chatId),
|
|
||||||
enabled: options?.enabled ?? !!chatId,
|
|
||||||
staleTime: 1000 * 60 * 1, // 1 minute
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new chat
|
// Create new chat
|
||||||
export const useCreateChat = () => {
|
export const useCreateChat = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -52,6 +39,17 @@ export const useMarkChatAsRead = () => {
|
|||||||
console.log("📤 ارسال درخواست markAsRead برای:", chatId);
|
console.log("📤 ارسال درخواست markAsRead برای:", chatId);
|
||||||
return markChatAsRead(chatId);
|
return markChatAsRead(chatId);
|
||||||
},
|
},
|
||||||
|
retry: (failureCount, error: unknown) => {
|
||||||
|
// اگر ۴۰۴ است، retry نکن
|
||||||
|
const axiosError = error as { response?: { status?: number } };
|
||||||
|
if (axiosError?.response?.status === 404) {
|
||||||
|
console.log("ℹ️ چت یافت نشد یا قبلاً mark شده - retry نمیکنیم");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// برای سایر خطاها، حداکثر ۱ بار retry کن
|
||||||
|
return failureCount < 1;
|
||||||
|
},
|
||||||
|
retryDelay: 1000,
|
||||||
onSuccess: (_, chatId) => {
|
onSuccess: (_, chatId) => {
|
||||||
console.log(
|
console.log(
|
||||||
"✅ پیامها با موفقیت به عنوان خوانده شده علامتگذاری شدند:",
|
"✅ پیامها با موفقیت به عنوان خوانده شده علامتگذاری شدند:",
|
||||||
@@ -62,8 +60,21 @@ export const useMarkChatAsRead = () => {
|
|||||||
// Update messages to mark as read
|
// Update messages to mark as read
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", chatId] });
|
queryClient.invalidateQueries({ queryKey: ["chat", "messages", chatId] });
|
||||||
},
|
},
|
||||||
onError: (error, chatId) => {
|
onError: (error: unknown, chatId) => {
|
||||||
console.error("❌ خطا در markAsRead:", chatId, error);
|
console.error("❌ خطا در markAsRead:", chatId, error);
|
||||||
|
// اگر ۴۰۴ دریافت کردیم، شاید چت وجود ندارد یا قبلاً mark شده
|
||||||
|
const axiosError = error as { response?: { status?: number } };
|
||||||
|
if (axiosError?.response?.status === 404) {
|
||||||
|
console.log("ℹ️ چت یافت نشد یا قبلاً mark شده:", chatId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useChatDetail = (chatId?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["chat", "detail", chatId],
|
||||||
|
queryFn: () => getChatDetail(chatId!),
|
||||||
|
enabled: !!chatId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ export interface ChatMessage {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
content: string;
|
content: string;
|
||||||
sender: "customer" | "seller";
|
sender: "customer" | "seller";
|
||||||
|
senderRef: "User" | "Seller";
|
||||||
senderInfo: ChatUser | ChatShop;
|
senderInfo: ChatUser | ChatShop;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
status: "sent" | "delivered" | "read";
|
status: "sent" | "delivered" | "read";
|
||||||
|
|||||||
Reference in New Issue
Block a user