219 lines
7.0 KiB
TypeScript
219 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useRef } from 'react';
|
|
import { usePrivy } from '@privy-io/react-auth';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { fetchUserById } from '@/shared/data_fetcher';
|
|
import { ChatBubbleLeftIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
|
import { profanity } from '@2toad/profanity';
|
|
interface ChatMessage {
|
|
id: string;
|
|
user: string;
|
|
message: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface UserData {
|
|
id: string;
|
|
username: string;
|
|
bio: string;
|
|
x_profile_url: string;
|
|
}
|
|
|
|
export default function GlobalChat() {
|
|
const { user, authenticated } = usePrivy();
|
|
const [socket, setSocket] = useState<Socket | null>(null);
|
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
const [newMessage, setNewMessage] = useState('');
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [userData, setUserData] = useState<Record<string, UserData>>({});
|
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
|
const [notification, setNotification] = useState<{ message: string; username: string } | null>(null);
|
|
const notificationTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
const scrollToBottom = () => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
};
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [messages]);
|
|
|
|
useEffect(() => {
|
|
if (!authenticated) return;
|
|
|
|
// Initialize socket connection
|
|
const socketInstance = io('https://wschat.duelfi.io', {
|
|
auth: {
|
|
token: user?.id // Using Privy user ID as authentication
|
|
}
|
|
});
|
|
|
|
socketInstance.on('connect', () => {
|
|
setIsConnected(true);
|
|
});
|
|
|
|
socketInstance.on('disconnect', () => {
|
|
setIsConnected(false);
|
|
});
|
|
|
|
socketInstance.on('chat message', async (message: ChatMessage) => {
|
|
setMessages(prev => [...prev, message]);
|
|
// Fetch user data if we haven't already
|
|
if (!userData[message.user]) {
|
|
const userInfo = await fetchUserById(message.user);
|
|
if (userInfo) {
|
|
setUserData(prev => ({
|
|
...prev,
|
|
[message.user]: userInfo
|
|
}));
|
|
// Show notification if chat is collapsed
|
|
if (isCollapsed && message.user !== user?.id) {
|
|
showNotification(message.message, userInfo.username);
|
|
}
|
|
}
|
|
} else if (isCollapsed && message.user !== user?.id) {
|
|
// Show notification if chat is collapsed and we already have the user data
|
|
showNotification(message.message, userData[message.user].username);
|
|
}
|
|
});
|
|
|
|
socketInstance.on('recent messages', (messages: ChatMessage[]) => {
|
|
setMessages(messages);
|
|
messages.forEach(message => {
|
|
if (!userData[message.user]) {
|
|
fetchUserById(message.user).then(userInfo => {
|
|
setUserData(prev => ({ ...prev, [message.user]: userInfo }));
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
setSocket(socketInstance);
|
|
|
|
return () => {
|
|
socketInstance.disconnect();
|
|
if (notificationTimeoutRef.current) {
|
|
clearTimeout(notificationTimeoutRef.current);
|
|
}
|
|
};
|
|
}, [authenticated, user, isCollapsed]);
|
|
|
|
const showNotification = (message: string, username: string) => {
|
|
// Clear any existing notification timeout
|
|
if (notificationTimeoutRef.current) {
|
|
clearTimeout(notificationTimeoutRef.current);
|
|
}
|
|
|
|
setNotification({ message, username });
|
|
|
|
// Set new timeout to clear notification
|
|
notificationTimeoutRef.current = setTimeout(() => {
|
|
setNotification(null);
|
|
}, 3000);
|
|
};
|
|
|
|
const sendMessage = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!socket || !newMessage.trim() || !user) return;
|
|
|
|
const message: ChatMessage = {
|
|
id: Date.now().toString(),
|
|
user: user.id,
|
|
message: newMessage.trim(),
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
socket.emit('chat message', message);
|
|
setNewMessage('');
|
|
};
|
|
|
|
if (!authenticated) {
|
|
return (
|
|
<div className="p-4 text-center text-gray-400">
|
|
Please log in to join the chat
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isCollapsed) {
|
|
return (
|
|
<div className="fixed bottom-4 right-4 flex items-center gap-2 z-50">
|
|
{notification && (
|
|
<div className="bg-[rgb(40,40,40)] text-white px-4 py-2 rounded-lg shadow-lg animate-fade-in-out">
|
|
<div className="text-sm font-semibold">{notification.username}</div>
|
|
<div className="text-sm text-gray-300">{notification.message}</div>
|
|
</div>
|
|
)}
|
|
<button
|
|
onClick={() => setIsCollapsed(false)}
|
|
className="bg-[rgb(248,144,22)] text-black p-4 rounded-full shadow-lg hover:scale-110 transition-all duration-300"
|
|
>
|
|
<ChatBubbleLeftIcon className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="fixed bottom-4 right-4 w-80 bg-[rgb(30,30,30)] rounded-lg shadow-lg z-50">
|
|
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-white">Global Chat</h3>
|
|
<div className="text-sm text-gray-400">
|
|
{isConnected ? 'Connected' : 'Disconnected'}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsCollapsed(true)}
|
|
className="text-gray-400 hover:text-white transition-colors"
|
|
>
|
|
<XMarkIcon className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="h-96 overflow-y-auto p-4 space-y-2">
|
|
{messages.map((msg) => (
|
|
<div
|
|
key={msg.id}
|
|
className={`p-2 rounded ${
|
|
msg.user === user?.id
|
|
? 'bg-[rgb(100,60,10)] bg-opacity-20'
|
|
: 'bg-[rgb(40,40,40)]'
|
|
}`}
|
|
>
|
|
<div className="text-sm text-gray-400 flex justify-between items-center">
|
|
<span>
|
|
{msg.user === user?.id ? 'You' : userData[msg.user]?.username || `User ${msg.user.slice(0, 6)}`}
|
|
</span>
|
|
<span className="text-xs">
|
|
{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</span>
|
|
</div>
|
|
<div className="text-white">{profanity.censor(msg.message)}</div>
|
|
</div>
|
|
))}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
<form onSubmit={sendMessage} className="p-4 border-t border-gray-700">
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={newMessage}
|
|
onChange={(e) => setNewMessage(e.target.value)}
|
|
placeholder="Type a message..."
|
|
className="flex-1 bg-[rgb(40,40,40)] text-white px-3 py-2 rounded focus:outline-none focus:ring-2 focus:ring-orange-500"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600 transition-colors"
|
|
>
|
|
Send
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|