import { useState, useEffect, useRef, useMemo, type ReactNode } from "react"; import { motion, AnimatePresence } from "motion/react"; import { useAppState } from "@/pages/chat/state"; import { MessagePanel, type MessagePanelState } from "./panels/MessagePanel"; import { ChatMessages } from "./ChatMessages"; import { ChatInputWrapper } from "./ChatInputWrapper"; import { ProfileDialog } from "@/pages/chat/ui/ProfileDialog"; import { setGlobalMessageHandler } from "@/core/websocket"; import type { Message, WebSocketMessage } from "@/core/types"; import defaultAvatar from "@/images/default-avatar.png"; import { DMPanel } from "./panels/DMPanel"; import useCall from "@/pages/chat/hooks/useCall"; import { TypingIndicator } from "./TypingIndicator"; import { OnlineStatus } from "./OnlineStatus"; import { typingManager } from "@/core/typingManager"; import { PublicChatPanel } from "./panels/PublicChatPanel"; interface MessagePanelRendererProps { panel: MessagePanel | null; } function ChatHeaderText({ panel }: { panel: MessagePanel | null }) { const { chat, user } = useAppState(); const otherTypingUsers = useMemo(() => { return Array .from(chat.typingUsers.entries()) .filter(([userId, username]) => userId !== user.currentUser?.id && username) .map(([, username]) => username!); }, [chat.typingUsers, user.currentUser?.id]); let content: ReactNode; if (panel instanceof DMPanel) { const recipientId = panel.getRecipientId()!; const isTyping = chat.dmTypingUsers.get(recipientId); content = isTyping ? : ; } else if (panel instanceof PublicChatPanel && otherTypingUsers.length > 0) { content = ; } else { return null; } return
{content}
; } export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) { const { applyPendingPanel, chat, setProfileDialog } = useAppState(); const messagePanelRef = useRef(null); const [panelState, setPanelState] = useState(null); const messagesEndRef = useRef(null); const previousMessageCountRef = useRef(0); const [replyTo, setReplyTo] = useState(null); const [replyToVisible, setReplyToVisible] = useState(Boolean(replyTo)); const [editMessage, setEditMessage] = useState(null); const [editVisible, setEditVisible] = useState(Boolean(editMessage)); const [pendingAction, setPendingAction] = useState(null); const { initiateCall } = useCall(); // Drag & drop const [isDragging, setIsDragging] = useState(false); const dragCounterRef = useRef(0); const addFilesRef = useRef void)>(null); useEffect(() => { if (!panel || !panelState) return; return () => { dragCounterRef.current = 0; setIsDragging(false); }; }, [panel, panelState]); useEffect(() => { if (replyTo) { setReplyToVisible(true); } }, [replyTo]); useEffect(() => { if (editMessage) { setEditVisible(true); } }, [editMessage]); // Handle panel state changes useEffect(() => { if (panel) { setPanelState(panel.getState()); // Store the handler for cleanup panel.onStateChange = (newState: MessagePanelState) => { setPanelState(newState); }; // Set up WebSocket message handler for this panel if (panel.handleWebSocketMessage) { setGlobalMessageHandler((message: WebSocketMessage) => panel.handleWebSocketMessage(message)); } } else { setPanelState(null); setGlobalMessageHandler(null); } return () => { if (panel) { if (panel.onStateChange) { panel.onStateChange = null; } if (typeof panel.destroy === 'function') { panel.destroy(); } } }; }, [panel]); // Handle chat switching animation useEffect(() => { if (chat.isSwitching && chat.pendingPanel) { // Apply pending panel when animation starts applyPendingPanel(); // End switching state after a brief delay to allow animation setTimeout(() => { chat.setIsSwitching(false); }, 200); } }, [chat.isSwitching, chat.pendingPanel, applyPendingPanel]); // Load messages when panel changes and animation is not running useEffect(() => { if (!chat.activePanel || chat.isSwitching) return; const panelState = chat.activePanel.getState(); if (panelState.messages.length === 0 && !panelState.isLoading) { chat.activePanel.loadMessages(); } }, [chat.activePanel, chat.isSwitching]); // Scroll to bottom only when new messages are added useEffect(() => { if (!panelState || chat.isSwitching) return; const currentMessageCount = panelState.messages.length; const previousMessageCount = previousMessageCountRef.current; const el = messagesEndRef.current; if (!el) return; // Scroll without animation when messages are initially loaded if (previousMessageCount === 0 && currentMessageCount > 0 && !panelState.isLoading) { el.scrollIntoView({ behavior: "instant", block: "end" }); } // Scroll with animation when a new message is added else if (currentMessageCount > previousMessageCount && previousMessageCount > 0) { // Defer to next frame to ensure layout is stable const id = requestAnimationFrame(() => { el.scrollIntoView({ behavior: "smooth", block: "end" }); }); return () => cancelAnimationFrame(id); } // Update the previous message count previousMessageCountRef.current = currentMessageCount; }, [panelState?.messages, panelState?.isLoading, chat.isSwitching]); function handleCallClick() { if (panel && panelState && panel.isDm()) { const dmPanel = panel as DMPanel; const userId = dmPanel.getDMUserId(); const username = dmPanel.getDMUsername(); if (userId && username) { initiateCall(userId, username); } } }; async function handleProfileClick() { if (!panel) return; try { const profileData = await panel.getProfile(); if (profileData) { setProfileDialog(profileData); } } catch (error) { console.error("Failed to get profile:", error); } } const panelKey = chat.activePanel?.getState().title || "empty"; return (
{ if (!e.dataTransfer) return; e.preventDefault(); e.stopPropagation(); dragCounterRef.current += 1; // Only show overlay when actual files are dragged const hasFiles = Array.from(e.dataTransfer.types || []).includes("Files"); if (hasFiles) setIsDragging(true); } : undefined} onDragOver={panel ? (e) => { if (!e.dataTransfer) return; e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = "copy"; } : undefined} onDragLeave={panel ? (e) => { e.preventDefault(); e.stopPropagation(); dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); if (dragCounterRef.current === 0) setIsDragging(false); } : undefined} onDrop={panel ? (e) => { if (!e.dataTransfer) return; e.preventDefault(); e.stopPropagation(); const files = Array.from(e.dataTransfer.files || []); if (files.length > 0 && addFilesRef.current) { addFilesRef.current(files); } setIsDragging(false); dragCounterRef.current = 0; } : undefined}>
Avatar

{panelState?.title || "Выбор чата"}

{panel?.isDm() && ( )}
{panelState?.isLoading ? (
Загрузка сообщений...
) : panelState && panel ? ( { if (editMessage || editVisible) { setPendingAction({ type: "reply", message: message }); setEditVisible(false); // onCloseEdit will apply pending } else { setReplyTo(message); } }} onEditSelect={(message) => { if (replyTo || replyToVisible) { setPendingAction({ type: "edit", message: message }); setReplyToVisible(false); // onCloseReply will apply pending } else { setEditMessage(message); } }} onDelete={(id) => panel.handleDeleteMessage(id)} onRetryMessage={(id) => panel.retryMessage(id)} >
) : (
Выберите чат на боковой панели, чтобы начать переписку
)} {panel && ( <> {isDragging && ( e.preventDefault()} onDrop={(e) => e.preventDefault()} >
Отпустите файл(ы) для добавления
)}
{ panel.handleSendMessage(text, replyTo?.id, files); setReplyTo(null); }} onSaveEdit={(content) => { if (editMessage) { panel.handleEditMessage(editMessage.id, content); setEditMessage(null); } }} replyTo={replyTo} replyToVisible={replyToVisible} onClearReply={() => { setPendingAction(null); setReplyToVisible(false); }} onCloseReply={() => { setReplyTo(null); if (pendingAction && pendingAction.type === "edit") { setEditMessage(pendingAction.message); setPendingAction(null); } }} editingMessage={editMessage} editVisible={editVisible} onClearEdit={() => { setPendingAction(null); setEditVisible(false); }} onCloseEdit={() => { setEditMessage(null); if (pendingAction && pendingAction.type === "reply") { setReplyTo(pendingAction.message); setPendingAction(null); } }} onProvideFileAdder={(adder) => { addFilesRef.current = adder; }} messagePanelRef={messagePanelRef} onTyping={() => { if (panel.isDm()) { const dmPanel = panel as DMPanel; dmPanel.handleTyping(); } else { typingManager.sendTyping(); } }} onStopTyping={() => { if (panel.isDm()) { const dmPanel = panel as DMPanel; typingManager.stopDmTypingOnMessage(dmPanel.getRecipientId()!); } else { typingManager.stopTypingOnMessage(); } }} /> )} {/* Profile Dialog */}
); }