import { useChatClient, usePresenceListener, useRoom, useTyping } from '@ably/chat/react'; import { clsx } from 'clsx'; import React, { useState } from 'react'; import { useRoomAvatar } from '../../hooks/use-room-avatar.tsx'; import { Avatar, AvatarData } from '../atoms/avatar.tsx'; import { ParticipantList } from './participant-list.tsx'; import { PresenceCount } from './presence-count.tsx'; import { PresenceIndicators } from './presence-indicators.tsx'; import { PresenceList } from './presence-list.tsx'; import { TypingIndicators } from './typing-indicators.tsx'; /** * Props for the RoomInfo component */ export interface RoomInfoProps { /** * Optional avatar data for the room to override context-managed avatar. * When provided, bypasses the AvatarContext and uses this data directly. * Useful for scenarios with external avatar management or testing. * If not provided, automatically fetches or creates avatar via useRoomAvatar hook. * * @example * // Using context-managed avatar (recommended) * * * @example * // Providing custom avatar data * */ roomAvatar?: AvatarData; /** * Position coordinates for rendering the participant list dropdown. * Defines where the participant list appears relative to the viewport. * Adjust based on component placement to prevent edge overflow. * * @default { top: 0, left: 150 } * * @example * // Position for sidebar placement * * */ position?: { top: number; left: number }; /** * Additional CSS class names to apply to the root container element. * Use for spacing, sizing, positioning, and theme customizations. * * @example * // Custom spacing and background * * * @example * // Compact mobile layout * * * @example * // Fixed positioning for overlays * * * @example * // Responsive design patterns * */ className?: string; } /** * RoomInfo component displays comprehensive information about a chat room with interactive features. * It must be used within the context of a ChatRoomProvider and AvatarProvider to function correctly. * * Features: * - Room avatar display * - Live presence count badge showing active participants * - Interactive hover tooltip with participant preview * - Expandable participant list with detailed user information * - In-place avatar editing with color and image customization * - Presence and typing indicators built-in * - Accessibility support with ARIA roles and keyboard navigation * - Customizable positioning * * Presence: * - Live participant count with visual badge * - Hover tooltip showing recent participants * - Detailed participant list with status indicators * - Current user highlighting and status * * Typing Indicators: * - Typing activity display * - Smart user exclusion (doesn't show own typing) * - Configurable display limits * * @example * // Sidebar room list item * const SidebarRoomItem = ({ roomName, isActive }) => { * return ( *
* *
* ); * }; * * * @example * // Custom avatar with external management * const ExternalAvatarRoom = ({ roomName, externalAvatar }) => { * return ( * * ); * }; * */ export const RoomInfo = ({ roomAvatar: propRoomAvatar, position = { top: 0, left: 150 }, className, }: RoomInfoProps) => { const { roomName } = useRoom(); const { presenceData } = usePresenceListener(); const { currentlyTyping } = useTyping(); const chatClient = useChatClient(); const currentClientId = chatClient.clientId; const [showTooltip, setShowTooltip] = useState(false); const [tooltipPosition, setTooltipPosition] = useState<'above' | 'below'>('above'); const [tooltipCoords, setTooltipCoords] = useState<{ top: number; left: number } | undefined>(); const [isOpen, setIsOpen] = useState(false); const { roomAvatar } = useRoomAvatar({ roomName }); const roomAvatarData = propRoomAvatar || roomAvatar; const onToggle = () => { setShowTooltip(false); // Hide tooltip when toggling participant list setIsOpen(!isOpen); }; /** * Handles mouse enter event on the room avatar * Calculates optimal tooltip position based on available space * * @param event - The mouse enter event */ const handleMouseEnter = (event: React.MouseEvent) => { const rect = event.currentTarget.getBoundingClientRect(); const tooltipHeight = 30; // Approximate tooltip height const spacing = 5; // Space between avatar and tooltip const spaceAbove = rect.top; const spaceBelow = window.innerHeight - rect.bottom; // Position above if there's enough space, otherwise below let finalTooltipPosition: 'above' | 'below'; if (spaceAbove >= tooltipHeight + spacing + 10) { finalTooltipPosition = 'above'; } else if (spaceBelow >= tooltipHeight + spacing + 10) { finalTooltipPosition = 'below'; } else { // If neither has enough space, use the side with more space finalTooltipPosition = spaceAbove > spaceBelow ? 'above' : 'below'; } setTooltipPosition(finalTooltipPosition); // Calculate coordinates for fixed positioning (viewport-relative) const horizontalCenter = (rect.left + rect.right) / 2; const verticalPos = finalTooltipPosition === 'above' ? rect.top - tooltipHeight - spacing : rect.bottom + spacing; setTooltipCoords({ top: verticalPos, left: horizontalCenter }); setShowTooltip(true); }; return (
{/* Room Avatar with Hover Tooltip */}
{ setShowTooltip(false); }} onClick={onToggle} role="button" aria-haspopup="dialog" aria-expanded={isOpen} aria-label={`${roomAvatarData?.displayName || roomName} (${String(presenceData.length || 0)} participants)`} tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle(); } }} >
{/* Present Count Badge */}
{/* Hover Tooltip */} {showTooltip && } {/* Participants Dropdown */} {isOpen && ( )}
{/* Room Information */}

{roomAvatarData?.displayName || roomName}

{/* Typing Indicators */}
); };