↓ {snr?.[0] ?? t("unknown.num")}
{t("unit.dbm")}
@@ -49,7 +51,7 @@ const RoutePath = (
))}
-
{routeBack && routeBack.length > 0 && (
diff --git a/src/components/UI/Checkbox/Checkbox.test.tsx b/src/components/UI/Checkbox/Checkbox.test.tsx
index 7e5ba73d..4e6bcb20 100644
--- a/src/components/UI/Checkbox/Checkbox.test.tsx
+++ b/src/components/UI/Checkbox/Checkbox.test.tsx
@@ -24,7 +24,7 @@ vi.mock("@components/UI/Label.tsx", () => ({
}));
vi.mock("@core/utils/cn.ts", () => ({
- cn: (...args) => args.filter(Boolean).join(" "),
+ cn: (...args: any[]) => args.filter(Boolean).join(" "),
}));
vi.mock("react", async () => {
diff --git a/src/components/generic/Filter/useFilterNode.ts b/src/components/generic/Filter/useFilterNode.ts
index da3b7347..7aa962a5 100644
--- a/src/components/generic/Filter/useFilterNode.ts
+++ b/src/components/generic/Filter/useFilterNode.ts
@@ -1,153 +1,169 @@
import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
-import { useMemo } from "react";
+import { useCallback, useMemo } from "react";
export type FilterState = {
nodeName: string;
hopsAway: [number, number];
lastHeard: [number, number];
- isFavorite: boolean | undefined; // undefined -> don't filter
- viaMqtt: boolean | undefined; // undefined -> don't filter
+ isFavorite: boolean | undefined;
+ viaMqtt: boolean | undefined;
snr: [number, number];
channelUtilization: [number, number];
airUtilTx: [number, number];
batteryLevel: [number, number];
voltage: [number, number];
- role: (Protobuf.Config.Config_DeviceConfig_Role)[];
- hwModel: (Protobuf.Mesh.HardwareModel)[];
+ role: Protobuf.Config.Config_DeviceConfig_Role[];
+ hwModel: Protobuf.Mesh.HardwareModel[];
};
+const shallowEqualArray = (a: any[], b: any[]) =>
+ a.length === b.length && a.every((v, i) => v === b[i]);
+
export function useFilterNode() {
- const defaultFilterValues = useMemo
(() => ({
- nodeName: "",
- hopsAway: [0, 7],
- lastHeard: [0, 864000], // 0-10 days
- isFavorite: undefined,
- viaMqtt: undefined,
- snr: [-20, 10],
- channelUtilization: [0, 100],
- airUtilTx: [0, 100],
- batteryLevel: [0, 101],
- voltage: [0, 5],
- role: Object.values(Protobuf.Config.Config_DeviceConfig_Role).filter(
- (v): v is Protobuf.Config.Config_DeviceConfig_Role =>
- typeof v === "number",
- ),
- hwModel: Object.values(Protobuf.Mesh.HardwareModel).filter(
- (v): v is Protobuf.Mesh.HardwareModel => typeof v === "number",
- ),
- }), []);
-
- function nodeFilter(
- node: Protobuf.Mesh.NodeInfo,
- filterOverrides?: Partial,
- ): boolean {
- const filterState: FilterState = {
- ...defaultFilterValues,
- ...filterOverrides,
- };
-
- if (!node.user) return false;
-
- const nodeName = filterState.nodeName.toLowerCase();
- if (
- !(
- node.user?.shortName.toLowerCase().includes(nodeName) ||
- node.user?.longName.toLowerCase().includes(nodeName) ||
- node?.num.toString().includes(nodeName) ||
- numberToHexUnpadded(node?.num).includes(
- nodeName.replace(/!/g, ""),
+ const defaultFilterValues = useMemo(
+ () => ({
+ nodeName: "",
+ hopsAway: [0, 7],
+ lastHeard: [0, 864000], // 0-10 days
+ isFavorite: undefined,
+ viaMqtt: undefined,
+ snr: [-20, 10],
+ channelUtilization: [0, 100],
+ airUtilTx: [0, 100],
+ batteryLevel: [0, 101],
+ voltage: [0, 5],
+ role: Object.values(Protobuf.Config.Config_DeviceConfig_Role).filter(
+ (v): v is Protobuf.Config.Config_DeviceConfig_Role =>
+ typeof v === "number",
+ ),
+ hwModel: Object.values(Protobuf.Mesh.HardwareModel).filter(
+ (v): v is Protobuf.Mesh.HardwareModel => typeof v === "number",
+ ),
+ }),
+ [],
+ );
+
+ const nodeFilter = useCallback(
+ (
+ node: Protobuf.Mesh.NodeInfo,
+ filterOverrides?: Partial,
+ ): boolean => {
+ const filterState: FilterState = {
+ ...defaultFilterValues,
+ ...filterOverrides,
+ };
+
+ if (!node.user) return false;
+
+ const nodeName = filterState.nodeName.toLowerCase();
+ if (
+ nodeName &&
+ !(
+ node.user?.shortName.toLowerCase().includes(nodeName) ||
+ node.user?.longName.toLowerCase().includes(nodeName) ||
+ node.num.toString().includes(nodeName) ||
+ numberToHexUnpadded(node.num).includes(nodeName.replace(/!/g, ""))
)
- )
- ) return false;
-
- const hops = node?.hopsAway ?? 7;
- if (hops < filterState.hopsAway[0] || hops > filterState.hopsAway[1]) {
- return false;
- }
-
- const secondsAgo = Date.now() / 1000 - (node?.lastHeard ?? 0);
- if (
- secondsAgo < filterState.lastHeard[0] ||
- (
- secondsAgo > filterState.lastHeard[1] &&
- filterState.lastHeard[1] !== defaultFilterValues.lastHeard[1]
- )
- ) return false;
-
- if (
- typeof filterState.isFavorite !== "undefined" &&
- node.isFavorite !== filterState.isFavorite
- ) return false;
-
- if (
- typeof filterState.viaMqtt !== "undefined" &&
- node.viaMqtt !== filterState.viaMqtt
- ) return false;
-
- const snr = node?.snr ?? -20;
- if (
- snr < filterState.snr[0] ||
- snr > filterState.snr[1]
- ) return false;
-
- const channelUtilization = node?.deviceMetrics?.channelUtilization ?? 0;
- if (
- channelUtilization < filterState.channelUtilization[0] ||
- channelUtilization > filterState.channelUtilization[1]
- ) return false;
-
- const airUtilTx = node?.deviceMetrics?.airUtilTx ?? 0;
- if (
- airUtilTx < filterState.airUtilTx[0] ||
- airUtilTx > filterState.airUtilTx[1]
- ) return false;
-
- const batt = node?.deviceMetrics?.batteryLevel ?? 101;
- if (
- batt < filterState.batteryLevel[0] ||
- batt > filterState.batteryLevel[1]
- ) return false;
-
- const voltage = node?.deviceMetrics?.voltage ?? 0;
- if (
- voltage < filterState.voltage[0] ||
- voltage > filterState.voltage[1]
- ) return false;
-
- const role: Protobuf.Config.Config_DeviceConfig_Role = node.user?.role ??
- Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
- if (!filterState.role.includes(role)) return false;
-
- const hwModel: Protobuf.Mesh.HardwareModel = node.user?.hwModel ??
- Protobuf.Mesh.HardwareModel.UNSET;
- if (!filterState.hwModel.includes(hwModel)) return false;
-
- // All conditions are true
- return true;
- }
-
- // deno-lint-ignore no-explicit-any
- function shallowEqualArray(a: any[], b: any[]) {
- return a.length === b.length && a.every((v, i) => v === b[i]);
- }
-
- function isFilterDirty(
- current: FilterState,
- overrides?: Partial,
- ): boolean {
- const base: FilterState = overrides
- ? { ...defaultFilterValues, ...overrides }
- : defaultFilterValues;
-
- return (Object.keys(base) as (keyof FilterState)[]).some((key) => {
- const curr = current[key];
- const def = base[key];
- return Array.isArray(def) && Array.isArray(curr)
- ? !shallowEqualArray(curr, def)
- : curr !== def;
- });
- }
+ ) {
+ return false;
+ }
+
+ const hops = node.hopsAway ?? 7;
+ if (hops < filterState.hopsAway[0] || hops > filterState.hopsAway[1]) {
+ return false;
+ }
+
+ const secondsAgo = Date.now() / 1000 - (node.lastHeard ?? 0);
+ if (
+ secondsAgo < filterState.lastHeard[0] ||
+ (secondsAgo > filterState.lastHeard[1] &&
+ filterState.lastHeard[1] !== defaultFilterValues.lastHeard[1])
+ ) {
+ return false;
+ }
+
+ if (
+ typeof filterState.isFavorite !== "undefined" &&
+ node.isFavorite !== filterState.isFavorite
+ ) {
+ return false;
+ }
+
+ if (
+ typeof filterState.viaMqtt !== "undefined" &&
+ node.viaMqtt !== filterState.viaMqtt
+ ) {
+ return false;
+ }
+
+ const snr = node.snr ?? -20;
+ if (snr < filterState.snr[0] || snr > filterState.snr[1]) return false;
+
+ const channelUtilization = node.deviceMetrics?.channelUtilization ?? 0;
+ if (
+ channelUtilization < filterState.channelUtilization[0] ||
+ channelUtilization > filterState.channelUtilization[1]
+ ) {
+ return false;
+ }
+
+ const airUtilTx = node.deviceMetrics?.airUtilTx ?? 0;
+ if (
+ airUtilTx < filterState.airUtilTx[0] ||
+ airUtilTx > filterState.airUtilTx[1]
+ ) {
+ return false;
+ }
+
+ const batt = node.deviceMetrics?.batteryLevel ?? 101;
+ if (
+ batt < filterState.batteryLevel[0] ||
+ batt > filterState.batteryLevel[1]
+ ) {
+ return false;
+ }
+
+ const voltage = node.deviceMetrics?.voltage ?? 0;
+ if (
+ voltage < filterState.voltage[0] ||
+ voltage > filterState.voltage[1]
+ ) {
+ return false;
+ }
+
+ const role: Protobuf.Config.Config_DeviceConfig_Role = node.user.role ??
+ Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
+ if (!filterState.role.includes(role)) return false;
+
+ const hwModel: Protobuf.Mesh.HardwareModel = node.user.hwModel ??
+ Protobuf.Mesh.HardwareModel.UNSET;
+ if (!filterState.hwModel.includes(hwModel)) return false;
+
+ return true;
+ },
+ [defaultFilterValues],
+ );
+
+ const isFilterDirty = useCallback(
+ (
+ current: FilterState,
+ overrides?: Partial,
+ ): boolean => {
+ const base: FilterState = overrides
+ ? { ...defaultFilterValues, ...overrides }
+ : defaultFilterValues;
+
+ return (Object.keys(base) as (keyof FilterState)[]).some((key) => {
+ const curr = current[key];
+ const def = base[key];
+ return Array.isArray(def) && Array.isArray(curr)
+ ? !shallowEqualArray(curr, def)
+ : curr !== def;
+ });
+ },
+ [defaultFilterValues],
+ );
return { nodeFilter, defaultFilterValues, isFilterDirty };
}
diff --git a/src/components/generic/Table/index.tsx b/src/components/generic/Table/index.tsx
index ae66a730..1ead83db 100755
--- a/src/components/generic/Table/index.tsx
+++ b/src/components/generic/Table/index.tsx
@@ -51,7 +51,7 @@ export const Table = ({ headings, rows }: TableProps) => {
return null;
}
if (cell.type === React.Fragment) {
- const childrenArray = React.Children.toArray(cell.props.children);
+ const childrenArray = React.Children.toArray((cell.props as any).children);
const firstElement = childrenArray.find((child) =>
React.isValidElement(child)
);
@@ -71,23 +71,23 @@ export const Table = ({ headings, rows }: TableProps) => {
const elementB = getElement(b[columnIndex]);
// Avatar contains the prop showFavorite which indicates isFavorite
- const favA = a[0]?.props?.children?.props?.showFavorite ?? false;
- const favB = b[0]?.props?.children?.props?.showFavorite ?? false;
+ const favA = (a[0] as any)?.props?.children?.props?.showFavorite ?? false;
+ const favB = (b[0] as any)?.props?.children?.props?.showFavorite ?? false;
// Always put favorites at the top
if (favA !== favB) return favA ? -1 : 1;
if (sortColumn === "Last Heard") {
- const aTimestamp = elementA?.props?.children?.props?.timestamp ?? 0;
- const bTimestamp = elementB?.props?.children?.props?.timestamp ?? 0;
+ const aTimestamp = (elementA?.props as any)?.children?.props?.timestamp ?? 0;
+ const bTimestamp = (elementB?.props as any)?.children?.props?.timestamp ?? 0;
if (aTimestamp < bTimestamp) return sortOrder === "asc" ? -1 : 1;
if (aTimestamp > bTimestamp) return sortOrder === "asc" ? 1 : -1;
return 0;
}
if (sortColumn === "Connection") {
- const aHopsStr = elementA?.props?.children[0];
- const bHopsStr = elementB?.props?.children[0];
+ const aHopsStr = (elementA?.props as any)?.children?.[0];
+ const bHopsStr = (elementB?.props as any)?.children?.[0];
const aNumHops = numericHops(aHopsStr);
const bNumHops = numericHops(bHopsStr);
if (aNumHops < bNumHops) return sortOrder === "asc" ? -1 : 1;
@@ -95,8 +95,8 @@ export const Table = ({ headings, rows }: TableProps) => {
return 0;
}
- const aValue = elementA?.props?.children;
- const bValue = elementB?.props?.children;
+ const aValue = (elementA?.props as any)?.children;
+ const bValue = (elementB?.props as any)?.children;
const valA = aValue ?? "";
const valB = bValue ?? "";
diff --git a/src/core/dto/PacketToMessageDTO.ts b/src/core/dto/PacketToMessageDTO.ts
index 6b50ebb4..3694c506 100644
--- a/src/core/dto/PacketToMessageDTO.ts
+++ b/src/core/dto/PacketToMessageDTO.ts
@@ -1,9 +1,6 @@
import type { Types } from "@meshtastic/js";
-import {
- Message,
- MessageState,
- MessageType,
-} from "../stores/messageStore/index.ts";
+import { MessageState, MessageType } from "../stores/messageStore/index.ts";
+import type { Message } from "../stores/messageStore/types.ts";
class PacketToMessageDTO {
channel: Types.ChannelNumber;
diff --git a/src/core/hooks/useBrowserFeatureDetection.ts b/src/core/hooks/useBrowserFeatureDetection.ts
index 3f2667f2..e792ca1b 100644
--- a/src/core/hooks/useBrowserFeatureDetection.ts
+++ b/src/core/hooks/useBrowserFeatureDetection.ts
@@ -10,8 +10,8 @@ interface BrowserSupport {
export function useBrowserFeatureDetection(): BrowserSupport {
const support = useMemo(() => {
const features: [BrowserFeature, boolean][] = [
- ["Web Bluetooth", !!navigator?.bluetooth],
- ["Web Serial", !!navigator?.serial],
+ ["Web Bluetooth", !!navigator.bluetooth],
+ ["Web Serial", !!navigator.serial],
[
"Secure Context",
globalThis.location.protocol === "https:" ||
diff --git a/src/core/hooks/useCookie.ts b/src/core/hooks/useCookie.ts
index df3d9d82..24e97b01 100644
--- a/src/core/hooks/useCookie.ts
+++ b/src/core/hooks/useCookie.ts
@@ -1,9 +1,9 @@
-import Cookies, { type CookieAttributes } from "js-cookie";
+import Cookies from "js-cookie";
import { useCallback, useState } from "react";
interface CookieHookResult {
value: T | undefined;
- setCookie: (value: T, options?: CookieAttributes) => void;
+ setCookie: (value: T, options?: Cookies.CookieAttributes) => void;
removeCookie: () => void;
}
@@ -22,7 +22,7 @@ function useCookie(
});
const setCookie = useCallback(
- (value: T, options?: CookieAttributes) => {
+ (value: T, options?: Cookies.CookieAttributes) => {
try {
Cookies.set(cookieName, JSON.stringify(value), options);
setCookieValue(value);
diff --git a/src/core/hooks/usePinnedItems.test.ts b/src/core/hooks/usePinnedItems.test.ts
index 3741ffdb..3b126666 100644
--- a/src/core/hooks/usePinnedItems.test.ts
+++ b/src/core/hooks/usePinnedItems.test.ts
@@ -6,7 +6,7 @@ const mockSetPinnedItems = vi.fn();
const mockUseLocalStorage = vi.fn();
vi.mock("@core/hooks/useLocalStorage.ts", () => ({
- default: (...args) => mockUseLocalStorage(...args),
+ default: (...args: any[]) => mockUseLocalStorage(...args),
}));
describe("usePinnedItems", () => {
diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts
index d8b7b03d..488a5d5d 100644
--- a/src/core/stores/deviceStore.ts
+++ b/src/core/stores/deviceStore.ts
@@ -114,7 +114,7 @@ export const useDeviceStore = createStore((set, get) => ({
addDevice: (id: number) => {
set(
- produce((draft) => {
+ produce((draft) => {
draft.devices.set(id, {
id,
status: Types.DeviceStatusEnum.DeviceDisconnected,
@@ -151,7 +151,7 @@ export const useDeviceStore = createStore((set, get) => ({
setStatus: (status: Types.DeviceStatusEnum) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.status = status;
@@ -161,7 +161,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setConfig: (config: Protobuf.Config.Config) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
switch (config.payloadVariant.case) {
@@ -203,7 +203,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
switch (config.payloadVariant.case) {
@@ -271,7 +271,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setWorkingConfig: (config: Protobuf.Config.Config) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) return;
const index = device.workingConfig.findIndex(
@@ -289,7 +289,7 @@ export const useDeviceStore = createStore((set, get) => ({
moduleConfig: Protobuf.ModuleConfig.ModuleConfig,
) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) return;
const index = device.workingModuleConfig.findIndex(
@@ -307,7 +307,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.hardware = hardware;
@@ -317,7 +317,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setPendingSettingsChanges: (state) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.pendingSettingsChanges = state;
@@ -327,7 +327,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addChannel: (channel: Protobuf.Channel.Channel) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.channels.set(channel.index, channel);
@@ -337,7 +337,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addWaypoint: (waypoint: Protobuf.Mesh.Waypoint) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
const index = device.waypoints.findIndex((wp) =>
@@ -354,7 +354,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addNodeInfo: (nodeInfo) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) return;
@@ -364,7 +364,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setActiveNode: (node) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.activeNode = node;
@@ -374,7 +374,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addUser: (user) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
@@ -389,7 +389,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addPosition: (position) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
@@ -404,7 +404,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addConnection: (connection) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.connection = connection;
@@ -414,7 +414,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addMetadata: (from, metadata) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.metadata.set(from, metadata);
@@ -424,7 +424,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
addTraceRoute: (traceroute) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) return;
const routes = device.traceroutes.get(traceroute.from) ?? [];
@@ -433,9 +433,9 @@ export const useDeviceStore = createStore((set, get) => ({
}),
);
},
- removeNode: (nodeNum) => {
+ removeNode: (nodeNum: number) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
@@ -446,7 +446,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setDialogOpen: (dialog: DialogVariant, open: boolean) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.dialog[dialog] = open;
@@ -461,7 +461,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
processPacket(data: ProcessPacketParams) {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) return;
const node = device.nodesMap.get(data.from);
@@ -484,7 +484,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
setMessageDraft: (message: string) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.messageDraft = message;
@@ -492,9 +492,9 @@ export const useDeviceStore = createStore((set, get) => ({
}),
);
},
- setNodeError: (nodeNum, error) => {
+ setNodeError: (nodeNum: number, error: string) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.nodeErrors.set(nodeNum, { node: nodeNum, error });
@@ -504,7 +504,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
clearNodeError: (nodeNum: number) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (device) {
device.nodeErrors.delete(nodeNum);
@@ -524,7 +524,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
incrementUnread: (nodeNum: number) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) return;
const currentCount = device.unreadCounts.get(nodeNum) ?? 0;
@@ -534,7 +534,7 @@ export const useDeviceStore = createStore((set, get) => ({
},
resetUnread: (nodeNum: number) => {
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
if (!device) return;
device.unreadCounts.set(nodeNum, 0);
@@ -610,10 +610,12 @@ export const useDeviceStore = createStore((set, get) => ({
}));
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
const node = device?.nodesMap.get(nodeNum);
- node.isFavorite = isFavorite;
+ if (node) {
+ node.isFavorite = isFavorite;
+ }
}),
);
},
@@ -631,10 +633,12 @@ export const useDeviceStore = createStore((set, get) => ({
}));
set(
- produce((draft) => {
+ produce((draft) => {
const device = draft.devices.get(id);
const node = device?.nodesMap.get(nodeNum);
- node.isIgnored = isIgnored;
+ if (node) {
+ node.isIgnored = isIgnored;
+ }
}),
);
},
@@ -651,7 +655,7 @@ export const useDeviceStore = createStore((set, get) => ({
removeDevice: (id) => {
set(
- produce((draft) => {
+ produce((draft) => {
draft.devices.delete(id);
}),
);
diff --git a/src/core/stores/storage/indexDB.ts b/src/core/stores/storage/indexDB.ts
index d7d89342..129fe9ae 100644
--- a/src/core/stores/storage/indexDB.ts
+++ b/src/core/stores/storage/indexDB.ts
@@ -1,4 +1,4 @@
-import { PersistStorage, StateStorage } from "zustand/middleware";
+import { PersistStorage, StateStorage, StorageValue } from "zustand/middleware";
import { del, get, set } from "idb-keyval";
import { ChannelId, MessageLogMap } from "@core/stores/messageStore/types.ts";
@@ -56,18 +56,18 @@ const reviver: JsonReviver = (_, value) => {
};
export const storageWithMapSupport: PersistStorage = {
- getItem: async (name): Promise => {
+ getItem: async (name): Promise | null> => {
const str = await zustandIndexDBStorage.getItem(name);
if (!str) return null;
try {
- const parsed = JSON.parse(str, reviver) as PersistedMessageState;
+ const parsed = JSON.parse(str, reviver) as StorageValue;
return parsed;
} catch (error) {
console.error(`Error parsing persisted state (${name}):`, error);
return null;
}
},
- setItem: async (name, newValue: PersistedMessageState): Promise => {
+ setItem: async (name, newValue: StorageValue): Promise => {
try {
const str = JSON.stringify(newValue, replacer);
await zustandIndexDBStorage.setItem(name, str);
diff --git a/src/core/utils/eventBus.test.ts b/src/core/utils/eventBus.test.ts
index 6d2fc6ee..2d874066 100644
--- a/src/core/utils/eventBus.test.ts
+++ b/src/core/utils/eventBus.test.ts
@@ -4,7 +4,7 @@ import { eventBus } from "@core/utils/eventBus.ts";
describe("EventBus", () => {
beforeEach(() => {
// Reset event listeners before each test
- eventBus.listeners = {};
+ eventBus.offAll();
});
it("should register an event listener and trigger it on emit", () => {
diff --git a/src/core/utils/eventBus.ts b/src/core/utils/eventBus.ts
index 618ab5a6..75c48d8c 100644
--- a/src/core/utils/eventBus.ts
+++ b/src/core/utils/eventBus.ts
@@ -34,6 +34,14 @@ class EventBus {
}
}
+ public offAll(event?: T): void {
+ if (event) {
+ this.listeners[event] = [];
+ } else {
+ this.listeners = {};
+ }
+ }
+
public emit(event: T, data: EventMap[T]): void {
if (!this.listeners[event]) return;
diff --git a/src/core/utils/ip.test.ts b/src/core/utils/ip.test.ts
index 2da62e57..6208e6d3 100644
--- a/src/core/utils/ip.test.ts
+++ b/src/core/utils/ip.test.ts
@@ -60,7 +60,7 @@ describe("IP Address Conversion Functions", () => {
for (const ip of testIps) {
const int = convertIpAddressToInt(ip);
expect(int).not.toBeNull();
- if (int !== null) {
+ if (int !== null && typeof int === "number") {
const convertedBack = convertIntToIpAddress(int);
expect(convertedBack).toBe(ip);
}
diff --git a/src/core/utils/sort.ts b/src/core/utils/sort.ts
new file mode 100644
index 00000000..2d1494f0
--- /dev/null
+++ b/src/core/utils/sort.ts
@@ -0,0 +1,18 @@
+export function intlSort(
+ arr: T[],
+ order: "asc" | "desc" = "asc",
+ locale: Intl.Locale,
+): T[] {
+ const collator = new Intl.Collator(locale, { sensitivity: "base" });
+
+ return arr.sort((a, b) => {
+ const stringA = String(a);
+ const stringB = String(b);
+
+ if (order === "asc") {
+ return collator.compare(stringA, stringB);
+ } else {
+ return collator.compare(stringB, stringA);
+ }
+ });
+}
diff --git a/src/i18n/config.ts b/src/i18n/config.ts
index 4a43392f..59f58c7f 100644
--- a/src/i18n/config.ts
+++ b/src/i18n/config.ts
@@ -35,7 +35,7 @@ i18next
"default": ["en"],
},
fallbackNS: ["common", "ui", "dialog"],
- debug: import.meta.env.DEV,
+ debug: import.meta.env.MODE === 'development',
supportedLngs: supportedLanguages?.map((lang) => lang.code),
ns: [
"channels",
diff --git a/src/i18n/locales/en/commandPalette.json b/src/i18n/locales/en/commandPalette.json
index 7b82e97b..1eed8987 100644
--- a/src/i18n/locales/en/commandPalette.json
+++ b/src/i18n/locales/en/commandPalette.json
@@ -1,7 +1,7 @@
{
"emptyState": "No results found.",
"page": {
- "title": "Command Palette"
+ "title": "Command Menu"
},
"pinGroup": {
"label": "Pin command group"
diff --git a/src/i18n/locales/en/ui.json b/src/i18n/locales/en/ui.json
index 6f0b6aa3..7e2e4708 100644
--- a/src/i18n/locales/en/ui.json
+++ b/src/i18n/locales/en/ui.json
@@ -80,6 +80,12 @@
},
"showPassword": {
"label": "Show password"
+ },
+ "deliveryStatus": {
+ "delivered": "Delivered",
+ "failed": "Delivery Failed",
+ "waiting": "Waiting",
+ "unknown": "Unknown"
}
},
"general": {
diff --git a/src/index.css b/src/index.css
index 1ae593b4..41165535 100644
--- a/src/index.css
+++ b/src/index.css
@@ -3,6 +3,10 @@
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
+@view-transition {
+ navigation: auto;
+}
+
@theme {
--font-mono:
Cascadia Code, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
@@ -89,6 +93,7 @@ body {
}
@layer base {
+
*,
::after,
::before,
@@ -134,4 +139,4 @@ img {
.animate-spin-slow {
animation: spin-slower 2s linear infinite;
-}
+}
\ No newline at end of file
diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx
index 69cfa2e2..6afc4546 100644
--- a/src/pages/Dashboard/index.tsx
+++ b/src/pages/Dashboard/index.tsx
@@ -4,14 +4,7 @@ import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import { Separator } from "@components/UI/Seperator.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
-import {
- BluetoothIcon,
- ListPlusIcon,
- NetworkIcon,
- PlusIcon,
- UsbIcon,
- UsersIcon,
-} from "lucide-react";
+import { ListPlusIcon, PlusIcon, UsersIcon } from "lucide-react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import LanguageSwitcher from "@components/LanguageSwitcher.tsx";
@@ -60,32 +53,6 @@ export const Dashboard = () => {
?.longName ??
t("unknown.shortName")}
-
- {device.connection?.connType === "ble" && (
- <>
-
- {t(
- "dashboard.connectionType_ble",
- )}
- >
- )}
- {device.connection?.connType === "serial" && (
- <>
-
- {t(
- "dashboard.connectionType_serial",
- )}
- >
- )}
- {device.connection?.connType === "http" && (
- <>
-
- {t(
- "dashboard.connectionType_network",
- )}
- >
- )}
-
{
{t("dashboard.noDevicesTitle")}
- {/* */}
{t("dashboard.noDevicesDescription")}
diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx
index 46fc0115..f587ba30 100644
--- a/src/pages/Messages.test.tsx
+++ b/src/pages/Messages.test.tsx
@@ -44,13 +44,13 @@ const mockUseDevice = {
describe.skip("Messages Page", () => {
beforeEach(() => {
- vi.mocked(useDevice).mockReturnValue(mockUseDevice);
+ vi.mocked(useDevice).mockReturnValue(mockUseDevice as any);
});
it("sorts unreads to the top", () => {
render();
const buttonOrder = screen.getAllByRole("button").filter((b) =>
- b.textContent.includes("Test Node")
+ b.textContent?.includes("Test Node")
);
expect(buttonOrder[0].textContent).toContain("TN2Test Node 210");
expect(buttonOrder[1].textContent).toContain("TN1Test Node 13");
@@ -62,7 +62,7 @@ describe.skip("Messages Page", () => {
render();
const nodeButton =
screen.getAllByRole("button").filter((b) =>
- b.textContent.includes("TN1Test Node 13")
+ b.textContent?.includes("TN1Test Node 13")
)[0];
fireEvent.click(nodeButton);
expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0);
@@ -72,7 +72,7 @@ describe.skip("Messages Page", () => {
render();
const nodeButton =
screen.getAllByRole("button").filter((b) =>
- b.textContent.includes("TN1Test Node 1")
+ b.textContent?.includes("TN1Test Node 1")
)[0];
fireEvent.click(nodeButton);
expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0);
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx
deleted file mode 100644
index c349cde3..00000000
--- a/src/pages/Nodes.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-import { LocationResponseDialog } from "@app/components/Dialog/LocationResponseDialog.tsx";
-import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteResponseDialog.tsx";
-import { Sidebar } from "@components/Sidebar.tsx";
-import { Avatar } from "@components/UI/Avatar.tsx";
-import { Mono } from "@components/generic/Mono.tsx";
-import { Table } from "@components/generic/Table/index.tsx";
-import { TimeAgo } from "@components/generic/TimeAgo.tsx";
-import { useDevice } from "@core/stores/deviceStore.ts";
-import { useAppStore } from "@core/stores/appStore.ts";
-import { Protobuf, type Types } from "@meshtastic/core";
-import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
-import { LockIcon, LockOpenIcon } from "lucide-react";
-import {
- type JSX,
- useCallback,
- useDeferredValue,
- useEffect,
- useMemo,
- useState,
-} from "react";
-import { base16 } from "rfc4648";
-import { Input } from "@components/UI/Input.tsx";
-import { PageLayout } from "@components/PageLayout.tsx";
-import {
- type FilterState,
- useFilterNode,
-} from "@components/generic/Filter/useFilterNode.ts";
-import { FilterControl } from "@components/generic/Filter/FilterControl.tsx";
-import { useTranslation } from "react-i18next";
-
-export interface DeleteNoteDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-const NodesPage = (): JSX.Element => {
- const { t } = useTranslation("nodes");
- const { getNodes, hardware, connection, hasNodeError, setDialogOpen } =
- useDevice();
- const { setNodeNumDetails } = useAppStore();
- const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode();
-
- const [selectedTraceroute, setSelectedTraceroute] = useState<
- Types.PacketMetadata | undefined
- >();
- const [selectedLocation, setSelectedLocation] = useState<
- Types.PacketMetadata | undefined
- >();
-
- const [filterState, setFilterState] = useState(() =>
- defaultFilterValues
- );
- const deferredFilterState = useDeferredValue(filterState);
-
- const filteredNodes = useMemo(
- () => getNodes((node) => nodeFilter(node, deferredFilterState)),
- [deferredFilterState, getNodes, nodeFilter],
- );
-
- useEffect(() => {
- if (!connection) return;
- connection.events.onTraceRoutePacket.subscribe(handleTraceroute);
- return () => {
- connection.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
- };
- }, [connection]);
-
- const handleTraceroute = useCallback(
- (traceroute: Types.PacketMetadata) => {
- setSelectedTraceroute(traceroute);
- },
- [],
- );
-
- useEffect(() => {
- if (!connection) return;
- connection.events.onPositionPacket.subscribe(handleLocation);
- return () => {
- connection.events.onPositionPacket.subscribe(handleLocation);
- };
- }, [connection]);
-
- const handleLocation = useCallback(
- (location: Types.PacketMetadata) => {
- if (location.to.valueOf() !== hardware.myNodeNum) return;
- setSelectedLocation(location);
- },
- [hardware.myNodeNum],
- );
-
- function handleNodeInfoDialog(nodeNum: number): void {
- setNodeNumDetails(nodeNum);
- setDialogOpen("nodeDetails", true);
- }
-
- return (
- <>
- }
- >
-
-
-
- setFilterState((prev) => ({
- ...prev,
- nodeName: e.target.value,
- }))}
- />
-
-
-
-
-
-
-
[
- ,
- handleNodeInfoDialog(node.num)}
- onKeyUp={(evt) => {
- evt.key === "Enter" && handleNodeInfoDialog(node.num);
- }}
- className="cursor-pointer underline ml-2 whitespace-break-spaces"
- tabIndex={0}
- role="button"
- >
- {node.user?.longName ?? numberToHexUnpadded(node.num)}
-
,
-
- {node.hopsAway !== undefined
- ? node?.viaMqtt === false && node.hopsAway === 0
- ? t("nodesTable.connectionStatus.direct")
- : `${node.hopsAway?.toString()} ${
- node.hopsAway ?? 0 > 1
- ? t("unit.hop.plural")
- : t("unit.hops_one")
- } ${t("nodesTable.connectionStatus.away")}`
- : t("nodesTable.connectionStatus.unknown")}
- {node?.viaMqtt === true
- ? t("nodesTable.connectionStatus.viaMqtt")
- : ""}
- ,
-
- {node.lastHeard === 0
- ? {t("nodesTable.lastHeardStatus.never")}
- : }
- ,
-
- {node.user?.publicKey && node.user?.publicKey.length > 0
- ?
- : }
- ,
-
- {node.snr}
- {t("unit.dbm")}/
- {Math.min(
- Math.max((node.snr + 10) * 5, 0),
- 100,
- )}%/{/* Percentage */}
- {(node.snr + 10) * 5}
- {t("unit.raw")}
- ,
-
- {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
- ,
-
- {base16
- .stringify(node.user?.macaddr ?? [])
- .match(/.{1,2}/g)
- ?.join(":") ?? t("unknown.shortName")}
- ,
- ])}
- />
- setSelectedTraceroute(undefined)}
- />
- setSelectedLocation(undefined)}
- />
-
-
- >
- );
-};
-
-export default NodesPage;
diff --git a/src/pages/Nodes/index.tsx b/src/pages/Nodes/index.tsx
new file mode 100644
index 00000000..f87a8645
--- /dev/null
+++ b/src/pages/Nodes/index.tsx
@@ -0,0 +1,89 @@
+// @/pages/NodesPage.tsx (abbreviated)
+import { useVirtualizer } from "@tanstack/react-virtual";
+import { JSX, useCallback, useDeferredValue, useRef, useState } from "react";
+import { NodeRow } from "./NodeRow.tsx";
+import {
+ FilterState,
+ useFilterNode,
+} from "@components/generic/Filter/useFilterNode.ts";
+import { PageLayout } from "@components/PageLayout.tsx";
+import { Sidebar } from "@components/Sidebar.tsx";
+import { useAppStore } from "@core/stores/appStore.ts";
+import { useDevice } from "@core/stores/deviceStore.ts";
+// ... other imports
+
+const NodesPage = (): JSX.Element => {
+ const { getNodes, hasNodeError, setDialogOpen } = useDevice();
+ const { setNodeNumDetails } = useAppStore();
+ const { nodeFilter, defaultFilterValues } = useFilterNode();
+
+ // No changes to filtering logic, `useDeferredValue` is still a great choice
+ const [filterState, setFilterState] = useState(() =>
+ defaultFilterValues
+ );
+ const deferredFilterState = useDeferredValue(filterState);
+ const filteredNodes = getNodes((node) =>
+ nodeFilter(node, deferredFilterState)
+ );
+
+ const parentRef = useRef(null);
+
+ const rowVirtualizer = useVirtualizer({
+ count: filteredNodes.length,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 64, // Estimate row height in pixels
+ overscan: 5, // Render 5 extra items in each direction
+ });
+
+ const handleNodeInfoDialog = useCallback(
+ (nodeNum: number): void => {
+ setNodeNumDetails(nodeNum);
+ setDialogOpen("nodeDetails", true);
+ },
+ [setNodeNumDetails, setDialogOpen],
+ );
+
+ return (
+ <>
+ }>
+ {/* ... Filter controls ... */}
+
+ {/* Your scrollable container with the ref */}
+
+ {/* Adjust height as needed */}
+ {/* A container to set the total size of the virtual list */}
+
+ {/* Map over the virtual items, not the full filteredNodes array */}
+ {rowVirtualizer.getVirtualItems().map((virtualItem) => {
+ const node = filteredNodes[virtualItem.index];
+ return (
+
+ );
+ })}
+
+
+ {/* ... Dialogs ... */}
+
+ >
+ );
+};
+
+export default NodesPage;
diff --git a/vite-env.d.ts b/vite-env.d.ts
new file mode 100644
index 00000000..484e03a4
--- /dev/null
+++ b/vite-env.d.ts
@@ -0,0 +1,11 @@
+///
+
+interface ImportMetaEnv {
+ readonly env: {
+ readonly VITE_COMMIT_HASH: string;
+ };
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/vite.config.ts b/vite.config.ts
index 05daf0e3..83bba01f 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -32,9 +32,9 @@ export default defineConfig({
targets: [
{
src: "src/i18n/locales/**/*",
- dest: "src/i18n/locales"
- }
- ]
+ dest: "src/i18n/locales",
+ },
+ ],
}),
],
define: {