import { useEffect, useState } from "react"; import { useAppStore } from "@core/stores/appStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts"; import { MessageType, useMessageStore, } from "@core/stores/messageStore/index.ts"; import { Protobuf } from "@meshtastic/core"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { DeviceImage } from "@components/generic/DeviceImage.tsx"; import { TimeAgo } from "@components/generic/TimeAgo.tsx"; import { Uptime } from "@components/generic/Uptime.tsx"; import { toast } from "@core/hooks/useToast.ts"; import { useFavoriteNode } from "../../../core/hooks/useFavoriteNode.ts"; import { useIgnoreNode } from "../../../core/hooks/useIgnoreNode.ts"; import { cn } from "@core/utils/cn.ts"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@components/UI/Accordion.tsx"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; import { Button } from "@components/UI/Button.tsx"; import { BellIcon, BellOffIcon, MapPinnedIcon, MessageSquareIcon, StarIcon, TrashIcon, WaypointsIcon, } from "lucide-react"; import { Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipTrigger, } from "@components/UI/Tooltip.tsx"; import { Separator } from "@components/UI/Seperator.tsx"; 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, setActivePage, getNode } = useDevice(); const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore(); const { setChatType, setActiveChat } = useMessageStore(); 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; setChatType(MessageType.Direct); setActiveChat(node.num); setActivePage("messages"); } 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.toFixed(2)}%`, }, { key: "voltage", label: t("nodeDetails.voltage"), value: node.deviceMetrics?.voltage, format: (val: number) => `${val.toFixed(2)}V`, }, ]; return ( {t("nodeDetails.title", { 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.position")}

{node.position ? ( <> {node.position.latitudeI && node.position.longitudeI && (

{t("locationResponse.coordinates")} {node.position.latitudeI / 1e7},{" "} {node.position.longitudeI / 1e7}

)} {node.position.altitude && (

{t("locationResponse.altitude")} {node.position.altitude} {t("unit.meter.one")}

)} ) :

{t("unknown.shortName")}

}
{node.deviceMetrics && (

{t("nodeDetails.deviceMetrics")}

{deviceMetricsMap.map( (metric) => metric.value !== undefined && (

{metric.label}: {metric.format(metric.value)}

), )} {node.deviceMetrics.uptimeSeconds && (

{t("nodeDetails.uptime")}

)}
)}

{t("nodeDetails.allRawMetrics")}

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