import { DeviceImage } from "@components/generic/DeviceImage.tsx"; import { TimeAgo } from "@components/generic/TimeAgo.tsx"; import { Uptime } from "@components/generic/Uptime.tsx"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@components/UI/Accordion.tsx"; import { Button } from "@components/UI/Button.tsx"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; import { Separator } from "@components/UI/Separator"; import { Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipTrigger, } from "@components/UI/Tooltip.tsx"; import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts"; import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts"; import { toast } from "@core/hooks/useToast.ts"; import { useAppStore, useDevice, useNodeDB } from "@core/stores"; import { cn } from "@core/utils/cn.ts"; import { Protobuf } from "@meshtastic/core"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { useNavigate } from "@tanstack/react-router"; import { fromByteArray } from "base64-js"; import { BellIcon, BellOffIcon, MapPinnedIcon, MessageSquareIcon, StarIcon, TrashIcon, WaypointsIcon, } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; export interface NodeDetailsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } export const NodeDetailsDialog = ({ open, onOpenChange, }: NodeDetailsDialogProps) => { const { t } = useTranslation("dialog"); const { setDialogOpen, connection } = useDevice(); const { getNode } = useNodeDB(); const navigate = useNavigate(); const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore(); const { updateFavorite } = useFavoriteNode(); const { updateIgnored } = useIgnoreNode(); const node = getNode(nodeNumDetails); const [isFavoriteState, setIsFavoriteState] = useState( node?.isFavorite ?? false, ); const [isIgnoredState, setIsIgnoredState] = useState( node?.isIgnored ?? false, ); useEffect(() => { if (!node) { return; } setIsFavoriteState(node?.isFavorite); setIsIgnoredState(node?.isIgnored); }, [node]); if (!node) { return; } function handleDirectMessage() { if (!node) { return; } navigate({ to: `/messages/direct/${node.num}` }); setDialogOpen("nodeDetails", false); } function handleRequestPosition() { if (!node) { return; } toast({ title: t("toast.requestingPosition.title", { ns: "ui" }), }); connection?.requestPosition(node.num).then(() => toast({ title: t("toast.positionRequestSent.title", { ns: "ui" }), }), ); onOpenChange(false); } function handleTraceroute() { if (!node) { return; } toast({ title: t("toast.sendingTraceroute.title", { ns: "ui" }), }); connection?.traceRoute(node.num).then(() => toast({ title: t("toast.tracerouteSent.title", { ns: "ui" }), }), ); onOpenChange(false); } function handleNodeRemove() { if (!node) { return; } setNodeNumToBeRemoved(node?.num); setDialogOpen("nodeRemoval", true); onOpenChange(false); } function handleToggleFavorite() { if (!node) { return; } updateFavorite({ nodeNum: node.num, isFavorite: !isFavoriteState }); setIsFavoriteState(!isFavoriteState); } function handleToggleIgnored() { if (!node) { return; } updateIgnored({ nodeNum: node.num, isIgnored: !isIgnoredState }); setIsIgnoredState(!isIgnoredState); } const deviceMetricsMap = [ { key: "airUtilTx", label: t("nodeDetails.airTxUtilization"), value: node.deviceMetrics?.airUtilTx, format: (val: number) => `${val.toFixed(2)}%`, }, { key: "channelUtilization", label: t("nodeDetails.channelUtilization"), value: node.deviceMetrics?.channelUtilization, format: (val: number) => `${val.toFixed(2)}%`, }, { key: "batteryLevel", label: t("nodeDetails.batteryLevel"), value: node.deviceMetrics?.batteryLevel, format: (val: number) => val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`, }, { key: "voltage", label: t("nodeDetails.voltage"), value: node.deviceMetrics?.voltage, format: (val: number) => `${val.toFixed(2)}V`, }, ]; const sectionClassName = "text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-4 rounded-lg mt-3"; return ( {t("nodeDetails.title", { interpolation: { escapeValue: false }, identifier: `${node.user?.longName ?? t("unknown.shortName")} (${ node.user?.shortName ?? t("unknown.shortName") })`, })}
{isIgnoredState ? t("nodeDetails.unignoreNode") : t("nodeDetails.ignoreNode")} {t("nodeDetails.removeNode")}

{t("nodeDetails.details")}

{t("nodeDetails.nodeNumber")} {node.num}
{t("nodeDetails.nodeHexPrefix")} !{numberToHexUnpadded(node.num)}
{t("nodeDetails.role")} {Protobuf.Config.Config_DeviceConfig_Role[ node.user?.role ?? 0 ]?.replace(/_/g, " ")}
{t("nodeDetails.lastHeard")} {node.lastHeard === 0 ? ( t("nodesTable.lastHeardStatus.never", { ns: "nodes", }) ) : ( )}
{t("nodeDetails.hardware")} {( Protobuf.Mesh.HardwareModel[ node.user?.hwModel ?? 0 ] ?? t("unknown.shortName") ).replace(/_/g, " ")}
{t("nodeDetails.messageable")} {node.user?.isUnmessagable ? t("no") : t("yes")}

{t("nodeDetails.security")}

{t("nodeDetails.publicKey")}
                          {node.user?.publicKey &&
                          node.user?.publicKey.length > 0
                            ? fromByteArray(node.user.publicKey)
                            : t("unknown.longName")}
                        
{node.isKeyManuallyVerified ? t("nodeDetails.KeyManuallyVerifiedTrue") : t("nodeDetails.KeyManuallyVerifiedFalse")}

{t("nodeDetails.position")}

{node.position ? ( {node.position.latitudeI && node.position.longitudeI && ( )} {node.position.altitude && ( )}
{t("locationResponse.coordinates")} {node.position.latitudeI / 1e7},{" "} {node.position.longitudeI / 1e7}
{t("locationResponse.altitude")} {node.position.altitude} {t("unit.meter.suffix")}
) : (

{t("unknown.longName")}

)}
{node.deviceMetrics && (

{t("nodeDetails.deviceMetrics")}

{deviceMetricsMap .filter((metric) => metric.value !== undefined) .map((metric) => ( ))} {node.deviceMetrics.uptimeSeconds && ( )}
{metric.label}: {metric.format(metric?.value ?? 0)}
{t("nodeDetails.uptime")}
)}

{t("nodeDetails.allRawMetrics")}

                      {JSON.stringify(node, null, 2)}
                    
); };