import { useMemo, useState } from 'react' export interface DocumentsPresenceParticipant { clientId: number userId: string name: string email?: string imageUrl?: string mode: 'edit' | 'view' typing?: boolean lastTypedAt?: number isSelf?: boolean } export interface DocumentsPresenceProps { participants: DocumentsPresenceParticipant[] typingNames: string[] } const MAX_VISIBLE_AVATARS = 3 /** Stable React keys (Yjs clientId is not used for PresenceRoom peers). */ function participantKey(p: DocumentsPresenceParticipant) { return `${p.userId}:${p.isSelf ? 'self' : 'peer'}` } function initialsFor(name: string) { const parts = name.trim().split(/\s+/).filter(Boolean) if (parts.length >= 2) return `${parts[0][0]}${parts[1][0]}`.toUpperCase() return (parts[0]?.slice(0, 2) || '?').toUpperCase() } function typingText(names: string[]) { if (names.length === 0) return null if (names.length === 1) return `${names[0]} is typing...` if (names.length === 2) return `${names[0]} and ${names[1]} are typing...` return `${names[0]}, ${names[1]}, and ${names.length - 2} more are typing...` } function ParticipantAvatar({ participant, index }: { participant: DocumentsPresenceParticipant; index: number }) { const label = `${participant.name}${participant.isSelf ? ' (you)' : ''} - ${ participant.mode === 'edit' ? 'Editing' : 'Viewing' }` return ( {participant.imageUrl ? ( ) : ( {initialsFor(participant.name)} )} ) } export function DocumentsPresence({ participants, typingNames }: DocumentsPresenceProps) { const [open, setOpen] = useState(false) const visibleParticipants = participants.slice(0, MAX_VISIBLE_AVATARS) const hiddenCount = Math.max(0, participants.length - MAX_VISIBLE_AVATARS) const message = typingText(typingNames) const heading = useMemo(() => { const count = participants.length if (count === 0) return 'No active viewers' if (count === 1) return '1 person here' return `${count} people here` }, [participants.length]) return (
{message ? ( {message} ) : null}
{visibleParticipants.map((participant, index) => ( ))} {hiddenCount > 0 ? ( ) : null}
{open ? (

{heading}

) : null}
) }