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 FriendsPanel component provides a slide-out panel for managing social connections, including friends list, pending requests, and user search functionality.
Overview
This panel appears as a fixed sidebar on the right side of the screen, offering three tabs:
- Friends: View all friends with online status and activity
- Requests: Manage incoming friend requests
- Add: Search for and send friend requests to new users
Source: src/components/Social/FriendsPanel.tsx:20-411
Props
| Prop | Type | Description |
|---|
isOpen | boolean | Controls panel visibility |
onClose | () => void | Callback when panel is closed |
onOpenChat | (friend: Friend) => void | Opens chat window with friend |
onViewProfile | (friendId: string) => void | Opens friend profile view |
State Management
Core State
const [friends, setFriends] = useState<Friend[]>([]);
const [onlineFriends, setOnlineFriends] = useState<Friend[]>([]);
const [requests, setRequests] = useState<FriendRequest[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [activeTab, setActiveTab] = useState<'friends' | 'requests' | 'add'>('friends');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
Source: src/components/Social/FriendsPanel.tsx:28-36
Tab State
Three tabs control the active view:
friends - Display all friends with online status
requests - Show pending friend requests
add - Search and add new friends
WebSocket Events
The component subscribes to real-time social events:
friend_online
const unsubOnline = onSocialEvent('friend_online', (data) => {
setOnlineFriends(prev => {
const friend = friends.find(f => f.id === data.userId);
if (friend && !prev.some(f => f.id === data.userId)) {
return [...prev, friend];
}
return prev;
});
});
Source: src/components/Social/FriendsPanel.tsx:47-55
friend_offline
const unsubOffline = onSocialEvent('friend_offline', (data) => {
setOnlineFriends(prev => prev.filter(f => f.id !== data.userId));
});
Source: src/components/Social/FriendsPanel.tsx:57-59
currently_watching
const unsubWatching = onSocialEvent('currently_watching', (data) => {
const userId = typeof data.userId === 'string' ? data.userId : '';
if (!userId) return;
const currentlyWatching = data.content as Friend['currentlyWatching'];
setOnlineFriends(prev => prev.map(friend => (
friend.id === userId
? { ...friend, currentlyWatching }
: friend
)));
});
Source: src/components/Social/FriendsPanel.tsx:61-70
friend_request / friend_accepted
const unsubRequest = onSocialEvent('friend_request', () => {
loadRequests();
});
const unsubAccepted = onSocialEvent('friend_accepted', () => {
loadFriends();
});
Source: src/components/Social/FriendsPanel.tsx:72-78
Data Loading
Load Friends and Requests
const loadFriendsAndRequests = async () => {
try {
setLoading(true);
setError(null);
await Promise.all([loadFriends(), loadRequests()]);
} catch (err) {
setError('Failed to load friends data. Please try again later.');
console.error('Failed to load friends data:', err);
} finally {
setLoading(false);
}
};
Source: src/components/Social/FriendsPanel.tsx:89-100
Load Friends
const loadFriends = async () => {
try {
const data = await getFriends();
setFriends(data.friends);
setOnlineFriends(data.online);
} catch (error) {
console.error('Failed to load friends:', error);
throw error;
}
};
Source: src/components/Social/FriendsPanel.tsx:102-111
Load Requests
const loadRequests = async () => {
try {
const data = await getPendingRequests();
setRequests(data);
} catch (error) {
console.error('Failed to load requests:', error);
throw error;
}
};
Source: src/components/Social/FriendsPanel.tsx:113-121
User Search
Search functionality with minimum 2-character query:
const handleSearch = async (query: string) => {
setSearchQuery(query);
if (query.length < 2) {
setSearchResults([]);
return;
}
setIsSearching(true);
try {
const results = await searchUsers(query);
// Filter out existing friends
setSearchResults(results.filter(r => !friends.some(f => f.id === r.id)));
} catch (error) {
console.error('Search failed:', error);
} finally {
setIsSearching(false);
}
};
Source: src/components/Social/FriendsPanel.tsx:123-139
Friend Request Management
Send Request
const handleSendRequest = async (userId: string) => {
try {
await sendFriendRequest(userId);
setSearchResults(prev => prev.filter(r => r.id !== userId));
loadRequests(); // Reload to show new pending request
} catch (error) {
console.error('Failed to send request:', error);
}
};
Source: src/components/Social/FriendsPanel.tsx:141-150
Accept Request
const handleAcceptRequest = async (fromId: string) => {
try {
await acceptFriendRequest(fromId);
setRequests(prev => prev.filter(r => r.fromId !== fromId));
loadFriends();
} catch (error) {
console.error('Failed to accept request:', error);
}
};
Source: src/components/Social/FriendsPanel.tsx:152-160
Reject Request
const handleRejectRequest = async (fromId: string) => {
try {
await rejectFriendRequest(fromId);
setRequests(prev => prev.filter(r => r.fromId !== fromId));
} catch (error) {
console.error('Failed to reject request:', error);
}
};
Source: src/components/Social/FriendsPanel.tsx:162-169
UI Layout
Panel Structure
Fixed right sidebar with slide-in animation:
<motion.div
initial={{ x: 300, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 300, opacity: 0 }}
className="fixed right-0 top-0 h-full w-80 bg-zinc-900 border-l border-zinc-800 z-50 flex flex-col"
>
Source: src/components/Social/FriendsPanel.tsx:179-184
Displays panel title with request badge:
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-purple-500" />
<h2 className="font-semibold">Friends</h2>
{requests.length > 0 && (
<span className="bg-purple-500 text-white text-xs px-2 py-0.5 rounded-full">
{requests.length}
</span>
)}
</div>
Source: src/components/Social/FriendsPanel.tsx:187-195
Tab Navigation
Three-tab layout with request count indicator:
<div className="flex border-b border-zinc-800">
<button onClick={() => setActiveTab('friends')} className={...}>
Friends ({friends.length})
</button>
<button onClick={() => setActiveTab('requests')} className={...}>
Requests
{requests.length > 0 && (
<span className="absolute -top-1 right-4 bg-red-500 ...">
{requests.length}
</span>
)}
</button>
<button onClick={() => setActiveTab('add')} className={...}>
<UserPlus className="w-4 h-4 mx-auto" />
</button>
</div>
Source: src/components/Social/FriendsPanel.tsx:202-232
Friends Tab
Displays friends with online status separation:
Online Friends Section
{onlineFriends.length > 0 && (
<div className="mb-4">
<h3 className="text-xs font-semibold text-zinc-500 uppercase px-2 mb-2">
Online ({onlineFriends.length})
</h3>
{onlineFriends.map(friend => (
<FriendItem
key={friend.id}
friend={friend}
isOnline={true}
onChat={() => onOpenChat(friend)}
onViewProfile={() => onViewProfile(friend.id)}
/>
))}
</div>
)}
Source: src/components/Social/FriendsPanel.tsx:252-266
All Friends Section
<div>
<h3 className="text-xs font-semibold text-zinc-500 uppercase px-2 mb-2">
All Friends
</h3>
{friends.map(friend => (
<FriendItem
key={friend.id}
friend={friend}
isOnline={onlineFriends.some(f => f.id === friend.id)}
onChat={() => onOpenChat(friend)}
onViewProfile={() => onViewProfile(friend.id)}
/>
))}
</div>
Source: src/components/Social/FriendsPanel.tsx:270-293
Requests Tab
Displays pending friend requests with accept/reject buttons:
{requests.map(request => (
<div key={request.fromId} className="flex items-center gap-3 p-3 rounded-lg hover:bg-zinc-800/50">
<div className="w-10 h-10 rounded-full bg-zinc-700 overflow-hidden">
{/* Avatar display */}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{request.fromName}</p>
<p className="text-xs text-zinc-500">{formatRelativeTime(request.sentAt)}</p>
</div>
<div className="flex gap-1">
<Button onClick={() => handleAcceptRequest(request.fromId)} className="text-green-500">
<Check className="w-4 h-4" />
</Button>
<Button onClick={() => handleRejectRequest(request.fromId)} className="text-red-500">
<X className="w-4 h-4" />
</Button>
</div>
</div>
))}
Source: src/components/Social/FriendsPanel.tsx:307-344
Add Friends Tab
Search interface with user results:
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
<Input
placeholder="Search by name or email..."
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
className="pl-10 bg-zinc-800 border-zinc-700"
/>
</div>
Source: src/components/Social/FriendsPanel.tsx:351-359
Search Results
{searchResults.map(user => (
<div key={user.id} className="flex items-center gap-3 p-3 rounded-lg bg-zinc-800/50">
<div className="w-10 h-10 rounded-full bg-zinc-700 overflow-hidden">
{/* Avatar */}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{user.displayName}</p>
</div>
<Button
size="sm"
onClick={() => handleSendRequest(user.id)}
className="bg-purple-600 hover:bg-purple-700"
>
<UserPlus className="w-4 h-4 mr-1" />
Add
</Button>
</div>
))}
Source: src/components/Social/FriendsPanel.tsx:365-390
FriendItem Component
Individual friend list item with online status and activity:
function FriendItem({ friend, isOnline, onChat, onViewProfile }: FriendItemProps) {
return (
<div className="flex items-center gap-3 p-2 rounded-lg hover:bg-zinc-800/50 group">
<div className="relative cursor-pointer" onClick={onViewProfile}>
<div className="w-10 h-10 rounded-full bg-zinc-700 overflow-hidden">
{/* Avatar */}
</div>
{isOnline && (
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-zinc-900" />
)}
</div>
<div className="flex-1 min-w-0 cursor-pointer" onClick={onViewProfile}>
<p className="font-medium truncate">{friend.name}</p>
{friend.currentlyWatching ? (
<div className="flex items-center gap-1 text-xs text-purple-400">
{friend.currentlyWatching.contentType === 'movie' ? <Film /> : <Tv />}
<span className="truncate">Watching {friend.currentlyWatching.title}</span>
</div>
) : isOnline ? (
<p className="text-xs text-green-500">Online</p>
) : (
<p className="text-xs text-zinc-500">Offline</p>
)}
</div>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={onChat}
>
<MessageCircle className="w-4 h-4" />
</Button>
</div>
);
}
Source: src/components/Social/FriendsPanel.tsx:420-466
Error Handling
Displays error state with retry option:
{error ? (
<div className="flex flex-col items-center justify-center py-12 text-center p-4">
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
<p className="text-red-400 font-medium mb-2">Failed to load friends</p>
<p className="text-zinc-500 text-sm mb-4">{error}</p>
<Button
variant="outline"
onClick={retryLoad}
className="border-zinc-700"
>
Retry
</Button>
</div>
) : (
// Normal content
)}
Source: src/components/Social/FriendsPanel.tsx:236-248
API Functions
getFriends() - Retrieves friends list with online status
getPendingRequests() - Retrieves incoming friend requests
searchUsers(query) - Searches for users by name or email
sendFriendRequest(userId) - Sends friend request to user
acceptFriendRequest(fromId) - Accepts incoming friend request
rejectFriendRequest(fromId) - Rejects incoming friend request
onSocialEvent(eventType, handler) - Subscribes to WebSocket events
formatRelativeTime(timestamp) - Formats timestamp as relative time