Show hop map in a larger modal. Closes #102.

This commit is contained in:
jkingsman
2026-03-24 16:14:43 -07:00
parent 3b28ebfa49
commit 74e1f49db8
2 changed files with 66 additions and 53 deletions
+57 -51
View File
@@ -51,7 +51,7 @@ export function PathModal({
onAnalyzePacket, onAnalyzePacket,
}: PathModalProps) { }: PathModalProps) {
const { distanceUnit } = useDistanceUnit(); const { distanceUnit } = useDistanceUnit();
const [expandedMaps, setExpandedMaps] = useState<Set<number>>(new Set()); const [mapModalIndex, setMapModalIndex] = useState<number | null>(null);
const hasResendActions = isOutgoingChan && messageId !== undefined && onResend; const hasResendActions = isOutgoingChan && messageId !== undefined && onResend;
const hasPaths = paths.length > 0; const hasPaths = paths.length > 0;
const showAnalyzePacket = hasPaths && packetId != null && onAnalyzePacket; const showAnalyzePacket = hasPaths && packetId != null && onAnalyzePacket;
@@ -141,59 +141,65 @@ export function PathModal({
</div> </div>
)} )}
{resolvedPaths.map((pathData, index) => { {resolvedPaths.map((pathData, index) => (
const mapExpanded = expandedMaps.has(index); <div key={index}>
const toggleMap = () => <div className="flex items-center justify-between mb-2 pb-1 border-b border-border">
setExpandedMaps((prev) => { {!hasSinglePath ? (
const next = new Set(prev); <div className="text-sm text-foreground/70 font-semibold">
if (next.has(index)) next.delete(index); Path {index + 1}{' '}
else next.add(index); <span className="font-normal text-muted-foreground">
return next; received {formatTime(pathData.received_at)}
}); </span>
return (
<div key={index}>
<div className="flex items-center justify-between mb-2 pb-1 border-b border-border">
{!hasSinglePath ? (
<div className="text-sm text-foreground/70 font-semibold">
Path {index + 1}{' '}
<span className="font-normal text-muted-foreground">
received {formatTime(pathData.received_at)}
</span>
</div>
) : (
<div />
)}
<button
onClick={toggleMap}
aria-expanded={mapExpanded}
className="text-xs text-primary hover:underline cursor-pointer shrink-0 ml-2"
>
{mapExpanded ? 'Hide map' : 'Map route'}
</button>
</div>
{mapExpanded && (
<div className="mb-2">
<Suspense
fallback={
<div
className="rounded border border-border bg-muted/30 animate-pulse"
style={{ height: 220 }}
/>
}
>
<PathRouteMap resolved={pathData.resolved} senderInfo={senderInfo} />
</Suspense>
</div> </div>
) : (
<div />
)} )}
<PathVisualization <button
resolved={pathData.resolved} onClick={() => setMapModalIndex(index)}
senderInfo={senderInfo} className="text-xs text-primary hover:underline cursor-pointer shrink-0 ml-2"
distanceUnit={distanceUnit} >
/> Map route
</button>
</div> </div>
); <PathVisualization
})} resolved={pathData.resolved}
senderInfo={senderInfo}
distanceUnit={distanceUnit}
/>
</div>
))}
{/* Map modal — opens when a "Map route" button is clicked */}
<Dialog open={mapModalIndex !== null} onOpenChange={(open) => !open && setMapModalIndex(null)}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{mapModalIndex !== null && !hasSinglePath
? `Path ${mapModalIndex + 1} Route Map`
: 'Route Map'}
</DialogTitle>
<DialogDescription>
Map of known node locations along this message route.
</DialogDescription>
</DialogHeader>
{mapModalIndex !== null && (
<Suspense
fallback={
<div
className="rounded border border-border bg-muted/30 animate-pulse"
style={{ height: 400 }}
/>
}
>
<PathRouteMap
resolved={resolvedPaths[mapModalIndex].resolved}
senderInfo={senderInfo}
height={400}
/>
</Suspense>
)}
</DialogContent>
</Dialog>
</div> </div>
)} )}
+9 -2
View File
@@ -8,6 +8,7 @@ import type { ResolvedPath, SenderInfo } from '../utils/pathUtils';
interface PathRouteMapProps { interface PathRouteMapProps {
resolved: ResolvedPath; resolved: ResolvedPath;
senderInfo: SenderInfo; senderInfo: SenderInfo;
height?: number;
} }
// Colors for hop markers (indexed by hop number - 1) // Colors for hop markers (indexed by hop number - 1)
@@ -82,7 +83,7 @@ function RouteMapBounds({ points }: { points: [number, number][] }) {
return null; return null;
} }
export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) { export function PathRouteMap({ resolved, senderInfo, height = 220 }: PathRouteMapProps) {
const points = collectPoints(resolved); const points = collectPoints(resolved);
const hasAnyGps = points.length > 0; const hasAnyGps = points.length > 0;
@@ -117,7 +118,7 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
className="rounded border border-border overflow-hidden" className="rounded border border-border overflow-hidden"
role="img" role="img"
aria-label="Map showing message route between nodes" aria-label="Map showing message route between nodes"
style={{ height: 220 }} style={{ height }}
> >
<MapContainer <MapContainer
center={center} center={center}
@@ -138,6 +139,8 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
icon={makeIcon('S', SENDER_COLOR)} icon={makeIcon('S', SENDER_COLOR)}
> >
<Tooltip direction="top" offset={[0, -14]}> <Tooltip direction="top" offset={[0, -14]}>
<span className="font-mono">{resolved.sender.prefix}</span>
{' · '}
{senderInfo.name || 'Sender'} {senderInfo.name || 'Sender'}
</Tooltip> </Tooltip>
</Marker> </Marker>
@@ -154,6 +157,8 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
icon={makeIcon(String(hopIdx + 1), getHopColor(hopIdx))} icon={makeIcon(String(hopIdx + 1), getHopColor(hopIdx))}
> >
<Tooltip direction="top" offset={[0, -14]}> <Tooltip direction="top" offset={[0, -14]}>
<span className="font-mono">{hop.prefix}</span>
{' · '}
{m.name || m.public_key.slice(0, 12)} {m.name || m.public_key.slice(0, 12)}
</Tooltip> </Tooltip>
</Marker> </Marker>
@@ -167,6 +172,8 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
icon={makeIcon('R', RECEIVER_COLOR)} icon={makeIcon('R', RECEIVER_COLOR)}
> >
<Tooltip direction="top" offset={[0, -14]}> <Tooltip direction="top" offset={[0, -14]}>
<span className="font-mono">{resolved.receiver.prefix}</span>
{' · '}
{resolved.receiver.name || 'Receiver'} {resolved.receiver.name || 'Receiver'}
</Tooltip> </Tooltip>
</Marker> </Marker>