import { useEffect, useMemo, useState } from 'react'; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip as RechartsTooltip } from 'recharts'; import { Star } from 'lucide-react'; import { api } from '../api'; import { formatTime } from '../utils/messageParser'; import { handleKeyboardActivate } from '../utils/a11y'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './ui/sheet'; import { toast } from './ui/sonner'; import type { Channel, ChannelDetail, PathHashWidthStats } from '../types'; interface ChannelInfoPaneProps { channelKey: string | null; onClose: () => void; channels: Channel[]; onToggleFavorite: (type: 'channel' | 'contact', id: string) => void; } export function ChannelInfoPane({ channelKey, onClose, channels, onToggleFavorite, }: ChannelInfoPaneProps) { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [showKey, setShowKey] = useState(false); // Get live channel data from channels array (real-time via WS) const liveChannel = channelKey ? (channels.find((c) => c.key === channelKey) ?? null) : null; useEffect(() => { setShowKey(false); if (!channelKey) { setDetail(null); return; } let cancelled = false; setLoading(true); api .getChannelDetail(channelKey) .then((data) => { if (!cancelled) setDetail(data); }) .catch((err) => { if (!cancelled) { console.error('Failed to fetch channel detail:', err); toast.error('Failed to load channel info'); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [channelKey]); // Use live channel data where available, fall back to detail snapshot const channel = liveChannel ?? detail?.channel ?? null; return ( !open && onClose()}> Channel Info Channel details and statistics {loading && !detail ? (
Loading...
) : channel ? (
{/* Header */}

{channel.is_hashtag && !channel.name.startsWith('#') ? `#${channel.name}` : channel.name}

{!channel.is_hashtag && !showKey ? ( ) : ( { navigator.clipboard.writeText(channel.key); toast.success('Channel key copied!'); }} title="Click to copy" > {channel.key.toLowerCase()} )}
{channel.is_hashtag ? 'Hashtag' : 'Private Key'} {channel.on_radio && ( On Radio )}
{/* Favorite toggle */}
{/* Message Activity */} {detail && detail.message_counts.all_time > 0 && (
Message Activity
)} {/* First Message */} {detail && detail.first_message_at && (
First Message

{formatTime(detail.first_message_at)}

)} {/* Hop Byte Widths (24h) */} {detail && detail.path_hash_width_24h.total_packets > 0 && (
Hop Byte Widths (24h)
)} {/* Top Senders 24h */} {detail && detail.top_senders_24h.length > 0 && (
Top Senders (24h)
{detail.top_senders_24h.map((sender, idx) => (
{sender.sender_name} {sender.message_count.toLocaleString()} msg {sender.message_count !== 1 ? 's' : ''}
))}
)}
) : (
Channel not found
)}
); } function SectionLabel({ children }: { children: React.ReactNode }) { return (

{children}

); } function InfoItem({ label, value }: { label: string; value: string }) { return (
{label}

{value}

); } const HOP_WIDTH_SEGMENTS = [ { key: 'single_byte', label: '1-byte', color: '#22c55e' }, { key: 'double_byte', label: '2-byte', color: '#0ea5e9' }, { key: 'triple_byte', label: '3-byte', color: '#8b5cf6' }, ] as const; const TOOLTIP_STYLE = { contentStyle: { backgroundColor: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: '6px', fontSize: '11px', color: 'hsl(var(--popover-foreground))', }, } as const; function HopWidthChart({ stats }: { stats: PathHashWidthStats }) { const data = useMemo( () => HOP_WIDTH_SEGMENTS.map(({ key, label, color }) => ({ name: label, value: stats[key] as number, color, })).filter((d) => d.value > 0), [stats] ); return (
{data.map((d) => ( ))} { const v = typeof value === 'number' ? value : Number(value); return [`${v.toLocaleString()} pkt${v !== 1 ? 's' : ''}`, name]; }} />
{data.map((d) => (
{d.name} {d.value.toLocaleString()}
))}

{stats.total_packets.toLocaleString()} total

); }