Documentation Index
Fetch the complete documentation index at: https://mintlify.com/SlasshyOverhere/StreamVault/llms.txt
Use this file to discover all available pages before exploring further.
The ChatWindow component provides a floating chat interface for direct messaging with friends, featuring real-time message delivery and typing indicators.
Overview
This component renders as a fixed popup window in the bottom-right corner of the screen, displaying a conversation thread with a specific friend. It supports real-time message delivery via WebSocket events and includes typing indicators.
Source: src/components/Social/ChatWindow.tsx:17-253
Props
| Prop | Type | Description |
|---|
friend | Friend | Friend object containing ID, name, avatar, and online status |
onClose | () => void | Callback when chat window is closed |
State Management
Core State
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [newMessage, setNewMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [sending, setSending] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
Source: src/components/Social/ChatWindow.tsx:23-30
Message State
- messages: Array of chat messages in chronological order
- newMessage: Current input text
- isTyping: Whether friend is currently typing
- loading: Initial message load state
- error: Error message if loading/sending fails
- sending: Whether a message is currently being sent
WebSocket Events
The component subscribes to real-time chat events:
chat_message
Receives incoming messages from the friend:
const unsubMessage = onSocialEvent('chat_message', (data) => {
if (data.fromUserId === friend.id) {
appendMessage(data.message as ChatMessage);
scrollToBottom();
}
});
Source: src/components/Social/ChatWindow.tsx:65-70
chat_message_sent
Confirms when your own message is delivered:
const unsubSent = onSocialEvent('chat_message_sent', (data) => {
if (data.friendId === friend.id) {
appendMessage(data.message as ChatMessage);
scrollToBottom();
}
});
Source: src/components/Social/ChatWindow.tsx:72-77
typing
Shows typing indicator when friend is typing:
const unsubTyping = onSocialEvent('typing', (data) => {
if (data.userId === friend.id) {
setIsTyping(true);
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => setIsTyping(false), 3000);
}
});
Source: src/components/Social/ChatWindow.tsx:79-87
Typing indicator auto-hides after 3 seconds of inactivity.
Message Management
Loading Chat History
const loadChatHistory = useCallback(async () => {
try {
setLoading(true);
setError(null);
const history = await getChatHistory(friend.id);
setMessages(history);
setTimeout(scrollToBottom, 100);
} catch (error) {
console.error('Failed to load chat history:', error);
setError('Failed to load chat history. Please try again later.');
} finally {
setLoading(false);
}
}, [friend.id, scrollToBottom]);
Source: src/components/Social/ChatWindow.tsx:47-60
Appending Messages
Deduplicates messages by ID to prevent duplicates:
const appendMessage = useCallback((message: ChatMessage) => {
setMessages((prev) => {
if (prev.some((existing) => existing.id === message.id)) {
return prev; // Skip duplicate
}
return [...prev, message];
});
}, []);
Source: src/components/Social/ChatWindow.tsx:32-39
Automatically scrolls to bottom when new messages arrive:
const scrollToBottom = useCallback(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, []);
Source: src/components/Social/ChatWindow.tsx:41-45
Sending Messages
Send Handler
const handleSend = async () => {
if (!newMessage.trim() || sending) return;
try {
setSending(true);
setError(null);
const sentMessage = await sendChatMessage(friend.id, newMessage.trim());
if (sentMessage) {
appendMessage(sentMessage);
setTimeout(scrollToBottom, 0);
}
setNewMessage('');
} catch (error) {
console.error('Failed to send message:', error);
setError('Failed to send message. Please try again.');
} finally {
setSending(false);
}
};
Source: src/components/Social/ChatWindow.tsx:99-117
Keyboard Handling
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
Source: src/components/Social/ChatWindow.tsx:119-124
Sends message on Enter, prevents send on Shift+Enter (for multiline support).
Typing Indicator
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewMessage(e.target.value);
if (e.target.value.trim()) {
sendTypingIndicator(friend.id);
}
};
Source: src/components/Social/ChatWindow.tsx:126-131
Sends typing indicator to friend when typing non-empty text.
UI Layout
Window Structure
Fixed popup with slide-up animation:
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 20, opacity: 0 }}
className="fixed bottom-4 right-4 w-80 h-96 bg-zinc-900 border border-zinc-800 rounded-lg shadow-xl z-50 flex flex-col"
>
Source: src/components/Social/ChatWindow.tsx:135-140
Dimensions: 320px wide (w-80), 384px tall (h-96)
Displays friend info with online status:
<div className="flex items-center gap-3 p-3 border-b border-zinc-800">
<div className="relative">
<div className="w-8 h-8 rounded-full bg-zinc-700 overflow-hidden">
{friend.avatar ? (
<img src={friend.avatar} alt="" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-zinc-400 text-sm">
{friend.name.charAt(0).toUpperCase()}
</div>
)}
</div>
{friend.isOnline && (
<div className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 rounded-full border-2 border-zinc-900" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{friend.name}</p>
{isTyping && (
<p className="text-xs text-purple-400">typing...</p>
)}
</div>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onClose}>
<X className="w-4 h-4" />
</Button>
</div>
Source: src/components/Social/ChatWindow.tsx:142-166
Message List
Scrollable message thread with grouped timestamps:
<ScrollArea className="flex-1 p-3" ref={scrollRef}>
<div className="space-y-3">
{messages.map((msg, index) => {
const isOwn = msg.senderId !== friend.id;
const showTimestamp = index === 0 ||
messages[index - 1].timestamp < msg.timestamp - 300000; // 5 min gap
return (
<div key={msg.id}>
{showTimestamp && (
<div className="text-center text-xs text-zinc-500 my-2">
{formatRelativeTime(msg.timestamp)}
</div>
)}
<div className={`flex ${isOwn ? 'justify-end' : 'justify-start'}`}>
<div
className={`max-w-[80%] px-3 py-2 rounded-lg text-sm ${
isOwn
? 'bg-purple-600 text-white rounded-br-sm'
: 'bg-zinc-800 text-white rounded-bl-sm'
}`}
>
{msg.text}
</div>
</div>
</div>
);
})}
</div>
</ScrollArea>
Source: src/components/Social/ChatWindow.tsx:169-222
Message Styling
- Own messages: Purple background, aligned right, rounded bottom-right corner sharp
- Friend messages: Dark gray background, aligned left, rounded bottom-left corner sharp
- Timestamps: Shown when 5+ minutes gap between messages
Message input with send button:
<div className="p-3 border-t border-zinc-800">
<div className="flex gap-2">
<Input
placeholder="Type a message..."
value={newMessage}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
disabled={sending}
className="flex-1 bg-zinc-800 border-zinc-700 text-sm"
/>
<Button
size="icon"
onClick={handleSend}
disabled={!newMessage.trim() || sending}
className="bg-purple-600 hover:bg-purple-700"
>
{sending ? (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
</Button>
</div>
</div>
Source: src/components/Social/ChatWindow.tsx:226-249
Empty States
Loading State
{loading ? (
<div className="flex items-center justify-center h-full text-zinc-500">
Loading messages...
</div>
) : // ...
}
Source: src/components/Social/ChatWindow.tsx:184-187
No Messages
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-zinc-500 text-sm">
<p>No messages yet</p>
<p className="text-xs">Say hi to {friend.name}!</p>
</div>
) : // ...
}
Source: src/components/Social/ChatWindow.tsx:188-193
Error Handling
Displays error state with retry button:
{error ? (
<div className="flex flex-col items-center justify-center h-full text-center p-4">
<AlertCircle className="w-8 h-8 text-red-500 mb-2" />
<p className="text-red-400 text-sm mb-2">Error loading chat</p>
<p className="text-zinc-500 text-xs">{error}</p>
<Button
variant="outline"
size="sm"
className="mt-3 border-zinc-700"
onClick={loadChatHistory}
>
Retry
</Button>
</div>
) : // ...
}
Source: src/components/Social/ChatWindow.tsx:170-183
Message Deduplication
The component prevents duplicate messages using ID-based filtering:
const appendMessage = useCallback((message: ChatMessage) => {
setMessages((prev) => {
// Check if message already exists
if (prev.some((existing) => existing.id === message.id)) {
return prev; // Skip duplicate
}
return [...prev, message];
});
}, []);
Source: src/components/Social/ChatWindow.tsx:32-39
This is important because both chat_message (incoming) and chat_message_sent (echo) events can fire for the same message.
Timestamp Grouping
Timestamps are displayed when there’s a 5-minute gap between messages:
const showTimestamp = index === 0 ||
messages[index - 1].timestamp < msg.timestamp - 300000; // 5 min gap
return (
<div key={msg.id}>
{showTimestamp && (
<div className="text-center text-xs text-zinc-500 my-2">
{formatRelativeTime(msg.timestamp)}
</div>
)}
{/* Message bubble */}
</div>
);
Source: src/components/Social/ChatWindow.tsx:197-206
Best Practices
Cleanup Timeouts
Always clear typing indicator timeout on unmount:
return () => {
unsubMessage();
unsubSent();
unsubTyping();
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
Source: src/components/Social/ChatWindow.tsx:89-96
Use setTimeout with 0ms delay to scroll after DOM updates:
if (sentMessage) {
appendMessage(sentMessage);
setTimeout(scrollToBottom, 0); // Wait for DOM update
}
Source: src/components/Social/ChatWindow.tsx:106-109
Prevent Empty Messages
Trim input and check for non-empty before sending:
if (!newMessage.trim() || sending) return;
Source: src/components/Social/ChatWindow.tsx:100
API Functions
getChatHistory(friendId) - Retrieves message history with friend
sendChatMessage(friendId, text) - Sends a chat message
sendTypingIndicator(friendId) - Broadcasts typing status
onSocialEvent(eventType, handler) - Subscribes to WebSocket events
formatRelativeTime(timestamp) - Formats timestamp as relative time
ChatMessage Interface
interface ChatMessage {
id: string;
senderId: string;
text: string;
timestamp: number; // Unix timestamp in milliseconds
}
Friend Interface
interface Friend {
id: string;
name: string;
avatar: string | null;
isOnline?: boolean;
currentlyWatching?: {
contentType: 'movie' | 'tvshow';
title: string;
} | null;
}