import type { ConversationSummary } from '@chat-app/shared/chat'; import type { PresenceState } from '@chat-app/shared/supabase'; import { useTranslation } from 'react-i18next'; import { useCall } from '../context/CallContext'; import { useNickname } from '../lib/friendNicknames'; import type { PeerPresence } from '../lib/usePeerPresence'; import { Avatar } from './Avatar'; import { GridIcon, InfoIcon, PhoneIcon, SearchIcon, UsersIcon, VideoIcon, } from './icons'; import { PinnedMessagesPill } from './PinnedMessagesPill'; const PRESENCE_DOT: Record = { online: 'bg-emerald-500', idle: 'bg-amber-400', dnd: 'bg-rose-500', invisible: 'bg-neutral-500', offline: 'bg-neutral-400 dark:bg-neutral-600', }; interface Props { conversation: ConversationSummary | null; peerPresence: PeerPresence | null; onInfoClick?: () => void; onMediaClick?: () => void; onProfileClick?: (ev: React.MouseEvent) => void; onSearchClick?: () => void; pinnedCount?: number; onOpenPinned?: () => void; } export function ConversationHeader({ conversation, peerPresence, onInfoClick, onMediaClick, onProfileClick, onSearchClick, pinnedCount = 0, onOpenPinned, }: Props) { if (!conversation) { return
; } return ( <> {/* Voice-channel rail rendered separately in ConversationPage so it can sit between the message surface and the in-call dock. */} ); } interface HeaderBarProps { conversation: ConversationSummary; peerPresence: PeerPresence | null; onInfoClick?: () => void; onMediaClick?: () => void; onProfileClick?: (ev: React.MouseEvent) => void; onSearchClick?: () => void; pinnedCount?: number; onOpenPinned?: () => void; } function HeaderBar({ conversation, peerPresence, onInfoClick, onMediaClick, onProfileClick, onSearchClick, pinnedCount = 0, onOpenPinned, }: HeaderBarProps) { const { t } = useTranslation(['app']); const isDm = conversation.type === 'dm'; // For DMs, route the peer's display name through the per-viewer nickname // override. Falls back to the real displayName when no nickname is set. const peerName = useNickname( conversation.peer?.userId, conversation.peer?.displayName ?? '?', ); const title = isDm ? peerName : (conversation.name ?? '?'); const handle = isDm ? '@' + (conversation.peer?.username ?? '?') : ''; const peerAvatar = isDm ? (conversation.peer?.avatarUrl ?? null) : (conversation.avatarUrl ?? null); // Hide presence when peer chose invisible — reciprocal privacy. const peerState = peerPresence?.state ?? null; const showPresence = isDm && peerState && peerState !== 'invisible'; // Subtitle priority: custom status_message when online (or idle/dnd), else // the localized presence label. Offline always wins → just "Offline". const presenceLabel = (() => { if (!peerState) return ''; if (peerState === 'offline') return t('app:presence.offline'); if (peerPresence?.statusMessage && peerPresence.statusMessage.trim().length > 0) { return peerPresence.statusMessage.trim(); } return t('app:presence.' + peerState); })(); return (
{!isDm && }

{title}

onOpenPinned?.()} />

{isDm ? ( <> {handle} {showPresence && ( <> · {presenceLabel} )} ) : ( <> {conversation.members.length} {t('app:nav.friends').toLowerCase()} )}

{!isDm && onInfoClick && ( )}
); } interface HeaderActionButtonProps { label: string; icon: (props: React.SVGProps) => React.JSX.Element; onClick?: () => void; disabled?: boolean; tone?: 'default' | 'accent'; } function HeaderActionButton({ label, icon: Icon, onClick, disabled, tone = 'default', }: HeaderActionButtonProps) { const toneClass = tone === 'accent' ? 'text-accent hover:bg-accent/10' : 'text-fg-muted hover:bg-surface-2 hover:text-fg dark:hover:bg-[#383a40]'; return ( ); } function CallHeaderButton({ conversationId, kind, }: { conversationId: string; kind: 'audio' | 'video'; }) { const { t } = useTranslation(['app']); const { state, startCall } = useCall(); const busy = state.kind !== 'idle'; const label = kind === 'video' ? t('app:call.start_video', { defaultValue: 'Video-Anruf' }) : t('app:call.start_audio'); const Icon = kind === 'video' ? VideoIcon : PhoneIcon; return ( void startCall(conversationId, kind)} disabled={busy} /> ); }