mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Add multipath tracking
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useCallback, useState, type ReactNode } from 'react';
|
||||
import type { Contact, Message, RadioConfig } from '../types';
|
||||
import type { Contact, Message, MessagePath, RadioConfig } from '../types';
|
||||
import { CONTACT_TYPE_REPEATER } from '../types';
|
||||
import { formatTime, parseSenderFromText } from '../utils/messageParser';
|
||||
import { pubkeysMatch } from '../utils/pubkey';
|
||||
import { getHopCount, type SenderInfo } from '../utils/pathUtils';
|
||||
import { formatHopCounts, type SenderInfo } from '../utils/pathUtils';
|
||||
import { ContactAvatar } from './ContactAvatar';
|
||||
import { PathModal } from './PathModal';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -62,6 +62,40 @@ function renderTextWithMentions(text: string, radioName?: string): ReactNode {
|
||||
return parts.length > 0 ? parts : text;
|
||||
}
|
||||
|
||||
// Clickable hop count badge that opens the path modal
|
||||
interface HopCountBadgeProps {
|
||||
paths: MessagePath[];
|
||||
onClick: () => void;
|
||||
variant: 'header' | 'inline';
|
||||
}
|
||||
|
||||
function HopCountBadge({ paths, onClick, variant }: HopCountBadgeProps) {
|
||||
const hopInfo = formatHopCounts(paths);
|
||||
// Single direct: "(d)", otherwise "(d/1/3 hops)"
|
||||
const label =
|
||||
hopInfo.allDirect && !hopInfo.hasMultiple
|
||||
? `(${hopInfo.display})`
|
||||
: `(${hopInfo.display} hops)`;
|
||||
|
||||
const className =
|
||||
variant === 'header'
|
||||
? 'font-normal text-muted-foreground/70 ml-1 text-[11px] cursor-pointer hover:text-primary hover:underline'
|
||||
: 'text-[10px] text-muted-foreground/50 ml-1 cursor-pointer hover:text-primary hover:underline';
|
||||
|
||||
return (
|
||||
<span
|
||||
className={className}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
title="View message path"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageList({
|
||||
messages,
|
||||
contacts,
|
||||
@@ -78,7 +112,7 @@ export function MessageList({
|
||||
const isInitialLoadRef = useRef<boolean>(true);
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
||||
const [selectedPath, setSelectedPath] = useState<{
|
||||
path: string;
|
||||
paths: MessagePath[];
|
||||
senderInfo: SenderInfo;
|
||||
} | null>(null);
|
||||
|
||||
@@ -336,27 +370,18 @@ export function MessageList({
|
||||
<span className="font-normal text-muted-foreground/70 ml-2 text-[11px]">
|
||||
{formatTime(msg.sender_timestamp || msg.received_at)}
|
||||
</span>
|
||||
{!msg.outgoing &&
|
||||
msg.path &&
|
||||
(getHopCount(msg.path) === 0 ? (
|
||||
<span className="font-normal text-muted-foreground/70 ml-1 text-[11px]">
|
||||
(direct)
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="font-normal text-muted-foreground/70 ml-1 text-[11px] cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedPath({
|
||||
path: msg.path!,
|
||||
senderInfo: getSenderInfo(msg, contact, sender),
|
||||
});
|
||||
}}
|
||||
title="View message path"
|
||||
>
|
||||
({getHopCount(msg.path)} hop{getHopCount(msg.path) !== 1 ? 's' : ''})
|
||||
</span>
|
||||
))}
|
||||
{!msg.outgoing && msg.paths && msg.paths.length > 0 && (
|
||||
<HopCountBadge
|
||||
paths={msg.paths}
|
||||
variant="header"
|
||||
onClick={() =>
|
||||
setSelectedPath({
|
||||
paths: msg.paths!,
|
||||
senderInfo: getSenderInfo(msg, contact, sender),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="break-words whitespace-pre-wrap">
|
||||
@@ -371,27 +396,18 @@ export function MessageList({
|
||||
<span className="text-[10px] text-muted-foreground/50 ml-2">
|
||||
{formatTime(msg.sender_timestamp || msg.received_at)}
|
||||
</span>
|
||||
{!msg.outgoing &&
|
||||
msg.path &&
|
||||
(getHopCount(msg.path) === 0 ? (
|
||||
<span className="text-[10px] text-muted-foreground/50 ml-1">
|
||||
(direct)
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="text-[10px] text-muted-foreground/50 ml-1 cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedPath({
|
||||
path: msg.path!,
|
||||
senderInfo: getSenderInfo(msg, contact, sender),
|
||||
});
|
||||
}}
|
||||
title="View message path"
|
||||
>
|
||||
({getHopCount(msg.path)} hop{getHopCount(msg.path) !== 1 ? 's' : ''})
|
||||
</span>
|
||||
))}
|
||||
{!msg.outgoing && msg.paths && msg.paths.length > 0 && (
|
||||
<HopCountBadge
|
||||
paths={msg.paths}
|
||||
variant="inline"
|
||||
onClick={() =>
|
||||
setSelectedPath({
|
||||
paths: msg.paths!,
|
||||
senderInfo: getSenderInfo(msg, contact, sender),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{msg.outgoing && (msg.acked > 0 ? ` ✓${msg.acked > 1 ? msg.acked : ''}` : ' ?')}
|
||||
@@ -431,7 +447,7 @@ export function MessageList({
|
||||
<PathModal
|
||||
open={true}
|
||||
onClose={() => setSelectedPath(null)}
|
||||
path={selectedPath.path}
|
||||
paths={selectedPath.paths}
|
||||
senderInfo={selectedPath.senderInfo}
|
||||
contacts={contacts}
|
||||
config={config ?? null}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Contact, RadioConfig } from '../types';
|
||||
import type { Contact, RadioConfig, MessagePath } from '../types';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -17,34 +17,59 @@ import {
|
||||
type ResolvedPath,
|
||||
type PathHop,
|
||||
} from '../utils/pathUtils';
|
||||
import { formatTime } from '../utils/messageParser';
|
||||
import { getMapFocusHash } from '../utils/urlHash';
|
||||
|
||||
interface PathModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
path: string;
|
||||
paths: MessagePath[];
|
||||
senderInfo: SenderInfo;
|
||||
contacts: Contact[];
|
||||
config: RadioConfig | null;
|
||||
}
|
||||
|
||||
export function PathModal({ open, onClose, path, senderInfo, contacts, config }: PathModalProps) {
|
||||
const resolved = resolvePath(path, senderInfo, contacts, config);
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||
<DialogContent className="max-w-md max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Message Path</DialogTitle>
|
||||
<DialogTitle>Message Path{!hasSinglePath && `s (${paths.length})`}</DialogTitle>
|
||||
<DialogDescription>
|
||||
This shows <em>one route</em> that this message traveled through the mesh network. Flood
|
||||
messages may arrive via multiple paths, and routers may be incorrectly identified due to
|
||||
prefix collisions between heard and non-heard router advertisements.
|
||||
{hasSinglePath ? (
|
||||
<>
|
||||
This shows <em>one route</em> 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 <strong>{paths.length} different routes</strong>.
|
||||
Routers may be incorrectly identified due to prefix collisions.
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
<PathVisualization resolved={resolved} senderInfo={senderInfo} />
|
||||
<div className="flex-1 overflow-y-auto py-2 space-y-4">
|
||||
{resolvedPaths.map((pathData, index) => (
|
||||
<div key={index}>
|
||||
{!hasSinglePath && (
|
||||
<div className="text-xs text-muted-foreground font-medium mb-2 pb-1 border-b border-border">
|
||||
Path {index + 1} — received {formatTime(pathData.received_at)}
|
||||
</div>
|
||||
)}
|
||||
<PathVisualization resolved={pathData.resolved} senderInfo={senderInfo} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user