import { useState, lazy, Suspense } from 'react'; import type { Contact, RadioConfig, MessagePath } from '../types'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { Button } from './ui/button'; import { resolvePath, parsePathHops, calculateDistance, isValidLocation, formatDistance, type SenderInfo, type ResolvedPath, type PathHop, } from '../utils/pathUtils'; import { formatTime } from '../utils/messageParser'; import { getMapFocusHash } from '../utils/urlHash'; const PathRouteMap = lazy(() => import('./PathRouteMap').then((m) => ({ default: m.PathRouteMap })) ); interface PathModalProps { open: boolean; onClose: () => void; paths: MessagePath[]; senderInfo: SenderInfo; contacts: Contact[]; config: RadioConfig | null; messageId?: number; isOutgoingChan?: boolean; isResendable?: boolean; onResend?: (messageId: number, newTimestamp?: boolean) => void; } export function PathModal({ open, onClose, paths, senderInfo, contacts, config, messageId, isOutgoingChan, isResendable, onResend, }: PathModalProps) { const [expandedMaps, setExpandedMaps] = useState>(new Set()); const hasResendActions = isOutgoingChan && messageId !== undefined && onResend; const hasPaths = paths.length > 0; // Resolve all paths const resolvedPaths = hasPaths ? paths.map((p) => ({ ...p, resolved: resolvePath(p.path, senderInfo, contacts, config, p.path_len), })) : []; const hasSinglePath = paths.length === 1; return ( !isOpen && onClose()}> {hasPaths ? `Message Path${!hasSinglePath ? `s (${paths.length})` : ''}` : 'Message Status'} {!hasPaths ? ( <>No echoes heard yet. Echoes appear when repeaters re-broadcast your message. ) : hasSinglePath ? ( <> This shows one route that this message traveled through the mesh network. Repeaters may be incorrectly identified due to prefix collisions between heard and non-heard repeater advertisements. ) : ( <> This message was received via {paths.length} different routes. Repeaters may be incorrectly identified due to prefix collisions. )} {hasPaths && (
{/* Raw path summary */}
{paths.map((p, index) => { const hops = parsePathHops(p.path, p.path_len); const rawPath = hops.length > 0 ? hops.join('->') : 'direct'; return (
Path {index + 1}:{' '} {rawPath}
); })}
{/* Straight-line distance (sender to receiver, same for all routes) */} {resolvedPaths.length > 0 && isValidLocation( resolvedPaths[0].resolved.sender.lat, resolvedPaths[0].resolved.sender.lon ) && isValidLocation( resolvedPaths[0].resolved.receiver.lat, resolvedPaths[0].resolved.receiver.lon ) && (
Straight-line distance: {formatDistance( calculateDistance( resolvedPaths[0].resolved.sender.lat, resolvedPaths[0].resolved.sender.lon, resolvedPaths[0].resolved.receiver.lat, resolvedPaths[0].resolved.receiver.lon )! )}
)} {resolvedPaths.map((pathData, index) => { const mapExpanded = expandedMaps.has(index); const toggleMap = () => setExpandedMaps((prev) => { const next = new Set(prev); if (next.has(index)) next.delete(index); else next.add(index); return next; }); return (
{!hasSinglePath ? (
Path {index + 1}{' '} — received {formatTime(pathData.received_at)}
) : (
)}
{mapExpanded && (
} >
)}
); })}
)}
{hasResendActions && (
{isResendable && ( )}
)}
); } interface PathVisualizationProps { resolved: ResolvedPath; senderInfo: SenderInfo; } function PathVisualization({ resolved, senderInfo }: PathVisualizationProps) { // Track previous location for each hop to calculate distances // Returns null if previous hop was ambiguous or has invalid location const getPrevLocation = (hopIndex: number): { lat: number | null; lon: number | null } | null => { if (hopIndex === 0) { // Check if sender has valid location if (!isValidLocation(resolved.sender.lat, resolved.sender.lon)) { return null; } return { lat: resolved.sender.lat, lon: resolved.sender.lon }; } const prevHop = resolved.hops[hopIndex - 1]; // If previous hop was ambiguous, we can't show meaningful distances if (prevHop.matches.length > 1) { return null; } // If previous hop was unknown, we also can't calculate if (prevHop.matches.length === 0) { return null; } // Check if previous hop has valid location if (isValidLocation(prevHop.matches[0].lat, prevHop.matches[0].lon)) { return { lat: prevHop.matches[0].lat, lon: prevHop.matches[0].lon }; } return null; }; return (
{/* Sender */} {/* Hops */} {resolved.hops.map((hop, index) => ( ))} {/* Receiver */} {/* Total distance */} {resolved.totalDistances && resolved.totalDistances.length > 0 && (
Presumed unambiguous distance covered:{' '} {resolved.hasGaps ? '>' : ''} {formatDistance(resolved.totalDistances[0])}
)}
); } interface PathNodeProps { label: string; name: string; prefix: string; distance: number | null; isFirst?: boolean; isLast?: boolean; /** Optional coordinates for map link */ lat?: number | null; lon?: number | null; /** Public key for map focus link (required if lat/lon provided) */ publicKey?: string; } function PathNode({ label, name, prefix, distance, isFirst, isLast, lat, lon, publicKey, }: PathNodeProps) { const hasLocation = isValidLocation(lat ?? null, lon ?? null) && publicKey; return (
{/* Vertical line and dot column */}
{!isFirst &&
}
{!isLast &&
}
{/* Content */}
{label}:{' '} {prefix}
{name} {distance !== null && ( - {formatDistance(distance)} )} {hasLocation && }
); } interface HopNodeProps { hop: PathHop; hopNumber: number; prevLocation: { lat: number | null; lon: number | null } | null; } function HopNode({ hop, hopNumber, prevLocation }: HopNodeProps) { const isAmbiguous = hop.matches.length > 1; const isUnknown = hop.matches.length === 0; // Calculate distance from previous location for a contact // Returns null if prev location unknown/ambiguous or contact has no valid location const getDistanceForContact = (contact: { lat: number | null; lon: number | null; }): number | null => { if (!prevLocation || prevLocation.lat === null || prevLocation.lon === null) { return null; } // Check if contact has valid location if (!isValidLocation(contact.lat, contact.lon)) { return null; } return calculateDistance(prevLocation.lat, prevLocation.lon, contact.lat, contact.lon); }; return (
{/* Vertical line and dot column */}
{/* Content */}
Hop {hopNumber}:{' '} {hop.prefix} {isAmbiguous && (ambiguous)}
{isUnknown ? (
<UNKNOWN>
) : isAmbiguous ? (
{hop.matches.map((contact) => { const dist = getDistanceForContact(contact); const hasLocation = isValidLocation(contact.lat, contact.lon); return (
{contact.name || contact.public_key.slice(0, 12)} {dist !== null && ( - {formatDistance(dist)} )} {hasLocation && ( )}
); })}
) : (
{hop.matches[0].name || hop.matches[0].public_key.slice(0, 12)} {hop.distanceFromPrev !== null && ( - {formatDistance(hop.distanceFromPrev)} )} {isValidLocation(hop.matches[0].lat, hop.matches[0].lon) && ( )}
)}
); } /** * Render clickable coordinates that open the map focused on the contact */ function CoordinateLink({ lat, lon, publicKey }: { lat: number; lon: number; publicKey: string }) { const handleClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // Open map in new tab with focus on this contact const url = window.location.origin + window.location.pathname + getMapFocusHash(publicKey); window.open(url, '_blank'); }; return ( { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); (e.currentTarget as HTMLElement).click(); } }} onClick={handleClick} title="View on map" > ({lat.toFixed(4)}, {lon.toFixed(4)}) ); } function calculateReceiverDistance(resolved: ResolvedPath): number | null { // Get last hop's location (if any) let prevLat: number | null = null; let prevLon: number | null = null; if (resolved.hops.length > 0) { const lastHop = resolved.hops[resolved.hops.length - 1]; // Only use last hop if it's unambiguous and has valid location if ( lastHop.matches.length === 1 && isValidLocation(lastHop.matches[0].lat, lastHop.matches[0].lon) ) { prevLat = lastHop.matches[0].lat; prevLon = lastHop.matches[0].lon; } } else { // No hops, calculate from sender to receiver (if sender has valid location) if (isValidLocation(resolved.sender.lat, resolved.sender.lon)) { prevLat = resolved.sender.lat; prevLon = resolved.sender.lon; } } if (prevLat === null || prevLon === null) { return null; } // Check receiver has valid location if (!isValidLocation(resolved.receiver.lat, resolved.receiver.lon)) { return null; } return calculateDistance(prevLat, prevLon, resolved.receiver.lat, resolved.receiver.lon); }