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

View File

@@ -51,7 +51,7 @@ export function PathModal({
onAnalyzePacket,
}: PathModalProps) {
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 hasPaths = paths.length > 0;
const showAnalyzePacket = hasPaths && packetId != null && onAnalyzePacket;
@@ -141,59 +141,65 @@ export function PathModal({
</div>
)}
{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 (
<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>
{resolvedPaths.map((pathData, index) => (
<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 />
)}
<PathVisualization
resolved={pathData.resolved}
senderInfo={senderInfo}
distanceUnit={distanceUnit}
/>
<button
onClick={() => setMapModalIndex(index)}
className="text-xs text-primary hover:underline cursor-pointer shrink-0 ml-2"
>
Map route
</button>
</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>
)}

View File

@@ -8,6 +8,7 @@ import type { ResolvedPath, SenderInfo } from '../utils/pathUtils';
interface PathRouteMapProps {
resolved: ResolvedPath;
senderInfo: SenderInfo;
height?: number;
}
// Colors for hop markers (indexed by hop number - 1)
@@ -82,7 +83,7 @@ function RouteMapBounds({ points }: { points: [number, number][] }) {
return null;
}
export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
export function PathRouteMap({ resolved, senderInfo, height = 220 }: PathRouteMapProps) {
const points = collectPoints(resolved);
const hasAnyGps = points.length > 0;
@@ -117,7 +118,7 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
className="rounded border border-border overflow-hidden"
role="img"
aria-label="Map showing message route between nodes"
style={{ height: 220 }}
style={{ height }}
>
<MapContainer
center={center}
@@ -138,6 +139,8 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
icon={makeIcon('S', SENDER_COLOR)}
>
<Tooltip direction="top" offset={[0, -14]}>
<span className="font-mono">{resolved.sender.prefix}</span>
{' · '}
{senderInfo.name || 'Sender'}
</Tooltip>
</Marker>
@@ -154,6 +157,8 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
icon={makeIcon(String(hopIdx + 1), getHopColor(hopIdx))}
>
<Tooltip direction="top" offset={[0, -14]}>
<span className="font-mono">{hop.prefix}</span>
{' · '}
{m.name || m.public_key.slice(0, 12)}
</Tooltip>
</Marker>
@@ -167,6 +172,8 @@ export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) {
icon={makeIcon('R', RECEIVER_COLOR)}
>
<Tooltip direction="top" offset={[0, -14]}>
<span className="font-mono">{resolved.receiver.prefix}</span>
{' · '}
{resolved.receiver.name || 'Receiver'}
</Tooltip>
</Marker>