import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import type { Message as MessageModel } from "@/api/generated/private/models/message"; import { useApiSocialChatsRetrieve } from "@/api/generated/private/chat/chat"; import { useInfiniteMessages } from "@/hooks/useInfiniteMessages"; import { useChatSocket, type ChatSocketEvent } from "@/hooks/useChatSocket"; import { useIntersectionLoader } from "@/hooks/useIntersectionLoader"; import { messagesQueryKey, type CursorPaginated } from "@/api/social/feed"; import Message from "@/components/social/chat/Message"; import MessageComposer from "@/components/social/chat/MessageComposer"; import Spinner from "@/components/ui/Spinner"; import EmptyState from "@/components/ui/EmptyState"; import Avatar from "@/components/ui/Avatar"; export default function ChatRoomPage() { const { t } = useTranslation("social"); const { chatId: chatIdParam } = useParams<{ chatId: string }>(); const chatId = Number(chatIdParam); const queryClient = useQueryClient(); const { data: chat } = useApiSocialChatsRetrieve(String(chatId)); const { messages, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteMessages({ chatId }); const [replyTo, setReplyTo] = useState(null); const [typingUsers, setTypingUsers] = useState([]); const bottomRef = useRef(null); // Top sentinel triggers loading older messages (scroll-back). const topSentinelRef = useIntersectionLoader( () => { if (hasNextPage && !isFetchingNextPage) void fetchNextPage(); }, { enabled: hasNextPage && !isLoading }, ); // Append a freshly received message into the cursor cache. const appendMessage = useCallback( (msg: MessageModel) => { queryClient.setQueryData<{ pages: CursorPaginated[]; pageParams: unknown[]; }>(messagesQueryKey(chatId), (old) => { if (!old) return old as never; const [first, ...rest] = old.pages; if (!first) return old; if (first.results.some((m) => m.id === msg.id)) return old; return { ...old, pages: [{ ...first, results: [msg, ...first.results] }, ...rest], }; }); }, [queryClient, chatId], ); const removeMessage = useCallback( (messageId: number) => { queryClient.setQueryData<{ pages: CursorPaginated[]; pageParams: unknown[]; }>(messagesQueryKey(chatId), (old) => { if (!old) return old as never; return { ...old, pages: old.pages.map((p) => ({ ...p, results: p.results.filter((m) => m.id !== messageId), })), }; }); }, [queryClient, chatId], ); const handleSocketEvent = useCallback( (event: ChatSocketEvent) => { if (event.type === "new_chat_message" || event.type === "new_reply_chat_message") { appendMessage({ id: event.message_id, chat: chatId, sender: null, reply_to: event.type === "new_reply_chat_message" ? event.reply_to_id : null, content: event.message, is_edited: false, edited_at: null, created_at: new Date(), updated_at: new Date(), media_files: [], reactions: [], }); } else if (event.type === "delete_chat_message") { removeMessage(event.message_id); } else if (event.type === "typing") { setTypingUsers((prev) => event.is_typing ? prev.includes(event.user) ? prev : [...prev, event.user] : prev.filter((u) => u !== event.user), ); } else if (event.type === "stop_typing") { setTypingUsers((prev) => prev.filter((u) => u !== event.user)); } }, [appendMessage, removeMessage, chatId], ); const { status, sendMessage, sendReply, sendReaction, sendTyping } = useChatSocket({ chatId: Number.isFinite(chatId) ? chatId : null, onEvent: handleSocketEvent, }); // Auto-scroll to bottom when new messages arrive. useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "auto" }); }, [messages.length]); function handleSend(text: string, replyToId?: number): boolean { return replyToId ? sendReply(text, replyToId) : sendMessage(text); } if (!Number.isFinite(chatId)) { return ; } return (
{chat?.name || `Chat #${chatId}`}
{status !== "open" && (
{t("chat.room.disconnected")}
)}
{hasNextPage && (
{isFetchingNextPage ? ( ) : ( {t("chat.room.loadingHistory")} )}
)} {isLoading && (
)} {!isLoading && messages.length === 0 && ( )} {messages.map((m) => ( sendReaction(msg.id, emoji)} /> ))}
{typingUsers.length > 0 && (
{typingUsers.length === 1 ? t("chat.room.typing", { user: typingUsers[0] }) : t("chat.room.typingMany")}
)} setReplyTo(null)} onSend={handleSend} onTyping={sendTyping} />
); }