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'; import { useDistanceUnit } from '../contexts/DistanceUnitContext'; import type { DistanceUnit } from '../utils/distanceUnits'; 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; packetId?: number | null; isOutgoingChan?: boolean; isResendable?: boolean; onResend?: (messageId: number, newTimestamp?: boolean) => void; onAnalyzePacket?: () => void; } export function PathModal({ open, onClose, paths, senderInfo, contacts, config, messageId, packetId, isOutgoingChan, isResendable, onResend, onAnalyzePacket, }: PathModalProps) { const { distanceUnit } = useDistanceUnit(); const [mapModalIndex, setMapModalIndex] = useState(null); const hasResendActions = isOutgoingChan && messageId !== undefined && onResend; const hasPaths = paths.length > 0; const showAnalyzePacket = hasPaths && packetId != null && onAnalyzePacket; // 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. Repeater identities are inferred from locally known advert and path data, so some hops may be missing or misidentified when that data is incomplete. ) : ( <> This message was received via {paths.length} different routes. Repeater identities are inferred from locally known advert and path data, so some hops may be missing or misidentified when that data is incomplete. )} {hasPaths && (
{showAnalyzePacket ? ( ) : null} {/* 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 )!, distanceUnit )}
)} {resolvedPaths.map((pathData, index) => (
{!hasSinglePath ? (
Path {index + 1}{' '} — received {formatTime(pathData.received_at)}
) : (
)}
))} {/* Map modal — opens when a "Map route" button is clicked */} !open && setMapModalIndex(null)} > {mapModalIndex !== null && !hasSinglePath ? `Path ${mapModalIndex + 1} Route Map` : 'Route Map'} Map of known node locations along this message route. {mapModalIndex !== null && ( } > )}
)}
{hasResendActions && (
{isResendable && ( )}
)}
); } interface PathVisualizationProps { resolved: ResolvedPath; senderInfo: SenderInfo; distanceUnit: DistanceUnit; } function PathVisualization({ resolved, senderInfo, distanceUnit }: 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], distanceUnit)}
)}
); } interface PathNodeProps { label: string; name: string; prefix: string; distance: number | null; distanceUnit: DistanceUnit; 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, distanceUnit, 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, distanceUnit)} )} {hasLocation && }
); } interface HopNodeProps { hop: PathHop; hopNumber: number; prevLocation: { lat: number | null; lon: number | null } | null; distanceUnit: DistanceUnit; } function HopNode({ hop, hopNumber, prevLocation, distanceUnit }: 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, distanceUnit)} )} {hasLocation && ( )}
); })}
) : (
{hop.matches[0].name || hop.matches[0].public_key.slice(0, 12)} {hop.distanceFromPrev !== null && ( - {formatDistance(hop.distanceFromPrev, distanceUnit)} )} {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); }