Browse Source

Persistent nodeDB initial work

pull/780/head
philon- 11 months ago
parent
commit
1877f14800
  1. 6
      packages/web/src/App.tsx
  2. 10
      packages/web/src/DeviceWrapper.tsx
  3. 10
      packages/web/src/components/CommandPalette/index.tsx
  4. 5
      packages/web/src/components/Dialog/DeviceNameDialog.tsx
  5. 4
      packages/web/src/components/Dialog/LocationResponseDialog.tsx
  6. 5
      packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx
  7. 7
      packages/web/src/components/Dialog/PKIBackupDialog.tsx
  8. 5
      packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx
  9. 6
      packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts
  10. 5
      packages/web/src/components/Dialog/RemoveNodeDialog.tsx
  11. 4
      packages/web/src/components/Dialog/TracerouteResponseDialog.tsx
  12. 13
      packages/web/src/components/PageComponents/Connect/BLE.tsx
  13. 12
      packages/web/src/components/PageComponents/Connect/HTTP.tsx
  14. 13
      packages/web/src/components/PageComponents/Connect/Serial.tsx
  15. 10
      packages/web/src/components/PageComponents/Messages/MessageItem.tsx
  16. 4
      packages/web/src/components/PageComponents/Messages/TraceRoute.tsx
  17. 18
      packages/web/src/components/Sidebar.tsx
  18. 19
      packages/web/src/core/hooks/useFavoriteNode.ts
  19. 22
      packages/web/src/core/hooks/useIgnoreNode.ts
  20. 6
      packages/web/src/core/stores/appStore/index.ts
  21. 251
      packages/web/src/core/stores/deviceStore/index.ts
  22. 36
      packages/web/src/core/stores/index.ts
  23. 374
      packages/web/src/core/stores/nodeDBStore/index.ts
  24. 8
      packages/web/src/core/stores/nodeDBStore/types.ts
  25. 19
      packages/web/src/core/stores/utils/deviceContext.ts
  26. 21
      packages/web/src/core/subscriptions.ts
  27. 14
      packages/web/src/pages/Dashboard/index.tsx
  28. 7
      packages/web/src/pages/Map/index.tsx
  29. 13
      packages/web/src/pages/Messages.tsx
  30. 6
      packages/web/src/pages/Nodes/index.tsx

6
packages/web/src/App.tsx

@ -15,10 +15,10 @@ import { MapProvider } from "react-map-gl/maplibre";
export function App() { export function App() {
const { getDevice } = useDeviceStore(); const { getDevice } = useDeviceStore();
const { selectedDevice, setConnectDialogOpen, connectDialogOpen } = const { selectedDeviceId, setConnectDialogOpen, connectDialogOpen } =
useAppStore(); useAppStore();
const device = getDevice(selectedDevice); const device = getDevice(selectedDeviceId);
// Sets up light/dark mode based on user preferences or system settings // Sets up light/dark mode based on user preferences or system settings
useTheme(); useTheme();
@ -33,7 +33,7 @@ export function App() {
/> />
{/* <Toaster /> */} {/* <Toaster /> */}
<TanStackRouterDevtools position="bottom-right" /> <TanStackRouterDevtools position="bottom-right" />
<DeviceWrapper device={device}> <DeviceWrapper deviceId={selectedDeviceId}>
<div <div
className="flex h-screen flex-col bg-background-primary text-text-primary" className="flex h-screen flex-col bg-background-primary text-text-primary"
style={{ scrollbarWidth: "thin" }} style={{ scrollbarWidth: "thin" }}

10
packages/web/src/DeviceWrapper.tsx

@ -1,13 +1,15 @@
import { type Device, DeviceContext } from "@core/stores"; import { CurrentDeviceContext } from "@core/stores";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
export interface DeviceWrapperProps { export interface DeviceWrapperProps {
children: ReactNode; children: ReactNode;
device?: Device; deviceId: number;
} }
export const DeviceWrapper = ({ children, device }: DeviceWrapperProps) => { export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => {
return ( return (
<DeviceContext.Provider value={device}>{children}</DeviceContext.Provider> <CurrentDeviceContext.Provider value={{ deviceId }}>
{children}
</CurrentDeviceContext.Provider>
); );
}; };

10
packages/web/src/components/CommandPalette/index.tsx

@ -8,7 +8,12 @@ import {
CommandList, CommandList,
} from "@components/UI/Command.tsx"; } from "@components/UI/Command.tsx";
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts"; import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
import { useAppStore, useDevice, useDeviceStore } from "@core/stores"; import {
useAppStore,
useDevice,
useDeviceStore,
useNodeDB,
} from "@core/stores";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useCommandState } from "cmdk"; import { useCommandState } from "cmdk";
@ -64,7 +69,8 @@ export const CommandPalette = () => {
setSelectedDevice, setSelectedDevice,
} = useAppStore(); } = useAppStore();
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
const { setDialogOpen, getNode, connection } = useDevice(); const { setDialogOpen, connection } = useDevice();
const { getNode } = useNodeDB();
const { pinnedItems, togglePinnedItem } = usePinnedItems({ const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups", storageName: "pinnedCommandMenuGroups",
}); });

5
packages/web/src/components/Dialog/DeviceNameDialog.tsx

@ -10,7 +10,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@components/UI/Dialog.tsx"; } from "@components/UI/Dialog.tsx";
import { useDevice } from "@core/stores"; import { useDevice, useNodeDB } from "@core/stores";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -33,7 +33,8 @@ export const DeviceNameDialog = ({
onOpenChange, onOpenChange,
}: DeviceNameDialogProps) => { }: DeviceNameDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { hardware, getNode, connection } = useDevice(); const { hardware, connection } = useDevice();
const { getNode } = useNodeDB();
const myNode = getNode(hardware.myNodeNum); const myNode = getNode(hardware.myNodeNum);
const defaultValues = { const defaultValues = {

4
packages/web/src/components/Dialog/LocationResponseDialog.tsx

@ -1,4 +1,4 @@
import { useDevice } from "@core/stores"; import { useNodeDB } from "@core/stores";
import type { Protobuf, Types } from "@meshtastic/core"; import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -23,7 +23,7 @@ export const LocationResponseDialog = ({
onOpenChange, onOpenChange,
}: LocationResponseDialogProps) => { }: LocationResponseDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { getNode } = useDevice(); const { getNode } = useNodeDB();
const from = getNode(location?.from ?? 0); const from = getNode(location?.from ?? 0);
const longName = const longName =

5
packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx

@ -27,7 +27,7 @@ import {
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts"; import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts"; import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
import { toast } from "@core/hooks/useToast.ts"; import { toast } from "@core/hooks/useToast.ts";
import { useAppStore, useDevice } from "@core/stores"; import { useAppStore, useDevice, useNodeDB } from "@core/stores";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
@ -55,7 +55,8 @@ export const NodeDetailsDialog = ({
onOpenChange, onOpenChange,
}: NodeDetailsDialogProps) => { }: NodeDetailsDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { setDialogOpen, connection, getNode } = useDevice(); const { setDialogOpen, connection } = useDevice();
const { getNode } = useNodeDB();
const navigate = useNavigate(); const navigate = useNavigate();
const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore(); const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore();
const { updateFavorite } = useFavoriteNode(); const { updateFavorite } = useFavoriteNode();

7
packages/web/src/components/Dialog/PKIBackupDialog.tsx

@ -1,3 +1,4 @@
import { Button } from "@components/UI/Button.tsx";
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@ -7,12 +8,11 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@components/UI/Dialog.tsx"; } from "@components/UI/Dialog.tsx";
import { useDevice, useNodeDB } from "@core/stores";
import { fromByteArray } from "base64-js"; import { fromByteArray } from "base64-js";
import { DownloadIcon, PrinterIcon } from "lucide-react"; import { DownloadIcon, PrinterIcon } from "lucide-react";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDevice } from "../../core/stores";
import { Button } from "../UI/Button.tsx";
export interface PkiBackupDialogProps { export interface PkiBackupDialogProps {
open: boolean; open: boolean;
@ -24,7 +24,8 @@ export const PkiBackupDialog = ({
onOpenChange, onOpenChange,
}: PkiBackupDialogProps) => { }: PkiBackupDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { config, setDialogOpen, getMyNode } = useDevice(); const { config, setDialogOpen } = useDevice();
const { getMyNode } = useNodeDB();
const privateKey = config.security?.privateKey; const privateKey = config.security?.privateKey;
const publicKey = config.security?.publicKey; const publicKey = config.security?.publicKey;

5
packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx

@ -6,7 +6,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@components/UI/Dialog.tsx"; } from "@components/UI/Dialog.tsx";
import { useDevice, useMessageStore } from "@core/stores"; import { useMessageStore, useNodeDB } from "@core/stores";
import { LockKeyholeOpenIcon } from "lucide-react"; import { LockKeyholeOpenIcon } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
@ -22,7 +22,8 @@ export const RefreshKeysDialog = ({
}: RefreshKeysDialogProps) => { }: RefreshKeysDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { activeChat } = useMessageStore(); const { activeChat } = useMessageStore();
const { nodeErrors, getNode } = useDevice(); const { nodeErrors, getNode } = useNodeDB();
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog(); const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
const nodeErrorNum = nodeErrors.get(activeChat); const nodeErrorNum = nodeErrors.get(activeChat);

6
packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts

@ -1,9 +1,9 @@
import { useDevice, useMessageStore } from "@core/stores"; import { useDevice, useMessageStore, useNodeDB } from "@core/stores";
import { useCallback } from "react"; import { useCallback } from "react";
export function useRefreshKeysDialog() { export function useRefreshKeysDialog() {
const { removeNode, setDialogOpen, clearNodeError, getNodeError } = const { setDialogOpen } = useDevice();
useDevice(); const { removeNode, clearNodeError, getNodeError } = useNodeDB();
const { activeChat } = useMessageStore(); const { activeChat } = useMessageStore();
const handleCloseDialog = useCallback(() => { const handleCloseDialog = useCallback(() => {

5
packages/web/src/components/Dialog/RemoveNodeDialog.tsx

@ -9,7 +9,7 @@ import {
DialogTitle, DialogTitle,
} from "@components/UI/Dialog.tsx"; } from "@components/UI/Dialog.tsx";
import { Label } from "@components/UI/Label.tsx"; import { Label } from "@components/UI/Label.tsx";
import { useAppStore, useDevice } from "@core/stores"; import { useAppStore, useDevice, useNodeDB } from "@core/stores";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export interface RemoveNodeDialogProps { export interface RemoveNodeDialogProps {
@ -22,7 +22,8 @@ export const RemoveNodeDialog = ({
onOpenChange, onOpenChange,
}: RemoveNodeDialogProps) => { }: RemoveNodeDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { connection, getNode, removeNode } = useDevice(); const { connection } = useDevice();
const { getNode, removeNode } = useNodeDB();
const { nodeNumToBeRemoved } = useAppStore(); const { nodeNumToBeRemoved } = useAppStore();
const onSubmit = () => { const onSubmit = () => {

4
packages/web/src/components/Dialog/TracerouteResponseDialog.tsx

@ -1,4 +1,4 @@
import { useDevice } from "@core/stores"; import { useNodeDB } from "@core/stores";
import type { Protobuf, Types } from "@meshtastic/core"; import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -25,7 +25,7 @@ export const TracerouteResponseDialog = ({
onOpenChange, onOpenChange,
}: TracerouteResponseDialogProps) => { }: TracerouteResponseDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { getNode } = useDevice(); const { getNode } = useNodeDB();
const route: number[] = traceroute?.data.route ?? []; const route: number[] = traceroute?.data.route ?? [];
const routeBack: number[] = traceroute?.data.routeBack ?? []; const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4); const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);

13
packages/web/src/components/PageComponents/Connect/BLE.tsx

@ -1,6 +1,11 @@
import { Mono } from "@components/generic/Mono.tsx"; import { Mono } from "@components/generic/Mono.tsx";
import { Button } from "@components/UI/Button.tsx"; import { Button } from "@components/UI/Button.tsx";
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores"; import {
useAppStore,
useDeviceStore,
useMessageStore,
useNodeDBStore,
} from "@core/stores";
import { subscribeAll } from "@core/subscriptions.ts"; import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts"; import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core"; import { MeshDevice } from "@meshtastic/core";
@ -12,7 +17,9 @@ import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
export const BLE = ({ closeDialog }: TabElementProps) => { export const BLE = ({ closeDialog }: TabElementProps) => {
const [connectionInProgress, setConnectionInProgress] = useState(false); const [connectionInProgress, setConnectionInProgress] = useState(false);
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]); const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
const { addDevice } = useDeviceStore(); const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore(); const messageStore = useMessageStore();
const { setSelectedDevice } = useAppStore(); const { setSelectedDevice } = useAppStore();
const { t } = useTranslation(); const { t } = useTranslation();
@ -29,11 +36,13 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
const id = randId(); const id = randId();
const transport = await TransportWebBluetooth.createFromDevice(bleDevice); const transport = await TransportWebBluetooth.createFromDevice(bleDevice);
const device = addDevice(id); const device = addDevice(id);
const nodeDB = addNodeDB(id);
const connection = new MeshDevice(transport, id); const connection = new MeshDevice(transport, id);
connection.configure(); connection.configure();
setSelectedDevice(id); setSelectedDevice(id);
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection, messageStore); subscribeAll(device, connection, messageStore, nodeDB);
const HEARTBEAT_INTERVAL = 5 * 60 * 1000; const HEARTBEAT_INTERVAL = 5 * 60 * 1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL); connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);

12
packages/web/src/components/PageComponents/Connect/HTTP.tsx

@ -4,7 +4,12 @@ import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx"; import { Label } from "@components/UI/Label.tsx";
import { Switch } from "@components/UI/Switch.tsx"; import { Switch } from "@components/UI/Switch.tsx";
import { Link } from "@components/UI/Typography/Link.tsx"; import { Link } from "@components/UI/Typography/Link.tsx";
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores"; import {
useAppStore,
useDeviceStore,
useMessageStore,
useNodeDBStore,
} from "@core/stores";
import { subscribeAll } from "@core/subscriptions.ts"; import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts"; import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core"; import { MeshDevice } from "@meshtastic/core";
@ -25,6 +30,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
const isURLHTTPS = location.protocol === "https:"; const isURLHTTPS = location.protocol === "https:";
const { addDevice } = useDeviceStore(); const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore(); const messageStore = useMessageStore();
const { setSelectedDevice } = useAppStore(); const { setSelectedDevice } = useAppStore();
@ -56,11 +62,13 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
const id = randId(); const id = randId();
const transport = await TransportHTTP.create(data.ip, data.tls); const transport = await TransportHTTP.create(data.ip, data.tls);
const device = addDevice(id); const device = addDevice(id);
const nodeDB = addNodeDB(id);
const connection = new MeshDevice(transport, id); const connection = new MeshDevice(transport, id);
connection.configure(); connection.configure();
setSelectedDevice(id); setSelectedDevice(id);
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection, messageStore); subscribeAll(device, connection, messageStore, nodeDB);
closeDialog(); closeDialog();
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {

13
packages/web/src/components/PageComponents/Connect/Serial.tsx

@ -1,6 +1,11 @@
import { Mono } from "@components/generic/Mono.tsx"; import { Mono } from "@components/generic/Mono.tsx";
import { Button } from "@components/UI/Button.tsx"; import { Button } from "@components/UI/Button.tsx";
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores"; import {
useAppStore,
useDeviceStore,
useMessageStore,
useNodeDBStore,
} from "@core/stores";
import { subscribeAll } from "@core/subscriptions.ts"; import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts"; import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core"; import { MeshDevice } from "@meshtastic/core";
@ -12,7 +17,9 @@ import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
export const Serial = ({ closeDialog }: TabElementProps) => { export const Serial = ({ closeDialog }: TabElementProps) => {
const [connectionInProgress, setConnectionInProgress] = useState(false); const [connectionInProgress, setConnectionInProgress] = useState(false);
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]); const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
const { addDevice } = useDeviceStore(); const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore(); const messageStore = useMessageStore();
const { setSelectedDevice } = useAppStore(); const { setSelectedDevice } = useAppStore();
const { t } = useTranslation(); const { t } = useTranslation();
@ -34,12 +41,14 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
const onConnect = async (port: SerialPort) => { const onConnect = async (port: SerialPort) => {
const id = randId(); const id = randId();
const device = addDevice(id); const device = addDevice(id);
const nodeDB = addNodeDB(id);
setSelectedDevice(id); setSelectedDevice(id);
const transport = await TransportWebSerial.createFromPort(port); const transport = await TransportWebSerial.createFromPort(port);
const connection = new MeshDevice(transport, id); const connection = new MeshDevice(transport, id);
connection.configure(); connection.configure();
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection, messageStore); subscribeAll(device, connection, messageStore, nodeDB);
const HEARTBEAT_INTERVAL = 5 * 60 * 1000; const HEARTBEAT_INTERVAL = 5 * 60 * 1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL); connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);

10
packages/web/src/components/PageComponents/Messages/MessageItem.tsx

@ -6,7 +6,12 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@components/UI/Tooltip.tsx"; } from "@components/UI/Tooltip.tsx";
import { MessageState, useDevice, useMessageStore } from "@core/stores"; import {
MessageState,
useDevice,
useMessageStore,
useNodeDB,
} from "@core/stores";
import type { Message } from "@core/stores/messageStore/types.ts"; import type { Message } from "@core/stores/messageStore/types.ts";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { type Protobuf, Types } from "@meshtastic/core"; import { type Protobuf, Types } from "@meshtastic/core";
@ -47,7 +52,8 @@ interface MessageItemProps {
} }
export const MessageItem = ({ message }: MessageItemProps) => { export const MessageItem = ({ message }: MessageItemProps) => {
const { config, getNode } = useDevice(); const { config } = useDevice();
const { getNode } = useNodeDB();
const { getMyNodeNum } = useMessageStore(); const { getMyNodeNum } = useMessageStore();
const { t, i18n } = useTranslation("messages"); const { t, i18n } = useTranslation("messages");

4
packages/web/src/components/PageComponents/Messages/TraceRoute.tsx

@ -1,4 +1,4 @@
import { useDevice } from "@core/stores"; import { useNodeDB } from "@core/stores";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -23,7 +23,7 @@ interface RoutePathProps {
} }
const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => { const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => {
const { getNode } = useDevice(); const { getNode } = useNodeDB();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (

18
packages/web/src/components/Sidebar.tsx

@ -2,7 +2,13 @@ import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { Spinner } from "@components/UI/Spinner.tsx"; import { Spinner } from "@components/UI/Spinner.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { type Page, useAppStore, useDevice, useSidebar } from "@core/stores"; import {
type Page,
useAppStore,
useDevice,
useNodeDB,
useSidebar,
} from "@core/stores";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
import { import {
@ -62,14 +68,8 @@ const CollapseToggleButton = () => {
}; };
export const Sidebar = ({ children }: SidebarProps) => { export const Sidebar = ({ children }: SidebarProps) => {
const { const { hardware, metadata, unreadCounts, setDialogOpen } = useDevice();
hardware, const { getNode, getNodesLength } = useNodeDB();
getNode,
getNodesLength,
metadata,
unreadCounts,
setDialogOpen,
} = useDevice();
const { setCommandPaletteOpen } = useAppStore(); const { setCommandPaletteOpen } = useAppStore();
const myNode = getNode(hardware.myNodeNum); const myNode = getNode(hardware.myNodeNum);
const { isCollapsed } = useSidebar(); const { isCollapsed } = useSidebar();

19
packages/web/src/core/hooks/useFavoriteNode.ts

@ -1,5 +1,7 @@
import { create } from "@bufbuild/protobuf";
import { useToast } from "@core/hooks/useToast.ts"; import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores"; import { useDevice, useNodeDB } from "@core/stores";
import { Protobuf } from "@meshtastic/core";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -9,7 +11,8 @@ interface FavoriteNodeOptions {
} }
export function useFavoriteNode() { export function useFavoriteNode() {
const { updateFavorite, getNode } = useDevice(); const { sendAdminMessage } = useDevice();
const { getNode, updateFavorite } = useNodeDB();
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
@ -20,6 +23,16 @@ export function useFavoriteNode() {
return; return;
} }
sendAdminMessage(
create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isFavorite ? "setFavoriteNode" : "removeFavoriteNode",
value: nodeNum,
},
}),
);
// TODO: Wait for response before changing the store
updateFavorite(nodeNum, isFavorite); updateFavorite(nodeNum, isFavorite);
toast({ toast({
@ -34,7 +47,7 @@ export function useFavoriteNode() {
}), }),
}); });
}, },
[updateFavorite, getNode, t, toast], [updateFavorite, sendAdminMessage, getNode, t, toast],
); );
return { updateFavorite: updateFavoriteCB }; return { updateFavorite: updateFavoriteCB };

22
packages/web/src/core/hooks/useIgnoreNode.ts

@ -1,5 +1,7 @@
import { create } from "@bufbuild/protobuf";
import { useToast } from "@core/hooks/useToast.ts"; import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores"; import { useDevice, useNodeDB } from "@core/stores";
import { Protobuf } from "@meshtastic/core";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -9,7 +11,9 @@ interface IgnoreNodeOptions {
} }
export function useIgnoreNode() { export function useIgnoreNode() {
const { updateIgnored, getNode } = useDevice(); const { sendAdminMessage } = useDevice();
const { getNode, updateIgnore } = useNodeDB();
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
@ -21,7 +25,17 @@ export function useIgnoreNode() {
return; return;
} }
updateIgnored(nodeNum, isIgnored); sendAdminMessage(
create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isIgnored ? "setFavoriteNode" : "removeFavoriteNode",
value: nodeNum,
},
}),
);
// TODO: Wait for response before changing the store
updateIgnore(nodeNum, isIgnored);
toast({ toast({
title: t("toast.ignoreNode.title", { title: t("toast.ignoreNode.title", {
@ -35,7 +49,7 @@ export function useIgnoreNode() {
}), }),
}); });
}, },
[updateIgnored, getNode, t, toast], [sendAdminMessage, updateIgnore, getNode, t, toast],
); );
return { updateIgnored: updateIgnoredCB }; return { updateIgnored: updateIgnoredCB };

6
packages/web/src/core/stores/appStore/index.ts

@ -14,7 +14,7 @@ interface ErrorState {
} }
interface AppState { interface AppState {
selectedDevice: number; selectedDeviceId: number;
devices: { devices: {
id: number; id: number;
num: number; num: number;
@ -48,7 +48,7 @@ interface AppState {
} }
export const useAppStore = create<AppState>()((set, get) => ({ export const useAppStore = create<AppState>()((set, get) => ({
selectedDevice: 0, selectedDeviceId: 0,
devices: [], devices: [],
currentPage: "messages", currentPage: "messages",
rasterSources: [], rasterSources: [],
@ -81,7 +81,7 @@ export const useAppStore = create<AppState>()((set, get) => ({
}, },
setSelectedDevice: (deviceId) => setSelectedDevice: (deviceId) =>
set(() => ({ set(() => ({
selectedDevice: deviceId, selectedDeviceId: deviceId,
})), })),
addDevice: (device) => addDevice: (device) =>
set((state) => ({ set((state) => ({

251
packages/web/src/core/stores/deviceStore/index.ts

@ -1,7 +1,6 @@
import { create, toBinary } from "@bufbuild/protobuf"; import { create, toBinary } from "@bufbuild/protobuf";
import { type MeshDevice, Protobuf, Types } from "@meshtastic/core"; import { type MeshDevice, Protobuf, Types } from "@meshtastic/core";
import { produce } from "immer"; import { produce } from "immer";
import { createContext, useContext } from "react";
import { create as createStore } from "zustand"; import { create as createStore } from "zustand";
export type Page = "messages" | "map" | "config" | "channels" | "nodes"; export type Page = "messages" | "map" | "config" | "channels" | "nodes";
@ -14,10 +13,6 @@ export interface ProcessPacketParams {
export type DialogVariant = keyof Device["dialog"]; export type DialogVariant = keyof Device["dialog"];
type NodeError = {
node: number;
error: string;
};
export type ValidConfigType = Exclude< export type ValidConfigType = Exclude<
Protobuf.Config.Config["payloadVariant"]["case"], Protobuf.Config.Config["payloadVariant"]["case"],
"deviceUi" | "sessionkey" | undefined "deviceUi" | "sessionkey" | undefined
@ -41,14 +36,12 @@ export interface Device {
number, number,
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[] Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
>; >;
nodeErrors: Map<number, NodeError>;
connection?: MeshDevice; connection?: MeshDevice;
activeNode: number; activeNode: number;
waypoints: Protobuf.Mesh.Waypoint[]; waypoints: Protobuf.Mesh.Waypoint[];
pendingSettingsChanges: boolean; pendingSettingsChanges: boolean;
messageDraft: string; messageDraft: string;
unreadCounts: Map<number, number>; unreadCounts: Map<number, number>;
nodesMap: Map<number, Protobuf.Mesh.NodeInfo>; // dont access directly, use getNodes, or getNode
dialog: { dialog: {
import: boolean; import: boolean;
QR: boolean; QR: boolean;
@ -97,36 +90,19 @@ export interface Device {
setPendingSettingsChanges: (state: boolean) => void; setPendingSettingsChanges: (state: boolean) => void;
addChannel: (channel: Protobuf.Channel.Channel) => void; addChannel: (channel: Protobuf.Channel.Channel) => void;
addWaypoint: (waypoint: Protobuf.Mesh.Waypoint) => void; addWaypoint: (waypoint: Protobuf.Mesh.Waypoint) => void;
addNodeInfo: (nodeInfo: Protobuf.Mesh.NodeInfo) => void;
addUser: (user: Types.PacketMetadata<Protobuf.Mesh.User>) => void;
addPosition: (position: Types.PacketMetadata<Protobuf.Mesh.Position>) => void;
addConnection: (connection: MeshDevice) => void; addConnection: (connection: MeshDevice) => void;
addTraceRoute: ( addTraceRoute: (
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>, traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>,
) => void; ) => void;
addMetadata: (from: number, metadata: Protobuf.Mesh.DeviceMetadata) => void; addMetadata: (from: number, metadata: Protobuf.Mesh.DeviceMetadata) => void;
removeNode: (nodeNum: number) => void;
setDialogOpen: (dialog: DialogVariant, open: boolean) => void; setDialogOpen: (dialog: DialogVariant, open: boolean) => void;
getDialogOpen: (dialog: DialogVariant) => boolean; getDialogOpen: (dialog: DialogVariant) => boolean;
processPacket: (data: ProcessPacketParams) => void;
setMessageDraft: (message: string) => void; setMessageDraft: (message: string) => void;
setNodeError: (nodeNum: number, error: string) => void;
clearNodeError: (nodeNum: number) => void;
getNodeError: (nodeNum: number) => NodeError | undefined;
hasNodeError: (nodeNum: number) => boolean;
incrementUnread: (nodeNum: number) => void; incrementUnread: (nodeNum: number) => void;
resetUnread: (nodeNum: number) => void; resetUnread: (nodeNum: number) => void;
getUnreadCount: (nodeNum: number) => number; getUnreadCount: (nodeNum: number) => number;
getAllUnreadCount: () => number; getAllUnreadCount: () => number;
getNodes: (
filter?: (node: Protobuf.Mesh.NodeInfo) => boolean,
) => Protobuf.Mesh.NodeInfo[];
getNodesLength: () => number;
getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined;
getMyNode: () => Protobuf.Mesh.NodeInfo;
sendAdminMessage: (message: Protobuf.Admin.AdminMessage) => void; sendAdminMessage: (message: Protobuf.Admin.AdminMessage) => void;
updateFavorite: (nodeNum: number, isFavorite: boolean) => void;
updateIgnored: (nodeNum: number, isIgnored: boolean) => void;
addClientNotification: ( addClientNotification: (
clientNotificationPacket: Protobuf.Mesh.ClientNotification, clientNotificationPacket: Protobuf.Mesh.ClientNotification,
) => void; ) => void;
@ -186,9 +162,7 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
}, },
pendingSettingsChanges: false, pendingSettingsChanges: false,
messageDraft: "", messageDraft: "",
nodeErrors: new Map(),
unreadCounts: new Map(), unreadCounts: new Map(),
nodesMap: new Map(),
clientNotifications: [], clientNotifications: [],
setStatus: (status: Types.DeviceStatusEnum) => { setStatus: (status: Types.DeviceStatusEnum) => {
@ -516,18 +490,6 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
}), }),
); );
}, },
addNodeInfo: (nodeInfo) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
device.nodesMap.set(nodeInfo.num, nodeInfo);
}),
);
},
setActiveNode: (node) => { setActiveNode: (node) => {
set( set(
produce<PrivateDeviceState>((draft) => { produce<PrivateDeviceState>((draft) => {
@ -538,38 +500,6 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
}), }),
); );
}, },
addUser: (user) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
const currentNode =
device.nodesMap.get(user.from) ??
create(Protobuf.Mesh.NodeInfoSchema);
currentNode.user = user.data;
currentNode.num = user.from;
device.nodesMap.set(user.from, currentNode);
}),
);
},
addPosition: (position) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
const currentNode =
device.nodesMap.get(position.from) ??
create(Protobuf.Mesh.NodeInfoSchema);
currentNode.position = position.data;
currentNode.num = position.from;
device.nodesMap.set(position.from, currentNode);
}),
);
},
addConnection: (connection) => { addConnection: (connection) => {
set( set(
produce<PrivateDeviceState>((draft) => { produce<PrivateDeviceState>((draft) => {
@ -603,17 +533,6 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
}), }),
); );
}, },
removeNode: (nodeNum: number) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
device.nodesMap.delete(nodeNum);
}),
);
},
setDialogOpen: (dialog: DialogVariant, open: boolean) => { setDialogOpen: (dialog: DialogVariant, open: boolean) => {
set( set(
produce<PrivateDeviceState>((draft) => { produce<PrivateDeviceState>((draft) => {
@ -631,31 +550,7 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
} }
return device.dialog[dialog]; return device.dialog[dialog];
}, },
processPacket(data: ProcessPacketParams) {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
const node = device.nodesMap.get(data.from);
if (node) {
node.lastHeard = data.time;
node.snr = data.snr;
device.nodesMap.set(data.from, node);
} else {
device.nodesMap.set(
data.from,
create(Protobuf.Mesh.NodeInfoSchema, {
num: data.from,
lastHeard: data.time,
snr: data.snr,
}),
);
}
}),
);
},
setMessageDraft: (message: string) => { setMessageDraft: (message: string) => {
set( set(
produce<PrivateDeviceState>((draft) => { produce<PrivateDeviceState>((draft) => {
@ -666,40 +561,6 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
}), }),
); );
}, },
setNodeError: (nodeNum: number, error: string) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.nodeErrors.set(nodeNum, { node: nodeNum, error });
}
}),
);
},
clearNodeError: (nodeNum: number) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.nodeErrors.delete(nodeNum);
}
}),
);
},
getNodeError: (nodeNum: number) => {
const device = get().devices.get(id);
if (!device) {
throw new Error(`Device ${id} not found`);
}
return device.nodeErrors.get(nodeNum);
},
hasNodeError: (nodeNum: number) => {
const device = get().devices.get(id);
if (!device) {
throw new Error(`Device ${id} not found`);
}
return device.nodeErrors.has(nodeNum);
},
incrementUnread: (nodeNum: number) => { incrementUnread: (nodeNum: number) => {
set( set(
produce<PrivateDeviceState>((draft) => { produce<PrivateDeviceState>((draft) => {
@ -744,48 +605,6 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
}), }),
); );
}, },
getNodes: (
filter?: (node: Protobuf.Mesh.NodeInfo) => boolean,
): Protobuf.Mesh.NodeInfo[] => {
const device = get().devices.get(id);
if (!device) {
return [];
}
const allNodes = Array.from(device.nodesMap.values()).filter(
(node) => node.num !== get().devices.get(id)?.hardware.myNodeNum,
);
if (filter) {
return allNodes.filter(filter);
}
return allNodes;
},
getNode: (nodeNum: number): Protobuf.Mesh.NodeInfo | undefined => {
const device = get().devices.get(id);
if (!device) {
return;
}
if (!device.nodesMap.has(nodeNum)) {
return undefined;
}
return device.nodesMap.get(nodeNum);
},
getMyNode: (): Protobuf.Mesh.NodeInfo => {
const device = get().devices.get(id);
if (!device) {
throw new Error(`Device ${id} not found`);
}
return (
device.nodesMap.get(device.hardware.myNodeNum) ??
create(Protobuf.Mesh.NodeInfoSchema)
);
},
getNodesLength: () => {
const device = get().devices.get(id);
if (!device) {
return 0;
}
return device.nodesMap.size;
},
sendAdminMessage(message: Protobuf.Admin.AdminMessage) { sendAdminMessage(message: Protobuf.Admin.AdminMessage) {
const device = get().devices.get(id); const device = get().devices.get(id);
@ -800,64 +619,6 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
); );
}, },
updateFavorite(nodeNum: number, isFavorite: boolean) {
const device = get().devices.get(id);
if (!device) {
return;
}
const node = device?.nodesMap.get(nodeNum);
if (!node) {
return;
}
device.sendAdminMessage(
create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isFavorite ? "setFavoriteNode" : "removeFavoriteNode",
value: nodeNum,
},
}),
);
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
const node = device?.nodesMap.get(nodeNum);
if (node) {
node.isFavorite = isFavorite;
}
}),
);
},
updateIgnored(nodeNum: number, isIgnored: boolean) {
const device = get().devices.get(id);
if (!device) {
return;
}
const node = device?.nodesMap.get(nodeNum);
if (!node) {
return;
}
device.sendAdminMessage(
create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isIgnored ? "setIgnoredNode" : "removeIgnoredNode",
value: nodeNum,
},
}),
);
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
const node = device?.nodesMap.get(nodeNum);
if (node) {
node.isIgnored = isIgnored;
}
}),
);
},
addClientNotification: ( addClientNotification: (
clientNotificationPacket: Protobuf.Mesh.ClientNotification, clientNotificationPacket: Protobuf.Mesh.ClientNotification,
) => { ) => {
@ -912,13 +673,3 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
getDevice: (id) => get().devices.get(id), getDevice: (id) => get().devices.get(id),
})); }));
export const DeviceContext = createContext<Device | undefined>(undefined);
export const useDevice = (): Device => {
const context = useContext(DeviceContext);
if (context === undefined) {
throw new Error("useDevice must be used within a DeviceProvider");
}
return context;
};

36
packages/web/src/core/stores/index.ts

@ -1,9 +1,12 @@
import { type Device, useDeviceStore } from "@core/stores/deviceStore";
import { type NodeDB, useNodeDBStore } from "@core/stores/nodeDBStore";
import { useDeviceContext } from "@core/stores/utils/deviceContext";
export { useAppStore } from "@core/stores/appStore"; export { useAppStore } from "@core/stores/appStore";
export { export {
type Device, type Device,
DeviceContext, type Page,
useDevice,
useDeviceStore, useDeviceStore,
type ValidConfigType, type ValidConfigType,
type ValidModuleConfigType, type ValidModuleConfigType,
@ -13,10 +16,35 @@ export {
MessageState, MessageState,
type MessageStore, type MessageStore,
MessageType, MessageType,
useMessageStore, useMessageStore, // TODO: Bring hook into this file
} from "@core/stores/messageStore"; } from "@core/stores/messageStore";
export { type NodeDB, useNodeDBStore } from "@core/stores/nodeDBStore";
export { export {
SidebarProvider, SidebarProvider,
useSidebar, useSidebar, // TODO: Bring hook into this file
} from "@core/stores/sidebarStore"; } from "@core/stores/sidebarStore";
export {
CurrentDeviceContext,
type DeviceContext,
useDeviceContext,
} from "@core/stores/utils/deviceContext";
// Define hooks to access the stores
export const useNodeDB = (): NodeDB => {
const { deviceId } = useDeviceContext();
const nodeDB = useNodeDBStore(
(s) => s.getNodeDB(deviceId) ?? s.addNodeDB(deviceId),
);
return nodeDB;
};
export const useDevice = (): Device => {
const { deviceId } = useDeviceContext();
const device = useDeviceStore(
(s) => s.getDevice(deviceId) ?? s.addDevice(deviceId),
);
return device;
};

374
packages/web/src/core/stores/nodeDBStore/index.ts

@ -1,157 +1,356 @@
import { create } from "@bufbuild/protobuf";
import { createStorageWithMapSupport } from "@core/stores/utils/indexDB.ts"; import { createStorageWithMapSupport } from "@core/stores/utils/indexDB.ts";
import type { Protobuf } from "@meshtastic/core"; import { Protobuf, type Types } from "@meshtastic/core";
import { produce } from "immer"; import { produce } from "immer";
import { create as createStore } from "zustand"; import { create as createStore } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import type { NodeError } from "./types"; import type { NodeError, ProcessPacketParams } from "./types";
const CURRENT_STORE_VERSION = 0; const CURRENT_STORE_VERSION = 0;
export interface nodeDB { export interface NodeDB {
userId: number; id: number;
myNodeNum: number | undefined;
nodeMap: Map<number, Protobuf.Mesh.NodeInfo>; nodeMap: Map<number, Protobuf.Mesh.NodeInfo>;
nodeErrors: Map<number, NodeError>; nodeErrors: Map<number, NodeError>;
addNode: (nodeInfo: Protobuf.Mesh.NodeInfo) => void; addNode: (nodeInfo: Protobuf.Mesh.NodeInfo) => void;
removeNode: (nodeNum: number) => void; removeNode: (nodeNum: number) => void;
processPacket: (data: ProcessPacketParams) => void;
addUser: (user: Types.PacketMetadata<Protobuf.Mesh.User>) => void;
addPosition: (position: Types.PacketMetadata<Protobuf.Mesh.Position>) => void;
updateFavorite: (nodeNum: number, isFavorite: boolean) => void;
updateIgnore: (nodeNum: number, isIgnored: boolean) => void;
setNodeNum: (nodeNum: number) => void;
setNodeError: (nodeNum: number, error: string) => void;
clearNodeError: (nodeNum: number) => void;
getNodesLength: () => number; getNodesLength: () => number;
getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined; getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined;
getNodes: ( getNodes: (
filter?: (node: Protobuf.Mesh.NodeInfo) => boolean, filter?: (node: Protobuf.Mesh.NodeInfo) => boolean,
) => Protobuf.Mesh.NodeInfo[]; ) => Protobuf.Mesh.NodeInfo[];
getMyNode: () => Protobuf.Mesh.NodeInfo;
setNodeError: (nodeNum: number, error: string) => void;
clearNodeError: (nodeNum: number) => void;
getNodeError: (nodeNum: number) => NodeError | undefined; getNodeError: (nodeNum: number) => NodeError | undefined;
hasNodeError: (nodeNum: number) => boolean; hasNodeError: (nodeNum: number) => boolean;
} }
export interface nodeDBState { export interface nodeDBState {
addNodeDB: (id: number) => nodeDB; addNodeDB: (id: number) => NodeDB;
removeNodeDB: (id: number) => void; removeNodeDB: (id: number) => void;
getNodeDBs: () => nodeDB[]; getNodeDBs: () => NodeDB[];
getNodeDB: (id: number) => nodeDB | undefined; getNodeDB: (id: number) => NodeDB | undefined;
} }
interface PrivateNodeDBState extends nodeDBState { interface PrivateNodeDBState extends nodeDBState {
nodeDBs: Map<number, nodeDB>; nodeDBs: Map<number, NodeDB>;
} }
type NodeDBPersisted = Pick<PrivateNodeDBState, "nodeDBs">; type NodeDBData = {
id: number;
myNodeNum: number | undefined;
nodeMap: Map<number, Protobuf.Mesh.NodeInfo>;
nodeErrors: Map<number, NodeError>;
};
export const useNodeDBStore = createStore<PrivateNodeDBState>()( type NodeDBPersisted = {
persist( nodeDBs: Map<number, NodeDBData>;
(set, get) => ({ };
nodeDBs: new Map(),
addNodeDB: (userId: number) => { function mergeIntoCurrent(target: NodeDB, incoming: NodeDB) {
set( for (const [n, info] of incoming.nodeMap) {
produce<PrivateNodeDBState>((draft) => { if (!target.nodeMap.has(n)) {
draft.nodeDBs.set(userId, { target.nodeMap.set(n, info);
userId: userId, }
nodeMap: new Map(), }
nodeErrors: new Map(), for (const [n, err] of incoming.nodeErrors) {
if (!target.nodeErrors.has(n)) {
target.nodeErrors.set(n, err);
}
}
}
addNode: (node: Protobuf.Mesh.NodeInfo) => function nodeDBFactory(
id: number,
get: () => PrivateNodeDBState,
set: typeof useNodeDBStore.setState,
data?: Partial<NodeDBData>,
): NodeDB {
const nodeMap = data?.nodeMap ?? new Map<number, Protobuf.Mesh.NodeInfo>();
const nodeErrors = data?.nodeErrors ?? new Map<number, NodeError>();
let myNodeNum = data?.myNodeNum;
return {
id,
myNodeNum,
nodeMap,
nodeErrors,
addNode: (node) =>
set( set(
produce((draft: PrivateNodeDBState) => { produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
nodeDB.nodeMap.set(node.num, node); nodeDB.nodeMap.set(node.num, node);
}), }),
), ),
removeNode: (nodeNum: number) => removeNode: (nodeNum) =>
set( set(
produce((draft: PrivateNodeDBState) => { produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
nodeDB.nodeMap.delete(nodeNum); nodeDB.nodeMap.delete(nodeNum);
}), }),
), ),
setNodeError: (nodeNum: number, error: string) => setNodeError: (nodeNum, error) =>
set( set(
produce((draft: PrivateNodeDBState) => { produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
nodeDB.nodeErrors.set(nodeNum, { node: nodeNum, error }); nodeDB.nodeErrors.set(nodeNum, { node: nodeNum, error });
}), }),
), ),
clearNodeError: (nodeNum: number) => clearNodeError: (nodeNum) =>
set( set(
produce((draft: PrivateNodeDBState) => { produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
nodeDB.nodeErrors.delete(nodeNum); nodeDB.nodeErrors.delete(nodeNum);
}), }),
), ),
processPacket: (data) =>
set(
produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) {
throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
}
const node = nodeDB.nodeMap.get(data.from);
if (node) {
node.lastHeard = data.time;
node.snr = data.snr;
nodeDB.nodeMap.set(data.from, node);
} else {
nodeDB.nodeMap.set(
data.from,
create(Protobuf.Mesh.NodeInfoSchema, {
num: data.from,
lastHeard: data.time,
snr: data.snr,
}),
);
}
}),
),
addUser: (user) =>
set(
produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) {
throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
}
const current =
nodeDB.nodeMap.get(user.from) ??
create(Protobuf.Mesh.NodeInfoSchema);
current.user = user.data;
current.num = user.from;
nodeDB.nodeMap.set(user.from, current);
}),
),
addPosition: (position) =>
set(
produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) {
throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
}
const current =
nodeDB.nodeMap.get(position.from) ??
create(Protobuf.Mesh.NodeInfoSchema);
current.position = position.data;
current.num = position.from;
nodeDB.nodeMap.set(position.from, current);
}),
),
setNodeNum: (nodeNum) =>
set(
produce<PrivateNodeDBState>((draft) => {
console.debug(
`Setting myNodeNum for nodeDB with id: ${id} to ${nodeNum}`,
);
const current = draft.nodeDBs.get(id);
if (!current) {
throw new Error(`No nodeDB found for id: ${id}`);
}
// There might already be an entry for this nodeNum, so we need to merge
// it into the current nodeDB.
myNodeNum = nodeNum;
current.myNodeNum = nodeNum;
for (const [k, other] of draft.nodeDBs) {
if (k === id) {
continue;
}
if (other.myNodeNum === nodeNum) {
mergeIntoCurrent(current, other); // current wins on conflicts
draft.nodeDBs.delete(k);
}
}
}),
),
updateFavorite: (nodeNum, isFavorite) =>
set(
produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) {
throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
}
const node = nodeDB.nodeMap.get(nodeNum);
if (node) {
node.isFavorite = isFavorite;
}
}),
),
updateIgnore: (nodeNum, isIgnored) =>
set(
produce<PrivateNodeDBState>((draft) => {
const nodeDB = draft.nodeDBs.get(id);
if (!nodeDB) {
throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
}
const node = nodeDB.nodeMap.get(nodeNum);
if (node) {
node.isIgnored = isIgnored;
}
}),
),
getNodesLength: () => { getNodesLength: () => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = get().nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
return nodeDB.nodeMap.size; return nodeDB.nodeMap.size;
}, },
getNode: (nodeNum: number) => { getNode: (nodeNum) => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = get().nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
return nodeDB.nodeMap.get(nodeNum); return nodeDB.nodeMap.get(nodeNum);
}, },
getNodes: (filter?) => { getNodes: (filter) => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = get().nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
} `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
const allNodes = Array.from(nodeDB.nodeMap.values()).filter(
(node) => node.num !== nodeDB.userId,
); );
if (filter) {
return allNodes.filter(filter);
} }
return allNodes; const all = Array.from(nodeDB.nodeMap.values()).filter(
(n) => n.num !== myNodeNum,
);
return filter ? all.filter(filter) : all;
}, },
getNodeError: (nodeNum: number) => { getMyNode: () => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = get().nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
return nodeDB.nodeErrors.get(nodeNum); if (!myNodeNum) {
throw new Error(`No myNodeNum set for nodeDB with id: ${id}`);
}
return (
nodeDB.nodeMap.get(myNodeNum) ?? create(Protobuf.Mesh.NodeInfoSchema)
);
}, },
hasNodeError: (nodeNum: number) => { getNodeError: (nodeNum) => {
const nodeDB = draft.nodeDBs.get(userId); const nodeDB = get().nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error(`NodeDB for userID ${userId} not found`); throw new Error(
`No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
);
} }
return nodeDB.nodeErrors.has(nodeNum) ?? false; return nodeDB.nodeErrors.get(nodeNum);
}, },
});
}),
);
const nodeDB = get().nodeDBs.get(userId);
hasNodeError: (nodeNum) => {
const nodeDB = get().nodeDBs.get(id);
if (!nodeDB) { if (!nodeDB) {
throw new Error( throw new Error(
`Failed to create or retrieve nodeDB for userID ${userId}`, `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`,
); );
} }
return nodeDB.nodeErrors.has(nodeNum);
},
};
}
export const useNodeDBStore = createStore<PrivateNodeDBState>()(
persist(
(set, get) => ({
nodeDBs: new Map(),
addNodeDB: (id) => {
const existing = get().nodeDBs.get(id);
if (existing) {
return existing;
}
const nodeDB = nodeDBFactory(id, get, set);
set(
produce<PrivateNodeDBState>((draft) => {
draft.nodeDBs.set(id, nodeDB);
}),
);
return nodeDB; return nodeDB;
}, },
removeNodeDB: (userId) => { removeNodeDB: (userId) => {
@ -161,17 +360,52 @@ export const useNodeDBStore = createStore<PrivateNodeDBState>()(
}), }),
); );
}, },
getNodeDBs: () => Array.from(get().nodeDBs.values()), getNodeDBs: () => Array.from(get().nodeDBs.values()),
getNodeDB: (userId) => get().nodeDBs.get(userId), getNodeDB: (id) => get().nodeDBs.get(id),
}), }),
{ {
name: "meshtastic-nodedb-store", name: "meshtastic-nodedb-store",
storage: createStorageWithMapSupport<NodeDBPersisted>(), storage: createStorageWithMapSupport<NodeDBPersisted>(),
version: CURRENT_STORE_VERSION, version: CURRENT_STORE_VERSION,
partialize: (s) => ({ partialize: (s): NodeDBPersisted => ({
nodeDBs: s.nodeDBs, nodeDBs: new Map(
Array.from(s.nodeDBs.entries()).map(([id, db]) => [
id,
{
id: db.id,
myNodeNum: db.myNodeNum,
nodeMap: db.nodeMap,
nodeErrors: db.nodeErrors,
},
]),
),
}), }),
onRehydrateStorage: () => (state) => {
if (!state) {
return;
}
console.debug("NodeDBStore: Rehydrating state", state);
useNodeDBStore.setState(
produce<PrivateNodeDBState>((draft) => {
const rebuilt = new Map<number, NodeDB>();
for (const [id, data] of (
draft.nodeDBs as unknown as Map<number, NodeDBData>
).entries()) {
rebuilt.set(
id,
nodeDBFactory(
id,
useNodeDBStore.getState,
useNodeDBStore.setState,
data,
),
);
}
draft.nodeDBs = rebuilt;
}),
);
},
}, },
), ),
); );

8
packages/web/src/core/stores/nodeDBStore/types.ts

@ -3,4 +3,10 @@ type NodeError = {
error: string; error: string;
}; };
export type { NodeError }; type ProcessPacketParams = {
from: number;
snr: number;
time: number;
};
export type { NodeError, ProcessPacketParams };

19
packages/web/src/core/stores/utils/deviceContext.ts

@ -0,0 +1,19 @@
import { createContext, useContext } from "react";
export type DeviceContext = {
deviceId: number; // Unique identifier for the device, not nodeNum
};
export const CurrentDeviceContext = createContext<DeviceContext | undefined>(
undefined,
);
export function useDeviceContext(): DeviceContext {
const ctx = useContext(CurrentDeviceContext);
if (!ctx) {
throw new Error(
"useDeviceContext must be used within CurrentDeviceContext provider",
);
}
return ctx;
}

21
packages/web/src/core/subscriptions.ts

@ -1,12 +1,18 @@
import { ensureDefaultUser } from "@core/dto/NodeNumToNodeInfoDTO.ts"; import { ensureDefaultUser } from "@core/dto/NodeNumToNodeInfoDTO.ts";
import PacketToMessageDTO from "@core/dto/PacketToMessageDTO.ts"; import PacketToMessageDTO from "@core/dto/PacketToMessageDTO.ts";
import { type Device, type MessageStore, MessageType } from "@core/stores"; import {
type Device,
type MessageStore,
MessageType,
type NodeDB,
} from "@core/stores";
import { type MeshDevice, Protobuf } from "@meshtastic/core"; import { type MeshDevice, Protobuf } from "@meshtastic/core";
export const subscribeAll = ( export const subscribeAll = (
device: Device, device: Device,
connection: MeshDevice, connection: MeshDevice,
messageStore: MessageStore, messageStore: MessageStore,
nodeDB: NodeDB,
) => { ) => {
let myNodeNum = 0; let myNodeNum = 0;
@ -52,20 +58,21 @@ export const subscribeAll = (
connection.events.onMyNodeInfo.subscribe((nodeInfo) => { connection.events.onMyNodeInfo.subscribe((nodeInfo) => {
device.setHardware(nodeInfo); device.setHardware(nodeInfo);
messageStore.setNodeNum(nodeInfo.myNodeNum); messageStore.setNodeNum(nodeInfo.myNodeNum);
nodeDB.setNodeNum(nodeInfo.myNodeNum);
myNodeNum = nodeInfo.myNodeNum; myNodeNum = nodeInfo.myNodeNum;
}); });
connection.events.onUserPacket.subscribe((user) => { connection.events.onUserPacket.subscribe((user) => {
device.addUser(user); nodeDB.addUser(user);
}); });
connection.events.onPositionPacket.subscribe((position) => { connection.events.onPositionPacket.subscribe((position) => {
device.addPosition(position); nodeDB.addPosition(position);
}); });
connection.events.onNodeInfoPacket.subscribe((nodeInfo) => { connection.events.onNodeInfoPacket.subscribe((nodeInfo) => {
const nodeWithUser = ensureDefaultUser(nodeInfo); const nodeWithUser = ensureDefaultUser(nodeInfo);
device.addNodeInfo(nodeWithUser); nodeDB.addNode(nodeWithUser);
}); });
connection.events.onChannelPacket.subscribe((channel) => { connection.events.onChannelPacket.subscribe((channel) => {
@ -106,7 +113,7 @@ export const subscribeAll = (
}); });
connection.events.onMeshPacket.subscribe((meshPacket) => { connection.events.onMeshPacket.subscribe((meshPacket) => {
device.processPacket({ nodeDB.processPacket({
from: meshPacket.from, from: meshPacket.from,
snr: meshPacket.rxSnr, snr: meshPacket.rxSnr,
time: meshPacket.rxTime, time: meshPacket.rxTime,
@ -128,7 +135,7 @@ export const subscribeAll = (
break; break;
case Protobuf.Mesh.Routing_Error.NO_CHANNEL: case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
console.error(`Routing Error: ${routingPacket.data.variant.value}`); console.error(`Routing Error: ${routingPacket.data.variant.value}`);
device.setNodeError( nodeDB.setNodeError(
routingPacket.from, routingPacket.from,
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value], Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value],
); );
@ -136,7 +143,7 @@ export const subscribeAll = (
break; break;
case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY: case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY:
console.error(`Routing Error: ${routingPacket.data.variant.value}`); console.error(`Routing Error: ${routingPacket.data.variant.value}`);
device.setNodeError( nodeDB.setNodeError(
routingPacket.from, routingPacket.from,
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value], Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value],
); );

14
packages/web/src/pages/Dashboard/index.tsx

@ -3,7 +3,7 @@ import { Button } from "@components/UI/Button.tsx";
import { Separator } from "@components/UI/Seperator.tsx"; import { Separator } from "@components/UI/Seperator.tsx";
import { Heading } from "@components/UI/Typography/Heading.tsx"; import { Heading } from "@components/UI/Typography/Heading.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { useAppStore, useDeviceStore } from "@core/stores"; import { useAppStore, useDeviceStore, useNodeDBStore } from "@core/stores";
import { ListPlusIcon, PlusIcon, UsersIcon } from "lucide-react"; import { ListPlusIcon, PlusIcon, UsersIcon } from "lucide-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -12,6 +12,7 @@ export const Dashboard = () => {
const { t } = useTranslation("dashboard"); const { t } = useTranslation("dashboard");
const { setConnectDialogOpen, setSelectedDevice } = useAppStore(); const { setConnectDialogOpen, setSelectedDevice } = useAppStore();
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
const { getNodeDB } = useNodeDBStore();
const devices = useMemo(() => getDevices(), [getDevices]); const devices = useMemo(() => getDevices(), [getDevices]);
@ -31,6 +32,11 @@ export const Dashboard = () => {
{devices.length ? ( {devices.length ? (
<ul className="grow divide-y divide-slate-200"> <ul className="grow divide-y divide-slate-200">
{devices.map((device) => { {devices.map((device) => {
const nodeDB = getNodeDB(device.id);
if (!nodeDB) {
return;
}
return ( return (
<li key={device.id}> <li key={device.id}>
<button <button
@ -42,7 +48,7 @@ export const Dashboard = () => {
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="truncate text-sm font-medium text-accent"> <p className="truncate text-sm font-medium text-accent">
{device.getNode(device.hardware.myNodeNum)?.user {nodeDB.getNode(device.hardware.myNodeNum)?.user
?.longName ?? t("unknown.shortName")} ?.longName ?? t("unknown.shortName")}
</p> </p>
<div className="mt-2 sm:flex sm:justify-between"> <div className="mt-2 sm:flex sm:justify-between">
@ -52,9 +58,9 @@ export const Dashboard = () => {
className="text-slate-400" className="text-slate-400"
aria-hidden="true" aria-hidden="true"
/> />
{device.getNodesLength() === 0 {nodeDB.getNodesLength() === 0
? 0 ? 0
: device.getNodesLength() - 1} : nodeDB.getNodesLength() - 1}
</div> </div>
</div> </div>
</div> </div>

7
packages/web/src/pages/Map/index.tsx

@ -6,7 +6,7 @@ import {
import { BaseMap } from "@components/Map.tsx"; import { BaseMap } from "@components/Map.tsx";
import { PageLayout } from "@components/PageLayout.tsx"; import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx"; import { Sidebar } from "@components/Sidebar.tsx";
import { useDevice } from "@core/stores"; import { useDevice, useNodeDB } from "@core/stores";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/core";
import { bbox, lineString } from "@turf/turf"; import { bbox, lineString } from "@turf/turf";
@ -30,7 +30,8 @@ const convertToLatLng = (position?: {
}); });
const MapPage = () => { const MapPage = () => {
const { getNodes, waypoints, hasNodeError } = useDevice(); const { waypoints } = useDevice();
const { getNodes, hasNodeError } = useNodeDB();
const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode(); const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode();
const { default: map } = useMap(); const { default: map } = useMap();
@ -79,7 +80,7 @@ const MapPage = () => {
return; return;
} }
if (validNodes.length === 1) { if (validNodes.length === 1 && validNodes[0]) {
map.easeTo({ map.easeTo({
zoom: map.getZoom(), zoom: map.getZoom(),
center: [ center: [

13
packages/web/src/pages/Messages.tsx

@ -13,6 +13,7 @@ import {
MessageType, MessageType,
useDevice, useDevice,
useMessageStore, useMessageStore,
useNodeDB,
useSidebar, useSidebar,
} from "@core/stores"; } from "@core/stores";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
@ -42,15 +43,9 @@ function SelectMessageChat() {
} }
export const MessagesPage = () => { export const MessagesPage = () => {
const { const { channels, getUnreadCount, resetUnread, connection } = useDevice();
channels, const { getNodes, getNode, hasNodeError } = useNodeDB();
getNodes,
getNode,
hasNodeError,
getUnreadCount,
resetUnread,
connection,
} = useDevice();
const { getMyNodeNum, getMessages, setMessageState } = useMessageStore(); const { getMyNodeNum, getMessages, setMessageState } = useMessageStore();
const { type, chatId } = useParams({ from: messagesWithParamsRoute.id }); const { type, chatId } = useParams({ from: messagesWithParamsRoute.id });

6
packages/web/src/pages/Nodes/index.tsx

@ -17,7 +17,7 @@ import { Sidebar } from "@components/Sidebar.tsx";
import { Avatar } from "@components/UI/Avatar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx";
import { Input } from "@components/UI/Input.tsx"; import { Input } from "@components/UI/Input.tsx";
import useLang from "@core/hooks/useLang.ts"; import useLang from "@core/hooks/useLang.ts";
import { useAppStore, useDevice } from "@core/stores"; import { useAppStore, useDevice, useNodeDB } from "@core/stores";
import { Protobuf, type Types } from "@meshtastic/core"; import { Protobuf, type Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { LockIcon, LockOpenIcon } from "lucide-react"; import { LockIcon, LockOpenIcon } from "lucide-react";
@ -40,8 +40,8 @@ export interface DeleteNoteDialogProps {
const NodesPage = (): JSX.Element => { const NodesPage = (): JSX.Element => {
const { t } = useTranslation("nodes"); const { t } = useTranslation("nodes");
const { currentLanguage } = useLang(); const { currentLanguage } = useLang();
const { getNodes, hardware, connection, hasNodeError, setDialogOpen } = const { hardware, connection, setDialogOpen } = useDevice();
useDevice(); const { getNodes, hasNodeError } = useNodeDB();
const { setNodeNumDetails } = useAppStore(); const { setNodeNumDetails } = useAppStore();
const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode(); const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode();

Loading…
Cancel
Save