mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-12 11:43:03 +02:00
Show hop map in a larger modal. Closes #102.
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user