From 4ab41b75074f5ff321f95b40e5deb3ace21595ad Mon Sep 17 00:00:00 2001 From: philon- Date: Fri, 9 May 2025 23:19:32 +0200 Subject: [PATCH] Favourites WIP --- .../NodeDetailsDialog/NodeDetailsDialog.tsx | 287 ++++++++++++++---- src/core/stores/deviceStore.ts | 14 + src/pages/Nodes.tsx | 4 +- 3 files changed, 236 insertions(+), 69 deletions(-) diff --git a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx index 33a45945..3f2fa454 100644 --- a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx +++ b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx @@ -1,5 +1,18 @@ +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 { Accordion, AccordionContent, @@ -14,51 +27,122 @@ import { DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; -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 { Button } from "@components/UI/Button.tsx"; + +import { + EarIcon, + EarOffIcon, + 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"; export interface NodeDetailsDialogProps { + node: Protobuf.Mesh.NodeInfo | undefined; open: boolean; onOpenChange: (open: boolean) => void; } export const NodeDetailsDialog = ({ + node, open, onOpenChange, }: NodeDetailsDialogProps) => { - const { getNode } = useDevice(); - const { nodeNumDetails } = useAppStore(); + const { setDialogOpen, connection, setActivePage, setFavorite } = useDevice(); + const { setNodeNumToBeRemoved } = useAppStore(); + const { setChatType, setActiveChat } = useMessageStore(); + const [isFavouriteState, setIsFavoriteState] = useState(false); + + useEffect(() => { + if (!node) return; + setIsFavoriteState(node?.isFavorite); + }, [node]); + + if (!node) return null; + + function handleDirectMessage() { + if (!node) return null; + + setChatType(MessageType.Direct); + setActiveChat(node.num); + setActivePage("messages"); + } + + function handleRequestPosition() { + if (!node) return null; + + toast({ + title: "Requesting position, please wait...", + }); + connection?.requestPosition(node.num).then(() => + toast({ + title: "Position request sent.", + }) + ); + onOpenChange(false); + } + + function handleTraceroute() { + if (!node) return null; - const device = getNode(nodeNumDetails); + toast({ + title: "Sending Traceroute, please wait...", + }); + connection?.traceRoute(node.num).then(() => + toast({ + title: "Traceroute sent.", + }) + ); + onOpenChange(false); + } - if (!device) return null; + function handleNodeRemove() { + if (!node) return null; + + setNodeNumToBeRemoved(node?.num); + setDialogOpen("nodeRemoval", true); + onOpenChange(false); + } + + function handleToggleFavourite() { + if (!node) return null; + setFavorite(node.num, !isFavouriteState); + setIsFavoriteState(!isFavouriteState); + } const deviceMetricsMap = [ { key: "airUtilTx", label: "Air TX utilization", - value: device.deviceMetrics?.airUtilTx, + value: node.deviceMetrics?.airUtilTx, format: (val: number) => `${val.toFixed(2)}%`, }, { key: "channelUtilization", label: "Channel utilization", - value: device.deviceMetrics?.channelUtilization, + value: node.deviceMetrics?.channelUtilization, format: (val: number) => `${val.toFixed(2)}%`, }, { key: "batteryLevel", label: "Battery level", - value: device.deviceMetrics?.batteryLevel, + value: node.deviceMetrics?.batteryLevel, format: (val: number) => `${val.toFixed(2)}%`, }, { key: "voltage", label: "Voltage", - value: device.deviceMetrics?.voltage, + value: node.deviceMetrics?.voltage, format: (val: number) => `${val.toFixed(2)}V`, }, ]; @@ -69,64 +153,133 @@ export const NodeDetailsDialog = ({ - Node Details for {device.user?.longName ?? "UNKNOWN"} ( - {device.user?.shortName ?? "UNK"}) + Node Details for {node.user?.longName ?? "UNKNOWN"} ( + {node.user?.shortName ?? "UNK"})
-
- -
-

Details:

-

- Hardware:{" "} - {Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]} -

-

Node Number: {device.num}

-

Node Hex: !{numberToHexUnpadded(device.num)}

-

- Role: {Protobuf.Config.Config_DeviceConfig_Role[ - device.user?.role ?? 0 - ]} -

-

- Last Heard: {device.lastHeard === 0 - ? "Never" - : } -

-
+
+ + + +
- {device.position && ( -
-

Position:

- {device.position.latitudeI && device.position.longitudeI && ( -

- Coordinates:{" "} - - {device.position.latitudeI / 1e7},{" "} - {device.position.longitudeI / 1e7} - -

- )} - {device.position.altitude && ( -

Altitude: {device.position.altitude}m

- )} + + + + + + + {node.isIgnored ? "Unignore node" : "Ignore node"} + + + + + + + + + + + + Remove node + + + + +
+ + + +
+
+
+

Details:

+

Node Number: {node.num}

+

Node Hex: !{numberToHexUnpadded(node.num)}

+

+ Role: {Protobuf.Config.Config_DeviceConfig_Role[ + node.user?.role ?? 0 + ].replace(/_/g, " ")} +

+

+ Last Heard: {node.lastHeard === 0 + ? "Never" + : } +

+

+ Hardware:{" "} + {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] + .replace(/_/g, " ")} +

- )} + +
+
+ +
+
+

Position:

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

+ Coordinates:{" "} + + {node.position.latitudeI / 1e7},{" "} + {node.position.longitudeI / 1e7} + +

+ )} + {node.position.altitude && ( +

Altitude: {node.position.altitude}m

+ )} + + ) + :

Unknown

} + +
- {device.deviceMetrics && ( + {node.deviceMetrics && (

Device Metrics: @@ -139,10 +292,10 @@ export const NodeDetailsDialog = ({

), )} - {device.deviceMetrics.uptimeSeconds && ( + {node.deviceMetrics.uptimeSeconds && (

Uptime:{" "} - +

)}
@@ -159,7 +312,7 @@ export const NodeDetailsDialog = ({
-                      {JSON.stringify(device, null, 2)}
+                      {JSON.stringify(node, null, 2)}
                     
diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index e4cfe823..e6cc35fa 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -93,6 +93,7 @@ export interface Device { getNodesLength: () => number; getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined; getMyNode: () => Protobuf.Mesh.NodeInfo; + setFavorite: (nodeNum: number, isFavorite: boolean) => void; } export interface DeviceState { @@ -594,6 +595,19 @@ export const useDeviceStore = createStore((set, get) => ({ } return device.nodesMap.size; }, + setFavorite(nodeNum: number, isFavorite: boolean) { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) throw new Error(`Device ${id} not found`); + + const node = device.nodesMap.get(nodeNum); + if (!node) throw new Error(`Node ${nodeNum} not found`); + + node.isFavorite = isFavorite; + }), + ); + }, }); }), ); diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx index 08621c3e..be49d77c 100644 --- a/src/pages/Nodes.tsx +++ b/src/pages/Nodes.tsx @@ -1,5 +1,5 @@ import { LocationResponseDialog } from "@app/components/Dialog/LocationResponseDialog.tsx"; -import { NodeOptionsDialog } from "@app/components/Dialog/NodeOptionsDialog.tsx"; +import { NodeDetailsDialog } from "@app/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx"; import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteResponseDialog.tsx"; import { Sidebar } from "@components/Sidebar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx"; @@ -164,7 +164,7 @@ const NodesPage = (): JSX.Element => { , ])} /> - setSelectedNode(undefined)}