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 (
{heading}
{participant.name} {participant.isSelf ? ' (you)' : ''}
{participant.mode === 'edit' ? 'Editing' : 'Viewing'} {participant.typing ? ' - typing' : ''}