import { useGetThreadDetails, useGetThreadMessages, setMessagesCache, deleteMessageFromCache, } from "../../requestHandler/use-chat-hooks"; import { useState, useRef, useEffect } from "react"; import { Loader2 } from "lucide-react"; import { useServiceGetProfile } from "../../requestHandler/use-profile-hooks"; import MessageList from "./MessageList"; import ChatHeader from "./ChatHeader"; import MessageInput from "./MessageInput"; import { MessageList as MessageListType, ThreadDetail, ThreadParticipant, } from "../../../src/api/types/models"; import useWebSocket from "../../hooks/useWebSocket"; import { useQueryClient } from "@tanstack/react-query"; import { toast } from "../../hooks/use-toast"; interface ThreadChatPageProps { threadId: string; } export default function ThreadChatPage({ threadId }: ThreadChatPageProps) { // Scroll to bottom on initial load const [isInitialLoad, setIsInitialLoad] = useState(true); const { data: threadDetails, isLoading: isLoadingThread } = useGetThreadDetails(threadId); const [replyToMessage, setReplyToMessage] = useState< MessageListType | undefined >(undefined); // Load messages using infinite query const { data: messagesData, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading: isLoadingMessages, } = useGetThreadMessages(threadId); // Flatten all pages of messages into a single array const allMessages = messagesData?.pages ? messagesData.pages.flatMap((page) => page.results) : []; const messageListRef = useRef<{ scrollToBottom: () => void; getContainer: () => HTMLDivElement | null; }>(null); const inputRef = useRef(null); const [messageText, setMessageText] = useState(""); const { data: user } = useServiceGetProfile(); const [prevScrollHeight, setPrevScrollHeight] = useState(0); const queryClient = useQueryClient(); // WebSocket implementation const { isConnected, sendWebSocketMessage, markMessagesAsRead, deleteMessage, newMessage, readReceipt, messageDeleted, resetReadReceipt, resetNewMessage, resetMessageDeleted, } = useWebSocket(threadId, user?.id); // Handle new messages from WebSocket useEffect(() => { if (newMessage) { setMessagesCache(queryClient, threadId, { id: newMessage.message_id, content: newMessage.content, sender: newMessage.sender_id, createdAt: new Date().toISOString(), replyTo: newMessage.reply_to_id, replyContent: newMessage.reply_content || "", imageUrl: newMessage.image_url || undefined, }); resetNewMessage(); setTimeout(() => { if (messageListRef.current) { messageListRef.current.scrollToBottom(); } }, 100); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [newMessage]); const otherParticipant = threadDetails?.participants?.find( (participant) => participant.username !== user?.username ) || undefined; useEffect(() => { if (isConnected && !isLoadingMessages && allMessages.length > 0) { markMessagesAsRead(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isConnected, isLoadingMessages, allMessages.length]); useEffect(() => { return () => { queryClient.removeQueries({ queryKey: ["getThreadMessages", threadId], }); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (readReceipt) { queryClient.setQueryData( ["getThreadDetails", threadId], (oldData: ThreadDetail) => { if (!oldData) return oldData; return { ...oldData, participants: oldData.participants?.map( (participant: ThreadParticipant) => { if ( participant.username && participant.username.toString() === readReceipt.reader_username ) { return { ...participant, lastReadAt: new Date().toISOString(), }; } return participant; } ), }; } ); resetReadReceipt(); } }, [readReceipt, threadId, queryClient, resetReadReceipt]); useEffect(() => { if ( !isLoadingMessages && messageListRef.current && allMessages.length > 0 && isConnected ) { if (isInitialLoad) { setIsInitialLoad(false); setTimeout(() => { if (messageListRef.current) { messageListRef.current.scrollToBottom(); } }, 100); } } }, [isLoadingMessages, allMessages.length, isInitialLoad, isConnected]); // Add effect to handle message deleted events useEffect(() => { if (messageDeleted) { deleteMessageFromCache(queryClient, threadId, messageDeleted.message_id); resetMessageDeleted(); const isOwner = messageDeleted.sender_id === user?.id; if (isOwner) { toast({ title: "پیام با موفقیت حذف شد", description: "پیام شما با موفقیت حذف شد.", }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [messageDeleted]); // Handle scroll to load more messages const handleScroll = () => { if (!messageListRef.current || isInitialLoad) return; const container = messageListRef.current.getContainer(); if (!container) return; const { scrollTop } = container; // Load more messages when scrolled near the top if (scrollTop < 100 && hasNextPage && !isFetchingNextPage) { // Save the current scroll height before loading more messages setPrevScrollHeight(container.scrollHeight); fetchNextPage(); } }; // Maintain scroll position after loading more messages useEffect(() => { if (!isFetchingNextPage && prevScrollHeight > 0 && messageListRef.current) { const container = messageListRef.current.getContainer(); if (!container) return; // Calculate the new position by finding the difference in height const newScrollHeight = container.scrollHeight; const scrollDifference = newScrollHeight - prevScrollHeight; // Adjust scroll position to maintain the view container.scrollTop = scrollDifference; // Reset the previous height setPrevScrollHeight(0); } }, [isFetchingNextPage, prevScrollHeight]); // Handle sending message const handleSendMessage = (text: string) => { if (isConnected) { sendWebSocketMessage(text, replyToMessage?.id, replyToMessage?.content); } setReplyToMessage(undefined); setMessageText(""); }; // Send an image the user already uploaded. Content stays empty; the bubble // renders the image with no accompanying text. const handleSendImage = (imageUrl: string) => { if (isConnected) { sendWebSocketMessage( "", replyToMessage?.id, replyToMessage?.content, imageUrl, ); } setReplyToMessage(undefined); }; // Team membership check: current user in teamUserIds → can attach images. const currentUserIsTeam = !!( user?.id && threadDetails?.teamUserIds?.includes(user.id) ); // Handle reply to message const handleReplyClick = (message: MessageListType) => { setReplyToMessage(message); // Focus on the input field setTimeout(() => { if (inputRef.current) { inputRef.current.focus(); } }, 0); }; // Handle message deletion const handleDeleteMessage = (messageId: string) => { if (isConnected) { deleteMessage(messageId); } }; return (
{!isConnected ? (

در حال اتصال به سرور پیام‌رسانی...

) : ( <>
{/* Messages list */}
{/* Message input component */} setReplyToMessage(undefined)} inputRef={inputRef} /> )}
); }