tweaks to chat
This commit is contained in:
parent
c59ef43710
commit
7273a28aee
|
|
@ -31,6 +31,31 @@ body {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
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 */
|
/* Grid animation */
|
||||||
@keyframes scrollGrid {
|
@keyframes scrollGrid {
|
||||||
from {
|
from {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { usePrivy } from '@privy-io/react-auth';
|
import { usePrivy } from '@privy-io/react-auth';
|
||||||
import { io, Socket } from 'socket.io-client';
|
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 {
|
interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -12,18 +13,38 @@ interface ChatMessage {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
bio: string;
|
||||||
|
x_profile_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function GlobalChat() {
|
export default function GlobalChat() {
|
||||||
const { user, authenticated } = usePrivy();
|
const { user, authenticated } = usePrivy();
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!authenticated) return;
|
if (!authenticated) return;
|
||||||
|
|
||||||
// Initialize socket connection
|
// Initialize socket connection
|
||||||
const socketInstance = io(process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3001', {
|
const socketInstance = io('http://api.duelfi.io:3040', {
|
||||||
auth: {
|
auth: {
|
||||||
token: user?.id // Using Privy user ID as authentication
|
token: user?.id // Using Privy user ID as authentication
|
||||||
}
|
}
|
||||||
|
|
@ -31,24 +52,67 @@ export default function GlobalChat() {
|
||||||
|
|
||||||
socketInstance.on('connect', () => {
|
socketInstance.on('connect', () => {
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
toast.success('Connected to chat');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socketInstance.on('disconnect', () => {
|
socketInstance.on('disconnect', () => {
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
toast.error('Disconnected from chat');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socketInstance.on('chat message', (message: ChatMessage) => {
|
socketInstance.on('chat message', async (message: ChatMessage) => {
|
||||||
setMessages(prev => [...prev, message]);
|
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);
|
setSocket(socketInstance);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socketInstance.disconnect();
|
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);
|
||||||
};
|
};
|
||||||
}, [authenticated, user]);
|
|
||||||
|
|
||||||
const sendMessage = (e: React.FormEvent) => {
|
const sendMessage = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
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 (
|
return (
|
||||||
<div className="fixed bottom-4 right-4 w-80 bg-[rgb(30,30,30)] rounded-lg shadow-lg">
|
<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">
|
<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>
|
<h3 className="text-lg font-semibold text-white">Global Chat</h3>
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-gray-400">
|
||||||
{isConnected ? 'Connected' : 'Disconnected'}
|
{isConnected ? 'Connected' : 'Disconnected'}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="h-96 overflow-y-auto p-4 space-y-2">
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<div key={msg.id} className="bg-[rgb(40,40,40)] p-2 rounded">
|
<div
|
||||||
<div className="text-sm text-gray-400">
|
key={msg.id}
|
||||||
{msg.user === user?.id ? 'You' : `User ${msg.user.slice(0, 6)}`}
|
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>
|
||||||
<div className="text-white">{msg.message}</div>
|
<div className="text-white">{msg.message}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={sendMessage} className="p-4 border-t border-gray-700">
|
<form onSubmit={sendMessage} className="p-4 border-t border-gray-700">
|
||||||
|
|
|
||||||
|
|
@ -215,13 +215,26 @@ export default function PrivyButton() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervalId = setInterval(() => {
|
if (wallets) {
|
||||||
fetchSolBalance();
|
fetchSolBalance();
|
||||||
}, 15000); // 5000 milliseconds = 5 seconds
|
}
|
||||||
|
}, [user, wallets]);
|
||||||
|
|
||||||
// Cleanup function to clear the interval on unmount or when `ready` changes
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (wallets && solWallet) {
|
||||||
|
fetchSolBalance();
|
||||||
|
}
|
||||||
|
}, 15000); // 15000 milliseconds = 15 seconds
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
if (wallets && solWallet) {
|
||||||
|
fetchSolBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function to clear the interval on unmount or when dependencies change
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [ready]);
|
}, [wallets, solWallet]);
|
||||||
|
|
||||||
const saveProfileChanges = async () => {
|
const saveProfileChanges = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
@ -273,12 +286,6 @@ export default function PrivyButton() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (wallets) {
|
|
||||||
fetchSolBalance();
|
|
||||||
}
|
|
||||||
}, [user, wallets]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user