From c4565d97b08b4e20e8055adad62ea30ce4132fec Mon Sep 17 00:00:00 2001
From: Hunter Thornsberry
Date: Fri, 21 Jun 2024 15:06:19 -0400
Subject: [PATCH 01/51] add ability to enable/disable HTTPS connections
---
.../PageComponents/Connect/HTTP.tsx | 20 ++++++-------------
1 file changed, 6 insertions(+), 14 deletions(-)
diff --git a/src/components/PageComponents/Connect/HTTP.tsx b/src/components/PageComponents/Connect/HTTP.tsx
index 30e70169..e5179bc1 100644
--- a/src/components/PageComponents/Connect/HTTP.tsx
+++ b/src/components/PageComponents/Connect/HTTP.tsx
@@ -24,17 +24,13 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
)
? "meshtastic.local"
: window.location.hostname,
- tls: location.protocol === "https:",
+ tls: false,
},
});
- const tlsEnabled = useWatch({
- control,
- name: "tls",
- defaultValue: location.protocol === "https:",
- });
const [connectionInProgress, setConnectionInProgress] = useState(false);
+ const [https, setHTTPS] = useState(false);
const onSubmit = handleSubmit(async (data) => {
setConnectionInProgress(true);
@@ -46,7 +42,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
await connection.connect({
address: data.ip,
fetchInterval: 2000,
- tls: data.tls,
+ tls: https,
});
setSelectedDevice(id);
@@ -60,8 +56,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
{
control={control}
render={({ field: { value, ...rest } }) => (
<>
-
+
{checked ? setHTTPS(true) : setHTTPS(false) }}
{...rest}
/>
>
From d8261a649bbdd10d7c676353a8ddbbec78c9256d Mon Sep 17 00:00:00 2001
From: Hunter Thornsberry
Date: Fri, 21 Jun 2024 15:09:29 -0400
Subject: [PATCH 02/51] add disabled
---
src/components/PageComponents/Connect/HTTP.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/PageComponents/Connect/HTTP.tsx b/src/components/PageComponents/Connect/HTTP.tsx
index e5179bc1..50bdfdcc 100644
--- a/src/components/PageComponents/Connect/HTTP.tsx
+++ b/src/components/PageComponents/Connect/HTTP.tsx
@@ -69,6 +69,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
{checked ? setHTTPS(true) : setHTTPS(false) }}
+ disabled={connectionInProgress}
{...rest}
/>
>
From fd9e327c85544fc8c30fe77fb99746591d494f5a Mon Sep 17 00:00:00 2001
From: sgtwilko
Date: Fri, 30 Aug 2024 01:01:44 +0100
Subject: [PATCH 03/51] Use window.location.host so that websites run on
non-standard ports include the port
---
src/components/PageComponents/Connect/HTTP.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/PageComponents/Connect/HTTP.tsx b/src/components/PageComponents/Connect/HTTP.tsx
index 1eb7ca5c..ae049be2 100644
--- a/src/components/PageComponents/Connect/HTTP.tsx
+++ b/src/components/PageComponents/Connect/HTTP.tsx
@@ -23,7 +23,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
window.location.hostname,
)
? "meshtastic.local"
- : window.location.hostname,
+ : window.location.host,
tls: location.protocol === "https:",
},
});
From bd48b02ef30078ea591d092c226784d581e96be8 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 18 Sep 2024 11:17:18 +0200
Subject: [PATCH 04/51] Updated traceroute response
---
.../PageComponents/Messages/TraceRoute.tsx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/src/components/PageComponents/Messages/TraceRoute.tsx
index e2adf30e..cdc20760 100644
--- a/src/components/PageComponents/Messages/TraceRoute.tsx
+++ b/src/components/PageComponents/Messages/TraceRoute.tsx
@@ -18,17 +18,17 @@ export const TraceRoute = ({
return route.length === 0 ? (
- {to?.user?.longName}↔{from?.user?.longName}
+ {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")}↔`;
- })}
+ {to?.user?.longName} ↔{" "}
+ {route.map(
+ (hop) =>
+ `${nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`} ↔ `,
+ )}
{from?.user?.longName}
From 93e04e1b69674086294fa5490ab83acdaf7d8eb3 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 18 Sep 2024 11:21:15 +0200
Subject: [PATCH 05/51] Add traceroute dialog
---
.../Dialog/TracerouteResponseDialog.tsx | 46 +++++++++++++++++++
1 file changed, 46 insertions(+)
create mode 100644 src/components/Dialog/TracerouteResponseDialog.tsx
diff --git a/src/components/Dialog/TracerouteResponseDialog.tsx b/src/components/Dialog/TracerouteResponseDialog.tsx
new file mode 100644
index 00000000..f3b1502f
--- /dev/null
+++ b/src/components/Dialog/TracerouteResponseDialog.tsx
@@ -0,0 +1,46 @@
+import { useDevice } from "@app/core/stores/deviceStore";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@components/UI/Dialog.js";
+import type { Protobuf, Types } from "@meshtastic/js";
+import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
+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 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 (
+
+ );
+};
From 2a5acb877143160468af4926793a0615c62c5f84 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 18 Sep 2024 11:34:27 +0200
Subject: [PATCH 06/51] Add node dialog
---
src/components/Dialog/NodeOptionsDialog.tsx | 75 +++++++++++++++++++++
1 file changed, 75 insertions(+)
create mode 100644 src/components/Dialog/NodeOptionsDialog.tsx
diff --git a/src/components/Dialog/NodeOptionsDialog.tsx b/src/components/Dialog/NodeOptionsDialog.tsx
new file mode 100644
index 00000000..9269f7f9
--- /dev/null
+++ b/src/components/Dialog/NodeOptionsDialog.tsx
@@ -0,0 +1,75 @@
+import { toast } from "@app/core/hooks/useToast";
+import { useDevice } from "@app/core/stores/deviceStore";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@components/UI/Dialog.js";
+import type { Protobuf } from "@meshtastic/js";
+import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
+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 { connection } = useDevice();
+ const longName =
+ node?.user?.longName ??
+ (node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
+ const shortName =
+ node?.user?.shortName ??
+ (node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
+
+ function handleTraceroute() {
+ if (!node) return;
+ toast({
+ title: "Sending Traceroute, please wait...",
+ });
+ connection?.traceRoute(node.num).then(() =>
+ toast({
+ title: "Traceroute sent.",
+ }),
+ );
+ onOpenChange();
+ }
+
+ function handleRequestPosition() {
+ if (!node) return;
+ toast({
+ title: "Requesting position, please wait...",
+ });
+ connection?.requestPosition(node.num).then(() =>
+ toast({
+ title: "Position request sent.",
+ }),
+ );
+ onOpenChange();
+ }
+
+ return (
+
+ );
+};
From 56ac1d55f41446f51d19ddbaefc5490ca38eccf7 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 18 Sep 2024 11:34:47 +0200
Subject: [PATCH 07/51] Implement on nodes page
---
src/pages/Nodes.tsx | 43 +++++++++++++++++++++++++++++++++++++++----
1 file changed, 39 insertions(+), 4 deletions(-)
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
index 038d81a8..4b6303fd 100644
--- a/src/pages/Nodes.tsx
+++ b/src/pages/Nodes.tsx
@@ -1,3 +1,5 @@
+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.js";
@@ -7,10 +9,10 @@ import { Table } from "@components/generic/Table/index.js";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react";
-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 { Fragment } from "react";
+import { Fragment, useCallback, useEffect, useState } from "react";
import { base16 } from "rfc4648";
export interface DeleteNoteDialogProps {
@@ -19,13 +21,36 @@ export interface DeleteNoteDialogProps {
}
export const NodesPage = (): JSX.Element => {
- const { nodes, hardware, setDialogOpen } = useDevice();
+ const { nodes, hardware, setDialogOpen, connection } = useDevice();
const { setNodeNumToBeRemoved } = useAppStore();
+ const [selectedNode, setSelectedNode] = useState<
+ Protobuf.Mesh.NodeInfo | undefined
+ >(undefined);
+ const [selectedTraceroute, setSelectedTraceroute] = useState<
+ Types.PacketMetadata | undefined
+ >();
const filteredNodes = Array.from(nodes.values()).filter(
(n) => n.num !== hardware.myNodeNum,
);
+ useEffect(() => {
+ connection?.events.onTraceRoutePacket.subscribe(handleTraceroute);
+ return () => {
+ connection?.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
+ };
+ }, [
+ connection?.events.onTraceRoutePacket.subscribe,
+ connection?.events.onTraceRoutePacket.unsubscribe,
+ ]);
+
+ const handleTraceroute = useCallback(
+ (traceroute: Types.PacketMetadata) => {
+ setSelectedTraceroute(traceroute);
+ },
+ [],
+ );
+
return (
<>
@@ -45,7 +70,7 @@ export const NodesPage = (): JSX.Element => {
]}
rows={filteredNodes.map((node) => [
,
-
+ setSelectedNode(node)}>
{node.user?.longName ??
(node.user?.macaddr
? `Meshtastic ${base16
@@ -105,6 +130,16 @@ export const NodesPage = (): JSX.Element => {
,
])}
/>
+ setSelectedNode(undefined)}
+ />
+ setSelectedTraceroute(undefined)}
+ />
From a2b9a33f6a36a142f54989f7b3abc49ffe497b73 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Thu, 19 Sep 2024 10:37:48 +0200
Subject: [PATCH 08/51] Cursor pointer
---
src/pages/Nodes.tsx | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
index 4b6303fd..d38a1086 100644
--- a/src/pages/Nodes.tsx
+++ b/src/pages/Nodes.tsx
@@ -39,10 +39,7 @@ export const NodesPage = (): JSX.Element => {
return () => {
connection?.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
};
- }, [
- connection?.events.onTraceRoutePacket.subscribe,
- connection?.events.onTraceRoutePacket.unsubscribe,
- ]);
+ }, [connection]);
const handleTraceroute = useCallback(
(traceroute: Types.PacketMetadata) => {
@@ -70,7 +67,7 @@ export const NodesPage = (): JSX.Element => {
]}
rows={filteredNodes.map((node) => [
,
- setSelectedNode(node)}>
+ setSelectedNode(node)}>
{node.user?.longName ??
(node.user?.macaddr
? `Meshtastic ${base16
@@ -104,7 +101,7 @@ export const NodesPage = (): JSX.Element => {
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
) : (
-
+
)}
,
From aa66e1f73cd1fdbc03d10eeb000b3aca1c08abf5 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Thu, 23 Jan 2025 23:45:24 +0100
Subject: [PATCH 09/51] Biome
---
src/pages/Nodes.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
index d38a1086..57b1a546 100644
--- a/src/pages/Nodes.tsx
+++ b/src/pages/Nodes.tsx
@@ -67,7 +67,11 @@ export const NodesPage = (): JSX.Element => {
]}
rows={filteredNodes.map((node) => [
,
- setSelectedNode(node)}>
+ setSelectedNode(node)}
+ >
{node.user?.longName ??
(node.user?.macaddr
? `Meshtastic ${base16
From f24041651cc3710416636056e8777cdf2a01dea2 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Fri, 24 Jan 2025 00:03:55 +0100
Subject: [PATCH 10/51] Fix legacy import
---
src/components/Dialog/NodeOptionsDialog.tsx | 2 +-
src/components/Dialog/TracerouteResponseDialog.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/Dialog/NodeOptionsDialog.tsx b/src/components/Dialog/NodeOptionsDialog.tsx
index 9269f7f9..f2ea85e2 100644
--- a/src/components/Dialog/NodeOptionsDialog.tsx
+++ b/src/components/Dialog/NodeOptionsDialog.tsx
@@ -5,7 +5,7 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
-} from "@components/UI/Dialog.js";
+} from "@components/UI/Dialog";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { Button } from "../UI/Button";
diff --git a/src/components/Dialog/TracerouteResponseDialog.tsx b/src/components/Dialog/TracerouteResponseDialog.tsx
index f3b1502f..87ecf40e 100644
--- a/src/components/Dialog/TracerouteResponseDialog.tsx
+++ b/src/components/Dialog/TracerouteResponseDialog.tsx
@@ -5,7 +5,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
-} from "@components/UI/Dialog.js";
+} from "@components/UI/Dialog";
import type { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute";
From 965e3247b0b4e5d8952f2b9168a1e00e424346b3 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Fri, 24 Jan 2025 00:49:27 +0100
Subject: [PATCH 11/51] Added 2way traceroute
---
.../Dialog/TracerouteResponseDialog.tsx | 3 ++-
.../PageComponents/Messages/TraceRoute.tsx | 16 +++++++++++++++-
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/src/components/Dialog/TracerouteResponseDialog.tsx b/src/components/Dialog/TracerouteResponseDialog.tsx
index 87ecf40e..0b081d55 100644
--- a/src/components/Dialog/TracerouteResponseDialog.tsx
+++ b/src/components/Dialog/TracerouteResponseDialog.tsx
@@ -23,6 +23,7 @@ export const TracerouteResponseDialog = ({
}: TracerouteResponseDialogProps): JSX.Element => {
const { nodes } = useDevice();
const route: number[] = traceroute?.data.route ?? [];
+ const routeBack: number[] = traceroute?.data.routeBack ?? [];
const from = nodes.get(traceroute?.from ?? 0);
const longName =
from?.user?.longName ??
@@ -38,7 +39,7 @@ export const TracerouteResponseDialog = ({
{`Traceroute: ${longName} (${shortName})`}
-
+
diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/src/components/PageComponents/Messages/TraceRoute.tsx
index 9d4c53e6..a8fa04dc 100644
--- a/src/components/PageComponents/Messages/TraceRoute.tsx
+++ b/src/components/PageComponents/Messages/TraceRoute.tsx
@@ -6,12 +6,14 @@ export interface TraceRouteProps {
from?: Protobuf.Mesh.NodeInfo;
to?: Protobuf.Mesh.NodeInfo;
route: Array;
+ routeBack?: Array;
}
export const TraceRoute = ({
from,
to,
route,
+ routeBack,
}: TraceRouteProps): JSX.Element => {
const { nodes } = useDevice();
@@ -22,8 +24,9 @@ export const TraceRoute = ({
) : (
-
+
+ Route traced towards destination:
{to?.user?.longName} ↔{" "}
{route.map(
(hop) =>
@@ -31,6 +34,17 @@ export const TraceRoute = ({
)}
{from?.user?.longName}
+ {routeBack ? (
+
+ Route traced back to us:
+ {from?.user?.longName} ↔{" "}
+ {routeBack.map(
+ (hop) =>
+ `${nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`} ↔ `,
+ )}
+ {to?.user?.longName}
+
+ ) : null}
);
};
From 54c73a8c0dab346e54706142b062313b532acd8e Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Fri, 24 Jan 2025 12:15:57 +0100
Subject: [PATCH 12/51] Add snr to traceroute
---
.../Dialog/TracerouteResponseDialog.tsx | 11 ++++-
.../PageComponents/Messages/TraceRoute.tsx | 45 +++++++++++++------
2 files changed, 41 insertions(+), 15 deletions(-)
diff --git a/src/components/Dialog/TracerouteResponseDialog.tsx b/src/components/Dialog/TracerouteResponseDialog.tsx
index 0b081d55..e26cafc2 100644
--- a/src/components/Dialog/TracerouteResponseDialog.tsx
+++ b/src/components/Dialog/TracerouteResponseDialog.tsx
@@ -24,6 +24,8 @@ export const TracerouteResponseDialog = ({
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 ??
@@ -39,7 +41,14 @@ export const TracerouteResponseDialog = ({
{`Traceroute: ${longName} (${shortName})`}
-
+
diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/src/components/PageComponents/Messages/TraceRoute.tsx
index a8fa04dc..c2dc1f5b 100644
--- a/src/components/PageComponents/Messages/TraceRoute.tsx
+++ b/src/components/PageComponents/Messages/TraceRoute.tsx
@@ -7,6 +7,8 @@ export interface TraceRouteProps {
to?: Protobuf.Mesh.NodeInfo;
route: Array;
routeBack?: Array;
+ snrTowards?: Array;
+ snrBack?: Array;
}
export const TraceRoute = ({
@@ -14,6 +16,8 @@ export const TraceRoute = ({
to,
route,
routeBack,
+ snrTowards,
+ snrBack,
}: TraceRouteProps): JSX.Element => {
const { nodes } = useDevice();
@@ -24,24 +28,37 @@ export const TraceRoute = ({
) : (
-
+
- Route traced towards destination:
- {to?.user?.longName} ↔{" "}
- {route.map(
- (hop) =>
- `${nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`} ↔ `,
- )}
+ Route to destination:
+ {to?.user?.longName}
+ ↓ {snrTowards ? snrTowards[0] : null}dB
+ {route.map((hop, i) => (
+ // biome-ignore lint/suspicious/noArrayIndexKey:
+
+
+ {nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
+
+ ↓ {snrTowards ? snrTowards[i + 1] : null}dB
+
+ ))}
{from?.user?.longName}
{routeBack ? (
-
- Route traced back to us:
- {from?.user?.longName} ↔{" "}
- {routeBack.map(
- (hop) =>
- `${nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`} ↔ `,
- )}
+
+ Route back:
+ {from?.user?.longName}
+ ↓ {snrBack ? snrBack[0] : null}dB
+ {routeBack.map((hop, i) => (
+ // biome-ignore lint/suspicious/noArrayIndexKey:
+
+
+ {nodes.get(hop)?.user?.longName ??
+ `!${numberToHexUnpadded(hop)}`}
+
+ ↓ {snrBack ? snrBack[i + 1] : null}dB
+
+ ))}
{to?.user?.longName}
) : null}
From 2fbf2a1173adf9675ca2da0cf06a9afeac26d9d9 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Fri, 24 Jan 2025 12:26:28 +0100
Subject: [PATCH 13/51] Fix show ?? if null
---
src/components/PageComponents/Messages/TraceRoute.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/src/components/PageComponents/Messages/TraceRoute.tsx
index c2dc1f5b..ba3b9e63 100644
--- a/src/components/PageComponents/Messages/TraceRoute.tsx
+++ b/src/components/PageComponents/Messages/TraceRoute.tsx
@@ -32,14 +32,14 @@ export const TraceRoute = ({
Route to destination:
{to?.user?.longName}
- ↓ {snrTowards ? snrTowards[0] : null}dB
+ ↓ {snrTowards?.[0] ? snrTowards[0] : "??"}dB
{route.map((hop, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey:
{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
- ↓ {snrTowards ? snrTowards[i + 1] : null}dB
+ ↓ {snrTowards?.[i + 1] ? snrTowards[i + 1] : "??"}dB
))}
{from?.user?.longName}
@@ -48,7 +48,7 @@ export const TraceRoute = ({
Route back:
{from?.user?.longName}
- ↓ {snrBack ? snrBack[0] : null}dB
+ ↓ {snrBack?.[0] ? snrBack[0] : "??"}dB
{routeBack.map((hop, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey:
@@ -56,7 +56,7 @@ export const TraceRoute = ({
{nodes.get(hop)?.user?.longName ??
`!${numberToHexUnpadded(hop)}`}
- ↓ {snrBack ? snrBack[i + 1] : null}dB
+ ↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB
))}
{to?.user?.longName}
From 9eeed9630b039fb1270f1ce387375776320610f3 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 29 Jan 2025 13:15:52 +0100
Subject: [PATCH 14/51] Fix
---
src/components/Dialog/NodeOptionsDialog.tsx | 1 +
src/components/Dialog/TracerouteResponseDialog.tsx | 1 +
src/components/PageComponents/Messages/TraceRoute.tsx | 9 ++-------
3 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/src/components/Dialog/NodeOptionsDialog.tsx b/src/components/Dialog/NodeOptionsDialog.tsx
index f2ea85e2..10a8d292 100644
--- a/src/components/Dialog/NodeOptionsDialog.tsx
+++ b/src/components/Dialog/NodeOptionsDialog.tsx
@@ -8,6 +8,7 @@ import {
} from "@components/UI/Dialog";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
+import type { JSX } from "react";
import { Button } from "../UI/Button";
export interface NodeOptionsDialogProps {
diff --git a/src/components/Dialog/TracerouteResponseDialog.tsx b/src/components/Dialog/TracerouteResponseDialog.tsx
index e26cafc2..9fce1cfa 100644
--- a/src/components/Dialog/TracerouteResponseDialog.tsx
+++ b/src/components/Dialog/TracerouteResponseDialog.tsx
@@ -8,6 +8,7 @@ import {
} 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 {
diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/src/components/PageComponents/Messages/TraceRoute.tsx
index ba3b9e63..15e0dd00 100644
--- a/src/components/PageComponents/Messages/TraceRoute.tsx
+++ b/src/components/PageComponents/Messages/TraceRoute.tsx
@@ -1,6 +1,7 @@
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;
@@ -21,13 +22,7 @@ export const TraceRoute = ({
}: TraceRouteProps): JSX.Element => {
const { nodes } = useDevice();
- return route.length === 0 ? (
-
-
- {to?.user?.longName} ↔ {from?.user?.longName}
-
-
- ) : (
+ return (
Route to destination:
From b3cde1bcd7a94a2bc944cdb5638bfb9ad612589c Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 29 Jan 2025 13:56:48 +0100
Subject: [PATCH 15/51] Move remove button to quick options
---
src/components/Dialog/NodeOptionsDialog.tsx | 46 ++++++++++++++-------
src/pages/Nodes.tsx | 18 +++-----
2 files changed, 36 insertions(+), 28 deletions(-)
diff --git a/src/components/Dialog/NodeOptionsDialog.tsx b/src/components/Dialog/NodeOptionsDialog.tsx
index 10a8d292..06a3f0b8 100644
--- a/src/components/Dialog/NodeOptionsDialog.tsx
+++ b/src/components/Dialog/NodeOptionsDialog.tsx
@@ -1,4 +1,5 @@
import { toast } from "@app/core/hooks/useToast";
+import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore";
import {
Dialog,
@@ -8,6 +9,7 @@ import {
} 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";
@@ -22,7 +24,8 @@ export const NodeOptionsDialog = ({
open,
onOpenChange,
}: NodeOptionsDialogProps): JSX.Element => {
- const { connection } = useDevice();
+ const { setDialogOpen, connection } = useDevice();
+ const { setNodeNumToBeRemoved } = useAppStore();
const longName =
node?.user?.longName ??
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
@@ -30,19 +33,6 @@ export const NodeOptionsDialog = ({
node?.user?.shortName ??
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
- function handleTraceroute() {
- if (!node) return;
- toast({
- title: "Sending Traceroute, please wait...",
- });
- connection?.traceRoute(node.num).then(() =>
- toast({
- title: "Traceroute sent.",
- }),
- );
- onOpenChange();
- }
-
function handleRequestPosition() {
if (!node) return;
toast({
@@ -56,6 +46,19 @@ export const NodeOptionsDialog = ({
onOpenChange();
}
+ function handleTraceroute() {
+ if (!node) return;
+ toast({
+ title: "Sending Traceroute, please wait...",
+ });
+ connection?.traceRoute(node.num).then(() =>
+ toast({
+ title: "Traceroute sent.",
+ }),
+ );
+ onOpenChange();
+ }
+
return (
+
+
+
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<
void;
addRasterSource: (source: RasterSource) => void;
@@ -42,6 +43,7 @@ interface AppState {
setNodeNumToBeRemoved: (nodeNum: number) => void;
setAccent: (color: AccentColor) => void;
setConnectDialogOpen: (open: boolean) => void;
+ setNodeNumDetails: (nodeNum: number) => void;
}
export const useAppStore = create()((set) => ({
@@ -57,6 +59,7 @@ export const useAppStore = create()((set) => ({
accent: "orange",
connectDialogOpen: false,
nodeNumToBeRemoved: 0,
+ nodeNumDetails: 0,
setRasterSources: (sources: RasterSource[]) => {
set(
@@ -124,4 +127,8 @@ export const useAppStore = create()((set) => ({
}),
);
},
+ setNodeNumDetails: (nodeNum) =>
+ set((state) => ({
+ nodeNumDetails: nodeNum,
+ })),
}));
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: "",
From 4abc78fff3a48372dc40374a9258ed195a0c1e71 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Sun, 2 Feb 2025 12:56:07 +0100
Subject: [PATCH 21/51] Add basic accordion element
---
src/components/UI/Accordion.tsx | 44 +++++++++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
create mode 100644 src/components/UI/Accordion.tsx
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) => (
+
+));
From 0c8901b5b2c64e0728532cb76c90c33092ae098b Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Sun, 2 Feb 2025 12:58:35 +0100
Subject: [PATCH 22/51] Make all raw data accordion in node details dialog
---
src/components/Dialog/NodeDetailsDialog.tsx | 66 +++++++--------------
1 file changed, 22 insertions(+), 44 deletions(-)
diff --git a/src/components/Dialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog.tsx
index 13a9782d..8c30d0df 100644
--- a/src/components/Dialog/NodeDetailsDialog.tsx
+++ b/src/components/Dialog/NodeDetailsDialog.tsx
@@ -1,16 +1,21 @@
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.tsx";
+} from "@components/UI/Dialog";
import { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
-import { useEffect } from "react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { DeviceImage } from "../generic/DeviceImage";
import { TimeAgo } from "../generic/TimeAgo";
import { Uptime } from "../generic/Uptime";
@@ -138,49 +143,22 @@ export const NodeDetailsDialog = ({
) : null}
- {device.environmentMetrics ? (
-
-
- Environment 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)}
-
+
+
+
+
+ All Raw Metrics:
+
+
+
+
+ {JSON.stringify(device, null, 2)}
+
+
+
+
) : null}
From 358f8a94d022e4402eb67414b4c0c641f1f51adb Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Sun, 2 Feb 2025 14:02:47 +0100
Subject: [PATCH 23/51] Update timeAgo to use Intl.RelativeTimeFormat
---
src/components/generic/TimeAgo.tsx | 45 +++++++++++++++++++-----------
1 file changed, 29 insertions(+), 16 deletions(-)
diff --git a/src/components/generic/TimeAgo.tsx b/src/components/generic/TimeAgo.tsx
index 7507a7a3..e8d5973c 100755
--- a/src/components/generic/TimeAgo.tsx
+++ b/src/components/generic/TimeAgo.tsx
@@ -4,28 +4,41 @@ export interface TimeAgoProps {
timestamp: number;
}
-const getTimeAgo = (timestamp: number): string => {
- const now = Date.now();
- const secondsPast = Math.floor((now - timestamp) / 1000);
+const getTimeAgo = (
+ unixTimestamp: number,
+ locale: Intl.LocalesArgument = "en",
+): string => {
+ const timestamp = new Date(unixTimestamp);
+ const diff = (new Date().getTime() - timestamp.getTime()) / 1000;
- if (secondsPast < 10) return "now";
- if (secondsPast < 60) return `${secondsPast} seconds ago`;
- if (secondsPast < 3600) return `${Math.floor(secondsPast / 60)} minutes ago`;
- if (secondsPast < 86400) return `${Math.floor(secondsPast / 3600)} hours ago`;
- return `${Math.floor(secondsPast / 86400)} days ago ${secondsPast}`;
+ 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 => {
const [timeAgo, setTimeAgo] = useState(getTimeAgo(timestamp));
- useEffect(() => {
- const interval = setInterval(() => {
- setTimeAgo(getTimeAgo(timestamp));
- }, 10000);
-
- return () => clearInterval(interval);
- }, [timestamp]);
-
useEffect(() => {
setTimeAgo(getTimeAgo(timestamp));
}, [timestamp]);
From 9d9c46f732251f8ca682826ebdc76ada27a2115b Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Sun, 2 Feb 2025 14:10:48 +0100
Subject: [PATCH 24/51] Remove unused ariables from Nodes.tsx
---
src/pages/Nodes.tsx | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
index 6da0b246..ff178b4a 100644
--- a/src/pages/Nodes.tsx
+++ b/src/pages/Nodes.tsx
@@ -1,16 +1,14 @@
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 { 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/TimeAgo.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf, type Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
-import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
+import { LockIcon, LockOpenIcon } from "lucide-react";
import { Fragment, type JSX, useCallback, useEffect, useState } from "react";
import { base16 } from "rfc4648";
@@ -20,8 +18,7 @@ export interface DeleteNoteDialogProps {
}
const NodesPage = (): JSX.Element => {
- const { nodes, hardware, setDialogOpen, connection } = useDevice();
- const { setNodeNumToBeRemoved } = useAppStore();
+ const { nodes, hardware, connection } = useDevice();
const [selectedNode, setSelectedNode] = useState<
Protobuf.Mesh.NodeInfo | undefined
>(undefined);
From 28c5fd64fe240847ddc857ab043dba5a49fc741f Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Sun, 2 Feb 2025 16:05:35 +0100
Subject: [PATCH 25/51] Add overflow-x-scroll to Node details dialog raw
metrics
---
src/components/Dialog/NodeDetailsDialog.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Dialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog.tsx
index 8c30d0df..09f96e12 100644
--- a/src/components/Dialog/NodeDetailsDialog.tsx
+++ b/src/components/Dialog/NodeDetailsDialog.tsx
@@ -152,7 +152,7 @@ export const NodeDetailsDialog = ({
All Raw Metrics:
-
+
{JSON.stringify(device, null, 2)}
From 76de3e0e02f5d9bc90cf473b83506bd0df8a4616 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Mon, 3 Feb 2025 17:19:27 +0100
Subject: [PATCH 26/51] Biome
---
src/pages/Nodes.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
index 18ed858c..135f0cbb 100644
--- a/src/pages/Nodes.tsx
+++ b/src/pages/Nodes.tsx
@@ -8,7 +8,7 @@ import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf, type Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
-import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
+import { LockIcon, LockOpenIcon } from "lucide-react";
import { Fragment, type JSX, useCallback, useEffect, useState } from "react";
import { base16 } from "rfc4648";
@@ -18,14 +18,13 @@ export interface DeleteNoteDialogProps {
}
const NodesPage = (): JSX.Element => {
- const { nodes, hardware, setDialogOpen } = useDevice();
+ const { nodes, hardware, connection } = useDevice();
const [selectedNode, setSelectedNode] = useState<
Protobuf.Mesh.NodeInfo | undefined
>(undefined);
const [selectedTraceroute, setSelectedTraceroute] = useState<
Types.PacketMetadata | undefined
>();
- const { setNodeNumToBeRemoved } = useAppStore();
const [searchTerm, setSearchTerm] = useState("");
const filteredNodes = Array.from(nodes.values()).filter((node) => {
From fb1b4c6cc55de6f340b65c9b10ec7699963397c9 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Mon, 3 Feb 2025 18:27:22 +0100
Subject: [PATCH 27/51] Update according to change request
---
src/components/PageComponents/Messages/TraceRoute.tsx | 6 ++----
src/components/generic/TimeAgo.tsx | 10 ++--------
src/components/generic/Uptime.tsx | 10 ++--------
3 files changed, 6 insertions(+), 20 deletions(-)
diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/src/components/PageComponents/Messages/TraceRoute.tsx
index 15e0dd00..11c77332 100644
--- a/src/components/PageComponents/Messages/TraceRoute.tsx
+++ b/src/components/PageComponents/Messages/TraceRoute.tsx
@@ -29,8 +29,7 @@ export const TraceRoute = ({
{to?.user?.longName}
↓ {snrTowards?.[0] ? snrTowards[0] : "??"}dB
{route.map((hop, i) => (
- // biome-ignore lint/suspicious/noArrayIndexKey:
-
+
{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
@@ -45,8 +44,7 @@ export const TraceRoute = ({
{from?.user?.longName}
↓ {snrBack?.[0] ? snrBack[0] : "??"}dB
{routeBack.map((hop, i) => (
- // biome-ignore lint/suspicious/noArrayIndexKey:
-
+
{nodes.get(hop)?.user?.longName ??
`!${numberToHexUnpadded(hop)}`}
diff --git a/src/components/generic/TimeAgo.tsx b/src/components/generic/TimeAgo.tsx
index e8d5973c..cd865dad 100755
--- a/src/components/generic/TimeAgo.tsx
+++ b/src/components/generic/TimeAgo.tsx
@@ -1,4 +1,4 @@
-import { type JSX, useEffect, useState } from "react";
+import type { JSX } from "react";
export interface TimeAgoProps {
timestamp: number;
@@ -37,11 +37,5 @@ const getTimeAgo = (
};
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
- const [timeAgo, setTimeAgo] = useState(getTimeAgo(timestamp));
-
- useEffect(() => {
- setTimeAgo(getTimeAgo(timestamp));
- }, [timestamp]);
-
- return {timeAgo};
+ return {getTimeAgo(timestamp)};
};
diff --git a/src/components/generic/Uptime.tsx b/src/components/generic/Uptime.tsx
index 59cceb4b..fdcf897a 100644
--- a/src/components/generic/Uptime.tsx
+++ b/src/components/generic/Uptime.tsx
@@ -1,4 +1,4 @@
-import { type JSX, useEffect, useState } from "react";
+import type { JSX } from "react";
export interface UptimeProps {
seconds: number;
@@ -13,11 +13,5 @@ const getUptime = (seconds: number): string => {
};
export const Uptime = ({ seconds }: UptimeProps): JSX.Element => {
- const [uptime, setUptime] = useState(getUptime(seconds));
-
- useEffect(() => {
- setUptime(getUptime(seconds));
- }, [seconds]);
-
- return {uptime};
+ return {getUptime(seconds)};
};
From 48eb931c37ac9aa5c35e029c5d66e8d390012e14 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Mon, 3 Feb 2025 19:07:27 +0100
Subject: [PATCH 28/51] Add time toltip to timeAgo
---
src/components/generic/TimeAgo.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/components/generic/TimeAgo.tsx b/src/components/generic/TimeAgo.tsx
index cd865dad..6148b3be 100755
--- a/src/components/generic/TimeAgo.tsx
+++ b/src/components/generic/TimeAgo.tsx
@@ -37,5 +37,9 @@ const getTimeAgo = (
};
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
- return {getTimeAgo(timestamp)};
+ return (
+
+ {getTimeAgo(timestamp)}
+
+ );
};
From 44b8dd308a27ce03160ada360fcd620162bfa4e2 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Mon, 3 Feb 2025 21:23:08 +0100
Subject: [PATCH 29/51] Update tooltip
---
src/components/generic/TimeAgo.tsx | 27 ++++++++++++++++++++++++---
1 file changed, 24 insertions(+), 3 deletions(-)
diff --git a/src/components/generic/TimeAgo.tsx b/src/components/generic/TimeAgo.tsx
index 6148b3be..85043c58 100755
--- a/src/components/generic/TimeAgo.tsx
+++ b/src/components/generic/TimeAgo.tsx
@@ -1,3 +1,10 @@
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipPortal,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@radix-ui/react-tooltip";
import type { JSX } from "react";
export interface TimeAgoProps {
@@ -38,8 +45,22 @@ const getTimeAgo = (
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
return (
-
- {getTimeAgo(timestamp)}
-
+
+
+
+ {getTimeAgo(timestamp)}
+
+
+
+ {new Date(timestamp).toLocaleString()}
+
+
+
+
);
};
From 5a1c207ffc45aa68bee91487499f345b820aaa34 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Tue, 4 Feb 2025 21:50:11 +0100
Subject: [PATCH 30/51] Remove useEffect
---
src/components/Dialog/NodeDetailsDialog.tsx | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/components/Dialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog.tsx
index 09f96e12..80c032bc 100644
--- a/src/components/Dialog/NodeDetailsDialog.tsx
+++ b/src/components/Dialog/NodeDetailsDialog.tsx
@@ -15,7 +15,6 @@ import {
} from "@components/UI/Dialog";
import { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
-import { useEffect, useState } from "react";
import { DeviceImage } from "../generic/DeviceImage";
import { TimeAgo } from "../generic/TimeAgo";
import { Uptime } from "../generic/Uptime";
@@ -31,12 +30,7 @@ export const NodeDetailsDialog = ({
}: NodeDetailsDialogProps) => {
const { nodes } = useDevice();
const { nodeNumDetails } = useAppStore();
- const [device, setDevice] = useState(null);
-
- useEffect(() => {
- if (!nodeNumDetails) return;
- setDevice(nodes.get(nodeNumDetails));
- }, [nodeNumDetails, nodes]);
+ const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
return device ? (
From c2a2e0ac19926ca011e696428e8b274c877ab127 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Tue, 4 Feb 2025 22:52:14 +0100
Subject: [PATCH 31/51] Add direct message button
---
src/components/Dialog/NodeOptionsDialog.tsx | 19 +++++++++++++++++--
src/core/stores/appStore.ts | 15 +++++++++++++++
src/pages/Messages.tsx | 11 ++++-------
3 files changed, 36 insertions(+), 9 deletions(-)
diff --git a/src/components/Dialog/NodeOptionsDialog.tsx b/src/components/Dialog/NodeOptionsDialog.tsx
index 83a01426..d62e4065 100644
--- a/src/components/Dialog/NodeOptionsDialog.tsx
+++ b/src/components/Dialog/NodeOptionsDialog.tsx
@@ -24,8 +24,13 @@ export const NodeOptionsDialog = ({
open,
onOpenChange,
}: NodeOptionsDialogProps): JSX.Element => {
- const { setDialogOpen, connection } = useDevice();
- const { setNodeNumToBeRemoved, setNodeNumDetails } = useAppStore();
+ const { setDialogOpen, connection, setActivePage } = useDevice();
+ const {
+ setNodeNumToBeRemoved,
+ setNodeNumDetails,
+ setChatType,
+ setActiveChat,
+ } = useAppStore();
const longName =
node?.user?.longName ??
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
@@ -33,6 +38,13 @@ export const NodeOptionsDialog = ({
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({
@@ -66,6 +78,9 @@ export const NodeOptionsDialog = ({
{`${longName} (${shortName})`}
+
+
+
diff --git a/src/core/stores/appStore.ts b/src/core/stores/appStore.ts
index 2e311e6e..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";
@@ -30,6 +31,8 @@ interface AppState {
accent: AccentColor;
connectDialogOpen: boolean;
nodeNumDetails: number;
+ activeChat: number;
+ chatType: "broadcast" | "direct";
setRasterSources: (sources: RasterSource[]) => void;
addRasterSource: (source: RasterSource) => void;
@@ -44,6 +47,8 @@ interface AppState {
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) => ({
@@ -60,6 +65,8 @@ export const useAppStore = create()((set) => ({
connectDialogOpen: false,
nodeNumToBeRemoved: 0,
nodeNumDetails: 0,
+ activeChat: Types.ChannelNumber.Primary,
+ chatType: "broadcast",
setRasterSources: (sources: RasterSource[]) => {
set(
@@ -131,4 +138,12 @@ export const useAppStore = create()((set) => ({
set((state) => ({
nodeNumDetails: nodeNum,
})),
+ setActiveChat: (chat) =>
+ set(() => ({
+ activeChat: chat,
+ })),
+ setChatType: (type) =>
+ set(() => ({
+ chatType: type,
+ })),
}));
diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx
index c06ce08f..7fe99f69 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;
From b8c1096568cda1f20186313c1b8fda8dbce9a0a3 Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 5 Feb 2025 21:06:55 +0100
Subject: [PATCH 32/51] Add short name row to nodes page
---
src/pages/Nodes.tsx | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
index 135f0cbb..a47877ab 100644
--- a/src/pages/Nodes.tsx
+++ b/src/pages/Nodes.tsx
@@ -64,7 +64,8 @@ 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"
>
From 175e98a08081c78007f0ee6c17f97b2d4418f33f Mon Sep 17 00:00:00 2001
From: Tilen Komel
Date: Wed, 5 Feb 2025 21:45:31 +0100
Subject: [PATCH 33/51] Add location respone dialog
---
.../Dialog/LocationResponseDialog.tsx | 62 +++++++++++++++++++
src/pages/Nodes.tsx | 29 ++++++++-
2 files changed, 89 insertions(+), 2 deletions(-)
create mode 100644 src/components/Dialog/LocationResponseDialog.tsx
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})`}
+
+
+
+
+
+
+ );
+};
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
index a47877ab..fa1c5788 100644
--- a/src/pages/Nodes.tsx
+++ b/src/pages/Nodes.tsx
@@ -1,3 +1,4 @@
+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";
@@ -25,6 +26,9 @@ const NodesPage = (): JSX.Element => {
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) => {
@@ -34,9 +38,10 @@ const NodesPage = (): JSX.Element => {
});
useEffect(() => {
- connection?.events.onTraceRoutePacket.subscribe(handleTraceroute);
+ if (!connection) return;
+ connection.events.onTraceRoutePacket.subscribe(handleTraceroute);
return () => {
- connection?.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
+ connection.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
};
}, [connection]);
@@ -47,6 +52,21 @@ const NodesPage = (): JSX.Element => {
[],
);
+ 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 (
<>
@@ -155,6 +175,11 @@ const NodesPage = (): JSX.Element => {
open={!!selectedTraceroute}
onOpenChange={() => setSelectedTraceroute(undefined)}
/>
+ setSelectedLocation(undefined)}
+ />
From 1fc72aa7be0b6cdaa4c89e159dc7a99ce73fed39 Mon Sep 17 00:00:00 2001
From: Dan Ditomaso
Date: Sat, 8 Feb 2025 08:32:29 -0500
Subject: [PATCH 34/51] fix: keep existing zoom on map ndoe click
---
src/pages/Map.tsx | 23 ++---------------------
1 file changed, 2 insertions(+), 21 deletions(-)
diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx
index 12a8e884..b2da0e69 100644
--- a/src/pages/Map.tsx
+++ b/src/pages/Map.tsx
@@ -44,7 +44,7 @@ const MapPage = (): JSX.Element => {
}
if (nodesWithPosition.length === 1) {
map.easeTo({
- zoom: 12,
+ zoom: map.getZoom(),
center: [
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
@@ -118,19 +118,6 @@ const MapPage = (): JSX.Element => {
>
{
- // const waypoint = new Protobuf.Waypoint({
- // name: "test",
- // description: "test description",
- // latitudeI: Math.trunc(e.lngLat.lat * 1e7),
- // longitudeI: Math.trunc(e.lngLat.lng * 1e7)
- // });
- // addWaypoint(waypoint);
- // connection?.sendWaypoint(waypoint, "broadcast");
- // }}
-
- // @ts-ignore
-
attributionControl={false}
renderWorldCopies={false}
maxPitch={0}
@@ -163,11 +150,6 @@ const MapPage = (): JSX.Element => {
))}
- {/* {rasterSources.map((source, index) => (
-
-
-
- ))} */}
{allNodes.map((node) => {
if (node.position?.latitudeI && node.num !== selectedNode?.num) {
return (
@@ -175,12 +157,11 @@ const MapPage = (): JSX.Element => {
key={node.num}
longitude={(node.position.longitudeI ?? 0) / 1e7}
latitude={(node.position.latitudeI ?? 0) / 1e7}
- // style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom"
onClick={() => {
setSelectedNode(node);
map?.easeTo({
- zoom: 12,
+ zoom: map.getZoom(),
center: [
(node.position?.longitudeI ?? 0) / 1e7,
(node.position?.latitudeI ?? 0) / 1e7,
From d9aff939930952585444a416e765369b34c7ac26 Mon Sep 17 00:00:00 2001
From: Dan Ditomaso
Date: Sat, 8 Feb 2025 21:49:17 -0500
Subject: [PATCH 35/51] fix: removed map zoom on marker click. feat: lined up
popup with map marker
---
.../PageComponents/Map/NodeDetail.tsx | 2 +-
src/pages/Map.tsx | 206 +++++++++---------
2 files changed, 102 insertions(+), 106 deletions(-)
diff --git a/src/components/PageComponents/Map/NodeDetail.tsx b/src/components/PageComponents/Map/NodeDetail.tsx
index 5203d077..6b33318d 100644
--- a/src/components/PageComponents/Map/NodeDetail.tsx
+++ b/src/components/PageComponents/Map/NodeDetail.tsx
@@ -30,7 +30,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
].replaceAll("_", " ");
return (
-
+
diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx
index b2da0e69..910de059 100644
--- a/src/pages/Map.tsx
+++ b/src/pages/Map.tsx
@@ -1,59 +1,94 @@
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
import { Avatar } from "@app/components/UI/Avatar";
-import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
-import { cn } from "@app/core/utils/cn.ts";
import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx";
-import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
-import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Protobuf } from "@meshtastic/js";
-import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { bbox, lineString } from "@turf/turf";
-import {
- BoxSelectIcon,
- MapPinIcon,
- ZoomInIcon,
- ZoomOutIcon,
-} from "lucide-react";
+import { MapPinIcon } from "lucide-react";
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
-import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
+import {
+ AttributionControl,
+ GeolocateControl,
+ Marker,
+ MarkerEvent,
+ NavigationControl,
+ Popup,
+ ScaleControl,
+ useMap,
+} from "react-map-gl";
import MapGl from "react-map-gl/maplibre";
+type NodePosition = {
+ latitude: number;
+ longitude: number;
+};
+
+const convertToLatLng = (position: {
+ latitudeI?: number;
+ longitudeI?: number;
+}): NodePosition => ({
+ latitude: (position.latitudeI ?? 0) / 1e7,
+ longitude: (position.longitudeI ?? 0) / 1e7,
+});
+
const MapPage = (): JSX.Element => {
const { nodes, waypoints } = useDevice();
- const { rasterSources, darkMode } = useAppStore();
+ const { darkMode } = useAppStore();
const { default: map } = useMap();
- const [zoom, setZoom] = useState(0);
const [selectedNode, setSelectedNode] =
useState
(null);
- const allNodes = useMemo(() => Array.from(nodes.values()), [nodes]);
+ // Filter out nodes without a valid position
+ const validNodes = useMemo(
+ () =>
+ Array.from(nodes.values()).filter(
+ (node): node is Protobuf.Mesh.NodeInfo =>
+ Boolean(node.position?.latitudeI),
+ ),
+ [nodes],
+ );
- const getBBox = useCallback(() => {
+ const handleMarkerClick = useCallback(
+ (node: Protobuf.Mesh.NodeInfo, event: { originalEvent: MouseEvent }) => {
+ event?.originalEvent?.stopPropagation();
+
+ setSelectedNode(node);
+
+ if (map) {
+ const position = convertToLatLng(node.position);
+ map.easeTo({
+ center: [position.longitude, position.latitude],
+ zoom: map?.getZoom(),
+ });
+ }
+ },
+ [map],
+ );
+
+ // Get the bounds of the map based on the nodes furtherest away from center
+ const getMapBounds = useCallback(() => {
if (!map) {
return;
}
- const nodesWithPosition = allNodes.filter(
- (node) => node.position?.latitudeI,
- );
- if (!nodesWithPosition.length) {
+
+ if (!validNodes.length) {
return;
}
- if (nodesWithPosition.length === 1) {
+ if (validNodes.length === 1) {
map.easeTo({
zoom: map.getZoom(),
center: [
- (nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
- (nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
+ (validNodes[0].position?.longitudeI ?? 0) / 1e7,
+ (validNodes[0].position?.latitudeI ?? 0) / 1e7,
],
});
return;
}
const line = lineString(
- nodesWithPosition.map((n) => [
+ validNodes.map((n) => [
(n.position?.latitudeI ?? 0) / 1e7,
(n.position?.longitudeI ?? 0) / 1e7,
]),
@@ -69,65 +104,53 @@ const MapPage = (): JSX.Element => {
if (center) {
map.easeTo(center);
}
- }, [allNodes, map]);
+ }, [validNodes, map]);
- useEffect(() => {
- map?.on("zoom", () => {
- setZoom(map?.getZoom() ?? 0);
- });
- }, [map]);
+ // Generate all markers
+ const markers = useMemo(
+ () =>
+ validNodes.map((node) => {
+ const position = convertToLatLng(node.position);
+ return (
+ handleMarkerClick(node, e)}
+ >
+
+
+ );
+ }),
+ [validNodes, handleMarkerClick],
+ );
useEffect(() => {
map?.on("load", () => {
- getBBox();
+ getMapBounds();
});
- }, [map, getBBox]);
+ }, [map, getMapBounds]);
return (
<>
-
-
- {rasterSources.map((source) => (
-
- ))}
-
-
-
+
+
{
color: darkMode ? "black" : "",
}}
/>
+
+
+
+
{waypoints.map((wp) => (
{
))}
- {allNodes.map((node) => {
- if (node.position?.latitudeI && node.num !== selectedNode?.num) {
- return (
-
{
- setSelectedNode(node);
- map?.easeTo({
- zoom: map.getZoom(),
- center: [
- (node.position?.longitudeI ?? 0) / 1e7,
- (node.position?.latitudeI ?? 0) / 1e7,
- ],
- });
- }}
- >
-
-
-
- {node.user?.longName ||
- `!${numberToHexUnpadded(node.num)}`}
-
-
-
- );
- }
- })}
- {selectedNode?.position && (
+ {markers}
+ {selectedNode ? (
setSelectedNode(null)}
>
- )}
+ ) : null}
>
From 2627b9035d4bc737c30a913b9654732514cfa9bc Mon Sep 17 00:00:00 2001
From: Dan Ditomaso
Date: Sat, 8 Feb 2025 23:11:54 -0500
Subject: [PATCH 36/51] feat: adding pluralizer util. fix: measurement unit in
marker popup
---
.../PageComponents/Map/NodeDetail.tsx | 8 ++++-
src/core/utils/string.ts | 31 +++++++++++++++++++
2 files changed, 38 insertions(+), 1 deletion(-)
create mode 100644 src/core/utils/string.ts
diff --git a/src/components/PageComponents/Map/NodeDetail.tsx b/src/components/PageComponents/Map/NodeDetail.tsx
index 5203d077..23112f34 100644
--- a/src/components/PageComponents/Map/NodeDetail.tsx
+++ b/src/components/PageComponents/Map/NodeDetail.tsx
@@ -1,6 +1,7 @@
import { Separator } from "@app/components/UI/Seperator";
import { H5 } from "@app/components/UI/Typography/H5.tsx";
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";
@@ -132,7 +133,12 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
className="ml-2 mr-1"
aria-label="Elevation"
/>
- {node.position?.altitude} ft
+
+ {formatQuantity(node.position?.altitude, {
+ one: "meter",
+ other: "meters",
+ })}
+
)}
diff --git a/src/core/utils/string.ts b/src/core/utils/string.ts
new file mode 100644
index 00000000..2cabf70d
--- /dev/null
+++ b/src/core/utils/string.ts
@@ -0,0 +1,31 @@
+interface PluralForms {
+ one: string;
+ other: string;
+ [key: string]: string;
+}
+
+interface FormatOptions {
+ locale?: string;
+ pluralRules?: Intl.PluralRulesOptions;
+ numberFormat?: Intl.NumberFormatOptions;
+}
+
+export function formatQuantity(
+ value: number,
+ forms: PluralForms,
+ options: FormatOptions = {},
+) {
+ const {
+ locale = "en-US",
+ pluralRules: pluralOptions = { type: "cardinal" },
+ numberFormat: numberOptions = {},
+ } = options;
+
+ const pluralRules = new Intl.PluralRules(locale, pluralOptions);
+ const numberFormat = new Intl.NumberFormat(locale, numberOptions);
+
+ const pluralCategory = pluralRules.select(value);
+ const word = forms[pluralCategory];
+
+ return `${numberFormat.format(value)} ${word}`;
+}
From 467effa62e1cc21c5000440bc57d6775735caac5 Mon Sep 17 00:00:00 2001
From: Dan Ditomaso
Date: Mon, 10 Feb 2025 12:24:57 -0500
Subject: [PATCH 37/51] fix: prevent empty/blank messages from being sent. fix:
count chars during copy/paste action
---
.../PageComponents/Messages/MessageInput.tsx | 29 +++++++++++++------
1 file changed, 20 insertions(+), 9 deletions(-)
diff --git a/src/components/PageComponents/Messages/MessageInput.tsx b/src/components/PageComponents/Messages/MessageInput.tsx
index 2e7954c9..d1cbc5ee 100644
--- a/src/components/PageComponents/Messages/MessageInput.tsx
+++ b/src/components/PageComponents/Messages/MessageInput.tsx
@@ -4,7 +4,13 @@ import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Types } from "@meshtastic/js";
import { SendIcon } from "lucide-react";
-import { type JSX, useCallback, useMemo, useState } from "react";
+import {
+ type JSX,
+ startTransition,
+ useCallback,
+ useMemo,
+ useState,
+} from "react";
export interface MessageInputProps {
to: Types.Destination;
@@ -63,11 +69,11 @@ export const MessageInput = ({
const handleInputChange = (e: React.ChangeEvent) => {
const newValue = e.target.value;
- const byteLength = new Blob([newValue]).size;
- if (byteLength <= maxBytes) {
+ const messageLength = newValue.length;
+ if (messageLength <= maxBytes) {
setLocalDraft(newValue);
debouncedSetMessageDraft(newValue);
- setMessageBytes(maxBytes - byteLength);
+ setMessageBytes(maxBytes - messageLength);
}
};
@@ -75,11 +81,15 @@ export const MessageInput = ({