mirror of
https://github.com/meshtastic/web.git
synced 2026-07-31 06:56:28 -04:00
feat(sdk,web): SDK NodesClient owns user/position/lastHeard/snr +
favourite-flag flips; legacy mirror retired
The SDK NodesClient now subscribes to onUserPacket / onPositionPacket /
onMeshPacket so user-record updates, GPS updates, and lastHeard / snr
refreshes flow into the signal-backed store + repository directly. The
favourite / ignored toggles also flip the local flag once the admin
message resolves successfully, so the UI no longer needs to mirror the
state into a parallel Zustand store.
packages/sdk
- NodesClient: new private patch(num, partial) helper that shallow-
merges into the existing entry (or seeds a placeholder Node) and
upserts to the repository in one shot. Used by:
- onUserPacket → patch user
- onPositionPacket → patch position
- onMeshPacket → patch lastHeard / snr (replaces the legacy nodeDB
processPacket flow)
- favorite / unfavorite / ignore / unignore — flag flip on Result.ok
- NodesClient.reset({ keepMyNode? }) — preserves the local node entry
when requested. Mirrors the previous removeAllNodes(true) semantics
the ResetNodeDb dialog relied on.
packages/web
- core/subscriptions.ts: drops the onUserPacket / onPositionPacket /
onNodeInfoPacket / onMeshPacket → legacy nodeDB write paths. The
unused nodeDB parameter is renamed `_nodeDB` for callsite stability;
it will disappear when the store itself is deleted in a follow-up.
- core/hooks/useFavoriteNode + useIgnoreNode: drop the legacy
updateFavorite / updateIgnore mirror — the SDK flips the flag on
success.
- components/Dialog/RemoveNodeDialog: now calls meshClient.nodes.remove
exclusively; no legacy fall-through.
- components/Dialog/ResetNodeDbDialog: calls
meshClient.nodes.reset({ keepMyNode: true }) and meshClient.chat.clearAll().
No legacy nodeDB calls.
- Test mocks for useFavoriteNode / useIgnoreNode / ResetNodeDbDialog
pruned to match.
Test counts: sdk 44 (unchanged), sdk-react 8, sdk-storage-sqlocal 24,
web 290. Production Vite build clean.
Remaining surface on the legacy nodeDB: addNode / addUser / addPosition /
processPacket / setNodeError-equivalent are now unused; the store retains
only updateFavorite / updateIgnore (no callers) and the per-device
plumbing required by the deviceContext hooks. Full deletion of
nodeDBStore queued in the plan-file follow-up.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { create } from "@bufbuild/protobuf";
|
||||||
import * as Protobuf from "@meshtastic/protobufs";
|
import * as Protobuf from "@meshtastic/protobufs";
|
||||||
import type { ResultType } from "better-result";
|
import type { ResultType } from "better-result";
|
||||||
import type { MeshClient } from "../../core/client/MeshClient.ts";
|
import type { MeshClient } from "../../core/client/MeshClient.ts";
|
||||||
@@ -41,16 +42,31 @@ export class NodesClient {
|
|||||||
this.list = this.store.read;
|
this.list = this.store.read;
|
||||||
this.errors = this.errorsStore.read;
|
this.errors = this.errorsStore.read;
|
||||||
|
|
||||||
client.events.onNodeInfoPacket.subscribe((info) => {
|
client.events.onNodeInfoPacket.subscribe((info) => this.handleIncoming(info));
|
||||||
this.handleIncoming(info);
|
|
||||||
|
client.events.onUserPacket.subscribe((packet) => {
|
||||||
|
this.patch(packet.from, { user: packet.data });
|
||||||
|
});
|
||||||
|
|
||||||
|
client.events.onPositionPacket.subscribe((packet) => {
|
||||||
|
this.patch(packet.from, { position: packet.data });
|
||||||
|
});
|
||||||
|
|
||||||
|
client.events.onMeshPacket.subscribe((packet) => {
|
||||||
|
// Every inbound mesh packet refreshes lastHeard / snr for the
|
||||||
|
// originating node — same semantics as the legacy nodeDB
|
||||||
|
// processPacket but routed through the SDK signal layer.
|
||||||
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
this.patch(packet.from, {
|
||||||
|
lastHeard: packet.rxTime > 0 ? packet.rxTime : nowSec,
|
||||||
|
snr: packet.rxSnr,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
client.events.onRoutingPacket.subscribe((packet) => {
|
client.events.onRoutingPacket.subscribe((packet) => {
|
||||||
if (packet.data.variant.case !== "errorReason") return;
|
if (packet.data.variant.case !== "errorReason") return;
|
||||||
const reason = packet.data.variant.value;
|
const reason = packet.data.variant.value;
|
||||||
if (reason === Protobuf.Mesh.Routing_Error.NONE) return;
|
if (reason === Protobuf.Mesh.Routing_Error.NONE) return;
|
||||||
// Reasons that indicate a node-level fault. Anything else is just a
|
|
||||||
// transient packet failure and not worth surfacing per-node.
|
|
||||||
if (
|
if (
|
||||||
reason === Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY ||
|
reason === Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY ||
|
||||||
reason === Protobuf.Mesh.Routing_Error.PKI_FAILED ||
|
reason === Protobuf.Mesh.Routing_Error.PKI_FAILED ||
|
||||||
@@ -76,11 +92,31 @@ export class NodesClient {
|
|||||||
|
|
||||||
const node = NodeMapper.fromProto(verdict.accepted);
|
const node = NodeMapper.fromProto(verdict.accepted);
|
||||||
this.store.set(node.num, node);
|
this.store.set(node.num, node);
|
||||||
// A successful update implicitly clears any prior PKI/no-channel error.
|
|
||||||
if (this.errorsStore.has(node.num)) this.errorsStore.delete(node.num);
|
if (this.errorsStore.has(node.num)) this.errorsStore.delete(node.num);
|
||||||
void this.repository.upsert(node).catch(() => {});
|
void this.repository.upsert(node).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shallow-merges a partial Node update over the existing entry. Creates a
|
||||||
|
* stub Node if none exists yet — matches the legacy nodeDB behaviour
|
||||||
|
* where a stray UserPacket / PositionPacket / mesh packet would seed a
|
||||||
|
* placeholder NodeInfo.
|
||||||
|
*/
|
||||||
|
private patch(nodeNum: number, partial: Partial<Node>): void {
|
||||||
|
if (nodeNum === 0) return;
|
||||||
|
const existing = this.store.get(nodeNum);
|
||||||
|
const next: Node = existing
|
||||||
|
? { ...existing, ...partial }
|
||||||
|
: {
|
||||||
|
num: nodeNum,
|
||||||
|
isFavorite: false,
|
||||||
|
isIgnored: false,
|
||||||
|
...partial,
|
||||||
|
};
|
||||||
|
this.store.set(nodeNum, next);
|
||||||
|
void this.repository.upsert(next).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
private async hydrate(): Promise<void> {
|
private async hydrate(): Promise<void> {
|
||||||
if (this.hydrated) return;
|
if (this.hydrated) return;
|
||||||
this.hydrated = true;
|
this.hydrated = true;
|
||||||
@@ -118,20 +154,28 @@ export class NodesClient {
|
|||||||
this.errorsStore.clear();
|
this.errorsStore.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public favorite(nodeNum: number): Promise<ResultType<number, Error>> {
|
public async favorite(nodeNum: number): Promise<ResultType<number, Error>> {
|
||||||
return favoriteNode(this.client, nodeNum);
|
const result = await favoriteNode(this.client, nodeNum);
|
||||||
|
if (result.status === "ok") this.patch(nodeNum, { isFavorite: true });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unfavorite(nodeNum: number): Promise<ResultType<number, Error>> {
|
public async unfavorite(nodeNum: number): Promise<ResultType<number, Error>> {
|
||||||
return removeFavoriteNode(this.client, nodeNum);
|
const result = await removeFavoriteNode(this.client, nodeNum);
|
||||||
|
if (result.status === "ok") this.patch(nodeNum, { isFavorite: false });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ignore(nodeNum: number): Promise<ResultType<number, Error>> {
|
public async ignore(nodeNum: number): Promise<ResultType<number, Error>> {
|
||||||
return ignoreNode(this.client, nodeNum);
|
const result = await ignoreNode(this.client, nodeNum);
|
||||||
|
if (result.status === "ok") this.patch(nodeNum, { isIgnored: true });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unignore(nodeNum: number): Promise<ResultType<number, Error>> {
|
public async unignore(nodeNum: number): Promise<ResultType<number, Error>> {
|
||||||
return removeIgnoredNode(this.client, nodeNum);
|
const result = await removeIgnoredNode(this.client, nodeNum);
|
||||||
|
if (result.status === "ok") this.patch(nodeNum, { isIgnored: false });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove(nodeNum: number): Promise<ResultType<number, Error>> {
|
public remove(nodeNum: number): Promise<ResultType<number, Error>> {
|
||||||
@@ -141,10 +185,40 @@ export class NodesClient {
|
|||||||
return removeNodeByNum(this.client, nodeNum);
|
return removeNodeByNum(this.client, nodeNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset(): Promise<ResultType<number, Error>> {
|
/**
|
||||||
void this.repository.clear().catch(() => {});
|
* Wipes every node except (optionally) the local "my node" entry, then
|
||||||
this.store.clear();
|
* sends the device-side reset admin message. Mirrors the legacy
|
||||||
this.errorsStore.clear();
|
* removeAllNodes(true) + resetNodes flow that the ResetNodeDb dialog
|
||||||
|
* relied on.
|
||||||
|
*/
|
||||||
|
public async reset(options: { keepMyNode?: boolean } = {}): Promise<ResultType<number, Error>> {
|
||||||
|
const myNodeNum = this.client.device.myNodeNum.value;
|
||||||
|
if (options.keepMyNode && myNodeNum !== undefined) {
|
||||||
|
const me = this.store.get(myNodeNum);
|
||||||
|
this.store.clear();
|
||||||
|
this.errorsStore.clear();
|
||||||
|
if (me) this.store.set(myNodeNum, me);
|
||||||
|
try {
|
||||||
|
await this.repository.clear();
|
||||||
|
if (me) await this.repository.upsert(me);
|
||||||
|
} catch {
|
||||||
|
// ok
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.store.clear();
|
||||||
|
this.errorsStore.clear();
|
||||||
|
try {
|
||||||
|
await this.repository.clear();
|
||||||
|
} catch {
|
||||||
|
// ok
|
||||||
|
}
|
||||||
|
}
|
||||||
return resetNodes(this.client);
|
return resetNodes(this.client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Drives the legacy `Protobuf.Mesh.NodeInfoSchema` create flow if a
|
||||||
|
* consumer needs a placeholder for a not-yet-known node. */
|
||||||
|
public createPlaceholder(nodeNum: number): Protobuf.Mesh.NodeInfo {
|
||||||
|
return create(Protobuf.Mesh.NodeInfoSchema, { num: nodeNum });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Label } from "@components/UI/Label.tsx";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
|
||||||
import { useAppStore, useDevice, useNodeDB } from "@core/stores";
|
import { useAppStore } from "@core/stores";
|
||||||
|
import { useActiveClient } from "@meshtastic/sdk-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DialogWrapper } from "./DialogWrapper.tsx";
|
import { DialogWrapper } from "./DialogWrapper.tsx";
|
||||||
|
|
||||||
@@ -11,14 +12,12 @@ export interface RemoveNodeDialogProps {
|
|||||||
|
|
||||||
export const RemoveNodeDialog = ({ open, onOpenChange }: RemoveNodeDialogProps) => {
|
export const RemoveNodeDialog = ({ open, onOpenChange }: RemoveNodeDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { connection } = useDevice();
|
const meshClient = useActiveClient();
|
||||||
const { removeNode } = useNodeDB();
|
|
||||||
const { nodeNumToBeRemoved } = useAppStore();
|
const { nodeNumToBeRemoved } = useAppStore();
|
||||||
const node = useNodeAsProto(nodeNumToBeRemoved);
|
const node = useNodeAsProto(nodeNumToBeRemoved);
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
connection?.removeNodeByNum(nodeNumToBeRemoved);
|
void meshClient?.nodes.remove(nodeNumToBeRemoved);
|
||||||
removeNode(nodeNumToBeRemoved);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { ResetNodeDbDialog } from "./ResetNodeDbDialog.tsx";
|
|||||||
|
|
||||||
const mockResetNodes = vi.fn();
|
const mockResetNodes = vi.fn();
|
||||||
const mockClearAll = vi.fn();
|
const mockClearAll = vi.fn();
|
||||||
const mockClearAllErrors = vi.fn();
|
|
||||||
const mockRemoveAllNodes = vi.fn();
|
|
||||||
|
|
||||||
const { mockUseActiveClient } = vi.hoisted(() => ({
|
const { mockUseActiveClient } = vi.hoisted(() => ({
|
||||||
mockUseActiveClient: vi.fn(),
|
mockUseActiveClient: vi.fn(),
|
||||||
@@ -15,27 +13,18 @@ vi.mock("@meshtastic/sdk-react", () => ({
|
|||||||
useActiveClient: mockUseActiveClient,
|
useActiveClient: mockUseActiveClient,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@core/stores", () => ({
|
|
||||||
CurrentDeviceContext: {
|
|
||||||
_currentValue: { deviceId: 1234 },
|
|
||||||
},
|
|
||||||
useNodeDB: () => ({
|
|
||||||
removeAllNodes: mockRemoveAllNodes,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("ResetNodeDbDialog", () => {
|
describe("ResetNodeDbDialog", () => {
|
||||||
const mockOnOpenChange = vi.fn();
|
const mockOnOpenChange = vi.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockUseActiveClient.mockReturnValue({
|
mockUseActiveClient.mockReturnValue({
|
||||||
nodes: { reset: mockResetNodes, clearAllErrors: mockClearAllErrors },
|
nodes: { reset: mockResetNodes },
|
||||||
chat: { clearAll: mockClearAll },
|
chat: { clearAll: mockClearAll },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls reset(), clears SDK errors + chat + legacy nodes after resolve", async () => {
|
it("calls reset({ keepMyNode: true }) then clears chat after resolve", async () => {
|
||||||
let resolveReset: ((value: { status: "ok"; value: number }) => void) | undefined;
|
let resolveReset: ((value: { status: "ok"; value: number }) => void) | undefined;
|
||||||
mockResetNodes.mockImplementation(
|
mockResetNodes.mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
@@ -48,22 +37,18 @@ describe("ResetNodeDbDialog", () => {
|
|||||||
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
|
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Reset Node Database" }));
|
fireEvent.click(screen.getByRole("button", { name: "Reset Node Database" }));
|
||||||
|
|
||||||
expect(mockResetNodes).toHaveBeenCalledTimes(1);
|
expect(mockResetNodes).toHaveBeenCalledWith({ keepMyNode: true });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockClearAll).not.toHaveBeenCalled();
|
expect(mockClearAll).not.toHaveBeenCalled();
|
||||||
expect(mockClearAllErrors).not.toHaveBeenCalled();
|
|
||||||
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
resolveReset?.({ status: "ok", value: 1 });
|
resolveReset?.({ status: "ok", value: 1 });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockClearAllErrors).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockClearAll).toHaveBeenCalledTimes(1);
|
expect(mockClearAll).toHaveBeenCalledTimes(1);
|
||||||
expect(mockRemoveAllNodes).toHaveBeenCalledWith(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,7 +62,5 @@ describe("ResetNodeDbDialog", () => {
|
|||||||
|
|
||||||
expect(mockResetNodes).not.toHaveBeenCalled();
|
expect(mockResetNodes).not.toHaveBeenCalled();
|
||||||
expect(mockClearAll).not.toHaveBeenCalled();
|
expect(mockClearAll).not.toHaveBeenCalled();
|
||||||
expect(mockClearAllErrors).not.toHaveBeenCalled();
|
|
||||||
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { toast } from "@core/hooks/useToast.ts";
|
import { toast } from "@core/hooks/useToast.ts";
|
||||||
import { useNodeDB } from "@core/stores";
|
|
||||||
import { useActiveClient } from "@meshtastic/sdk-react";
|
import { useActiveClient } from "@meshtastic/sdk-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DialogWrapper } from "../DialogWrapper.tsx";
|
import { DialogWrapper } from "../DialogWrapper.tsx";
|
||||||
@@ -12,22 +11,15 @@ export interface ResetNodeDbDialogProps {
|
|||||||
export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps) => {
|
export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const meshClient = useActiveClient();
|
const meshClient = useActiveClient();
|
||||||
// The legacy nodeDB still holds an in-memory snapshot for unmigrated
|
|
||||||
// consumers; sweep it alongside the SDK clear.
|
|
||||||
const { removeAllNodes } = useNodeDB();
|
|
||||||
|
|
||||||
const handleResetNodeDb = () => {
|
const handleResetNodeDb = () => {
|
||||||
if (!meshClient) return;
|
if (!meshClient) return;
|
||||||
meshClient.nodes
|
meshClient.nodes
|
||||||
.reset()
|
.reset({ keepMyNode: true })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.status === "error") throw result.error;
|
if (result.status === "error") throw result.error;
|
||||||
meshClient.nodes.clearAllErrors();
|
|
||||||
return meshClient.chat.clearAll();
|
return meshClient.chat.clearAll();
|
||||||
})
|
})
|
||||||
.then(() => {
|
|
||||||
removeAllNodes(true);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast({ title: t("resetNodeDb.failedTitle") });
|
toast({ title: t("resetNodeDb.failedTitle") });
|
||||||
console.error("Failed to reset Node DB:", error);
|
console.error("Failed to reset Node DB:", error);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { act, renderHook } from "@testing-library/react";
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { useFavoriteNode } from "./useFavoriteNode.ts";
|
import { useFavoriteNode } from "./useFavoriteNode.ts";
|
||||||
|
|
||||||
const mockUpdateFavorite = vi.fn();
|
|
||||||
const mockToast = vi.fn();
|
const mockToast = vi.fn();
|
||||||
const mockSdkFavorite = vi.fn();
|
const mockSdkFavorite = vi.fn();
|
||||||
const mockSdkUnfavorite = vi.fn();
|
const mockSdkUnfavorite = vi.fn();
|
||||||
@@ -15,15 +14,6 @@ vi.mock("@meshtastic/sdk-react", () => ({
|
|||||||
useActiveClient: mockUseActiveClient,
|
useActiveClient: mockUseActiveClient,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@core/stores", () => ({
|
|
||||||
CurrentDeviceContext: {
|
|
||||||
_currentValue: { deviceId: 1234 },
|
|
||||||
},
|
|
||||||
useNodeDB: () => ({
|
|
||||||
updateFavorite: mockUpdateFavorite,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@core/hooks/useToast.ts", () => ({
|
vi.mock("@core/hooks/useToast.ts", () => ({
|
||||||
useToast: () => ({
|
useToast: () => ({
|
||||||
toast: mockToast,
|
toast: mockToast,
|
||||||
@@ -47,7 +37,7 @@ describe("useFavoriteNode hook", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls updateFavorite and shows correct toast", () => {
|
it("calls SDK favorite and shows correct toast", () => {
|
||||||
const { result } = renderHook(() => useFavoriteNode());
|
const { result } = renderHook(() => useFavoriteNode());
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -55,7 +45,6 @@ describe("useFavoriteNode hook", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSdkFavorite).toHaveBeenCalledWith(1234);
|
expect(mockSdkFavorite).toHaveBeenCalledWith(1234);
|
||||||
expect(mockUpdateFavorite).toHaveBeenCalledWith(1234, true);
|
|
||||||
expect(mockToast).toHaveBeenCalledWith({
|
expect(mockToast).toHaveBeenCalledWith({
|
||||||
title: "Added Test Node to favorites.",
|
title: "Added Test Node to favorites.",
|
||||||
});
|
});
|
||||||
@@ -69,7 +58,6 @@ describe("useFavoriteNode hook", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSdkUnfavorite).toHaveBeenCalledWith(1234);
|
expect(mockSdkUnfavorite).toHaveBeenCalledWith(1234);
|
||||||
expect(mockUpdateFavorite).toHaveBeenCalledWith(1234, false);
|
|
||||||
expect(mockToast).toHaveBeenCalledWith({
|
expect(mockToast).toHaveBeenCalledWith({
|
||||||
title: "Removed Test Node from favorites.",
|
title: "Removed Test Node from favorites.",
|
||||||
});
|
});
|
||||||
@@ -98,7 +86,8 @@ describe("useFavoriteNode hook", () => {
|
|||||||
result.current.updateFavorite({ nodeNum: 9999, isFavorite: false });
|
result.current.updateFavorite({ nodeNum: 9999, isFavorite: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpdateFavorite).not.toHaveBeenCalled();
|
expect(mockSdkFavorite).not.toHaveBeenCalled();
|
||||||
|
expect(mockSdkUnfavorite).not.toHaveBeenCalled();
|
||||||
expect(mockToast).not.toHaveBeenCalled();
|
expect(mockToast).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useToast } from "@core/hooks/useToast.ts";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { useNodeDB } from "@core/stores";
|
|
||||||
import { useActiveClient } from "@meshtastic/sdk-react";
|
import { useActiveClient } from "@meshtastic/sdk-react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -9,11 +8,12 @@ interface FavoriteNodeOptions {
|
|||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the favorite flag on a node. Drives the SDK NodesClient which
|
||||||
|
* sends the matching admin message and flips the local flag on success.
|
||||||
|
*/
|
||||||
export function useFavoriteNode() {
|
export function useFavoriteNode() {
|
||||||
const meshClient = useActiveClient();
|
const meshClient = useActiveClient();
|
||||||
// Mirror to the legacy nodeDB until the favorite/ignore-flag projection on
|
|
||||||
// the SDK Node entity drives the UI directly.
|
|
||||||
const { updateFavorite } = useNodeDB();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -25,9 +25,6 @@ export function useFavoriteNode() {
|
|||||||
|
|
||||||
void (isFavorite ? meshClient.nodes.favorite(nodeNum) : meshClient.nodes.unfavorite(nodeNum));
|
void (isFavorite ? meshClient.nodes.favorite(nodeNum) : meshClient.nodes.unfavorite(nodeNum));
|
||||||
|
|
||||||
// TODO: drive store mutation off the admin-message ack instead of optimistic.
|
|
||||||
updateFavorite(nodeNum, isFavorite);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("toast.favoriteNode.title", {
|
title: t("toast.favoriteNode.title", {
|
||||||
action: isFavorite
|
action: isFavorite
|
||||||
@@ -40,7 +37,7 @@ export function useFavoriteNode() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[meshClient, updateFavorite, t, toast],
|
[meshClient, t, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { updateFavorite: updateFavoriteCB };
|
return { updateFavorite: updateFavoriteCB };
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { act, renderHook } from "@testing-library/react";
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { useIgnoreNode } from "./useIgnoreNode.ts";
|
import { useIgnoreNode } from "./useIgnoreNode.ts";
|
||||||
|
|
||||||
const mockUpdateIgnore = vi.fn();
|
|
||||||
const mockToast = vi.fn();
|
const mockToast = vi.fn();
|
||||||
const mockSdkIgnore = vi.fn();
|
const mockSdkIgnore = vi.fn();
|
||||||
const mockSdkUnignore = vi.fn();
|
const mockSdkUnignore = vi.fn();
|
||||||
@@ -15,15 +14,6 @@ vi.mock("@meshtastic/sdk-react", () => ({
|
|||||||
useActiveClient: mockUseActiveClient,
|
useActiveClient: mockUseActiveClient,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@core/stores", () => ({
|
|
||||||
CurrentDeviceContext: {
|
|
||||||
_currentValue: { deviceId: 1234 },
|
|
||||||
},
|
|
||||||
useNodeDB: () => ({
|
|
||||||
updateIgnore: mockUpdateIgnore,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@core/hooks/useToast.ts", () => ({
|
vi.mock("@core/hooks/useToast.ts", () => ({
|
||||||
useToast: () => ({
|
useToast: () => ({
|
||||||
toast: mockToast,
|
toast: mockToast,
|
||||||
@@ -47,7 +37,7 @@ describe("useIgnoreNode hook", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls updateIgnored and shows correct toast", () => {
|
it("calls SDK ignore and shows correct toast", () => {
|
||||||
const { result } = renderHook(() => useIgnoreNode());
|
const { result } = renderHook(() => useIgnoreNode());
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -55,7 +45,6 @@ describe("useIgnoreNode hook", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSdkIgnore).toHaveBeenCalledWith(1234);
|
expect(mockSdkIgnore).toHaveBeenCalledWith(1234);
|
||||||
expect(mockUpdateIgnore).toHaveBeenCalledWith(1234, true);
|
|
||||||
expect(mockToast).toHaveBeenCalledWith({
|
expect(mockToast).toHaveBeenCalledWith({
|
||||||
title: "Added Test Node to ignore list",
|
title: "Added Test Node to ignore list",
|
||||||
});
|
});
|
||||||
@@ -69,7 +58,6 @@ describe("useIgnoreNode hook", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSdkUnignore).toHaveBeenCalledWith(1234);
|
expect(mockSdkUnignore).toHaveBeenCalledWith(1234);
|
||||||
expect(mockUpdateIgnore).toHaveBeenCalledWith(1234, false);
|
|
||||||
expect(mockToast).toHaveBeenCalledWith({
|
expect(mockToast).toHaveBeenCalledWith({
|
||||||
title: "Removed Test Node from ignore list",
|
title: "Removed Test Node from ignore list",
|
||||||
});
|
});
|
||||||
@@ -98,7 +86,8 @@ describe("useIgnoreNode hook", () => {
|
|||||||
result.current.updateIgnored({ nodeNum: 9999, isIgnored: false });
|
result.current.updateIgnored({ nodeNum: 9999, isIgnored: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpdateIgnore).not.toHaveBeenCalled();
|
expect(mockSdkIgnore).not.toHaveBeenCalled();
|
||||||
|
expect(mockSdkUnignore).not.toHaveBeenCalled();
|
||||||
expect(mockToast).not.toHaveBeenCalled();
|
expect(mockToast).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useToast } from "@core/hooks/useToast.ts";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { useNodeDB } from "@core/stores";
|
|
||||||
import { useActiveClient } from "@meshtastic/sdk-react";
|
import { useActiveClient } from "@meshtastic/sdk-react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -9,9 +8,12 @@ interface IgnoreNodeOptions {
|
|||||||
isIgnored: boolean;
|
isIgnored: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the ignored flag on a node. Drives the SDK NodesClient which
|
||||||
|
* sends the matching admin message and flips the local flag on success.
|
||||||
|
*/
|
||||||
export function useIgnoreNode() {
|
export function useIgnoreNode() {
|
||||||
const meshClient = useActiveClient();
|
const meshClient = useActiveClient();
|
||||||
const { updateIgnore } = useNodeDB();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -23,8 +25,6 @@ export function useIgnoreNode() {
|
|||||||
|
|
||||||
void (isIgnored ? meshClient.nodes.ignore(nodeNum) : meshClient.nodes.unignore(nodeNum));
|
void (isIgnored ? meshClient.nodes.ignore(nodeNum) : meshClient.nodes.unignore(nodeNum));
|
||||||
|
|
||||||
updateIgnore(nodeNum, isIgnored);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("toast.ignoreNode.title", {
|
title: t("toast.ignoreNode.title", {
|
||||||
nodeName: node.user?.longName ?? "node",
|
nodeName: node.user?.longName ?? "node",
|
||||||
@@ -37,7 +37,7 @@ export function useIgnoreNode() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[meshClient, updateIgnore, t, toast],
|
[meshClient, t, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { updateIgnored: updateIgnoredCB };
|
return { updateIgnored: updateIgnoredCB };
|
||||||
|
|||||||
@@ -5,18 +5,20 @@ import { type MeshDevice, Protobuf } from "@meshtastic/sdk";
|
|||||||
/**
|
/**
|
||||||
* Wires up the legacy MeshDevice event stream into the web's Zustand stores.
|
* Wires up the legacy MeshDevice event stream into the web's Zustand stores.
|
||||||
*
|
*
|
||||||
* Note: the SDK chat slice already persists messages via the configured
|
* Note: the SDK now owns chat persistence (via SqlocalMessageRepository) and
|
||||||
* SqlocalMessageRepository, so this function no longer copies messages into
|
* the entire NodesClient surface — node info, user, position, lastHeard /
|
||||||
* the legacy messageStore. Unread-count increments stay here because that
|
* snr, favourite / ignored flags, and PKI-error tracking. This handler no
|
||||||
* logic still lives on the device store; it migrates to the SDK in a
|
* longer mirrors any of that into the legacy stores; what remains is
|
||||||
* follow-up "unread" cross-cutting commit.
|
* device-store-only state (waypoints, traceroutes, neighbour info, dialog
|
||||||
|
* open triggers, unread counts).
|
||||||
*/
|
*/
|
||||||
export const subscribeAll = (
|
export const subscribeAll = (
|
||||||
device: Device,
|
device: Device,
|
||||||
connection: MeshDevice,
|
connection: MeshDevice,
|
||||||
// biome-ignore lint/correctness/noUnusedFunctionParameters: kept for callsite stability while messageStore is being retired
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: kept for callsite stability while messageStore is being retired
|
||||||
_messageStore: MessageStore,
|
_messageStore: MessageStore,
|
||||||
nodeDB: NodeDB,
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: kept for callsite stability while nodeDB is being retired
|
||||||
|
_nodeDB: NodeDB,
|
||||||
) => {
|
) => {
|
||||||
let myNodeNum = 0;
|
let myNodeNum = 0;
|
||||||
|
|
||||||
@@ -62,20 +64,8 @@ export const subscribeAll = (
|
|||||||
myNodeNum = nodeInfo.myNodeNum;
|
myNodeNum = nodeInfo.myNodeNum;
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.events.onUserPacket.subscribe((user) => {
|
// onUserPacket / onPositionPacket / onNodeInfoPacket are handled by the
|
||||||
nodeDB.addUser(user);
|
// SDK NodesClient (see packages/sdk/src/features/nodes/NodesClient.ts).
|
||||||
});
|
|
||||||
|
|
||||||
connection.events.onPositionPacket.subscribe((position) => {
|
|
||||||
nodeDB.addPosition(position);
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: Node handling is managed by the nodeDB
|
|
||||||
// Nodes are added via subscriptions.ts and stored in nodeDB
|
|
||||||
// Configuration is handled directly by meshDevice.configure() in useConnections
|
|
||||||
connection.events.onNodeInfoPacket.subscribe((nodeInfo) => {
|
|
||||||
nodeDB.addNode(nodeInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.events.onChannelPacket.subscribe((channel) => {
|
connection.events.onChannelPacket.subscribe((channel) => {
|
||||||
device.addChannel(channel);
|
device.addChannel(channel);
|
||||||
@@ -111,13 +101,8 @@ export const subscribeAll = (
|
|||||||
device.setPendingSettingsChanges(state);
|
device.setPendingSettingsChanges(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.events.onMeshPacket.subscribe((meshPacket) => {
|
// onMeshPacket → lastHeard / snr per-node updates are handled by the SDK
|
||||||
nodeDB.processPacket({
|
// NodesClient.
|
||||||
from: meshPacket.from,
|
|
||||||
snr: meshPacket.rxSnr,
|
|
||||||
time: meshPacket.rxTime,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.events.onClientNotificationPacket.subscribe((clientNotificationPacket) => {
|
connection.events.onClientNotificationPacket.subscribe((clientNotificationPacket) => {
|
||||||
device.addClientNotification(clientNotificationPacket);
|
device.addClientNotification(clientNotificationPacket);
|
||||||
|
|||||||
Reference in New Issue
Block a user