refactor: switch to using Bun (#718)

This commit is contained in:
Dan Ditomaso
2025-07-16 22:30:10 -04:00
committed by GitHub
parent acfd7ebd2d
commit 704d06cfe7
401 changed files with 35294 additions and 39107 deletions
@@ -1,42 +1,39 @@
import { create } from "@bufbuild/protobuf";
import { Protobuf } from "@meshtastic/core";
class NodeInfoFactory {
private static createDefaultUser(num: number): Protobuf.Mesh.User {
const userIdHex = num.toString(16).toUpperCase().padStart(2, "0");
const userId = `!${userIdHex}`;
const last4 = userIdHex.slice(-4);
const longName = `Meshtastic ${last4}`;
const shortName = last4;
function createDefaultUser(num: number): Protobuf.Mesh.User {
const userIdHex = num.toString(16).toUpperCase().padStart(2, "0");
const userId = `!${userIdHex}`;
const last4 = userIdHex.slice(-4);
const longName = `Meshtastic ${last4}`;
const shortName = last4;
return create(Protobuf.Mesh.UserSchema, {
id: userId,
longName: longName.toString(),
shortName: shortName.toString(),
});
return create(Protobuf.Mesh.UserSchema, {
id: userId,
longName: longName.toString(),
shortName: shortName.toString(),
});
}
export function ensureDefaultUser(
node: Protobuf.Mesh.NodeInfo,
): Protobuf.Mesh.NodeInfo {
if (!node) {
return node;
}
public static ensureDefaultUser(
node: Protobuf.Mesh.NodeInfo,
): Protobuf.Mesh.NodeInfo {
if (!node) {
if (!node.user) {
if (node.num === undefined || node.num === null) {
console.error(
`NodeInfoFactory.ensureDefaultUser: Cannot create default user for node because 'num' is missing.`,
node,
);
return node;
}
if (!node.user) {
if (node.num === undefined || node.num === null) {
console.error(
`NodeInfoFactory.ensureDefaultUser: Cannot create default user for node because 'num' is missing.`,
node,
);
return node;
}
node.user = NodeInfoFactory.createDefaultUser(node.num);
}
return node;
// Assign the created user directly
node.user = createDefaultUser(node.num);
}
}
export default NodeInfoFactory;
return node;
}
@@ -1,6 +1,7 @@
import type { Types } from "@meshtastic/core";
import { MessageState, MessageType } from "@core/stores/messageStore/index.ts";
import { Message } from "@core/stores/messageStore/types.ts";
import type { Message } from "@core/stores/messageStore/types.ts";
import type { Types } from "@meshtastic/core";
class PacketToMessageDTO {
channel: Types.ChannelNumber;
to: number;
@@ -16,25 +17,22 @@ class PacketToMessageDTO {
this.to = data.to;
this.from = data.from;
this.messageId = data.id;
this.state = data.from !== nodeNum
? MessageState.Ack
: MessageState.Waiting;
this.state =
data.from !== nodeNum ? MessageState.Ack : MessageState.Waiting;
this.message = data.data;
this.type = (data.type === "direct")
? MessageType.Direct
: MessageType.Broadcast;
this.type =
data.type === "direct" ? MessageType.Direct : MessageType.Broadcast;
let dateTimestamp = Date.now();
if (data.rxTime instanceof Date) {
const timeValue = data.rxTime.getTime();
if (!isNaN(timeValue)) {
if (!Number.isNaN(timeValue)) {
dateTimestamp = timeValue;
}
} else if (data.rxTime != null) {
console.warn(
`Received rxTime in PacketToMessageDTO was not a Date object as expected (type: ${typeof data
.rxTime}, value: ${data.rxTime}). Using current time as fallback.`,
`Received rxTime in PacketToMessageDTO was not a Date object as expected (type: ${typeof data.rxTime}, value: ${data.rxTime}). Using current time as fallback.`,
);
}
this.date = dateTimestamp;
@@ -15,7 +15,7 @@ export function useBrowserFeatureDetection(): BrowserSupport {
[
"Secure Context",
globalThis.location.protocol === "https:" ||
globalThis.location.hostname === "localhost",
globalThis.location.hostname === "localhost",
],
];
@@ -4,9 +4,9 @@ interface UseCopyToClipboardProps {
timeout?: number;
}
export function useCopyToClipboard(
{ timeout = 2000 }: UseCopyToClipboardProps = {},
) {
export function useCopyToClipboard({
timeout = 2000,
}: UseCopyToClipboardProps = {}) {
const [isCopied, setIsCopied] = useState<boolean>(false);
const timeoutRef = useRef<number | null>(null);
@@ -1,7 +1,7 @@
import type { Protobuf } from "@meshtastic/core";
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useFavoriteNode } from "./useFavoriteNode.ts";
import { Protobuf } from "@meshtastic/core";
const mockNode = {
num: 1234,
@@ -1,6 +1,6 @@
import { useCallback } from "react";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
interface FavoriteNodeOptions {
@@ -16,7 +16,9 @@ export function useFavoriteNode() {
const updateFavoriteCB = useCallback(
({ nodeNum, isFavorite }: FavoriteNodeOptions) => {
const node = getNode(nodeNum);
if (!node) return;
if (!node) {
return;
}
updateFavorite(nodeNum, isFavorite);
@@ -32,7 +34,7 @@ export function useFavoriteNode() {
}),
});
},
[updateFavorite, getNode],
[updateFavorite, getNode, t, toast],
);
return { updateFavorite: updateFavoriteCB };
@@ -1,7 +1,7 @@
import type { Protobuf } from "@meshtastic/core";
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useIgnoreNode } from "./useIgnoreNode.ts";
import { Protobuf } from "@meshtastic/core";
const mockNode = {
num: 1234,
+6 -4
View File
@@ -1,6 +1,6 @@
import { useCallback } from "react";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
interface IgnoreNodeOptions {
@@ -17,7 +17,9 @@ export function useIgnoreNode() {
const updateIgnoredCB = useCallback(
({ nodeNum, isIgnored }: IgnoreNodeOptions) => {
const node = getNode(nodeNum);
if (!node) return;
if (!node) {
return;
}
updateIgnored(nodeNum, isIgnored);
@@ -33,7 +35,7 @@ export function useIgnoreNode() {
}),
});
},
[updateIgnored, getNode],
[updateIgnored, getNode, t, toast],
);
return { updateIgnored: updateIgnoredCB };
@@ -1,7 +1,7 @@
import { Button } from "@components/UI/Button.tsx";
import { useCallback, useEffect, useRef } from "react";
import { useToast } from "@core/hooks/useToast.ts";
import useLocalStorage from "@core/hooks/useLocalStorage.ts";
import { useToast } from "@core/hooks/useToast.ts";
import { useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
interface UseBackupReminderOptions {
@@ -23,9 +23,13 @@ const REMINDER_DAYS_FOREVER = 3650;
const STORAGE_KEY = "key_backup_reminder";
function isReminderExpired(expires?: string): boolean {
if (!expires) return true;
if (!expires) {
return true;
}
const expiryDate = new Date(expires);
if (isNaN(expiryDate.getTime())) return true; // Invalid date passed
if (Number.isNaN(expiryDate.getTime())) {
return true; // Invalid date passed
}
const now = new Date();
return now.getTime() >= expiryDate.getTime();
@@ -41,23 +45,26 @@ export function useBackupReminder({
const { toast } = useToast();
const toastShownRef = useRef(false);
const [reminderState, setReminderState] = useLocalStorage<
ReminderState | null
>(
STORAGE_KEY,
null,
const [reminderState, setReminderState] =
useLocalStorage<ReminderState | null>(STORAGE_KEY, null);
const setReminderExpiry = useCallback(
(days: number) => {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + days);
setReminderState({ expires: expiryDate.toISOString() });
},
[setReminderState],
);
const setReminderExpiry = useCallback((days: number) => {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + days);
setReminderState({ expires: expiryDate.toISOString() });
}, [setReminderState]);
useEffect(() => {
if (!enabled || toastShownRef.current) return;
if (!enabled || toastShownRef.current) {
return;
}
if (!isReminderExpired(reminderState?.expires)) return;
if (!isReminderExpired(reminderState?.expires)) {
return;
}
toastShownRef.current = true;
@@ -116,5 +123,10 @@ export function useBackupReminder({
enabled,
message,
onAccept,
reminderInDays,
reminderState?.expires,
setReminderExpiry,
t,
toast,
]);
}
+5 -5
View File
@@ -1,11 +1,11 @@
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
FALLBACK_LANGUAGE_CODE,
Lang,
LangCode,
type Lang,
type LangCode,
supportedLanguages,
} from "@app/i18n-config.ts";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import useLocalStorage from "./useLocalStorage.ts";
const STORAGE_KEY = "language";
@@ -47,7 +47,7 @@ function useLang() {
console.warn("Failed to change language:", e);
}
},
[i18n, setLanguageInStorage],
[i18n.language, i18n.changeLanguage, setLanguageInStorage],
);
const compare = useCallback(
@@ -1,6 +1,6 @@
import { act, renderHook } from "@testing-library/react";
import useLocalStorage from "./useLocalStorage.ts";
import { beforeEach, describe, expect, it } from "vitest";
import useLocalStorage from "./useLocalStorage.ts";
describe("useLocalStorage", () => {
const key = "test-key";
+79 -126
View File
@@ -1,13 +1,9 @@
// taken from https://react-hooked.vercel.app/docs/useLocalStorage/
import { useCallback, useEffect, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface WindowEventMap {
"local-storage": CustomEvent;
"local-storage": CustomEvent<{ key: string }>;
}
}
@@ -19,14 +15,6 @@ type UseLocalStorageOptions<T> = {
const IS_SERVER = typeof window === "undefined";
/**
* Hook for persisting state to localStorage.
*
* @param {string} key - The key to use for localStorage.
* @param {T | (() => T)} initialValue - The initial value to use, if not found in localStorage.
* @param {UseLocalStorageOptions<T>} options - Options for the hook.
* @returns A tuple of [storedValue, setValue, removeValue].
*/
export default function useLocalStorage<T>(
key: string,
initialValue: T | (() => T),
@@ -34,149 +22,114 @@ export default function useLocalStorage<T>(
): [T, Dispatch<SetStateAction<T>>, () => void] {
const { initializeWithValue = true } = options;
const serializer = useCallback<(value: T) => string>(
(value) => {
if (options.serializer) {
return options.serializer(value);
}
const serializerRef = useRef(options.serializer ?? JSON.stringify);
const deserializerRef = useRef(options.deserializer ?? JSON.parse);
return JSON.stringify(value);
},
[options],
const initialValueRef = useRef<T>(
typeof initialValue === "function"
? (initialValue as () => T)()
: initialValue,
);
const deserializer = useCallback<(value: string) => T>(
(value) => {
if (options.deserializer) {
return options.deserializer(value);
}
// Support 'undefined' as a value
if (value === "undefined") {
return undefined as unknown as T;
}
const getInitialValue = useCallback(() => initialValueRef.current, []);
const defaultValue = initialValue instanceof Function
? initialValue()
: initialValue;
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch (error) {
console.error("Error parsing JSON:", error);
return defaultValue; // Return initialValue if parsing fails
}
return parsed as T;
},
[options, initialValue],
);
// Get from local storage then
// parse stored json or return initialValue
const readValue = useCallback((): T => {
const initialValueToUse = initialValue instanceof Function
? initialValue()
: initialValue;
// Prevent build error "window is undefined" but keep working
if (IS_SERVER) {
return initialValueToUse;
const [storedValue, setStoredValue] = useState<T>(() => {
if (!initializeWithValue || IS_SERVER) {
return getInitialValue();
}
try {
const raw = globalThis.localStorage.getItem(key);
return raw ? deserializer(raw) : initialValueToUse;
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error);
return initialValueToUse;
const item = window.localStorage.getItem(key);
return item ? deserializerRef.current(item) : getInitialValue();
} catch (err) {
console.warn(`Error reading localStorage key “${key}”:`, err);
return getInitialValue();
}
}, [initialValue, key, deserializer]);
const [storedValue, setStoredValue] = useState(() => {
if (initializeWithValue) {
return readValue();
}
return initialValue instanceof Function ? initialValue() : initialValue;
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: Dispatch<SetStateAction<T>> = useCallback(
(value) => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`,
`Tried setting localStorage key “${key}” in a non-client environment.`,
);
return;
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(readValue()) : value;
// Save to local storage
globalThis.localStorage.setItem(key, serializer(newValue));
// Save state
setStoredValue(newValue);
// We dispatch a custom event so every similar useLocalStorage hook is notified
globalThis.dispatchEvent(new StorageEvent("local-storage", { key }));
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error);
setStoredValue((prev) => {
const newValue =
typeof value === "function"
? (value as (prev: T) => T)(prev)
: value;
window.localStorage.setItem(key, serializerRef.current(newValue));
window.dispatchEvent(
new CustomEvent("local-storage", { detail: { key } }),
);
return newValue;
});
} catch (err) {
console.warn(`Error setting localStorage key “${key}”:`, err);
}
},
[key, serializer, readValue],
[key],
);
const removeValue = useCallback(() => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(
`Tried removing localStorage key “${key}” even though environment is not a client`,
`Tried removing localStorage key “${key}” in a non-client environment.`,
);
return;
}
try {
window.localStorage.removeItem(key);
setStoredValue(getInitialValue());
window.dispatchEvent(
new CustomEvent("local-storage", { detail: { key } }),
);
} catch (err) {
console.warn(`Error removing localStorage key “${key}”:`, err);
}
}, [key, getInitialValue]);
useEffect(() => {
if (IS_SERVER) {
return;
}
const defaultValue = initialValue instanceof Function
? initialValue()
: initialValue;
const handleStorageChange = (
event: StorageEvent | CustomEvent<{ key: string }>,
) => {
const eventKey = "key" in event ? event.key : event.detail?.key;
// Remove the key from local storage
globalThis.localStorage.removeItem(key);
// Save state with default value
setStoredValue(defaultValue);
// We dispatch a custom event so every similar useLocalStorage hook is notified
globalThis.dispatchEvent(new StorageEvent("local-storage", { key }));
}, [key]);
useEffect(() => {
setStoredValue(readValue());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key]);
const handleStorageChange = useCallback(
(event: StorageEvent | CustomEvent) => {
if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {
return;
if (eventKey === key) {
try {
const item = window.localStorage.getItem(key);
setStoredValue(
item ? deserializerRef.current(item) : getInitialValue(),
);
} catch (err) {
console.warn(`Error syncing localStorage key “${key}”:`, err);
setStoredValue(getInitialValue());
}
}
setStoredValue(readValue());
},
[key, readValue],
);
useEffect(() => {
addEventListener("storage", handleStorageChange);
// this is a custom event, triggered in writeValueToLocalStorage
addEventListener("local-storage", handleStorageChange);
return () => {
removeEventListener("storage", handleStorageChange);
removeEventListener("local-storage", handleStorageChange);
};
}, []);
window.addEventListener("storage", handleStorageChange);
window.addEventListener(
"local-storage",
handleStorageChange as EventListener,
);
return () => {
window.removeEventListener("storage", handleStorageChange);
window.removeEventListener(
"local-storage",
handleStorageChange as EventListener,
);
};
}, [key, getInitialValue]);
return [storedValue, setValue, removeValue];
}
@@ -11,7 +11,7 @@ describe("usePasswordVisibilityToggle Hook", () => {
it("should initialize with visibility set to true if initialVisible is true", () => {
const { result } = renderHook(() =>
usePasswordVisibilityToggle({ initialVisible: true })
usePasswordVisibilityToggle({ initialVisible: true }),
);
expect(result.current.isVisible).toBe(true);
});
@@ -27,7 +27,7 @@ describe("usePasswordVisibilityToggle Hook", () => {
it("should toggle visibility from true to false when toggleVisibility is called", () => {
const { result } = renderHook(() =>
usePasswordVisibilityToggle({ initialVisible: true })
usePasswordVisibilityToggle({ initialVisible: true }),
);
expect(result.current.isVisible).toBe(true);
act(() => {
@@ -55,7 +55,7 @@ describe("usePasswordVisibilityToggle Hook", () => {
it("should return a stable toggleVisibility function reference (due to useCallback)", () => {
const { result, rerender } = renderHook(() =>
usePasswordVisibilityToggle()
usePasswordVisibilityToggle(),
);
const initialToggleFunc = result.current.toggleVisibility;
rerender();
@@ -9,9 +9,9 @@ interface UsePasswordVisibilityToggleProps {
* @param {boolean} [options.initialVisible=false]
* @returns {{isVisible: boolean, toggleVisibility: () => void}}
*/
export function usePasswordVisibilityToggle(
{ initialVisible = false }: UsePasswordVisibilityToggleProps = {},
) {
export function usePasswordVisibilityToggle({
initialVisible = false,
}: UsePasswordVisibilityToggleProps = {}) {
const [isVisible, setIsVisible] = useState<boolean>(initialVisible);
const toggleVisibility = useCallback(() => {
+10 -5
View File
@@ -7,11 +7,16 @@ export function usePinnedItems({ storageName }: { storageName: string }) {
[],
);
const togglePinnedItem = useCallback((label: string) => {
setPinnedItems((prev) =>
prev.includes(label) ? prev.filter((g) => g !== label) : [...prev, label]
);
}, []);
const togglePinnedItem = useCallback(
(label: string) => {
setPinnedItems((prev) =>
prev.includes(label)
? prev.filter((g) => g !== label)
: [...prev, label],
);
},
[setPinnedItems],
);
return {
pinnedItems,
@@ -54,12 +54,14 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
const FLAGS_BITMASKS = useMemo(() => {
return Object.fromEntries(
Object.entries(FLAGS_CONFIG).map(([key, conf]) => [key, conf.value]),
) as { [K in FlagName]: typeof FLAGS_CONFIG[K]["value"] };
) as { [K in FlagName]: (typeof FLAGS_CONFIG)[K]["value"] };
}, []);
const utils = useMemo(() => {
const decode = (value: number): FlagName[] => {
if (value === FLAGS_CONFIG.UNSET.value) return ["UNSET"];
if (value === FLAGS_CONFIG.UNSET.value) {
return ["UNSET"];
}
const activeFlags: FlagName[] = [];
for (const key in FLAGS_CONFIG) {
@@ -80,14 +82,17 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
return FLAGS_CONFIG.UNSET.value;
}
return flagNames.reduce((acc, name) => {
if (name === "UNSET") return acc;
if (name === "UNSET") {
return acc;
}
return acc | FLAGS_CONFIG[name].value;
}, 0);
};
const hasFlag = (value: number, flagName: FlagName): boolean => {
return (value & FLAGS_CONFIG[flagName].value) ===
FLAGS_CONFIG[flagName].value;
return (
(value & FLAGS_CONFIG[flagName].value) === FLAGS_CONFIG[flagName].value
);
};
const getAllFlags = (): typeof FLAGS_CONFIG => {
@@ -117,7 +122,7 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
const setFlag = useCallback((flagName: FlagName, enabled: boolean) => {
const currentFlagValue = FLAGS_CONFIG[flagName].value;
setFlagsValue((prev) =>
enabled ? prev | currentFlagValue : prev & ~currentFlagValue
enabled ? prev | currentFlagValue : prev & ~currentFlagValue,
);
}, []);
+4 -2
View File
@@ -14,7 +14,7 @@ export function useTheme() {
);
const [preference, setPreference] = useState<Theme>(() =>
typeof window !== "undefined" ? getStoredPreference() : "light"
typeof window !== "undefined" ? getStoredPreference() : "light",
);
const theme = preference === "system" ? getSystemTheme() : preference;
@@ -24,7 +24,9 @@ export function useTheme() {
}, [theme]);
useEffect(() => {
if (preference !== "system") return;
if (preference !== "system") {
return;
}
const media = globalThis.matchMedia("(prefers-color-scheme: dark)");
const updateTheme = () => setPreference(getStoredPreference());
@@ -1,6 +1,6 @@
import { act, renderHook } from "@testing-library/react";
import { useToast } from "@core/hooks/useToast.ts";
import { Button } from "@components/UI/Button.tsx";
import { useToast } from "@core/hooks/useToast.ts";
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("useToast", () => {
+21 -20
View File
@@ -1,6 +1,5 @@
import { type ReactNode, useSyncExternalStore } from "react";
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.tsx";
import { type ReactNode, useSyncExternalStore } from "react";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
@@ -31,21 +30,21 @@ type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
@@ -81,7 +80,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
@@ -103,10 +102,10 @@ export const reducer = (state: State, action: Action): State => {
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
...t,
open: false,
}
: t,
),
};
}
@@ -156,7 +155,9 @@ function toast({ delay = 0, ...props }: Toast) {
id,
open: true,
onOpenChange: (open: boolean) => {
if (!open) dismiss();
if (!open) {
dismiss();
}
},
},
});
@@ -0,0 +1,24 @@
import {
useDevice,
type ValidConfigType,
type ValidModuleConfigType,
} from "@core/stores/deviceStore.ts";
type UseWaitForConfigProps =
| { configCase: ValidConfigType; moduleConfigCase?: never }
| { configCase?: never; moduleConfigCase: ValidModuleConfigType };
export function useWaitForConfig({
configCase,
moduleConfigCase,
}: UseWaitForConfigProps): void {
const { config, moduleConfig } = useDevice();
const isDataDefined = configCase
? config[configCase] !== undefined
: moduleConfig[moduleConfigCase as ValidModuleConfigType] !== undefined;
if (!isDataDefined) {
throw new Promise<void>(() => {});
}
}
-5
View File
@@ -13,11 +13,6 @@ interface ErrorState {
message: string;
}
interface ErrorState {
field: string;
message: string;
}
interface AppState {
selectedDevice: number;
devices: {
@@ -1,6 +1,6 @@
import type { Protobuf } from "@meshtastic/core";
import { vi } from "vitest";
import { type Device } from "./deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import type { Device } from "./deviceStore.ts";
/**
* You can spread this base mock in your tests and override only the
+105 -50
View File
@@ -1,5 +1,5 @@
import { create, toBinary } from "@bufbuild/protobuf";
import { MeshDevice, Protobuf, Types } from "@meshtastic/core";
import { type MeshDevice, Protobuf, Types } from "@meshtastic/core";
import { produce } from "immer";
import { createContext, useContext } from "react";
import { create as createStore } from "zustand";
@@ -78,9 +78,10 @@ export interface Device {
getWorkingModuleConfig: (
payloadVariant: ValidModuleConfigType,
) =>
| Protobuf.LocalOnly.LocalModuleConfig[
Exclude<ValidModuleConfigType, undefined>
]
| Protobuf.LocalOnly.LocalModuleConfig[Exclude<
ValidModuleConfigType,
undefined
>]
| undefined;
removeWorkingConfig: (payloadVariant?: ValidConfigType) => void;
removeWorkingModuleConfig: (payloadVariant?: ValidModuleConfigType) => void;
@@ -305,7 +306,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
const index = device.workingConfig.findIndex(
(wc) => wc.payloadVariant.case === config.payloadVariant.case,
);
@@ -324,11 +327,13 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
const index = device.workingModuleConfig.findIndex(
(wmc) =>
wmc.payloadVariant.case ===
moduleConfig.payloadVariant.case,
moduleConfig.payloadVariant.case,
);
if (index !== -1) {
@@ -342,7 +347,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
getWorkingConfig: (payloadVariant: ValidConfigType) => {
const device = get().devices.get(id);
if (!device) return;
if (!device) {
return;
}
const workingConfig = device.workingConfig.find(
(c) => c.payloadVariant.case === payloadVariant,
@@ -351,13 +358,17 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
if (
workingConfig?.payloadVariant.case === "deviceUi" ||
workingConfig?.payloadVariant.case === "sessionkey"
) return;
) {
return;
}
return workingConfig?.payloadVariant.value;
},
getWorkingModuleConfig: (payloadVariant: ValidModuleConfigType) => {
const device = get().devices.get(id);
if (!device) return;
if (!device) {
return;
}
return device.workingModuleConfig.find(
(c) => c.payloadVariant.case === payloadVariant,
@@ -368,7 +379,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
if (!payloadVariant) {
device.workingConfig = [];
@@ -392,7 +405,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
if (!payloadVariant) {
device.workingModuleConfig = [];
@@ -414,28 +429,34 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
getEffectiveConfig<K extends ValidConfigType>(
payloadVariant: K,
): Protobuf.LocalOnly.LocalConfig[K] | undefined {
if (!payloadVariant) return;
if (!payloadVariant) {
return;
}
const device = get().devices.get(id);
if (!device) return;
if (!device) {
return;
}
return {
...device.config[payloadVariant],
...(device.workingConfig.find(
...device.workingConfig.find(
(c) => c.payloadVariant.case === payloadVariant,
)?.payloadVariant.value),
)?.payloadVariant.value,
};
},
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
payloadVariant: K,
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined {
const device = get().devices.get(id);
if (!device) return;
if (!device) {
return;
}
return {
...device.moduleConfig[payloadVariant],
...(device.workingModuleConfig.find(
...device.workingModuleConfig.find(
(c) => c.payloadVariant.case === payloadVariant,
)?.payloadVariant.value),
)?.payloadVariant.value,
};
},
@@ -474,8 +495,8 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
const index = device.waypoints.findIndex((wp) =>
wp.id === waypoint.id
const index = device.waypoints.findIndex(
(wp) => wp.id === waypoint.id,
);
if (index !== -1) {
device.waypoints[index] = waypoint;
@@ -491,7 +512,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
device.nodesMap.set(nodeInfo.num, nodeInfo);
}),
);
@@ -513,7 +536,8 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
if (!device) {
return;
}
const currentNode = device.nodesMap.get(user.from) ??
const currentNode =
device.nodesMap.get(user.from) ??
create(Protobuf.Mesh.NodeInfoSchema);
currentNode.user = user.data;
currentNode.num = user.from;
@@ -528,7 +552,8 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
if (!device) {
return;
}
const currentNode = device.nodesMap.get(position.from) ??
const currentNode =
device.nodesMap.get(position.from) ??
create(Protobuf.Mesh.NodeInfoSchema);
currentNode.position = position.data;
currentNode.num = position.from;
@@ -560,7 +585,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
const routes = device.traceroutes.get(traceroute.from) ?? [];
routes.push(traceroute);
device.traceroutes.set(traceroute.from, routes);
@@ -590,14 +617,18 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
},
getDialogOpen: (dialog: DialogVariant) => {
const device = get().devices.get(id);
if (!device) throw new Error(`Device ${id} not found`);
if (!device) {
throw new Error(`Device ${id} not found`);
}
return device.dialog[dialog];
},
processPacket(data: ProcessPacketParams) {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
const node = device.nodesMap.get(data.from);
if (node) {
node.lastHeard = data.time;
@@ -648,19 +679,25 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
},
getNodeError: (nodeNum: number) => {
const device = get().devices.get(id);
if (!device) throw new Error(`Device ${id} not found`);
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`);
if (!device) {
throw new Error(`Device ${id} not found`);
}
return device.nodeErrors.has(nodeNum);
},
incrementUnread: (nodeNum: number) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
const currentCount = device.unreadCounts.get(nodeNum) ?? 0;
device.unreadCounts.set(nodeNum, currentCount + 1);
}),
@@ -688,7 +725,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) return;
if (!device) {
return;
}
device.unreadCounts.set(nodeNum, 0);
if (device.unreadCounts.get(nodeNum) === 0) {
device.unreadCounts.delete(nodeNum);
@@ -726,8 +765,10 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
if (!device) {
throw new Error(`Device ${id} not found`);
}
return device.nodesMap.get(device.hardware.myNodeNum) ??
create(Protobuf.Mesh.NodeInfoSchema);
return (
device.nodesMap.get(device.hardware.myNodeNum) ??
create(Protobuf.Mesh.NodeInfoSchema)
);
},
getNodesLength: () => {
const device = get().devices.get(id);
@@ -739,7 +780,9 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
sendAdminMessage(message: Protobuf.Admin.AdminMessage) {
const device = get().devices.get(id);
if (!device) return;
if (!device) {
return;
}
device.connection?.sendPacket(
toBinary(Protobuf.Admin.AdminMessageSchema, message),
@@ -750,16 +793,22 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
updateFavorite(nodeNum: number, isFavorite: boolean) {
const device = get().devices.get(id);
if (!device) return;
if (!device) {
return;
}
const node = device?.nodesMap.get(nodeNum);
if (!node) return;
if (!node) {
return;
}
device.sendAdminMessage(create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isFavorite ? "setFavoriteNode" : "removeFavoriteNode",
value: nodeNum,
},
}));
device.sendAdminMessage(
create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isFavorite ? "setFavoriteNode" : "removeFavoriteNode",
value: nodeNum,
},
}),
);
set(
produce<PrivateDeviceState>((draft) => {
@@ -773,16 +822,22 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
},
updateIgnored(nodeNum: number, isIgnored: boolean) {
const device = get().devices.get(id);
if (!device) return;
if (!device) {
return;
}
const node = device?.nodesMap.get(nodeNum);
if (!node) return;
if (!node) {
return;
}
device.sendAdminMessage(create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isIgnored ? "setIgnoredNode" : "removeIgnoredNode",
value: nodeNum,
},
}));
device.sendAdminMessage(
create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: isIgnored ? "setIgnoredNode" : "removeIgnoredNode",
value: nodeNum,
},
}),
);
set(
produce<PrivateDeviceState>((draft) => {
@@ -1,9 +1,5 @@
import { create } from "zustand";
// import { persist } from "zustand/middleware";
import { produce } from "immer";
import { Types } from "@meshtastic/core";
// import { storageWithMapSupport } from "../storage/indexDB.ts";
import {
import type {
ChannelId,
ClearMessageParams,
ConversationId,
@@ -14,6 +10,10 @@ import {
NodeNum,
SetMessageStateParams,
} from "@core/stores/messageStore/types.ts";
import type { Types } from "@meshtastic/core";
// import { persist } from "zustand/middleware";
import { produce } from "immer";
import { create } from "zustand";
export enum MessageState {
Ack = "ack",
@@ -72,9 +72,11 @@ export const useMessageStore = create<MessageStore>()(
chatType: MessageType.Broadcast,
nodeNum: 0,
setNodeNum: (nodeNum) => {
set(produce((state: MessageStore) => {
state.nodeNum = nodeNum;
}));
set(
produce((state: MessageStore) => {
state.nodeNum = nodeNum;
}),
);
},
getMyNodeNum: () => get().nodeNum,
saveMessage: (message: Message) => {
@@ -88,10 +90,9 @@ export const useMessageStore = create<MessageStore>()(
new Map<MessageId, Message>(),
);
}
state.messages.direct.get(conversationId)!.set(
message.messageId,
message,
);
state.messages.direct
.get(conversationId)
?.set(message.messageId, message);
} else if (message.type === MessageType.Broadcast) {
const channelId = message.channel as ChannelId;
if (!state.messages.broadcast.has(channelId)) {
@@ -100,10 +101,9 @@ export const useMessageStore = create<MessageStore>()(
new Map<MessageId, Message>(),
);
}
state.messages.broadcast.get(channelId)!.set(
message.messageId,
message,
);
state.messages.broadcast
.get(channelId)
?.set(message.messageId, message);
}
}),
);
@@ -124,7 +124,8 @@ export const useMessageStore = create<MessageStore>()(
if (messageLog) {
targetMessage = messageLog.get(params.messageId);
}
} else { // Broadcast
} else {
// Broadcast
messageLog = state.messages.broadcast.get(params.channelId);
if (messageLog) {
targetMessage = messageLog.get(params.messageId);
@@ -135,9 +136,9 @@ export const useMessageStore = create<MessageStore>()(
targetMessage.state = params.newState ?? MessageState.Ack;
} else {
console.warn(
`Message or conversation/channel not found for state update. Params: ${
JSON.stringify(params)
}`,
`Message or conversation/channel not found for state update. Params: ${JSON.stringify(
params,
)}`,
);
}
}),
@@ -209,20 +210,26 @@ export const useMessageStore = create<MessageStore>()(
return get().draft.get(key) ?? "";
},
setDraft: (key, message) => {
set(produce((state: MessageStore) => {
state.draft.set(key, message);
}));
set(
produce((state: MessageStore) => {
state.draft.set(key, message);
}),
);
},
clearDraft: (key) => {
set(produce((state: MessageStore) => {
state.draft.delete(key);
}));
set(
produce((state: MessageStore) => {
state.draft.delete(key);
}),
);
},
deleteAllMessages: () => {
set(produce((state: MessageStore) => {
state.messages.direct = new Map<ConversationId, MessageLogMap>();
state.messages.broadcast = new Map<ChannelId, MessageLogMap>();
}));
set(
produce((state: MessageStore) => {
state.messages.direct = new Map<ConversationId, MessageLogMap>();
state.messages.broadcast = new Map<ChannelId, MessageLogMap>();
}),
);
},
}),
// {
@@ -1,3 +1,4 @@
import { Types } from "@meshtastic/core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
getConversationId,
@@ -11,14 +12,13 @@ import type {
Message,
MessageLogMap,
} from "./types.ts";
import { Types } from "@meshtastic/core";
vi.mock("../storage/indexDB.ts", () => {
const memoryStorage: Record<string, string> = {};
return {
storageWithMapSupport: {
getItem: vi.fn(async (name: string): Promise<string | null> => {
return await memoryStorage[name] ?? null;
return (await memoryStorage[name]) ?? null;
}),
setItem: vi.fn(async (name: string, value: string): Promise<void> => {
memoryStorage[name] = await value;
@@ -94,14 +94,17 @@ describe("useMessageStore", () => {
const initialState = useMessageStore.getState();
beforeEach(() => {
useMessageStore.setState({
...initialState,
messages: {
direct: new Map<ConversationId, MessageLogMap>(),
broadcast: new Map<ChannelId, MessageLogMap>(),
useMessageStore.setState(
{
...initialState,
messages: {
direct: new Map<ConversationId, MessageLogMap>(),
broadcast: new Map<ChannelId, MessageLogMap>(),
},
draft: new Map<Types.Destination, string>(),
},
draft: new Map<Types.Destination, string>(),
}, true);
true,
);
});
it("should have correct initial state", () => {
@@ -167,22 +170,22 @@ describe("useMessageStore", () => {
const convId1 = getConversationId(myNodeNum, otherNodeNum1);
expect(
state.messages.direct.get(convId1)?.get(
directMessageToOther1.messageId,
),
state.messages.direct
.get(convId1)
?.get(directMessageToOther1.messageId),
).toEqual(directMessageToOther1);
expect(
state.messages.direct.get(convId1)?.get(
directMessageFromOther1.messageId,
),
state.messages.direct
.get(convId1)
?.get(directMessageFromOther1.messageId),
).toEqual(directMessageFromOther1);
const channelId = broadcastMessage1.channel;
expect(
state.messages.broadcast.get(channelId)?.get(
broadcastMessage1.messageId,
),
state.messages.broadcast
.get(channelId)
?.get(broadcastMessage1.messageId),
).toEqual(broadcastMessage1);
});
});
@@ -266,9 +269,10 @@ describe("useMessageStore", () => {
directMessageToOther1.from,
directMessageToOther1.to,
);
const message = useMessageStore.getState().messages.direct.get(
conversationId,
)?.get(directMessageToOther1.messageId);
const message = useMessageStore
.getState()
.messages.direct.get(conversationId)
?.get(directMessageToOther1.messageId);
expect(message?.state).toBe(MessageState.Ack);
});
@@ -284,9 +288,10 @@ describe("useMessageStore", () => {
directMessageFromOther1.from,
directMessageFromOther1.to,
);
const message = useMessageStore.getState().messages.direct.get(
conversationId,
)?.get(directMessageFromOther1.messageId);
const message = useMessageStore
.getState()
.messages.direct.get(conversationId)
?.get(directMessageFromOther1.messageId);
expect(message?.state).toBe(MessageState.Failed);
});
@@ -297,9 +302,10 @@ describe("useMessageStore", () => {
messageId: broadcastMessage1.messageId,
newState: MessageState.Ack,
});
const message = useMessageStore.getState().messages.broadcast.get(
broadcastChannel,
)?.get(broadcastMessage1.messageId);
const message = useMessageStore
.getState()
.messages.broadcast.get(broadcastChannel)
?.get(broadcastMessage1.messageId);
expect(message?.state).toBe(MessageState.Ack);
});
@@ -422,8 +428,9 @@ describe("useMessageStore", () => {
});
const state = useMessageStore.getState();
expect(state.messages.broadcast.get(channelId)?.get(messageIdToDelete))
.toBeUndefined();
expect(
state.messages.broadcast.get(channelId)?.get(messageIdToDelete),
).toBeUndefined();
});
it("should clean up empty conversation/channel Maps", () => {
@@ -462,8 +469,9 @@ describe("useMessageStore", () => {
messageId: broadcastMessage1.messageId,
});
expect(useMessageStore.getState().messages.broadcast.has(broadcastChanId))
.toBe(false);
expect(
useMessageStore.getState().messages.broadcast.has(broadcastChanId),
).toBe(false);
});
it("should not error when trying to delete non-existent message", () => {
@@ -556,8 +564,9 @@ describe("useMessageStore", () => {
expect(useMessageStore.getState().messages.direct.size).toBeGreaterThan(
0,
);
expect(useMessageStore.getState().messages.broadcast.size)
.toBeGreaterThan(0);
expect(
useMessageStore.getState().messages.broadcast.size,
).toBeGreaterThan(0);
useMessageStore.getState().deleteAllMessages();
@@ -1,5 +1,8 @@
import { Types } from "@meshtastic/core";
import { MessageState, MessageType } from "@core/stores/messageStore/index.ts";
import type {
MessageState,
MessageType,
} from "@core/stores/messageStore/index.ts";
import type { Types } from "@meshtastic/core";
type NodeNum = number;
type MessageId = number;
@@ -31,31 +34,31 @@ type GetMessagesParams =
type SetMessageStateParams =
| {
type: MessageType.Direct;
nodeA: NodeNum;
nodeB: NodeNum;
messageId: MessageId; // ID of the message within that chat
newState?: MessageState; // Optional new state, defaults to Ack
}
type: MessageType.Direct;
nodeA: NodeNum;
nodeB: NodeNum;
messageId: MessageId; // ID of the message within that chat
newState?: MessageState; // Optional new state, defaults to Ack
}
| {
type: MessageType.Broadcast;
channelId: ChannelId;
messageId: MessageId;
newState?: MessageState; // Optional new state, defaults to Ack
};
type: MessageType.Broadcast;
channelId: ChannelId;
messageId: MessageId;
newState?: MessageState; // Optional new state, defaults to Ack
};
type ClearMessageParams =
| {
type: MessageType.Direct;
nodeA: NodeNum;
nodeB: NodeNum;
messageId: MessageId;
}
type: MessageType.Direct;
nodeA: NodeNum;
nodeB: NodeNum;
messageId: MessageId;
}
| {
type: MessageType.Broadcast;
channelId: ChannelId;
messageId: MessageId;
};
type: MessageType.Broadcast;
channelId: ChannelId;
messageId: MessageId;
};
export type {
ChannelId,
+20 -15
View File
@@ -1,4 +1,5 @@
import React, { createContext, useContext, useMemo, useState } from "react";
import type React from "react";
import { createContext, useContext, useMemo, useState } from "react";
interface SidebarContextProps {
isCollapsed: boolean;
@@ -10,25 +11,29 @@ const SidebarContext = createContext<SidebarContextProps | undefined>(
undefined,
);
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = (
{ children },
) => {
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
const toggleSidebar = useMemo(() => () => {
setIsCollapsed((prev) => !prev);
}, []);
const toggleSidebar = useMemo(
() => () => {
setIsCollapsed((prev) => !prev);
},
[],
);
const value = useMemo(() => ({
isCollapsed,
setIsCollapsed,
toggleSidebar,
}), [isCollapsed, toggleSidebar]);
const value = useMemo(
() => ({
isCollapsed,
setIsCollapsed,
toggleSidebar,
}),
[isCollapsed, toggleSidebar],
);
return (
<SidebarContext.Provider value={value}>
{children}
</SidebarContext.Provider>
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
);
};
+20 -10
View File
@@ -1,6 +1,13 @@
import { PersistStorage, StateStorage, StorageValue } from "zustand/middleware";
import type {
ChannelId,
MessageLogMap,
} from "@core/stores/messageStore/types.ts";
import { del, get, set } from "idb-keyval";
import { ChannelId, MessageLogMap } from "@core/stores/messageStore/types.ts";
import type {
PersistStorage,
StateStorage,
StorageValue,
} from "zustand/middleware";
type PersistedMessageState = {
messages: {
@@ -26,8 +33,8 @@ type SerializedMap<K = unknown, V = unknown> = {
__dataType: "Map";
value: Array<[K, V]>;
};
// deno-lint-ignore no-explicit-any
type JsonReplacer = (this: any, key: string, value: unknown) => unknown;
type JsonReplacer = (key: string, value: unknown) => unknown;
const replacer: JsonReplacer = (_, value) => {
if (value instanceof Map) {
const map = value as Map<unknown, unknown>;
@@ -39,8 +46,8 @@ const replacer: JsonReplacer = (_, value) => {
}
return value;
};
// deno-lint-ignore no-explicit-any
type JsonReviver = (this: any, key: string, value: unknown) => unknown;
type JsonReviver = (key: string, value: unknown) => unknown;
function isSerializedMap(value: unknown): value is SerializedMap {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
@@ -60,11 +67,14 @@ export const storageWithMapSupport: PersistStorage<PersistedMessageState> = {
name,
): Promise<StorageValue<PersistedMessageState> | null> => {
const str = await zustandIndexDBStorage.getItem(name);
if (!str) return null;
if (!str) {
return null;
}
try {
const parsed = JSON.parse(str, reviver) as StorageValue<
PersistedMessageState
>;
const parsed = JSON.parse(
str,
reviver,
) as StorageValue<PersistedMessageState>;
return parsed;
} catch (error) {
console.error(`Error parsing persisted state (${name}):`, error);
+7 -7
View File
@@ -1,8 +1,8 @@
import type { Device } from "@core/stores/deviceStore.ts";
import { MeshDevice, Protobuf } from "@meshtastic/core";
import { type MessageStore, MessageType } from "./stores/messageStore/index.ts";
import { ensureDefaultUser } from "@core/dto/NodeNumToNodeInfoDTO.ts";
import PacketToMessageDTO from "@core/dto/PacketToMessageDTO.ts";
import NodeInfoFactory from "@core/dto/NodeNumToNodeInfoDTO.ts";
import type { Device } from "@core/stores/deviceStore.ts";
import { type MeshDevice, Protobuf } from "@meshtastic/core";
import { type MessageStore, MessageType } from "./stores/messageStore/index.ts";
export const subscribeAll = (
device: Device,
@@ -65,7 +65,7 @@ export const subscribeAll = (
});
connection.events.onNodeInfoPacket.subscribe((nodeInfo) => {
const nodeWithUser = NodeInfoFactory.ensureDefaultUser(nodeInfo);
const nodeWithUser = ensureDefaultUser(nodeInfo);
device.addNodeInfo(nodeWithUser);
});
@@ -85,11 +85,11 @@ export const subscribeAll = (
const message = dto.toMessage();
messageStore.saveMessage(message);
if (message.type == MessageType.Direct) {
if (message.type === MessageType.Direct) {
if (message.to === myNodeNum) {
device.incrementUnread(messagePacket.from);
}
} else if (message.type == MessageType.Broadcast) {
} else if (message.type === MessageType.Broadcast) {
if (message.from !== myNodeNum) {
device.incrementUnread(message.channel);
}
+40
View File
@@ -0,0 +1,40 @@
export interface RGBColor {
r: number;
g: number;
b: number;
a: number;
}
export const hexToRgb = (hex: number): RGBColor => ({
r: (hex & 0xff0000) >> 16,
g: (hex & 0x00ff00) >> 8,
b: hex & 0x0000ff,
a: 255,
});
export const rgbToHex = (c: RGBColor): number =>
(Math.round(c.a) << 24) |
(Math.round(c.r) << 16) |
(Math.round(c.g) << 8) |
Math.round(c.b);
export const isLightColor = (c: RGBColor): boolean =>
(c.r * 299 + c.g * 587 + c.b * 114) / 1000 > 127.5;
export const getColorFromText = (text: string): RGBColor => {
if (!text) {
return { r: 0, g: 0, b: 0, a: 255 };
}
let hash = 0;
for (let i = 0; i < text.length; i++) {
hash = text.charCodeAt(i) + ((hash << 5) - hash);
hash |= 0; // force 32‑bit
}
return {
r: (hash & 0xff0000) >> 16,
g: (hash & 0x00ff00) >> 8,
b: hash & 0x0000ff,
a: 255,
};
};
+1 -4
View File
@@ -39,10 +39,7 @@ describe("dotPaths", () => {
it("handles mixed nested structures", () => {
const obj = {
a: [
{ b: 1, c: [2, 3] },
{ d: { e: 4 } },
],
a: [{ b: 1, c: [2, 3] }, { d: { e: 4 } }],
f: 5,
};
expect(dotPaths(obj)).toEqual([
+3 -6
View File
@@ -1,19 +1,16 @@
export type DotPath = { [key: string]: unknown } | unknown[];
export const dotPaths = <T extends DotPath>(
obj: T,
prefix = "",
): string[] => {
export const dotPaths = <T extends DotPath>(obj: T, prefix = ""): string[] => {
if (Array.isArray(obj)) {
return obj.flatMap((v, i) =>
v && typeof v === "object"
? dotPaths(v as DotPath, `${prefix}${i}.`)
: [`${prefix}${i}`]
: [`${prefix}${i}`],
);
}
return Object.entries(obj).flatMap(([k, v]) =>
v && typeof v === "object"
? dotPaths(v as DotPath, `${prefix}${k}.`)
: [`${prefix}${k}`]
: [`${prefix}${k}`],
);
};
+1 -1
View File
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { eventBus } from "@core/utils/eventBus.ts";
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("EventBus", () => {
beforeEach(() => {
+6 -2
View File
@@ -26,7 +26,9 @@ class EventBus {
}
public off<T extends EventName>(event: T, callback: EventCallback<T>): void {
if (!this.listeners[event]) return;
if (!this.listeners[event]) {
return;
}
const callbackIndex = this.listeners[event]?.indexOf(callback);
if (callbackIndex !== undefined && callbackIndex > -1) {
@@ -43,7 +45,9 @@ class EventBus {
}
public emit<T extends EventName>(event: T, data: EventMap[T]): void {
if (!this.listeners[event]) return;
if (!this.listeners[event]) {
return;
}
this.listeners[event]?.forEach((callback) => {
callback(data);
+2 -1
View File
@@ -62,7 +62,8 @@ export default function newGithubIssueUrl(
}
function validateOptions(options: GithubIssueUrlOptions): ValidatedOptions {
const repoUrl = options.repoUrl ??
const repoUrl =
options.repoUrl ??
(options.user && options.repo
? `https://github.com/${options.user}/${options.repo}`
: undefined);
+2 -3
View File
@@ -1,7 +1,7 @@
export function intlSort<T extends PropertyKey>(
arr: T[],
order: "asc" | "desc" = "asc",
locale: Intl.Locale,
order: "asc" | "desc" = "asc",
): T[] {
const collator = new Intl.Collator(locale, { sensitivity: "base" });
@@ -11,8 +11,7 @@ export function intlSort<T extends PropertyKey>(
if (order === "asc") {
return collator.compare(stringA, stringB);
} else {
return collator.compare(stringB, stringA);
}
return collator.compare(stringB, stringA);
});
}
+2 -1
View File
@@ -41,7 +41,8 @@ export function validateMaxByteLength(
): LengthValidationResult {
// Ensure maxByteLength is valid
if (
typeof maxByteLength !== "number" || !Number.isInteger(maxByteLength) ||
typeof maxByteLength !== "number" ||
!Number.isInteger(maxByteLength) ||
maxByteLength < 0
) {
console.warn(