import { useEffect, useRef, useState } from 'react'; import { Bell, BellOff, ChevronsLeftRight, Globe2, Info, Route, Star, Trash2 } from 'lucide-react'; import { toast } from './ui/sonner'; import { DirectTraceIcon } from './DirectTraceIcon'; import { ContactPathDiscoveryModal } from './ContactPathDiscoveryModal'; import { ChannelFloodScopeOverrideModal } from './ChannelFloodScopeOverrideModal'; import { ChannelPathHashModeOverrideModal } from './ChannelPathHashModeOverrideModal'; import { handleKeyboardActivate } from '../utils/a11y'; import { isPublicChannelKey } from '../utils/publicChannel'; import { stripRegionScopePrefix } from '../utils/regionScope'; import { isPrefixOnlyContact } from '../utils/pubkey'; import { cn } from '../lib/utils'; import { ContactAvatar } from './ContactAvatar'; import { ContactStatusInfo } from './ContactStatusInfo'; import type { Channel, Contact, Conversation, PathDiscoveryResponse, RadioConfig } from '../types'; import { CONTACT_TYPE_ROOM } from '../types'; interface ChatHeaderProps { conversation: Conversation; contacts: Contact[]; channels: Channel[]; config: RadioConfig | null; notificationsSupported: boolean; notificationsEnabled: boolean; notificationsPermission: NotificationPermission | 'unsupported'; onTrace: () => void; onPathDiscovery: (publicKey: string) => Promise; onToggleNotifications: () => void; pushSupported?: boolean; pushSubscribed?: boolean; pushEnabledForConversation?: boolean; onTogglePush?: () => void; onOpenPushSettings?: () => void; onToggleFavorite: (type: 'channel' | 'contact', id: string) => void; onToggleMute?: (key: string) => void; onSetChannelFloodScopeOverride?: (key: string, floodScopeOverride: string) => void; onSetChannelPathHashModeOverride?: (key: string, pathHashModeOverride: number | null) => void; onDeleteChannel: (key: string) => void; onDeleteContact: (publicKey: string) => void; onOpenContactInfo?: (publicKey: string) => void; onOpenChannelInfo?: (channelKey: string) => void; } export function ChatHeader({ conversation, contacts, channels, config, notificationsSupported, notificationsEnabled, notificationsPermission, onTrace, onPathDiscovery, onToggleNotifications, pushSupported, pushSubscribed, pushEnabledForConversation, onTogglePush, onOpenPushSettings, onToggleFavorite, onToggleMute, onSetChannelFloodScopeOverride, onSetChannelPathHashModeOverride, onDeleteChannel, onDeleteContact, onOpenContactInfo, onOpenChannelInfo, }: ChatHeaderProps) { const [showKey, setShowKey] = useState(false); const [pathDiscoveryOpen, setPathDiscoveryOpen] = useState(false); const [channelOverrideOpen, setChannelOverrideOpen] = useState(false); const [pathHashModeOverrideOpen, setPathHashModeOverrideOpen] = useState(false); const [notifDropdownOpen, setNotifDropdownOpen] = useState(false); const notifDropdownRef = useRef(null); useEffect(() => { setShowKey(false); setPathDiscoveryOpen(false); setChannelOverrideOpen(false); setPathHashModeOverrideOpen(false); setNotifDropdownOpen(false); }, [conversation.id]); // Close notification dropdown on outside click useEffect(() => { if (!notifDropdownOpen) return; const handler = (e: MouseEvent) => { if (notifDropdownRef.current && !notifDropdownRef.current.contains(e.target as Node)) { setNotifDropdownOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [notifDropdownOpen]); const activeChannel = conversation.type === 'channel' ? channels.find((channel) => channel.key === conversation.id) : undefined; const activeFloodScopeOverride = conversation.type === 'channel' ? (activeChannel?.flood_scope_override ?? null) : null; const activeFloodScopeLabel = activeFloodScopeOverride ? stripRegionScopePrefix(activeFloodScopeOverride) : null; const activeFloodScopeDisplay = activeFloodScopeOverride ? activeFloodScopeOverride : null; const activePathHashModeOverride = conversation.type === 'channel' ? (activeChannel?.path_hash_mode_override ?? null) : null; const showPathHashModeOverride = conversation.type === 'channel' && onSetChannelPathHashModeOverride && config?.path_hash_mode_supported; const isPrivateChannel = conversation.type === 'channel' && !activeChannel?.is_hashtag; const activeContact = conversation.type === 'contact' ? contacts.find((contact) => contact.public_key === conversation.id) : null; const activeContactIsRoomServer = activeContact?.type === CONTACT_TYPE_ROOM; const activeContactIsPrefixOnly = activeContact ? isPrefixOnlyContact(activeContact.public_key) : false; const titleClickable = (conversation.type === 'contact' && onOpenContactInfo) || (conversation.type === 'channel' && onOpenChannelInfo); const isFav = conversation.type === 'contact' ? (activeContact?.favorite ?? false) : conversation.type === 'channel' ? (activeChannel?.favorite ?? false) : false; const favoriteTitle = conversation.type === 'contact' ? isFav ? 'Remove from favorites. Favorite contacts stay loaded on the radio for ACK support.' : 'Add to favorites. Favorite contacts stay loaded on the radio for ACK support.' : isFav ? 'Remove from favorites' : 'Add to favorites'; const handleEditFloodScopeOverride = () => { if (conversation.type !== 'channel' || !onSetChannelFloodScopeOverride) return; setChannelOverrideOpen(true); }; const handleEditPathHashModeOverride = () => { if (conversation.type !== 'channel' || !onSetChannelPathHashModeOverride) return; setPathHashModeOverrideOpen(true); }; const handleOpenConversationInfo = () => { if (conversation.type === 'contact' && onOpenContactInfo) { onOpenContactInfo(conversation.id); return; } if (conversation.type === 'channel' && onOpenChannelInfo) { onOpenChannelInfo(conversation.id); } }; return (
{conversation.type === 'contact' && onOpenContactInfo && ( )}

{titleClickable ? ( ) : ( {conversation.type === 'channel' && !conversation.name.startsWith('#') && activeChannel?.is_hashtag ? '#' : ''} {conversation.name} )}

{isPrivateChannel && !showKey ? ( ) : ( { e.stopPropagation(); navigator.clipboard.writeText(conversation.id); toast.success( conversation.type === 'channel' ? 'Channel key copied!' : 'Contact key copied!' ); }} title="Click to copy" aria-label={ conversation.type === 'channel' ? 'Copy channel key' : 'Copy contact key' } > {conversation.type === 'channel' ? conversation.id.toLowerCase() : conversation.id} )}
{conversation.type === 'channel' && activeFloodScopeDisplay && ( )}
{conversation.type === 'contact' && activeContact && (
)}
{conversation.type === 'contact' && !activeContactIsRoomServer && ( )} {conversation.type === 'contact' && !activeContactIsRoomServer && ( )} {(notificationsSupported || pushSupported || (conversation.type === 'channel' && onToggleMute)) && !activeContactIsRoomServer && (
{notifDropdownOpen && (
{notificationsSupported && ( )} {pushSupported && onTogglePush && ( <> All notification types require a trusted HTTPS context. Depending on your browser, a snakeoil certificate may not be sufficient. {onOpenPushSettings && (

Manage Web Push enabled devices in{' '} .

)} )} {conversation.type === 'channel' && onToggleMute && ( <>
)}
)}
)} {conversation.type === 'channel' && onSetChannelFloodScopeOverride && ( )} {showPathHashModeOverride && ( )} {(conversation.type === 'channel' || conversation.type === 'contact') && ( )} {!(conversation.type === 'channel' && isPublicChannelKey(conversation.id)) && ( )}
{conversation.type === 'contact' && activeContact && ( setPathDiscoveryOpen(false)} contact={activeContact} contacts={contacts} radioName={config?.name ?? null} onDiscover={onPathDiscovery} /> )} {conversation.type === 'channel' && onSetChannelFloodScopeOverride && ( setChannelOverrideOpen(false)} roomName={conversation.name} currentOverride={activeFloodScopeDisplay} onSetOverride={(value) => onSetChannelFloodScopeOverride(conversation.id, value)} /> )} {showPathHashModeOverride && ( setPathHashModeOverrideOpen(false)} channelName={conversation.name} currentOverride={activePathHashModeOverride} radioDefault={config?.path_hash_mode ?? 0} onSetOverride={(value) => onSetChannelPathHashModeOverride(conversation.id, value)} /> )}
); }