import type { Contact, RadioConfig, MessagePath } from '../types'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog'; import { Button } from './ui/button'; import { resolvePath, calculateDistance, isValidLocation, formatDistance, type SenderInfo, type ResolvedPath, type PathHop, } from '../utils/pathUtils'; import { formatTime } from '../utils/messageParser'; import { getMapFocusHash } from '../utils/urlHash'; interface PathModalProps { open: boolean; onClose: () => void; paths: MessagePath[]; senderInfo: SenderInfo; contacts: Contact[]; config: RadioConfig | null; } export function PathModal({ open, onClose, paths, senderInfo, contacts, config }: PathModalProps) { // Resolve all paths const resolvedPaths = paths.map((p) => ({ ...p, resolved: resolvePath(p.path, senderInfo, contacts, config), })); const hasSinglePath = paths.length === 1; return ( !isOpen && onClose()}> Message Path{!hasSinglePath && `s (${paths.length})`} {hasSinglePath ? ( <> This shows one route that this message traveled through the mesh network. Routers may be incorrectly identified due to prefix collisions between heard and non-heard router advertisements. ) : ( <> This message was received via {paths.length} different routes. Routers may be incorrectly identified due to prefix collisions. )}
{resolvedPaths.map((pathData, index) => (
{!hasSinglePath && (
Path {index + 1} — received {formatTime(pathData.received_at)}
)}
))} {/* Straight-line distance shown once for multi-path (same for all routes) */} {!hasSinglePath && resolvedPaths.length > 0 && (() => { const first = resolvedPaths[0].resolved; if ( isValidLocation(first.sender.lat, first.sender.lon) && isValidLocation(first.receiver.lat, first.receiver.lon) ) { return (
Straight-line distance: {formatDistance( calculateDistance( first.sender.lat, first.sender.lon, first.receiver.lat, first.receiver.lon )! )}
); } return null; })()}
); } interface PathVisualizationProps { resolved: ResolvedPath; senderInfo: SenderInfo; /** If true, hide the straight-line distance (shown once at container level for multi-path) */ hideStraightLine?: boolean; } function PathVisualization({ resolved, senderInfo, hideStraightLine }: 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])}
)} {/* Straight-line distance (when both sender and receiver have coordinates) */} {!hideStraightLine && isValidLocation(resolved.sender.lat, resolved.sender.lon) && isValidLocation(resolved.receiver.lat, resolved.receiver.lon) && (
0 ? 'pt-1' : 'pt-3 mt-3 border-t border-border' } > Straight-line distance: {formatDistance( calculateDistance( resolved.sender.lat, resolved.sender.lon, resolved.receiver.lat, resolved.receiver.lon )! )}
)}
); } 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}
{name} ({prefix}) {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} {isAmbiguous && (ambiguous)}
{isUnknown ? (
<UNKNOWN {hop.prefix}>
) : 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)}{' '} ({contact.public_key.slice(0, 2).toUpperCase()}) {dist !== null && ( - {formatDistance(dist)} )} {hasLocation && ( )}
); })}
) : (
{hop.matches[0].name || hop.matches[0].public_key.slice(0, 12)}{' '} ({hop.prefix}) {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 ( ({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); }