tweaks to chat

This commit is contained in:
Sewmina 2025-06-02 08:26:56 +05:30
parent c59ef43710
commit 7273a28aee
3 changed files with 160 additions and 24 deletions

View File

@ -31,6 +31,31 @@ body {
font-weight: 400;
font-style: normal;
}
/* Chat notification animation */
@keyframes fade-in-out {
0% {
opacity: 0;
transform: translateX(20px);
}
10% {
opacity: 1;
transform: translateX(0);
}
90% {
opacity: 1;
transform: translateX(0);
}
100% {
opacity: 0;
transform: translateX(20px);
}
}
.animate-fade-in-out {
animation: fade-in-out 3s ease-in-out forwards;
}
/* Grid animation */
@keyframes scrollGrid {
from {

View File

@ -1,9 +1,10 @@
"use client";
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import { usePrivy } from '@privy-io/react-auth';
import { io, Socket } from 'socket.io-client';
import { toast } from 'sonner';
import { fetchUserById } from '@/shared/data_fetcher';
import { ChatBubbleLeftIcon, XMarkIcon } from '@heroicons/react/24/outline';
interface ChatMessage {
id: string;
@ -12,18 +13,38 @@ interface ChatMessage {
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(process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3001', {
const socketInstance = io('http://api.duelfi.io:3040', {
auth: {
token: user?.id // Using Privy user ID as authentication
}
@ -31,24 +52,67 @@ export default function GlobalChat() {
socketInstance.on('connect', () => {
setIsConnected(true);
toast.success('Connected to chat');
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
toast.error('Disconnected from chat');
});
socketInstance.on('chat message', (message: ChatMessage) => {
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]);
}, [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();
@ -73,24 +137,64 @@ export default function GlobalChat() {
);
}
if (isCollapsed) {
return (
<div className="fixed bottom-4 right-4 flex items-center gap-2">
{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">
<div className="p-4 border-b border-gray-700">
<h3 className="text-lg font-semibold text-white">Global Chat</h3>
<div className="text-sm text-gray-400">
{isConnected ? 'Connected' : 'Disconnected'}
<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="bg-[rgb(40,40,40)] p-2 rounded">
<div className="text-sm text-gray-400">
{msg.user === user?.id ? 'You' : `User ${msg.user.slice(0, 6)}`}
<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">{msg.message}</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={sendMessage} className="p-4 border-t border-gray-700">

View File

@ -215,13 +215,26 @@ export default function PrivyButton() {
};
useEffect(() => {
const intervalId = setInterval(() => {
if (wallets) {
fetchSolBalance();
}, 15000); // 5000 milliseconds = 5 seconds
}
}, [user, wallets]);
useEffect(() => {
const intervalId = setInterval(() => {
if (wallets && solWallet) {
fetchSolBalance();
}
}, 15000); // 15000 milliseconds = 15 seconds
// Cleanup function to clear the interval on unmount or when `ready` changes
// Initial fetch
if (wallets && solWallet) {
fetchSolBalance();
}
// Cleanup function to clear the interval on unmount or when dependencies change
return () => clearInterval(intervalId);
}, [ready]);
}, [wallets, solWallet]);
const saveProfileChanges = async () => {
if (!user) {
@ -273,12 +286,6 @@ export default function PrivyButton() {
}
};
useEffect(() => {
if (wallets) {
fetchSolBalance();
}
}, [user, wallets]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {