diff --git a/package.json b/package.json index 1a8a9261..a1441b0d 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "react-map-gl": "7.1.9", "react-qrcode-logo": "^3.0.0", "rfc4648": "^1.5.4", - "timeago-react": "^3.0.6", "vite-plugin-node-polyfills": "^0.23.0", "zustand": "5.0.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0d0f030..7a2029e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,9 +113,6 @@ importers: rfc4648: specifier: ^1.5.4 version: 1.5.4 - timeago-react: - specifier: ^3.0.6 - version: 3.0.6(react@19.0.0) vite-plugin-node-polyfills: specifier: ^0.23.0 version: 0.23.0(rollup@4.29.1)(vite@5.3.6(@types/node@22.12.0)) @@ -2817,14 +2814,6 @@ packages: through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} - timeago-react@3.0.6: - resolution: {integrity: sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ==} - peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - - timeago.js@4.0.2: - resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==} - timers-browserify@2.0.12: resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} engines: {node: '>=0.6.0'} @@ -6498,13 +6487,6 @@ snapshots: dependencies: readable-stream: 3.6.2 - timeago-react@3.0.6(react@19.0.0): - dependencies: - react: 19.0.0 - timeago.js: 4.0.2 - - timeago.js@4.0.2: {} - timers-browserify@2.0.12: dependencies: setimmediate: 1.0.5 diff --git a/public/devices/README.md b/public/devices/README.md new file mode 100644 index 00000000..0e40ce8d --- /dev/null +++ b/public/devices/README.md @@ -0,0 +1,5 @@ +# Copyright Notice +Copyright © 2024 Meshtastic LLC. All Rights Reserved. + +## In reference to the GNU GPLv3 License terms defined in Section 7e +Images (or assets) in this directory are protected under international copyright laws and treaties. Unauthorized reproduction, distribution, modification, or use of these images in any form, commercial or otherwise, outside of official Meshtastic creative works or its Backers and Partners is strictly prohibited without prior written consent from the copyright holder (Meshtastic LLC). diff --git a/public/devices/diy.svg b/public/devices/diy.svg new file mode 100644 index 00000000..823467ed --- /dev/null +++ b/public/devices/diy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-ht62-esp32c3-sx1262.svg b/public/devices/heltec-ht62-esp32c3-sx1262.svg new file mode 100644 index 00000000..c52534ef --- /dev/null +++ b/public/devices/heltec-ht62-esp32c3-sx1262.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-mesh-node-t114-case.svg b/public/devices/heltec-mesh-node-t114-case.svg new file mode 100644 index 00000000..b2abe639 --- /dev/null +++ b/public/devices/heltec-mesh-node-t114-case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-mesh-node-t114.svg b/public/devices/heltec-mesh-node-t114.svg new file mode 100644 index 00000000..779a8f6a --- /dev/null +++ b/public/devices/heltec-mesh-node-t114.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-v3-case.svg b/public/devices/heltec-v3-case.svg new file mode 100644 index 00000000..1b1d3c55 --- /dev/null +++ b/public/devices/heltec-v3-case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-v3.svg b/public/devices/heltec-v3.svg new file mode 100644 index 00000000..13a5fa64 --- /dev/null +++ b/public/devices/heltec-v3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-vision-master-e213.svg b/public/devices/heltec-vision-master-e213.svg new file mode 100644 index 00000000..2c1cca09 --- /dev/null +++ b/public/devices/heltec-vision-master-e213.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-vision-master-e290.svg b/public/devices/heltec-vision-master-e290.svg new file mode 100644 index 00000000..ca7d296a --- /dev/null +++ b/public/devices/heltec-vision-master-e290.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-vision-master-t190.svg b/public/devices/heltec-vision-master-t190.svg new file mode 100644 index 00000000..55db34f9 --- /dev/null +++ b/public/devices/heltec-vision-master-t190.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-wireless-paper-V1_0.svg b/public/devices/heltec-wireless-paper-V1_0.svg new file mode 100644 index 00000000..cb3f188d --- /dev/null +++ b/public/devices/heltec-wireless-paper-V1_0.svg @@ -0,0 +1 @@ + diff --git a/public/devices/heltec-wireless-paper.svg b/public/devices/heltec-wireless-paper.svg new file mode 100644 index 00000000..cb3f188d --- /dev/null +++ b/public/devices/heltec-wireless-paper.svg @@ -0,0 +1 @@ + diff --git a/public/devices/heltec-wireless-tracker-V1-0.svg b/public/devices/heltec-wireless-tracker-V1-0.svg new file mode 100644 index 00000000..a5392595 --- /dev/null +++ b/public/devices/heltec-wireless-tracker-V1-0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-wireless-tracker.svg b/public/devices/heltec-wireless-tracker.svg new file mode 100644 index 00000000..a5392595 --- /dev/null +++ b/public/devices/heltec-wireless-tracker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/heltec-wsl-v3.svg b/public/devices/heltec-wsl-v3.svg new file mode 100644 index 00000000..1741223e --- /dev/null +++ b/public/devices/heltec-wsl-v3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/nano-g2-ultra.svg b/public/devices/nano-g2-ultra.svg new file mode 100644 index 00000000..6dbe47af --- /dev/null +++ b/public/devices/nano-g2-ultra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/pico.svg b/public/devices/pico.svg new file mode 100644 index 00000000..82ce6526 --- /dev/null +++ b/public/devices/pico.svg @@ -0,0 +1,2956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/devices/promicro.svg b/public/devices/promicro.svg new file mode 100644 index 00000000..3dc26021 --- /dev/null +++ b/public/devices/promicro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/rak-wismeshtap.svg b/public/devices/rak-wismeshtap.svg new file mode 100644 index 00000000..34e77876 --- /dev/null +++ b/public/devices/rak-wismeshtap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/rak11310.svg b/public/devices/rak11310.svg new file mode 100644 index 00000000..8f526a47 --- /dev/null +++ b/public/devices/rak11310.svg @@ -0,0 +1,2339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/devices/rak2560.svg b/public/devices/rak2560.svg new file mode 100644 index 00000000..b8514f01 --- /dev/null +++ b/public/devices/rak2560.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/rak4631.svg b/public/devices/rak4631.svg new file mode 100644 index 00000000..6dc2957a --- /dev/null +++ b/public/devices/rak4631.svg @@ -0,0 +1,3514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/devices/rak4631_case.svg b/public/devices/rak4631_case.svg new file mode 100644 index 00000000..a0b2bbb8 --- /dev/null +++ b/public/devices/rak4631_case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/rpipicow.svg b/public/devices/rpipicow.svg new file mode 100644 index 00000000..cb4b1f68 --- /dev/null +++ b/public/devices/rpipicow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/seeed-sensecap-indicator.svg b/public/devices/seeed-sensecap-indicator.svg new file mode 100644 index 00000000..f7bf9db0 --- /dev/null +++ b/public/devices/seeed-sensecap-indicator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/seeed-xiao-s3.svg b/public/devices/seeed-xiao-s3.svg new file mode 100644 index 00000000..04e97fe0 --- /dev/null +++ b/public/devices/seeed-xiao-s3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/station-g2.svg b/public/devices/station-g2.svg new file mode 100644 index 00000000..8d2e0aed --- /dev/null +++ b/public/devices/station-g2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/t-deck.svg b/public/devices/t-deck.svg new file mode 100644 index 00000000..cdc53c5d --- /dev/null +++ b/public/devices/t-deck.svg @@ -0,0 +1 @@ +QWERTYIUPOASDFGHKJLaltZXCVBMN \ No newline at end of file diff --git a/public/devices/t-echo.svg b/public/devices/t-echo.svg new file mode 100644 index 00000000..e178a50f --- /dev/null +++ b/public/devices/t-echo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/t-watch-s3.svg b/public/devices/t-watch-s3.svg new file mode 100644 index 00000000..19084c19 --- /dev/null +++ b/public/devices/t-watch-s3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tbeam-s3-core.svg b/public/devices/tbeam-s3-core.svg new file mode 100644 index 00000000..f42e6d2c --- /dev/null +++ b/public/devices/tbeam-s3-core.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tbeam.svg b/public/devices/tbeam.svg new file mode 100644 index 00000000..cd0475c6 --- /dev/null +++ b/public/devices/tbeam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tlora-c6.svg b/public/devices/tlora-c6.svg new file mode 100644 index 00000000..8b626638 --- /dev/null +++ b/public/devices/tlora-c6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tlora-t3s3-epaper.svg b/public/devices/tlora-t3s3-epaper.svg new file mode 100644 index 00000000..6f2e8452 --- /dev/null +++ b/public/devices/tlora-t3s3-epaper.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tlora-t3s3-v1.svg b/public/devices/tlora-t3s3-v1.svg new file mode 100644 index 00000000..1f8847d4 --- /dev/null +++ b/public/devices/tlora-t3s3-v1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tlora-v2-1-1_6.svg b/public/devices/tlora-v2-1-1_6.svg new file mode 100644 index 00000000..dbe36ef5 --- /dev/null +++ b/public/devices/tlora-v2-1-1_6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tlora-v2-1-1_8.svg b/public/devices/tlora-v2-1-1_8.svg new file mode 100644 index 00000000..dbe36ef5 --- /dev/null +++ b/public/devices/tlora-v2-1-1_8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/tracker-t1000-e.svg b/public/devices/tracker-t1000-e.svg new file mode 100644 index 00000000..6f7a06c9 --- /dev/null +++ b/public/devices/tracker-t1000-e.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/devices/unknown.svg b/public/devices/unknown.svg new file mode 100644 index 00000000..1d2cd87b --- /dev/null +++ b/public/devices/unknown.svg @@ -0,0 +1,160 @@ + + diff --git a/public/devices/wio-tracker-wm1110.svg b/public/devices/wio-tracker-wm1110.svg new file mode 100644 index 00000000..15ace5c5 --- /dev/null +++ b/public/devices/wio-tracker-wm1110.svg @@ -0,0 +1 @@ +LoRaWI FILEDRESETGNSSBLE \ No newline at end of file diff --git a/public/devices/wm1110_dev_kit.svg b/public/devices/wm1110_dev_kit.svg new file mode 100644 index 00000000..94aefe30 --- /dev/null +++ b/public/devices/wm1110_dev_kit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Dialog/DialogManager.tsx b/src/components/Dialog/DialogManager.tsx index e1769751..d571ffcb 100644 --- a/src/components/Dialog/DialogManager.tsx +++ b/src/components/Dialog/DialogManager.tsx @@ -6,6 +6,8 @@ import { QRDialog } from "@components/Dialog/QRDialog.tsx"; import { RebootDialog } from "@components/Dialog/RebootDialog.tsx"; import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; +import type { JSX } from "react"; +import { NodeDetailsDialog } from "./NodeDetailsDialog"; export const DialogManager = (): JSX.Element => { const { channels, config, dialog, setDialogOpen } = useDevice(); @@ -56,6 +58,12 @@ export const DialogManager = (): JSX.Element => { setDialogOpen("pkiBackup", open); }} /> + { + setDialogOpen("nodeDetails", open); + }} + /> ); }; diff --git a/src/components/Dialog/LocationResponseDialog.tsx b/src/components/Dialog/LocationResponseDialog.tsx new file mode 100644 index 00000000..825046e6 --- /dev/null +++ b/src/components/Dialog/LocationResponseDialog.tsx @@ -0,0 +1,62 @@ +import { useDevice } from "@app/core/stores/deviceStore"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog"; +import type { Protobuf, Types } from "@meshtastic/js"; +import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; +import type { JSX } from "react"; + +export interface LocationResponseDialogProps { + location: Types.PacketMetadata | undefined; + open: boolean; + onOpenChange: () => void; +} + +export const LocationResponseDialog = ({ + location, + open, + onOpenChange, +}: LocationResponseDialogProps): JSX.Element => { + const { nodes } = useDevice(); + + const from = nodes.get(location?.from ?? 0); + const longName = + from?.user?.longName ?? + (from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown"); + const shortName = + from?.user?.shortName ?? + (from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK"); + + return ( + + + + {`Location: ${longName} (${shortName})`} + + +
+ +

+ Coordinates:{" "} + + {location?.data.latitudeI / 1e7},{" "} + {location?.data.longitudeI / 1e7} + +

+

Altitude: {location?.data.altitude}m

+
+
+
+
+
+ ); +}; diff --git a/src/components/Dialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog.tsx new file mode 100644 index 00000000..80c032bc --- /dev/null +++ b/src/components/Dialog/NodeDetailsDialog.tsx @@ -0,0 +1,163 @@ +import { useAppStore } from "@app/core/stores/appStore"; +import { useDevice } from "@app/core/stores/deviceStore"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@components/UI/Accordion"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog"; +import { Protobuf } from "@meshtastic/js"; +import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; +import { DeviceImage } from "../generic/DeviceImage"; +import { TimeAgo } from "../generic/TimeAgo"; +import { Uptime } from "../generic/Uptime"; + +export interface NodeDetailsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export const NodeDetailsDialog = ({ + open, + onOpenChange, +}: NodeDetailsDialogProps) => { + const { nodes } = useDevice(); + const { nodeNumDetails } = useAppStore(); + const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails); + + return device ? ( + + + + + Node Details for {device.user?.longName ?? "UNKNOWN"} ( + {device.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} + +

+ ) : null} + {device.position.altitude ? ( +

Altitude: {device.position.altitude}m

+ ) : null} +
+ ) : null} + + {device.deviceMetrics ? ( +
+

+ Device Metrics: +

+ {device.deviceMetrics.airUtilTx ? ( +

+ Air TX utilization:{" "} + {device.deviceMetrics.airUtilTx.toFixed(2)}% +

+ ) : null} + {device.deviceMetrics.channelUtilization ? ( +

+ Channel utilization:{" "} + {device.deviceMetrics.channelUtilization.toFixed(2)}% +

+ ) : null} + {device.deviceMetrics.batteryLevel ? ( +

+ Battery level:{" "} + {device.deviceMetrics.batteryLevel.toFixed(2)}% +

+ ) : null} + {device.deviceMetrics.voltage ? ( +

Voltage: {device.deviceMetrics.voltage.toFixed(2)}V

+ ) : null} + {device.deviceMetrics.uptimeSeconds ? ( +

+ Uptime:{" "} + +

+ ) : null} +
+ ) : null} + + {device ? ( +
+ + + +

+ All Raw Metrics: +

+
+ +
+                        {JSON.stringify(device, null, 2)}
+                      
+
+
+
+
+ ) : null} +
+
+
+
+ ) : null; +}; diff --git a/src/components/Dialog/NodeOptionsDialog.tsx b/src/components/Dialog/NodeOptionsDialog.tsx new file mode 100644 index 00000000..d62e4065 --- /dev/null +++ b/src/components/Dialog/NodeOptionsDialog.tsx @@ -0,0 +1,117 @@ +import { toast } from "@app/core/hooks/useToast"; +import { useAppStore } from "@app/core/stores/appStore"; +import { useDevice } from "@app/core/stores/deviceStore"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog"; +import type { Protobuf } from "@meshtastic/js"; +import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; +import { TrashIcon } from "lucide-react"; +import type { JSX } from "react"; +import { Button } from "../UI/Button"; + +export interface NodeOptionsDialogProps { + node: Protobuf.Mesh.NodeInfo | undefined; + open: boolean; + onOpenChange: () => void; +} + +export const NodeOptionsDialog = ({ + node, + open, + onOpenChange, +}: NodeOptionsDialogProps): JSX.Element => { + const { setDialogOpen, connection, setActivePage } = useDevice(); + const { + setNodeNumToBeRemoved, + setNodeNumDetails, + setChatType, + setActiveChat, + } = useAppStore(); + const longName = + node?.user?.longName ?? + (node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown"); + const shortName = + node?.user?.shortName ?? + (node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK"); + + function handleDirectMessage() { + if (!node) return; + setChatType("direct"); + setActiveChat(node.num); + setActivePage("messages"); + } + + function handleRequestPosition() { + if (!node) return; + toast({ + title: "Requesting position, please wait...", + }); + connection?.requestPosition(node.num).then(() => + toast({ + title: "Position request sent.", + }), + ); + onOpenChange(); + } + + function handleTraceroute() { + if (!node) return; + toast({ + title: "Sending Traceroute, please wait...", + }); + connection?.traceRoute(node.num).then(() => + toast({ + title: "Traceroute sent.", + }), + ); + onOpenChange(); + } + + return ( + + + + {`${longName} (${shortName})`} + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ ); +}; diff --git a/src/components/Dialog/TracerouteResponseDialog.tsx b/src/components/Dialog/TracerouteResponseDialog.tsx new file mode 100644 index 00000000..9fce1cfa --- /dev/null +++ b/src/components/Dialog/TracerouteResponseDialog.tsx @@ -0,0 +1,57 @@ +import { useDevice } from "@app/core/stores/deviceStore"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog"; +import type { Protobuf, Types } from "@meshtastic/js"; +import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; +import type { JSX } from "react"; +import { TraceRoute } from "../PageComponents/Messages/TraceRoute"; + +export interface TracerouteResponseDialogProps { + traceroute: Types.PacketMetadata | undefined; + open: boolean; + onOpenChange: () => void; +} + +export const TracerouteResponseDialog = ({ + traceroute, + open, + onOpenChange, +}: TracerouteResponseDialogProps): JSX.Element => { + const { nodes } = useDevice(); + const route: number[] = traceroute?.data.route ?? []; + const routeBack: number[] = traceroute?.data.routeBack ?? []; + const snrTowards = traceroute?.data.snrTowards ?? []; + const snrBack = traceroute?.data.snrBack ?? []; + const from = nodes.get(traceroute?.from ?? 0); + const longName = + from?.user?.longName ?? + (from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown"); + const shortName = + from?.user?.shortName ?? + (from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK"); + const to = nodes.get(traceroute?.to ?? 0); + return ( + + + + {`Traceroute: ${longName} (${shortName})`} + + + + + + + ); +}; diff --git a/src/components/PageComponents/Map/NodeDetail.tsx b/src/components/PageComponents/Map/NodeDetail.tsx index e2e49728..8f37120b 100644 --- a/src/components/PageComponents/Map/NodeDetail.tsx +++ b/src/components/PageComponents/Map/NodeDetail.tsx @@ -4,7 +4,7 @@ import { Subtle } from "@app/components/UI/Typography/Subtle.tsx"; import { formatQuantity } from "@app/core/utils/string"; import { Avatar } from "@components/UI/Avatar"; import { Mono } from "@components/generic/Mono.tsx"; -import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx"; +import { TimeAgo } from "@components/generic/TimeAgo.tsx"; import { Protobuf } from "@meshtastic/js"; import type { Protobuf as ProtobufType } from "@meshtastic/js"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/src/components/PageComponents/Messages/TraceRoute.tsx index 5b768580..11c77332 100644 --- a/src/components/PageComponents/Messages/TraceRoute.tsx +++ b/src/components/PageComponents/Messages/TraceRoute.tsx @@ -1,36 +1,60 @@ import { useDevice } from "@app/core/stores/deviceStore.ts"; import type { Protobuf } from "@meshtastic/js"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; +import type { JSX } from "react"; export interface TraceRouteProps { from?: Protobuf.Mesh.NodeInfo; to?: Protobuf.Mesh.NodeInfo; route: Array; + routeBack?: Array; + snrTowards?: Array; + snrBack?: Array; } export const TraceRoute = ({ from, to, route, + routeBack, + snrTowards, + snrBack, }: TraceRouteProps): JSX.Element => { const { nodes } = useDevice(); - return route.length === 0 ? ( + return (
- {to?.user?.longName}↔{from?.user?.longName} - -
- ) : ( -
- - {to?.user?.longName}↔ - {route.map((hop) => { - const node = nodes.get(hop); - return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}↔`; - })} +

Route to destination:

+

{to?.user?.longName}

+

↓ {snrTowards?.[0] ? snrTowards[0] : "??"}dB

+ {route.map((hop, i) => ( + +

+ {nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`} +

+

↓ {snrTowards?.[i + 1] ? snrTowards[i + 1] : "??"}dB

+
+ ))} {from?.user?.longName}
+ {routeBack ? ( + +

Route back:

+

{from?.user?.longName}

+

↓ {snrBack?.[0] ? snrBack[0] : "??"}dB

+ {routeBack.map((hop, i) => ( + +

+ {nodes.get(hop)?.user?.longName ?? + `!${numberToHexUnpadded(hop)}`} +

+

↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB

+
+ ))} + {to?.user?.longName} +
+ ) : null}
); }; diff --git a/src/components/UI/Accordion.tsx b/src/components/UI/Accordion.tsx new file mode 100644 index 00000000..a44017b1 --- /dev/null +++ b/src/components/UI/Accordion.tsx @@ -0,0 +1,44 @@ +import { cn } from "@core/utils/cn.ts"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; +import { type ComponentRef, forwardRef } from "react"; + +export const Accordion = AccordionPrimitive.Root; + +export const AccordionHeader = AccordionPrimitive.Header; + +export const AccordionItem = AccordionPrimitive.Item; + +export const AccordionTrigger = forwardRef< + ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + {props.children} + + +)); + +export const AccordionContent = forwardRef< + ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); diff --git a/src/components/UI/Dialog.tsx b/src/components/UI/Dialog.tsx index 019b8142..d989b842 100644 --- a/src/components/UI/Dialog.tsx +++ b/src/components/UI/Dialog.tsx @@ -44,7 +44,7 @@ const DialogContent = React.forwardRef< ["className"]; +} + +const hardwareModelToFilename: { [key: string]: string } = { + DIY_V1: "diy.svg", + NANO_G2_ULTRA: "nano-g2-ultra.svg", + TBEAM: "tbeam.svg", + HELTEC_HT62: "heltec-ht62-esp32c3-sx1262.svg", + RPI_PICO: "pico.svg", + T_DECK: "t-deck.svg", + HELTEC_MESH_NODE_T114: "heltec-mesh-node-t114.svg", + HELTEC_MESH_NODE_T114_CASE: "heltec-mesh-node-t114-case.svg", + HELTEC_V3: "heltec-v3.svg", + HELTEC_V3_CASE: "heltec-v3-case.svg", + HELTEC_VISION_MASTER_E213: "heltec-vision-master-e213.svg", + HELTEC_VISION_MASTER_E290: "heltec-vision-master-e290.svg", + HELTEC_VISION_MASTER_T190: "heltec-vision-master-t190.svg", + HELTEC_WIRELESS_PAPER: "heltec-wireless-paper.svg", + HELTEC_WIRELESS_PAPER_V1_0: "heltec-wireless-paper-V1_0.svg", + HELTEC_WIRELESS_TRACKER: "heltec-wireless-tracker.svg", + HELTEC_WIRELESS_TRACKER_V1_0: "heltec-wireless-tracker-V1-0.svg", + HELTEC_WSL_V3: "heltec-wsl-v3.svg", + TLORA_C6: "tlora-c6.svg", + TLORA_T3_S3: "tlora-t3s3-v1.svg", + TLORA_T3_S3_EPAPER: "tlora-t3s3-epaper.svg", + TLORA_V2: "tlora-v2-1-1_6.svg", + TLORA_V2_1_1P6: "tlora-v2-1-1_6.svg", + TLORA_V2_1_1P8: "tlora-v2-1-1_8.svg", + RAK11310: "rak11310.svg", + RAK2560: "rak2560.svg", + RAK4631: "rak4631.svg", + RAK4631_CASE: "rak4631_case.svg", + WIO_WM1110: "wio-tracker-wm1110.svg", + WM1110_DEV_KIT: "wm1110_dev_kit.svg", + STATION_G2: "station-g2.svg", + TBEAM_V0P7: "tbeam-s3-core.svg", + T_ECHO: "t-echo.svg", + TRACKER_T1000_E: "tracker-t1000-e.svg", + T_WATCH_S3: "t-watch-s3.svg", + SEEED_XIAO_S3: "seeed-xiao-s3.svg", + SENSECAP_INDICATOR: "seeed-sensecap-indicator.svg", + PROMICRO: "promicro.svg", + RPIPICOW: "rpipicow.svg", + UNKNOWN: "unknown.svg", +}; + +export const DeviceImage = ({ deviceType, className }: DeviceImageProps) => { + const getPath = (device: string) => `/devices/${device}`; + const device = hardwareModelToFilename[deviceType] || "unknown.svg"; + return {device}; +}; diff --git a/src/components/generic/Table/tmp/TimeAgo.tsx b/src/components/generic/Table/tmp/TimeAgo.tsx deleted file mode 100755 index 32a766ff..00000000 --- a/src/components/generic/Table/tmp/TimeAgo.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import TimeAgoReact from "timeago-react"; - -export interface TimeAgoProps { - timestamp: number; -} - -export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => { - return ; -}; diff --git a/src/components/generic/TimeAgo.tsx b/src/components/generic/TimeAgo.tsx new file mode 100755 index 00000000..85043c58 --- /dev/null +++ b/src/components/generic/TimeAgo.tsx @@ -0,0 +1,66 @@ +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipProvider, + TooltipTrigger, +} from "@radix-ui/react-tooltip"; +import type { JSX } from "react"; + +export interface TimeAgoProps { + timestamp: number; +} + +const getTimeAgo = ( + unixTimestamp: number, + locale: Intl.LocalesArgument = "en", +): string => { + const timestamp = new Date(unixTimestamp); + const diff = (new Date().getTime() - timestamp.getTime()) / 1000; + + const minutes = Math.floor(diff / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const months = Math.floor(days / 30); + const years = Math.floor(months / 12); + const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); + + if (years > 0) { + return rtf.format(0 - years, "year"); + } + if (months > 0) { + return rtf.format(0 - months, "month"); + } + if (days > 0) { + return rtf.format(0 - days, "day"); + } + if (hours > 0) { + return rtf.format(0 - hours, "hour"); + } + if (minutes > 0) { + return rtf.format(0 - minutes, "minute"); + } + return rtf.format(Math.floor(0 - diff), "second"); +}; + +export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => { + return ( + + + + {getTimeAgo(timestamp)} + + + + {new Date(timestamp).toLocaleString()} + + + + + ); +}; diff --git a/src/components/generic/Uptime.tsx b/src/components/generic/Uptime.tsx new file mode 100644 index 00000000..fdcf897a --- /dev/null +++ b/src/components/generic/Uptime.tsx @@ -0,0 +1,17 @@ +import type { JSX } from "react"; + +export interface UptimeProps { + seconds: number; +} + +const getUptime = (seconds: number): string => { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor(((seconds % 86400) % 3600) / 60); + const secondsLeft = Math.floor(((seconds % 86400) % 3600) % 60); + return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`; +}; + +export const Uptime = ({ seconds }: UptimeProps): JSX.Element => { + return {getUptime(seconds)}; +}; diff --git a/src/core/stores/appStore.ts b/src/core/stores/appStore.ts index c7bc2c62..d9e213c8 100644 --- a/src/core/stores/appStore.ts +++ b/src/core/stores/appStore.ts @@ -1,3 +1,4 @@ +import { Types } from "@meshtastic/js"; import { produce } from "immer"; import { create } from "zustand"; @@ -29,6 +30,9 @@ interface AppState { nodeNumToBeRemoved: number; accent: AccentColor; connectDialogOpen: boolean; + nodeNumDetails: number; + activeChat: number; + chatType: "broadcast" | "direct"; setRasterSources: (sources: RasterSource[]) => void; addRasterSource: (source: RasterSource) => void; @@ -42,6 +46,9 @@ interface AppState { setNodeNumToBeRemoved: (nodeNum: number) => void; setAccent: (color: AccentColor) => void; setConnectDialogOpen: (open: boolean) => void; + setNodeNumDetails: (nodeNum: number) => void; + setActiveChat: (chat: number) => void; + setChatType: (type: "broadcast" | "direct") => void; } export const useAppStore = create()((set) => ({ @@ -57,6 +64,9 @@ export const useAppStore = create()((set) => ({ accent: "orange", connectDialogOpen: false, nodeNumToBeRemoved: 0, + nodeNumDetails: 0, + activeChat: Types.ChannelNumber.Primary, + chatType: "broadcast", setRasterSources: (sources: RasterSource[]) => { set( @@ -124,4 +134,16 @@ export const useAppStore = create()((set) => ({ }), ); }, + setNodeNumDetails: (nodeNum) => + set((state) => ({ + nodeNumDetails: nodeNum, + })), + setActiveChat: (chat) => + set(() => ({ + activeChat: chat, + })), + setChatType: (type) => + set(() => ({ + chatType: type, + })), })); diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index bd407611..a4ad7b69 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -26,7 +26,8 @@ export type DialogVariant = | "reboot" | "deviceName" | "nodeRemoval" - | "pkiBackup"; + | "pkiBackup" + | "nodeDetails"; export interface Device { id: number; @@ -62,6 +63,7 @@ export interface Device { deviceName: boolean; nodeRemoval: boolean; pkiBackup: boolean; + nodeDetails: boolean; }; setStatus: (status: Types.DeviceStatusEnum) => void; @@ -145,6 +147,7 @@ export const useDeviceStore = create((set, get) => ({ deviceName: false, nodeRemoval: false, pkiBackup: false, + nodeDetails: false, }, pendingSettingsChanges: false, messageDraft: "", diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index 02ce37c6..2ca47ceb 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -1,3 +1,4 @@ +import { useAppStore } from "@app/core/stores/appStore"; import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx"; import { PageLayout } from "@components/PageLayout.tsx"; import { Sidebar } from "@components/Sidebar.tsx"; @@ -5,21 +6,17 @@ import { Avatar } from "@components/UI/Avatar.tsx"; import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx"; import { useToast } from "@core/hooks/useToast.ts"; -import { Device, useDevice, useDeviceStore } from "@core/stores/deviceStore.ts"; +import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf, Types } from "@meshtastic/js"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { getChannelName } from "@pages/Channels.tsx"; import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react"; import { useState } from "react"; -const MessagesPage = () => { +export const MessagesPage = () => { const { channels, nodes, hardware, messages, traceroutes, connection } = useDevice(); - const [chatType, setChatType] = - useState("broadcast"); - const [activeChat, setActiveChat] = useState( - Types.ChannelNumber.Primary, - ); + const { activeChat, chatType, setActiveChat, setChatType } = useAppStore(); const [searchTerm, setSearchTerm] = useState(""); const filteredNodes = Array.from(nodes.values()).filter((node) => { if (node.num === hardware.myNodeNum) return false; diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx index dd4a9765..6ad69f65 100644 --- a/src/pages/Nodes.tsx +++ b/src/pages/Nodes.tsx @@ -1,17 +1,18 @@ +import { LocationResponseDialog } from "@app/components/Dialog/LocationResponseDialog"; +import { NodeOptionsDialog } from "@app/components/Dialog/NodeOptionsDialog"; +import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteResponseDialog"; import Footer from "@app/components/UI/Footer"; -import { useAppStore } from "@app/core/stores/appStore"; import { Sidebar } from "@components/Sidebar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx"; import { Button } from "@components/UI/Button.tsx"; import { Mono } from "@components/generic/Mono.tsx"; import { Table } from "@components/generic/Table/index.tsx"; -import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx"; +import { TimeAgo } from "@components/generic/TimeAgo.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; -import { Protobuf } from "@meshtastic/js"; +import { Protobuf, type Types } from "@meshtastic/js"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; -import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react"; -import { useState } from "react"; -import { Fragment, type JSX } from "react"; +import { LockIcon, LockOpenIcon } from "lucide-react"; +import { Fragment, type JSX, useCallback, useEffect, useState } from "react"; import { base16 } from "rfc4648"; export interface DeleteNoteDialogProps { @@ -20,8 +21,16 @@ export interface DeleteNoteDialogProps { } const NodesPage = (): JSX.Element => { - const { nodes, hardware, setDialogOpen } = useDevice(); - const { setNodeNumToBeRemoved } = useAppStore(); + const { nodes, hardware, connection } = useDevice(); + const [selectedNode, setSelectedNode] = useState< + Protobuf.Mesh.NodeInfo | undefined + >(undefined); + const [selectedTraceroute, setSelectedTraceroute] = useState< + Types.PacketMetadata | undefined + >(); + const [selectedLocation, setSelectedLocation] = useState< + Types.PacketMetadata | undefined + >(); const [searchTerm, setSearchTerm] = useState(""); const filteredNodes = Array.from(nodes.values()).filter((node) => { @@ -30,6 +39,36 @@ const NodesPage = (): JSX.Element => { return nodeName.toLowerCase().includes(searchTerm.toLowerCase()); }); + useEffect(() => { + if (!connection) return; + connection.events.onTraceRoutePacket.subscribe(handleTraceroute); + return () => { + connection.events.onTraceRoutePacket.unsubscribe(handleTraceroute); + }; + }, [connection]); + + const handleTraceroute = useCallback( + (traceroute: Types.PacketMetadata) => { + setSelectedTraceroute(traceroute); + }, + [], + ); + + useEffect(() => { + if (!connection) return; + connection.events.onPositionPacket.subscribe(handleLocation); + return () => { + connection.events.onPositionPacket.subscribe(handleLocation); + }; + }, [connection]); + + const handleLocation = useCallback( + (location: Types.PacketMetadata) => { + setSelectedLocation(location); + }, + [], + ); + return ( <> @@ -47,21 +86,38 @@ const NodesPage = (): JSX.Element => { [
- +
, -

+

setSelectedNode(node)} + className="cursor-pointer" + > + {node.user?.shortName ?? + (node.user?.macaddr + ? `${base16 + .stringify(node.user?.macaddr.subarray(4, 6) ?? []) + .toLowerCase()}` + : `${numberToHexUnpadded(node.num).slice(-4)}`)} +

, + +

setSelectedNode(node)} + className="cursor-pointer" + > {node.user?.longName ?? (node.user?.macaddr ? `Meshtastic ${base16 @@ -95,7 +151,7 @@ const NodesPage = (): JSX.Element => { {node.user?.publicKey && node.user?.publicKey.length > 0 ? ( ) : ( - + )} , @@ -108,19 +164,23 @@ const NodesPage = (): JSX.Element => { : "-"} {node.viaMqtt === true ? ", via MQTT" : ""} , - , ])} /> + setSelectedNode(undefined)} + /> + setSelectedTraceroute(undefined)} + /> + setSelectedLocation(undefined)} + />