Skip to content
File

Blob: src/client/components/presence/avatar-stack.tsx

typescript60 lines
1import type { Awareness } from "y-protocols/awareness";
2import { awarenessColor, useAwareness, type AwarenessState } from "@/client/hooks/use-sync";
3import { useCollabIdentity } from "@/client/lib/collab-identity";
4 
5interface AvatarStackProps {
6 awareness: Awareness | null;
7 localClientId: number | null;
8}
9 
10interface RemoteUser {
11 clientId: number;
12 color: string;
13 name: string;
14 avatar_url: string | null;
15}
16 
17export function AvatarStack({ awareness, localClientId }: AvatarStackProps) {
18 const { resolveIdentity } = useCollabIdentity();
19 const states = useAwareness(awareness);
20 
21 const remoteUsers: RemoteUser[] = [];
22 states.forEach((state: AwarenessState, clientId: number) => {
23 if (clientId === localClientId || !state.user) return;
24 const userId = state.user.userId ?? null;
25 const identity = resolveIdentity(userId, clientId);
26 remoteUsers.push({
27 clientId,
28 color: awarenessColor(userId, clientId),
29 name: identity.name,
30 avatar_url: identity.avatar_url,
31 });
32 });
33 
34 if (remoteUsers.length === 0) return null;
35 
36 return (
37 <div className="flex -space-x-1.5">
38 {remoteUsers.slice(0, 5).map((u) => (
39 <div
40 key={u.clientId}
41 className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full border-2 border-canvas text-[10px] font-medium text-white"
42 style={{ backgroundColor: u.color }}
43 title={u.name}
44 >
45 {u.avatar_url ? (
46 <img src={u.avatar_url} alt={u.name} className="h-full w-full object-cover" />
47 ) : (
48 <span style={{ textShadow: "0 0 2px rgba(0,0,0,0.5)" }}>{u.name.charAt(0).toUpperCase()}</span>
49 )}
50 </div>
51 ))}
52 {remoteUsers.length > 5 && (
53 <div className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-canvas bg-zinc-700 text-[10px] font-medium text-zinc-300">
54 +{remoteUsers.length - 5}
55 </div>
56 )}
57 </div>
58 );
59}