import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { useState, useRef, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { MessageCircle, Send, Plus, Search, Users, Check, CheckCheck, Loader2, Phone, Video, MoreVertical, Smile, Paperclip, Volume2, VolumeX, Bell } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { io, Socket } from "socket.io-client"; import { toast } from "sonner"; interface ThreadParticipant { id: string; username: string; name: string | null; } interface ChatMessage { id: number; threadId: number; senderId: string | null; body: string; messageType: string | null; status: string | null; sentAt: string; senderName: string | null; senderUsername: string | null; } interface ChatThread { id: number; type: string; name: string | null; participants: ThreadParticipant[]; lastMessage?: ChatMessage; unreadCount: number; latestMessageAt: string | null; } interface User { id: string; username: string; name: string | null; } async function fetchThreads(): Promise { const response = await fetch("/api/chat/threads", { credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch threads"); return response.json(); } async function fetchThread(id: number): Promise<{ messages: ChatMessage[]; participants: ThreadParticipant[] }> { const response = await fetch(`/api/chat/threads/${id}`, { credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch thread"); return response.json(); } async function fetchUsers(): Promise { const response = await fetch("/api/chat/users", { credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch users"); return response.json(); } async function createDirectThread(userId: string): Promise { const response = await fetch("/api/chat/threads/direct", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId }), credentials: "include", }); if (!response.ok) throw new Error("Failed to create thread"); return response.json(); } export default function Chat() { const queryClient = useQueryClient(); const { user } = useAuth(); const [selectedThread, setSelectedThread] = useState(null); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [socket, setSocket] = useState(null); const [onlineUsers, setOnlineUsers] = useState>(new Set()); const [typingUsers, setTypingUsers] = useState>>(new Map()); const [showNewChatDialog, setShowNewChatDialog] = useState(false); const [isMuted, setIsMuted] = useState(() => { const stored = localStorage.getItem("chat-muted"); return stored === "true"; }); const messagesEndRef = useRef(null); const typingTimeoutRef = useRef(null); const audioRef = useRef(null); useEffect(() => { audioRef.current = new Audio("data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2teleQsQKZfZ0KQPCQQ4odPQnAoDBD+k0syXCQQFQqfQyJMIBAZFqc7EkQcDBkiqzMCPBgMHSKnKvI0FAwdJqci5iwQDB0mpxrWJAwMHS6jEsYcCAwdMp8KuhQIDB0ynwKqDAQMHTabAp4EBAwdNpr6kfwEDB06mvqJ9AQMHTqW+oHsCAwdPpb2eegIDB0+lvJx4AgMHT6S8mnYCAwdQpLuYdQIDB1Cku5ZzAgMHUKS7lHEBAwdRo7uScAEDB1GjupBvAQMHUqO6jm0BAwdSoruMawEDB1Kiuopp"); }, []); const playNotificationSound = () => { if (!isMuted && audioRef.current) { audioRef.current.currentTime = 0; audioRef.current.play().catch(() => {}); } }; const toggleMute = () => { setIsMuted(prev => { const newVal = !prev; localStorage.setItem("chat-muted", String(newVal)); return newVal; }); }; const { data: threads = [], isLoading: loadingThreads } = useQuery({ queryKey: ["chat-threads"], queryFn: fetchThreads, }); const { data: threadData } = useQuery({ queryKey: ["chat-thread", selectedThread], queryFn: () => selectedThread ? fetchThread(selectedThread) : null, enabled: !!selectedThread, }); const { data: users = [] } = useQuery({ queryKey: ["chat-users"], queryFn: fetchUsers, }); useEffect(() => { if (threadData?.messages) { setMessages(threadData.messages); } }, [threadData]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); useEffect(() => { if (!user) return; const newSocket = io({ path: "/socket.io", }); newSocket.on("connect", () => { newSocket.emit("user:join", { id: user.id, username: user.username }); }); newSocket.on("users:online", (users: { userId: string }[]) => { setOnlineUsers(new Set(users.map(u => u.userId))); }); newSocket.on("user:online", ({ userId }: { userId: string }) => { setOnlineUsers(prev => new Set([...prev, userId])); }); newSocket.on("user:offline", ({ userId }: { userId: string }) => { setOnlineUsers(prev => { const newSet = new Set(prev); newSet.delete(userId); return newSet; }); }); newSocket.on("message:new", (message: ChatMessage) => { if (message.threadId === selectedThread) { setMessages(prev => [...prev, message]); } queryClient.invalidateQueries({ queryKey: ["chat-threads"] }); if (message.senderId !== user?.id) { playNotificationSound(); toast(
{message.senderName?.[0]?.toUpperCase() || message.senderUsername?.[0]?.toUpperCase() || "?"}
{message.senderName || message.senderUsername}
{message.body}
, { duration: 4000, position: "top-right", } ); } }); newSocket.on("typing:update", ({ threadId, userId, isTyping }: { threadId: number; userId: string; isTyping: boolean }) => { setTypingUsers(prev => { const newMap = new Map(prev); const threadTyping = newMap.get(threadId) || new Set(); if (isTyping) { threadTyping.add(userId); } else { threadTyping.delete(userId); } newMap.set(threadId, threadTyping); return newMap; }); }); setSocket(newSocket); return () => { newSocket.disconnect(); }; }, [user, selectedThread, queryClient]); useEffect(() => { if (socket && selectedThread) { socket.emit("thread:join", selectedThread); socket.emit("thread:read", selectedThread); } }, [socket, selectedThread]); const handleSendMessage = () => { if (!input.trim() || !selectedThread || !socket) return; socket.emit("message:send", { threadId: selectedThread, body: input, }); setInput(""); if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } socket.emit("typing:stop", { threadId: selectedThread }); }; const handleInputChange = (e: React.ChangeEvent) => { setInput(e.target.value); if (socket && selectedThread) { socket.emit("typing:start", { threadId: selectedThread }); if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } typingTimeoutRef.current = setTimeout(() => { socket.emit("typing:stop", { threadId: selectedThread }); }, 2000); } }; const handleStartChat = async (targetUserId: string) => { try { const thread = await createDirectThread(targetUserId); queryClient.invalidateQueries({ queryKey: ["chat-threads"] }); setSelectedThread(thread.id); setShowNewChatDialog(false); } catch (error) { console.error("Error starting chat:", error); } }; const getThreadDisplayName = (thread: ChatThread) => { if (thread.type === "group" && thread.name) { return thread.name; } const otherParticipant = thread.participants.find(p => p.id !== user?.id); return otherParticipant?.name || otherParticipant?.username || "Conversa"; }; const getInitials = (name: string | null | undefined) => { if (!name) return "?"; return name.split(" ").map(n => n[0]).join("").toUpperCase().slice(0, 2); }; const formatTime = (dateString: string) => { const date = new Date(dateString); return date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }); }; const selectedThreadData = threads.find(t => t.id === selectedThread); return (
{getInitials(user?.name || user?.username)} Chat Interno
Nova Conversa
{users.map((u) => (
handleStartChat(u.id)} data-testid={`user-${u.id}`} > {getInitials(u.name || u.username)}
{u.name || u.username}
@{u.username}
{onlineUsers.has(u.id) && (
)}
))}
{loadingThreads ? (
) : threads.length === 0 ? (

Nenhuma conversa ainda

Clique em + para iniciar uma conversa

) : (
{threads.map((thread) => { const displayName = getThreadDisplayName(thread); const otherParticipant = thread.participants.find(p => p.id !== user?.id); const isOnline = otherParticipant && onlineUsers.has(otherParticipant.id); return (
setSelectedThread(thread.id)} data-testid={`thread-${thread.id}`} >
{thread.type === "group" ? ( ) : ( getInitials(displayName) )} {isOnline && (
)}
{displayName} {thread.lastMessage && ( {formatTime(thread.lastMessage.sentAt)} )}
{thread.lastMessage && ( <> {thread.lastMessage.senderId === user?.id && ( )} {thread.lastMessage.body} )}
{thread.unreadCount > 0 && (
{thread.unreadCount}
)}
); })}
)}
{!selectedThread ? (

Chat Interno Arcádia

Envie e receba mensagens com sua equipe em tempo real.

) : ( <>
{selectedThreadData?.type === "group" ? ( ) : ( getInitials(selectedThreadData ? getThreadDisplayName(selectedThreadData) : "") )}
{selectedThreadData ? getThreadDisplayName(selectedThreadData) : ""}
{typingUsers.get(selectedThread)?.size ? ( digitando... ) : ( "online" )}
{messages.map((msg) => { const isMe = msg.senderId === user?.id; return (
{!isMe && selectedThreadData?.type === "group" && (
{msg.senderName || msg.senderUsername}
)}

{msg.body}

{formatTime(msg.sentAt)} {isMe && ( )}
); })}
e.key === "Enter" && !e.shiftKey && handleSendMessage()} placeholder="Digite uma mensagem" className="flex-1 bg-[#2a3942] border-0 text-white placeholder:text-[#8696a0] focus-visible:ring-0 rounded-lg" data-testid="input-message" />
)}
); }