refactor: switch to using Bun (#718)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>(() => {});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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([
|
||||
|
||||
@@ -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,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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user