diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index e56d391e..809278ef 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -15,10 +15,10 @@ import { MapProvider } from "react-map-gl/maplibre"; export function App() { const { getDevice } = useDeviceStore(); - const { selectedDevice, setConnectDialogOpen, connectDialogOpen } = + const { selectedDeviceId, setConnectDialogOpen, connectDialogOpen } = useAppStore(); - const device = getDevice(selectedDevice); + const device = getDevice(selectedDeviceId); // Sets up light/dark mode based on user preferences or system settings useTheme(); @@ -33,7 +33,7 @@ export function App() { /> {/* */} - +
{ +export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => { return ( - {children} + + {children} + ); }; diff --git a/packages/web/src/components/CommandPalette/index.tsx b/packages/web/src/components/CommandPalette/index.tsx index 5c9753db..ab6403ea 100644 --- a/packages/web/src/components/CommandPalette/index.tsx +++ b/packages/web/src/components/CommandPalette/index.tsx @@ -8,7 +8,12 @@ import { CommandList, } from "@components/UI/Command.tsx"; 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 { useNavigate } from "@tanstack/react-router"; import { useCommandState } from "cmdk"; @@ -64,7 +69,8 @@ export const CommandPalette = () => { setSelectedDevice, } = useAppStore(); const { getDevices } = useDeviceStore(); - const { setDialogOpen, getNode, connection } = useDevice(); + const { setDialogOpen, connection } = useDevice(); + const { getNode } = useNodeDB(); const { pinnedItems, togglePinnedItem } = usePinnedItems({ storageName: "pinnedCommandMenuGroups", }); diff --git a/packages/web/src/components/Dialog/DeviceNameDialog.tsx b/packages/web/src/components/Dialog/DeviceNameDialog.tsx index ee8b15ba..055d11f8 100644 --- a/packages/web/src/components/Dialog/DeviceNameDialog.tsx +++ b/packages/web/src/components/Dialog/DeviceNameDialog.tsx @@ -10,7 +10,7 @@ import { DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; -import { useDevice } from "@core/stores"; +import { useDevice, useNodeDB } from "@core/stores"; import { zodResolver } from "@hookform/resolvers/zod"; import { Protobuf } from "@meshtastic/core"; import { useForm } from "react-hook-form"; @@ -33,7 +33,8 @@ export const DeviceNameDialog = ({ onOpenChange, }: DeviceNameDialogProps) => { const { t } = useTranslation("dialog"); - const { hardware, getNode, connection } = useDevice(); + const { hardware, connection } = useDevice(); + const { getNode } = useNodeDB(); const myNode = getNode(hardware.myNodeNum); const defaultValues = { diff --git a/packages/web/src/components/Dialog/LocationResponseDialog.tsx b/packages/web/src/components/Dialog/LocationResponseDialog.tsx index e501da25..0fa9aff9 100644 --- a/packages/web/src/components/Dialog/LocationResponseDialog.tsx +++ b/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 { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { useTranslation } from "react-i18next"; @@ -23,7 +23,7 @@ export const LocationResponseDialog = ({ onOpenChange, }: LocationResponseDialogProps) => { const { t } = useTranslation("dialog"); - const { getNode } = useDevice(); + const { getNode } = useNodeDB(); const from = getNode(location?.from ?? 0); const longName = diff --git a/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx b/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx index a6b9104e..a55e6794 100644 --- a/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx +++ b/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx @@ -27,7 +27,7 @@ import { import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts"; import { useIgnoreNode } from "@core/hooks/useIgnoreNode.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 { Protobuf } from "@meshtastic/core"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; @@ -55,7 +55,8 @@ export const NodeDetailsDialog = ({ onOpenChange, }: NodeDetailsDialogProps) => { const { t } = useTranslation("dialog"); - const { setDialogOpen, connection, getNode } = useDevice(); + const { setDialogOpen, connection } = useDevice(); + const { getNode } = useNodeDB(); const navigate = useNavigate(); const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore(); const { updateFavorite } = useFavoriteNode(); diff --git a/packages/web/src/components/Dialog/PKIBackupDialog.tsx b/packages/web/src/components/Dialog/PKIBackupDialog.tsx index a3f296af..df68b59d 100644 --- a/packages/web/src/components/Dialog/PKIBackupDialog.tsx +++ b/packages/web/src/components/Dialog/PKIBackupDialog.tsx @@ -1,3 +1,4 @@ +import { Button } from "@components/UI/Button.tsx"; import { Dialog, DialogClose, @@ -7,12 +8,11 @@ import { DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; +import { useDevice, useNodeDB } from "@core/stores"; import { fromByteArray } from "base64-js"; import { DownloadIcon, PrinterIcon } from "lucide-react"; import React from "react"; import { useTranslation } from "react-i18next"; -import { useDevice } from "../../core/stores"; -import { Button } from "../UI/Button.tsx"; export interface PkiBackupDialogProps { open: boolean; @@ -24,7 +24,8 @@ export const PkiBackupDialog = ({ onOpenChange, }: PkiBackupDialogProps) => { const { t } = useTranslation("dialog"); - const { config, setDialogOpen, getMyNode } = useDevice(); + const { config, setDialogOpen } = useDevice(); + const { getMyNode } = useNodeDB(); const privateKey = config.security?.privateKey; const publicKey = config.security?.publicKey; diff --git a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx b/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx index 388f6361..19a51e17 100644 --- a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx +++ b/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx @@ -6,7 +6,7 @@ import { DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; -import { useDevice, useMessageStore } from "@core/stores"; +import { useMessageStore, useNodeDB } from "@core/stores"; import { LockKeyholeOpenIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; @@ -22,7 +22,8 @@ export const RefreshKeysDialog = ({ }: RefreshKeysDialogProps) => { const { t } = useTranslation("dialog"); const { activeChat } = useMessageStore(); - const { nodeErrors, getNode } = useDevice(); + const { nodeErrors, getNode } = useNodeDB(); + const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog(); const nodeErrorNum = nodeErrors.get(activeChat); diff --git a/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts b/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts index 822abdca..aa7352cb 100644 --- a/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts +++ b/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"; export function useRefreshKeysDialog() { - const { removeNode, setDialogOpen, clearNodeError, getNodeError } = - useDevice(); + const { setDialogOpen } = useDevice(); + const { removeNode, clearNodeError, getNodeError } = useNodeDB(); const { activeChat } = useMessageStore(); const handleCloseDialog = useCallback(() => { diff --git a/packages/web/src/components/Dialog/RemoveNodeDialog.tsx b/packages/web/src/components/Dialog/RemoveNodeDialog.tsx index 2bb7e181..9574fa16 100644 --- a/packages/web/src/components/Dialog/RemoveNodeDialog.tsx +++ b/packages/web/src/components/Dialog/RemoveNodeDialog.tsx @@ -9,7 +9,7 @@ import { DialogTitle, } from "@components/UI/Dialog.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"; export interface RemoveNodeDialogProps { @@ -22,7 +22,8 @@ export const RemoveNodeDialog = ({ onOpenChange, }: RemoveNodeDialogProps) => { const { t } = useTranslation("dialog"); - const { connection, getNode, removeNode } = useDevice(); + const { connection } = useDevice(); + const { getNode, removeNode } = useNodeDB(); const { nodeNumToBeRemoved } = useAppStore(); const onSubmit = () => { diff --git a/packages/web/src/components/Dialog/TracerouteResponseDialog.tsx b/packages/web/src/components/Dialog/TracerouteResponseDialog.tsx index 4748e2d4..cf27fe2d 100644 --- a/packages/web/src/components/Dialog/TracerouteResponseDialog.tsx +++ b/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 { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { useTranslation } from "react-i18next"; @@ -25,7 +25,7 @@ export const TracerouteResponseDialog = ({ onOpenChange, }: TracerouteResponseDialogProps) => { const { t } = useTranslation("dialog"); - const { getNode } = useDevice(); + const { getNode } = useNodeDB(); const route: number[] = traceroute?.data.route ?? []; const routeBack: number[] = traceroute?.data.routeBack ?? []; const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4); diff --git a/packages/web/src/components/PageComponents/Connect/BLE.tsx b/packages/web/src/components/PageComponents/Connect/BLE.tsx index 40a5d307..9d50b64c 100644 --- a/packages/web/src/components/PageComponents/Connect/BLE.tsx +++ b/packages/web/src/components/PageComponents/Connect/BLE.tsx @@ -1,6 +1,11 @@ import { Mono } from "@components/generic/Mono.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 { randId } from "@core/utils/randId.ts"; import { MeshDevice } from "@meshtastic/core"; @@ -12,7 +17,9 @@ import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx"; export const BLE = ({ closeDialog }: TabElementProps) => { const [connectionInProgress, setConnectionInProgress] = useState(false); const [bleDevices, setBleDevices] = useState([]); + const { addDevice } = useDeviceStore(); + const { addNodeDB } = useNodeDBStore(); const messageStore = useMessageStore(); const { setSelectedDevice } = useAppStore(); const { t } = useTranslation(); @@ -29,11 +36,13 @@ export const BLE = ({ closeDialog }: TabElementProps) => { const id = randId(); const transport = await TransportWebBluetooth.createFromDevice(bleDevice); const device = addDevice(id); + const nodeDB = addNodeDB(id); + const connection = new MeshDevice(transport, id); connection.configure(); setSelectedDevice(id); device.addConnection(connection); - subscribeAll(device, connection, messageStore); + subscribeAll(device, connection, messageStore, nodeDB); const HEARTBEAT_INTERVAL = 5 * 60 * 1000; connection.setHeartbeatInterval(HEARTBEAT_INTERVAL); diff --git a/packages/web/src/components/PageComponents/Connect/HTTP.tsx b/packages/web/src/components/PageComponents/Connect/HTTP.tsx index 566ef36d..b4ac3591 100644 --- a/packages/web/src/components/PageComponents/Connect/HTTP.tsx +++ b/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 { Switch } from "@components/UI/Switch.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 { randId } from "@core/utils/randId.ts"; import { MeshDevice } from "@meshtastic/core"; @@ -25,6 +30,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => { const isURLHTTPS = location.protocol === "https:"; const { addDevice } = useDeviceStore(); + const { addNodeDB } = useNodeDBStore(); const messageStore = useMessageStore(); const { setSelectedDevice } = useAppStore(); @@ -56,11 +62,13 @@ export const HTTP = ({ closeDialog }: TabElementProps) => { const id = randId(); const transport = await TransportHTTP.create(data.ip, data.tls); const device = addDevice(id); + const nodeDB = addNodeDB(id); + const connection = new MeshDevice(transport, id); connection.configure(); setSelectedDevice(id); device.addConnection(connection); - subscribeAll(device, connection, messageStore); + subscribeAll(device, connection, messageStore, nodeDB); closeDialog(); } catch (error) { if (error instanceof Error) { diff --git a/packages/web/src/components/PageComponents/Connect/Serial.tsx b/packages/web/src/components/PageComponents/Connect/Serial.tsx index 2a671c7c..4015606a 100644 --- a/packages/web/src/components/PageComponents/Connect/Serial.tsx +++ b/packages/web/src/components/PageComponents/Connect/Serial.tsx @@ -1,6 +1,11 @@ import { Mono } from "@components/generic/Mono.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 { randId } from "@core/utils/randId.ts"; import { MeshDevice } from "@meshtastic/core"; @@ -12,7 +17,9 @@ import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx"; export const Serial = ({ closeDialog }: TabElementProps) => { const [connectionInProgress, setConnectionInProgress] = useState(false); const [serialPorts, setSerialPorts] = useState([]); + const { addDevice } = useDeviceStore(); + const { addNodeDB } = useNodeDBStore(); const messageStore = useMessageStore(); const { setSelectedDevice } = useAppStore(); const { t } = useTranslation(); @@ -34,12 +41,14 @@ export const Serial = ({ closeDialog }: TabElementProps) => { const onConnect = async (port: SerialPort) => { const id = randId(); const device = addDevice(id); + const nodeDB = addNodeDB(id); + setSelectedDevice(id); const transport = await TransportWebSerial.createFromPort(port); const connection = new MeshDevice(transport, id); connection.configure(); device.addConnection(connection); - subscribeAll(device, connection, messageStore); + subscribeAll(device, connection, messageStore, nodeDB); const HEARTBEAT_INTERVAL = 5 * 60 * 1000; connection.setHeartbeatInterval(HEARTBEAT_INTERVAL); diff --git a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx index 3a76ac55..4f75ccef 100644 --- a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx +++ b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx @@ -6,7 +6,12 @@ import { TooltipProvider, TooltipTrigger, } 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 { cn } from "@core/utils/cn.ts"; import { type Protobuf, Types } from "@meshtastic/core"; @@ -47,7 +52,8 @@ interface MessageItemProps { } export const MessageItem = ({ message }: MessageItemProps) => { - const { config, getNode } = useDevice(); + const { config } = useDevice(); + const { getNode } = useNodeDB(); const { getMyNodeNum } = useMessageStore(); const { t, i18n } = useTranslation("messages"); diff --git a/packages/web/src/components/PageComponents/Messages/TraceRoute.tsx b/packages/web/src/components/PageComponents/Messages/TraceRoute.tsx index 2449d291..a8f64498 100644 --- a/packages/web/src/components/PageComponents/Messages/TraceRoute.tsx +++ b/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 { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { useTranslation } from "react-i18next"; @@ -23,7 +23,7 @@ interface RoutePathProps { } const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => { - const { getNode } = useDevice(); + const { getNode } = useNodeDB(); const { t } = useTranslation(); return ( diff --git a/packages/web/src/components/Sidebar.tsx b/packages/web/src/components/Sidebar.tsx index f86de173..02d47357 100644 --- a/packages/web/src/components/Sidebar.tsx +++ b/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 { Spinner } from "@components/UI/Spinner.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 { useLocation, useNavigate } from "@tanstack/react-router"; import { @@ -62,14 +68,8 @@ const CollapseToggleButton = () => { }; export const Sidebar = ({ children }: SidebarProps) => { - const { - hardware, - getNode, - getNodesLength, - metadata, - unreadCounts, - setDialogOpen, - } = useDevice(); + const { hardware, metadata, unreadCounts, setDialogOpen } = useDevice(); + const { getNode, getNodesLength } = useNodeDB(); const { setCommandPaletteOpen } = useAppStore(); const myNode = getNode(hardware.myNodeNum); const { isCollapsed } = useSidebar(); diff --git a/packages/web/src/core/hooks/useFavoriteNode.ts b/packages/web/src/core/hooks/useFavoriteNode.ts index d3358766..4c8faf08 100644 --- a/packages/web/src/core/hooks/useFavoriteNode.ts +++ b/packages/web/src/core/hooks/useFavoriteNode.ts @@ -1,5 +1,7 @@ +import { create } from "@bufbuild/protobuf"; 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 { useTranslation } from "react-i18next"; @@ -9,7 +11,8 @@ interface FavoriteNodeOptions { } export function useFavoriteNode() { - const { updateFavorite, getNode } = useDevice(); + const { sendAdminMessage } = useDevice(); + const { getNode, updateFavorite } = useNodeDB(); const { t } = useTranslation(); const { toast } = useToast(); @@ -20,6 +23,16 @@ export function useFavoriteNode() { return; } + sendAdminMessage( + create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: isFavorite ? "setFavoriteNode" : "removeFavoriteNode", + value: nodeNum, + }, + }), + ); + + // TODO: Wait for response before changing the store updateFavorite(nodeNum, isFavorite); toast({ @@ -34,7 +47,7 @@ export function useFavoriteNode() { }), }); }, - [updateFavorite, getNode, t, toast], + [updateFavorite, sendAdminMessage, getNode, t, toast], ); return { updateFavorite: updateFavoriteCB }; diff --git a/packages/web/src/core/hooks/useIgnoreNode.ts b/packages/web/src/core/hooks/useIgnoreNode.ts index 05e6cdc7..30f98852 100644 --- a/packages/web/src/core/hooks/useIgnoreNode.ts +++ b/packages/web/src/core/hooks/useIgnoreNode.ts @@ -1,5 +1,7 @@ +import { create } from "@bufbuild/protobuf"; 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 { useTranslation } from "react-i18next"; @@ -9,7 +11,9 @@ interface IgnoreNodeOptions { } export function useIgnoreNode() { - const { updateIgnored, getNode } = useDevice(); + const { sendAdminMessage } = useDevice(); + const { getNode, updateIgnore } = useNodeDB(); + const { t } = useTranslation(); const { toast } = useToast(); @@ -21,7 +25,17 @@ export function useIgnoreNode() { 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({ title: t("toast.ignoreNode.title", { @@ -35,7 +49,7 @@ export function useIgnoreNode() { }), }); }, - [updateIgnored, getNode, t, toast], + [sendAdminMessage, updateIgnore, getNode, t, toast], ); return { updateIgnored: updateIgnoredCB }; diff --git a/packages/web/src/core/stores/appStore/index.ts b/packages/web/src/core/stores/appStore/index.ts index bdf2d67f..1daa725c 100644 --- a/packages/web/src/core/stores/appStore/index.ts +++ b/packages/web/src/core/stores/appStore/index.ts @@ -14,7 +14,7 @@ interface ErrorState { } interface AppState { - selectedDevice: number; + selectedDeviceId: number; devices: { id: number; num: number; @@ -48,7 +48,7 @@ interface AppState { } export const useAppStore = create()((set, get) => ({ - selectedDevice: 0, + selectedDeviceId: 0, devices: [], currentPage: "messages", rasterSources: [], @@ -81,7 +81,7 @@ export const useAppStore = create()((set, get) => ({ }, setSelectedDevice: (deviceId) => set(() => ({ - selectedDevice: deviceId, + selectedDeviceId: deviceId, })), addDevice: (device) => set((state) => ({ diff --git a/packages/web/src/core/stores/deviceStore/index.ts b/packages/web/src/core/stores/deviceStore/index.ts index 9ee04008..76db556c 100644 --- a/packages/web/src/core/stores/deviceStore/index.ts +++ b/packages/web/src/core/stores/deviceStore/index.ts @@ -1,7 +1,6 @@ import { create, toBinary } from "@bufbuild/protobuf"; import { type MeshDevice, Protobuf, Types } from "@meshtastic/core"; import { produce } from "immer"; -import { createContext, useContext } from "react"; import { create as createStore } from "zustand"; export type Page = "messages" | "map" | "config" | "channels" | "nodes"; @@ -14,10 +13,6 @@ export interface ProcessPacketParams { export type DialogVariant = keyof Device["dialog"]; -type NodeError = { - node: number; - error: string; -}; export type ValidConfigType = Exclude< Protobuf.Config.Config["payloadVariant"]["case"], "deviceUi" | "sessionkey" | undefined @@ -41,14 +36,12 @@ export interface Device { number, Types.PacketMetadata[] >; - nodeErrors: Map; connection?: MeshDevice; activeNode: number; waypoints: Protobuf.Mesh.Waypoint[]; pendingSettingsChanges: boolean; messageDraft: string; unreadCounts: Map; - nodesMap: Map; // dont access directly, use getNodes, or getNode dialog: { import: boolean; QR: boolean; @@ -97,36 +90,19 @@ export interface Device { setPendingSettingsChanges: (state: boolean) => void; addChannel: (channel: Protobuf.Channel.Channel) => void; addWaypoint: (waypoint: Protobuf.Mesh.Waypoint) => void; - addNodeInfo: (nodeInfo: Protobuf.Mesh.NodeInfo) => void; - addUser: (user: Types.PacketMetadata) => void; - addPosition: (position: Types.PacketMetadata) => void; addConnection: (connection: MeshDevice) => void; addTraceRoute: ( traceroute: Types.PacketMetadata, ) => void; addMetadata: (from: number, metadata: Protobuf.Mesh.DeviceMetadata) => void; - removeNode: (nodeNum: number) => void; setDialogOpen: (dialog: DialogVariant, open: boolean) => void; getDialogOpen: (dialog: DialogVariant) => boolean; - processPacket: (data: ProcessPacketParams) => 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; resetUnread: (nodeNum: number) => void; getUnreadCount: (nodeNum: number) => 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; - updateFavorite: (nodeNum: number, isFavorite: boolean) => void; - updateIgnored: (nodeNum: number, isIgnored: boolean) => void; addClientNotification: ( clientNotificationPacket: Protobuf.Mesh.ClientNotification, ) => void; @@ -186,9 +162,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, pendingSettingsChanges: false, messageDraft: "", - nodeErrors: new Map(), unreadCounts: new Map(), - nodesMap: new Map(), clientNotifications: [], setStatus: (status: Types.DeviceStatusEnum) => { @@ -516,18 +490,6 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - addNodeInfo: (nodeInfo) => { - set( - produce((draft) => { - const device = draft.devices.get(id); - - if (!device) { - return; - } - device.nodesMap.set(nodeInfo.num, nodeInfo); - }), - ); - }, setActiveNode: (node) => { set( produce((draft) => { @@ -538,38 +500,6 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - addUser: (user) => { - set( - produce((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((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) => { set( produce((draft) => { @@ -603,17 +533,6 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - removeNode: (nodeNum: number) => { - set( - produce((draft) => { - const device = draft.devices.get(id); - if (!device) { - return; - } - device.nodesMap.delete(nodeNum); - }), - ); - }, setDialogOpen: (dialog: DialogVariant, open: boolean) => { set( produce((draft) => { @@ -631,31 +550,7 @@ export const useDeviceStore = createStore((set, get) => ({ } return device.dialog[dialog]; }, - processPacket(data: ProcessPacketParams) { - set( - produce((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) => { set( produce((draft) => { @@ -666,40 +561,6 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - setNodeError: (nodeNum: number, error: string) => { - set( - produce((draft) => { - const device = draft.devices.get(id); - if (device) { - device.nodeErrors.set(nodeNum, { node: nodeNum, error }); - } - }), - ); - }, - clearNodeError: (nodeNum: number) => { - set( - produce((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) => { set( produce((draft) => { @@ -744,48 +605,6 @@ export const useDeviceStore = createStore((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) { const device = get().devices.get(id); @@ -800,64 +619,6 @@ export const useDeviceStore = createStore((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((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((draft) => { - const device = draft.devices.get(id); - const node = device?.nodesMap.get(nodeNum); - if (node) { - node.isIgnored = isIgnored; - } - }), - ); - }, addClientNotification: ( clientNotificationPacket: Protobuf.Mesh.ClientNotification, ) => { @@ -912,13 +673,3 @@ export const useDeviceStore = createStore((set, get) => ({ getDevice: (id) => get().devices.get(id), })); - -export const DeviceContext = createContext(undefined); - -export const useDevice = (): Device => { - const context = useContext(DeviceContext); - if (context === undefined) { - throw new Error("useDevice must be used within a DeviceProvider"); - } - return context; -}; diff --git a/packages/web/src/core/stores/index.ts b/packages/web/src/core/stores/index.ts index f10e6042..76fde626 100644 --- a/packages/web/src/core/stores/index.ts +++ b/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 { type Device, - DeviceContext, - useDevice, + type Page, useDeviceStore, type ValidConfigType, type ValidModuleConfigType, @@ -13,10 +16,35 @@ export { MessageState, type MessageStore, MessageType, - useMessageStore, + useMessageStore, // TODO: Bring hook into this file } from "@core/stores/messageStore"; +export { type NodeDB, useNodeDBStore } from "@core/stores/nodeDBStore"; + export { SidebarProvider, - useSidebar, + useSidebar, // TODO: Bring hook into this file } 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; +}; diff --git a/packages/web/src/core/stores/nodeDBStore/index.ts b/packages/web/src/core/stores/nodeDBStore/index.ts index 9591f65a..0da05bb3 100644 --- a/packages/web/src/core/stores/nodeDBStore/index.ts +++ b/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 type { Protobuf } from "@meshtastic/core"; +import { Protobuf, type Types } from "@meshtastic/core"; import { produce } from "immer"; import { create as createStore } from "zustand"; import { persist } from "zustand/middleware"; -import type { NodeError } from "./types"; +import type { NodeError, ProcessPacketParams } from "./types"; const CURRENT_STORE_VERSION = 0; -export interface nodeDB { - userId: number; +export interface NodeDB { + id: number; + myNodeNum: number | undefined; nodeMap: Map; nodeErrors: Map; addNode: (nodeInfo: Protobuf.Mesh.NodeInfo) => void; removeNode: (nodeNum: number) => void; + processPacket: (data: ProcessPacketParams) => void; + addUser: (user: Types.PacketMetadata) => void; + addPosition: (position: Types.PacketMetadata) => 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; getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined; getNodes: ( filter?: (node: Protobuf.Mesh.NodeInfo) => boolean, ) => Protobuf.Mesh.NodeInfo[]; + getMyNode: () => Protobuf.Mesh.NodeInfo; - setNodeError: (nodeNum: number, error: string) => void; - clearNodeError: (nodeNum: number) => void; getNodeError: (nodeNum: number) => NodeError | undefined; hasNodeError: (nodeNum: number) => boolean; } export interface nodeDBState { - addNodeDB: (id: number) => nodeDB; + addNodeDB: (id: number) => NodeDB; removeNodeDB: (id: number) => void; - getNodeDBs: () => nodeDB[]; - getNodeDB: (id: number) => nodeDB | undefined; + getNodeDBs: () => NodeDB[]; + getNodeDB: (id: number) => NodeDB | undefined; } interface PrivateNodeDBState extends nodeDBState { - nodeDBs: Map; + nodeDBs: Map; } -type NodeDBPersisted = Pick; +type NodeDBData = { + id: number; + myNodeNum: number | undefined; + nodeMap: Map; + nodeErrors: Map; +}; -export const useNodeDBStore = createStore()( - persist( - (set, get) => ({ - nodeDBs: new Map(), - addNodeDB: (userId: number) => { - set( - produce((draft) => { - draft.nodeDBs.set(userId, { - userId: userId, - nodeMap: new Map(), - nodeErrors: new Map(), - - addNode: (node: Protobuf.Mesh.NodeInfo) => - set( - produce((draft: PrivateNodeDBState) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - nodeDB.nodeMap.set(node.num, node); - }), - ), +type NodeDBPersisted = { + nodeDBs: Map; +}; - removeNode: (nodeNum: number) => - set( - produce((draft: PrivateNodeDBState) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - nodeDB.nodeMap.delete(nodeNum); - }), - ), +function mergeIntoCurrent(target: NodeDB, incoming: NodeDB) { + for (const [n, info] of incoming.nodeMap) { + if (!target.nodeMap.has(n)) { + target.nodeMap.set(n, info); + } + } + for (const [n, err] of incoming.nodeErrors) { + if (!target.nodeErrors.has(n)) { + target.nodeErrors.set(n, err); + } + } +} - setNodeError: (nodeNum: number, error: string) => - set( - produce((draft: PrivateNodeDBState) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - nodeDB.nodeErrors.set(nodeNum, { node: nodeNum, error }); - }), - ), +function nodeDBFactory( + id: number, + get: () => PrivateNodeDBState, + set: typeof useNodeDBStore.setState, + data?: Partial, +): NodeDB { + const nodeMap = data?.nodeMap ?? new Map(); + const nodeErrors = data?.nodeErrors ?? new Map(); + let myNodeNum = data?.myNodeNum; - clearNodeError: (nodeNum: number) => - set( - produce((draft: PrivateNodeDBState) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - nodeDB.nodeErrors.delete(nodeNum); - }), - ), + return { + id, + myNodeNum, + nodeMap, + nodeErrors, - getNodesLength: () => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - return nodeDB.nodeMap.size; - }, - - getNode: (nodeNum: number) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - return nodeDB.nodeMap.get(nodeNum); - }, - - getNodes: (filter?) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - const allNodes = Array.from(nodeDB.nodeMap.values()).filter( - (node) => node.num !== nodeDB.userId, - ); - if (filter) { - return allNodes.filter(filter); - } - return allNodes; - }, - - getNodeError: (nodeNum: number) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - return nodeDB.nodeErrors.get(nodeNum); - }, - - hasNodeError: (nodeNum: number) => { - const nodeDB = draft.nodeDBs.get(userId); - if (!nodeDB) { - throw new Error(`NodeDB for userID ${userId} not found`); - } - return nodeDB.nodeErrors.has(nodeNum) ?? false; - }, - }); - }), - ); + addNode: (node) => + set( + produce((draft) => { + const nodeDB = draft.nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + nodeDB.nodeMap.set(node.num, node); + }), + ), + + removeNode: (nodeNum) => + set( + produce((draft) => { + const nodeDB = draft.nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + nodeDB.nodeMap.delete(nodeNum); + }), + ), + + setNodeError: (nodeNum, error) => + set( + produce((draft) => { + const nodeDB = draft.nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + nodeDB.nodeErrors.set(nodeNum, { node: nodeNum, error }); + }), + ), + + clearNodeError: (nodeNum) => + set( + produce((draft) => { + const nodeDB = draft.nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + nodeDB.nodeErrors.delete(nodeNum); + }), + ), - const nodeDB = get().nodeDBs.get(userId); + processPacket: (data) => + set( + produce((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, + }), + ); + } + }), + ), - if (!nodeDB) { - throw new Error( - `Failed to create or retrieve nodeDB for userID ${userId}`, + addUser: (user) => + set( + produce((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((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((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((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((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: () => { + const nodeDB = get().nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + return nodeDB.nodeMap.size; + }, + + getNode: (nodeNum) => { + const nodeDB = get().nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + return nodeDB.nodeMap.get(nodeNum); + }, + + getNodes: (filter) => { + const nodeDB = get().nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + const all = Array.from(nodeDB.nodeMap.values()).filter( + (n) => n.num !== myNodeNum, + ); + return filter ? all.filter(filter) : all; + }, + + getMyNode: () => { + const nodeDB = get().nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + if (!myNodeNum) { + throw new Error(`No myNodeNum set for nodeDB with id: ${id}`); + } + return ( + nodeDB.nodeMap.get(myNodeNum) ?? create(Protobuf.Mesh.NodeInfoSchema) + ); + }, + + getNodeError: (nodeNum) => { + const nodeDB = get().nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + return nodeDB.nodeErrors.get(nodeNum); + }, + + hasNodeError: (nodeNum) => { + const nodeDB = get().nodeDBs.get(id); + if (!nodeDB) { + throw new Error( + `No nodeDB found for myNodeNum: ${myNodeNum} (id: ${id})`, + ); + } + return nodeDB.nodeErrors.has(nodeNum); + }, + }; +} + +export const useNodeDBStore = createStore()( + 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((draft) => { + draft.nodeDBs.set(id, nodeDB); + }), + ); return nodeDB; }, removeNodeDB: (userId) => { @@ -161,17 +360,52 @@ export const useNodeDBStore = createStore()( }), ); }, - getNodeDBs: () => Array.from(get().nodeDBs.values()), - getNodeDB: (userId) => get().nodeDBs.get(userId), + getNodeDB: (id) => get().nodeDBs.get(id), }), { name: "meshtastic-nodedb-store", storage: createStorageWithMapSupport(), version: CURRENT_STORE_VERSION, - partialize: (s) => ({ - nodeDBs: s.nodeDBs, + partialize: (s): NodeDBPersisted => ({ + 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((draft) => { + const rebuilt = new Map(); + for (const [id, data] of ( + draft.nodeDBs as unknown as Map + ).entries()) { + rebuilt.set( + id, + nodeDBFactory( + id, + useNodeDBStore.getState, + useNodeDBStore.setState, + data, + ), + ); + } + draft.nodeDBs = rebuilt; + }), + ); + }, }, ), ); diff --git a/packages/web/src/core/stores/nodeDBStore/types.ts b/packages/web/src/core/stores/nodeDBStore/types.ts index cba10d51..a1e36e6a 100644 --- a/packages/web/src/core/stores/nodeDBStore/types.ts +++ b/packages/web/src/core/stores/nodeDBStore/types.ts @@ -3,4 +3,10 @@ type NodeError = { error: string; }; -export type { NodeError }; +type ProcessPacketParams = { + from: number; + snr: number; + time: number; +}; + +export type { NodeError, ProcessPacketParams }; diff --git a/packages/web/src/core/stores/utils/deviceContext.ts b/packages/web/src/core/stores/utils/deviceContext.ts new file mode 100644 index 00000000..8a608bf9 --- /dev/null +++ b/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( + undefined, +); + +export function useDeviceContext(): DeviceContext { + const ctx = useContext(CurrentDeviceContext); + if (!ctx) { + throw new Error( + "useDeviceContext must be used within CurrentDeviceContext provider", + ); + } + return ctx; +} diff --git a/packages/web/src/core/subscriptions.ts b/packages/web/src/core/subscriptions.ts index 3415cc25..689d0efe 100644 --- a/packages/web/src/core/subscriptions.ts +++ b/packages/web/src/core/subscriptions.ts @@ -1,12 +1,18 @@ import { ensureDefaultUser } from "@core/dto/NodeNumToNodeInfoDTO.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"; export const subscribeAll = ( device: Device, connection: MeshDevice, messageStore: MessageStore, + nodeDB: NodeDB, ) => { let myNodeNum = 0; @@ -52,20 +58,21 @@ export const subscribeAll = ( connection.events.onMyNodeInfo.subscribe((nodeInfo) => { device.setHardware(nodeInfo); messageStore.setNodeNum(nodeInfo.myNodeNum); + nodeDB.setNodeNum(nodeInfo.myNodeNum); myNodeNum = nodeInfo.myNodeNum; }); connection.events.onUserPacket.subscribe((user) => { - device.addUser(user); + nodeDB.addUser(user); }); connection.events.onPositionPacket.subscribe((position) => { - device.addPosition(position); + nodeDB.addPosition(position); }); connection.events.onNodeInfoPacket.subscribe((nodeInfo) => { const nodeWithUser = ensureDefaultUser(nodeInfo); - device.addNodeInfo(nodeWithUser); + nodeDB.addNode(nodeWithUser); }); connection.events.onChannelPacket.subscribe((channel) => { @@ -106,7 +113,7 @@ export const subscribeAll = ( }); connection.events.onMeshPacket.subscribe((meshPacket) => { - device.processPacket({ + nodeDB.processPacket({ from: meshPacket.from, snr: meshPacket.rxSnr, time: meshPacket.rxTime, @@ -128,7 +135,7 @@ export const subscribeAll = ( break; case Protobuf.Mesh.Routing_Error.NO_CHANNEL: console.error(`Routing Error: ${routingPacket.data.variant.value}`); - device.setNodeError( + nodeDB.setNodeError( routingPacket.from, Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value], ); @@ -136,7 +143,7 @@ export const subscribeAll = ( break; case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY: console.error(`Routing Error: ${routingPacket.data.variant.value}`); - device.setNodeError( + nodeDB.setNodeError( routingPacket.from, Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value], ); diff --git a/packages/web/src/pages/Dashboard/index.tsx b/packages/web/src/pages/Dashboard/index.tsx index d7903efb..783b8c39 100644 --- a/packages/web/src/pages/Dashboard/index.tsx +++ b/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 { Heading } from "@components/UI/Typography/Heading.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 { useMemo } from "react"; import { useTranslation } from "react-i18next"; @@ -12,6 +12,7 @@ export const Dashboard = () => { const { t } = useTranslation("dashboard"); const { setConnectDialogOpen, setSelectedDevice } = useAppStore(); const { getDevices } = useDeviceStore(); + const { getNodeDB } = useNodeDBStore(); const devices = useMemo(() => getDevices(), [getDevices]); @@ -31,6 +32,11 @@ export const Dashboard = () => { {devices.length ? (
    {devices.map((device) => { + const nodeDB = getNodeDB(device.id); + if (!nodeDB) { + return; + } + return (
diff --git a/packages/web/src/pages/Map/index.tsx b/packages/web/src/pages/Map/index.tsx index 39d23dc2..b7b2217a 100644 --- a/packages/web/src/pages/Map/index.tsx +++ b/packages/web/src/pages/Map/index.tsx @@ -6,7 +6,7 @@ import { import { BaseMap } from "@components/Map.tsx"; import { PageLayout } from "@components/PageLayout.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 type { Protobuf } from "@meshtastic/core"; import { bbox, lineString } from "@turf/turf"; @@ -30,7 +30,8 @@ const convertToLatLng = (position?: { }); const MapPage = () => { - const { getNodes, waypoints, hasNodeError } = useDevice(); + const { waypoints } = useDevice(); + const { getNodes, hasNodeError } = useNodeDB(); const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode(); const { default: map } = useMap(); @@ -79,7 +80,7 @@ const MapPage = () => { return; } - if (validNodes.length === 1) { + if (validNodes.length === 1 && validNodes[0]) { map.easeTo({ zoom: map.getZoom(), center: [ diff --git a/packages/web/src/pages/Messages.tsx b/packages/web/src/pages/Messages.tsx index af0c0a37..288f28e4 100644 --- a/packages/web/src/pages/Messages.tsx +++ b/packages/web/src/pages/Messages.tsx @@ -13,6 +13,7 @@ import { MessageType, useDevice, useMessageStore, + useNodeDB, useSidebar, } from "@core/stores"; import { cn } from "@core/utils/cn.ts"; @@ -42,15 +43,9 @@ function SelectMessageChat() { } export const MessagesPage = () => { - const { - channels, - getNodes, - getNode, - hasNodeError, - getUnreadCount, - resetUnread, - connection, - } = useDevice(); + const { channels, getUnreadCount, resetUnread, connection } = useDevice(); + const { getNodes, getNode, hasNodeError } = useNodeDB(); + const { getMyNodeNum, getMessages, setMessageState } = useMessageStore(); const { type, chatId } = useParams({ from: messagesWithParamsRoute.id }); diff --git a/packages/web/src/pages/Nodes/index.tsx b/packages/web/src/pages/Nodes/index.tsx index 6e06b024..7f3448ad 100644 --- a/packages/web/src/pages/Nodes/index.tsx +++ b/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 { Input } from "@components/UI/Input.tsx"; 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 { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { LockIcon, LockOpenIcon } from "lucide-react"; @@ -40,8 +40,8 @@ export interface DeleteNoteDialogProps { const NodesPage = (): JSX.Element => { const { t } = useTranslation("nodes"); const { currentLanguage } = useLang(); - const { getNodes, hardware, connection, hasNodeError, setDialogOpen } = - useDevice(); + const { hardware, connection, setDialogOpen } = useDevice(); + const { getNodes, hasNodeError } = useNodeDB(); const { setNodeNumDetails } = useAppStore(); const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode();