chore: lint/format all files (#604)
* chore: lint/format all files * Fix config sidebar button state (#602) * chore: Update deno.lock version and add Radix UI slider component (#601) * fix: improve how table addresses even/odd rows --------- Co-authored-by: philon- <philon-@users.noreply.github.com> Co-authored-by: Kamil Dzieniszewski <kamil.dzieniszewski@gmail.com>
This commit is contained in:
co-authored by
philon-
Kamil Dzieniszewski
parent
b7bdb1a502
commit
480ca46a95
@@ -3,7 +3,7 @@ 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 userIdHex = num.toString(16).toUpperCase().padStart(2, "0");
|
||||
const userId = `!${userIdHex}`;
|
||||
const last4 = userIdHex.slice(-4);
|
||||
const longName = `Meshtastic ${last4}`;
|
||||
@@ -19,14 +19,19 @@ class NodeInfoFactory {
|
||||
});
|
||||
}
|
||||
|
||||
public static ensureDefaultUser(node: Protobuf.Mesh.NodeInfo): Protobuf.Mesh.NodeInfo {
|
||||
public static ensureDefaultUser(
|
||||
node: Protobuf.Mesh.NodeInfo,
|
||||
): Protobuf.Mesh.NodeInfo {
|
||||
if (!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);
|
||||
console.error(
|
||||
`NodeInfoFactory.ensureDefaultUser: Cannot create default user for node because 'num' is missing.`,
|
||||
node,
|
||||
);
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -37,4 +42,4 @@ class NodeInfoFactory {
|
||||
}
|
||||
}
|
||||
|
||||
export default NodeInfoFactory;
|
||||
export default NodeInfoFactory;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { Types } from "@meshtastic/js";
|
||||
import { Message, MessageType, MessageState } from "../stores/messageStore/index.ts";
|
||||
import {
|
||||
Message,
|
||||
MessageState,
|
||||
MessageType,
|
||||
} from "../stores/messageStore/index.ts";
|
||||
|
||||
class PacketToMessageDTO {
|
||||
channel: Types.ChannelNumber;
|
||||
@@ -16,9 +20,13 @@ 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) {
|
||||
@@ -27,9 +35,11 @@ class PacketToMessageDTO {
|
||||
if (!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.`);
|
||||
} 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.`,
|
||||
);
|
||||
}
|
||||
this.date = dateTimestamp;
|
||||
}
|
||||
@@ -48,4 +58,4 @@ class PacketToMessageDTO {
|
||||
}
|
||||
}
|
||||
|
||||
export default PacketToMessageDTO;
|
||||
export default PacketToMessageDTO;
|
||||
|
||||
@@ -4,7 +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);
|
||||
|
||||
@@ -19,7 +21,7 @@ export function useCopyToClipboard({ timeout = 2000 }: UseCopyToClipboardProps =
|
||||
const copy = useCallback(
|
||||
async (text: string) => {
|
||||
if (!navigator?.clipboard) {
|
||||
console.warn('Clipboard API not available');
|
||||
console.warn("Clipboard API not available");
|
||||
setIsCopied(false);
|
||||
return false;
|
||||
}
|
||||
@@ -39,13 +41,13 @@ export function useCopyToClipboard({ timeout = 2000 }: UseCopyToClipboardProps =
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to copy text to clipboard:', error);
|
||||
console.error("Failed to copy text to clipboard:", error);
|
||||
setIsCopied(false);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[timeout]
|
||||
[timeout],
|
||||
);
|
||||
|
||||
return { isCopied, copy };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,16 @@ function isReminderExpired(expires?: string): boolean {
|
||||
export function useBackupReminder({
|
||||
enabled,
|
||||
message,
|
||||
onAccept = () => { },
|
||||
onAccept = () => {},
|
||||
reminderInDays = REMINDER_DAYS_ONE_WEEK,
|
||||
}: UseBackupReminderOptions) {
|
||||
const { toast } = useToast();
|
||||
const toastShownRef = useRef(false);
|
||||
const [reminderState, setReminderState] = useLocalStorage<ReminderState | null>(
|
||||
const [reminderState, setReminderState] = useLocalStorage<
|
||||
ReminderState | null
|
||||
>(
|
||||
STORAGE_KEY,
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
const setReminderExpiry = useCallback((days: number) => {
|
||||
@@ -73,7 +75,7 @@ export function useBackupReminder({
|
||||
setReminderExpiry(reminderInDays);
|
||||
}}
|
||||
>
|
||||
Remind me in {reminderInDays} day{reminderInDays > 1 ? 's' : ''}
|
||||
Remind me in {reminderInDays} day{reminderInDays > 1 ? "s" : ""}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -108,6 +110,5 @@ export function useBackupReminder({
|
||||
enabled,
|
||||
message,
|
||||
onAccept,
|
||||
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import useLocalStorage from './useLocalStorage'
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import useLocalStorage from "./useLocalStorage.ts";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe('useLocalStorage', () => {
|
||||
const key = 'test-key'
|
||||
describe("useLocalStorage", () => {
|
||||
const key = "test-key";
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should initialize with initial value if localStorage is empty', () => {
|
||||
const { result } = renderHook(() => useLocalStorage(key, 'initial'))
|
||||
const [value] = result.current
|
||||
expect(value).toBe('initial')
|
||||
})
|
||||
it("should initialize with initial value if localStorage is empty", () => {
|
||||
const { result } = renderHook(() => useLocalStorage(key, "initial"));
|
||||
const [value] = result.current;
|
||||
expect(value).toBe("initial");
|
||||
});
|
||||
|
||||
it('should read existing value from localStorage', () => {
|
||||
localStorage.setItem(key, JSON.stringify('stored'))
|
||||
const { result } = renderHook(() => useLocalStorage(key, 'initial'))
|
||||
const [value] = result.current
|
||||
expect(value).toBe('stored')
|
||||
})
|
||||
it("should read existing value from localStorage", () => {
|
||||
localStorage.setItem(key, JSON.stringify("stored"));
|
||||
const { result } = renderHook(() => useLocalStorage(key, "initial"));
|
||||
const [value] = result.current;
|
||||
expect(value).toBe("stored");
|
||||
});
|
||||
|
||||
it('should update localStorage when setValue is called', () => {
|
||||
const { result } = renderHook(() => useLocalStorage(key, 'initial'))
|
||||
const [, setValue] = result.current
|
||||
it("should update localStorage when setValue is called", () => {
|
||||
const { result } = renderHook(() => useLocalStorage(key, "initial"));
|
||||
const [, setValue] = result.current;
|
||||
|
||||
act(() => {
|
||||
setValue('updated')
|
||||
})
|
||||
setValue("updated");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(key)).toBe(JSON.stringify('updated'))
|
||||
expect(result.current[0]).toBe('updated')
|
||||
})
|
||||
expect(localStorage.getItem(key)).toBe(JSON.stringify("updated"));
|
||||
expect(result.current[0]).toBe("updated");
|
||||
});
|
||||
|
||||
it('should remove value from localStorage when removeValue is called', () => {
|
||||
const { result } = renderHook(() => useLocalStorage(key, 'initial'))
|
||||
const [, setValue, removeValue] = result.current
|
||||
it("should remove value from localStorage when removeValue is called", () => {
|
||||
const { result } = renderHook(() => useLocalStorage(key, "initial"));
|
||||
const [, setValue, removeValue] = result.current;
|
||||
|
||||
act(() => {
|
||||
setValue('to-be-removed')
|
||||
})
|
||||
setValue("to-be-removed");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
removeValue()
|
||||
})
|
||||
removeValue();
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(key)).toBeNull()
|
||||
expect(result.current[0]).toBe('initial')
|
||||
})
|
||||
})
|
||||
expect(localStorage.getItem(key)).toBeNull();
|
||||
expect(result.current[0]).toBe("initial");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,8 +55,9 @@ export default function useLocalStorage<T>(
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
const defaultValue =
|
||||
initialValue instanceof Function ? initialValue() : initialValue;
|
||||
const defaultValue = initialValue instanceof Function
|
||||
? initialValue()
|
||||
: initialValue;
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
@@ -74,8 +75,9 @@ export default function useLocalStorage<T>(
|
||||
// Get from local storage then
|
||||
// parse stored json or return initialValue
|
||||
const readValue = useCallback((): T => {
|
||||
const initialValueToUse =
|
||||
initialValue instanceof Function ? initialValue() : initialValue;
|
||||
const initialValueToUse = initialValue instanceof Function
|
||||
? initialValue()
|
||||
: initialValue;
|
||||
|
||||
// Prevent build error "window is undefined" but keep working
|
||||
if (IS_SERVER) {
|
||||
@@ -83,7 +85,7 @@ export default function useLocalStorage<T>(
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
const raw = globalThis.localStorage.getItem(key);
|
||||
return raw ? deserializer(raw) : initialValueToUse;
|
||||
} catch (error) {
|
||||
console.warn(`Error reading localStorage key “${key}”:`, error);
|
||||
@@ -115,13 +117,13 @@ export default function useLocalStorage<T>(
|
||||
const newValue = value instanceof Function ? value(readValue()) : value;
|
||||
|
||||
// Save to local storage
|
||||
window.localStorage.setItem(key, serializer(newValue));
|
||||
globalThis.localStorage.setItem(key, serializer(newValue));
|
||||
|
||||
// Save state
|
||||
setStoredValue(newValue);
|
||||
|
||||
// We dispatch a custom event so every similar useLocalStorage hook is notified
|
||||
window.dispatchEvent(new StorageEvent("local-storage", { key }));
|
||||
globalThis.dispatchEvent(new StorageEvent("local-storage", { key }));
|
||||
} catch (error) {
|
||||
console.warn(`Error setting localStorage key “${key}”:`, error);
|
||||
}
|
||||
@@ -137,17 +139,18 @@ export default function useLocalStorage<T>(
|
||||
);
|
||||
}
|
||||
|
||||
const defaultValue =
|
||||
initialValue instanceof Function ? initialValue() : initialValue;
|
||||
const defaultValue = initialValue instanceof Function
|
||||
? initialValue()
|
||||
: initialValue;
|
||||
|
||||
// Remove the key from local storage
|
||||
window.localStorage.removeItem(key);
|
||||
globalThis.localStorage.removeItem(key);
|
||||
|
||||
// Save state with default value
|
||||
setStoredValue(defaultValue);
|
||||
|
||||
// We dispatch a custom event so every similar useLocalStorage hook is notified
|
||||
window.dispatchEvent(new StorageEvent("local-storage", { key }));
|
||||
globalThis.dispatchEvent(new StorageEvent("local-storage", { key }));
|
||||
}, [key]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -176,4 +179,4 @@ export default function useLocalStorage<T>(
|
||||
}, []);
|
||||
|
||||
return [storedValue, setValue, removeValue];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { usePasswordVisibilityToggle } from './usePasswordVisibilityToggle.ts';
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { usePasswordVisibilityToggle } from "./usePasswordVisibilityToggle.ts";
|
||||
|
||||
describe('usePasswordVisibilityToggle Hook', () => {
|
||||
it('should initialize with visibility set to false by default', () => {
|
||||
describe("usePasswordVisibilityToggle Hook", () => {
|
||||
it("should initialize with visibility set to false by default", () => {
|
||||
const { result } = renderHook(() => usePasswordVisibilityToggle());
|
||||
expect(result.current.isVisible).toBe(false);
|
||||
expect(typeof result.current.toggleVisibility).toBe('function');
|
||||
expect(typeof result.current.toggleVisibility).toBe("function");
|
||||
});
|
||||
|
||||
it('should initialize with visibility set to true if initialVisible is true', () => {
|
||||
it("should initialize with visibility set to true if initialVisible is true", () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordVisibilityToggle({ initialVisible: true })
|
||||
);
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle visibility from false to true when toggleVisibility is called', () => {
|
||||
it("should toggle visibility from false to true when toggleVisibility is called", () => {
|
||||
const { result } = renderHook(() => usePasswordVisibilityToggle());
|
||||
expect(result.current.isVisible).toBe(false);
|
||||
act(() => {
|
||||
@@ -25,7 +25,7 @@ describe('usePasswordVisibilityToggle Hook', () => {
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle visibility from true to false when toggleVisibility is called', () => {
|
||||
it("should toggle visibility from true to false when toggleVisibility is called", () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordVisibilityToggle({ initialVisible: true })
|
||||
);
|
||||
@@ -36,7 +36,7 @@ describe('usePasswordVisibilityToggle Hook', () => {
|
||||
expect(result.current.isVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle visibility correctly multiple times', () => {
|
||||
it("should toggle visibility correctly multiple times", () => {
|
||||
const { result } = renderHook(() => usePasswordVisibilityToggle());
|
||||
expect(result.current.isVisible).toBe(false);
|
||||
act(() => {
|
||||
@@ -53,8 +53,10 @@ describe('usePasswordVisibilityToggle Hook', () => {
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a stable toggleVisibility function reference (due to useCallback)', () => {
|
||||
const { result, rerender } = renderHook(() => usePasswordVisibilityToggle());
|
||||
it("should return a stable toggleVisibility function reference (due to useCallback)", () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
usePasswordVisibilityToggle()
|
||||
);
|
||||
const initialToggleFunc = result.current.toggleVisibility;
|
||||
rerender();
|
||||
expect(result.current.toggleVisibility).toBe(initialToggleFunc);
|
||||
@@ -63,4 +65,4 @@ describe('usePasswordVisibilityToggle Hook', () => {
|
||||
});
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
interface UsePasswordVisibilityToggleProps {
|
||||
initialVisible?: boolean;
|
||||
@@ -6,15 +6,17 @@ interface UsePasswordVisibilityToggleProps {
|
||||
/**
|
||||
* Manages the state for toggling password visibility.
|
||||
*
|
||||
* @param {boolean} [options.initialVisible=false]
|
||||
* @returns {{isVisible: boolean, toggleVisibility: () => void}}
|
||||
* @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(() => {
|
||||
setIsVisible(prev => !prev);
|
||||
setIsVisible((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
return { isVisible, toggleVisibility };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { usePinnedItems } from "./usePinnedItems.ts";
|
||||
|
||||
const mockSetPinnedItems = vi.fn();
|
||||
const mockUseLocalStorage = vi.fn();
|
||||
|
||||
vi.mock("@core/hooks/useLocalStorage.ts", () => ({
|
||||
default: (...args: any[]) => mockUseLocalStorage(...args),
|
||||
default: (...args) => mockUseLocalStorage(...args),
|
||||
}));
|
||||
|
||||
describe("usePinnedItems", () => {
|
||||
@@ -45,7 +45,10 @@ describe("usePinnedItems", () => {
|
||||
});
|
||||
|
||||
it("removes an item if it's already pinned", () => {
|
||||
mockUseLocalStorage.mockReturnValue([["item1", "item2"], mockSetPinnedItems]);
|
||||
mockUseLocalStorage.mockReturnValue([
|
||||
["item1", "item2"],
|
||||
mockSetPinnedItems,
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePinnedItems({ storageName: "test-storage" })
|
||||
|
||||
@@ -2,13 +2,14 @@ import useLocalStorage from "@core/hooks/useLocalStorage.ts";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function usePinnedItems({ storageName }: { storageName: string }) {
|
||||
const [pinnedItems, setPinnedItems] = useLocalStorage<string[]>(storageName, []);
|
||||
const [pinnedItems, setPinnedItems] = useLocalStorage<string[]>(
|
||||
storageName,
|
||||
[],
|
||||
);
|
||||
|
||||
const togglePinnedItem = useCallback((label: string) => {
|
||||
setPinnedItems((prev) =>
|
||||
prev.includes(label)
|
||||
? prev.filter((g) => g !== label)
|
||||
: [...prev, label]
|
||||
prev.includes(label) ? prev.filter((g) => g !== label) : [...prev, label]
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,81 +1,79 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useToast } from "@core/hooks/useToast.ts"
|
||||
import { Button } from '@components/UI/Button.tsx'
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe('useToast', () => {
|
||||
describe("useToast", () => {
|
||||
beforeEach(() => {
|
||||
// Reset toast memory state before each test
|
||||
// our hook uses global memory to store toasts
|
||||
// @ts-expect-error - internal test reset
|
||||
globalThis.memoryState = { toasts: [] }
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
globalThis.memoryState = { toasts: [] };
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should create a toast with title, description, and action', () => {
|
||||
const { result } = renderHook(() => useToast())
|
||||
it("should create a toast with title, description, and action", () => {
|
||||
const { result } = renderHook(() => useToast());
|
||||
|
||||
act(() => {
|
||||
result.current.toast({
|
||||
title: 'Backup Reminder',
|
||||
description: 'Don\'t forget to backup!',
|
||||
action: <Button>Backup Now</Button>
|
||||
})
|
||||
vi.runAllTimers()
|
||||
})
|
||||
title: "Backup Reminder",
|
||||
description: "Don't forget to backup!",
|
||||
action: <Button>Backup Now</Button>,
|
||||
});
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
const toast = result.current.toasts[0]
|
||||
expect(result.current.toasts.length).toBe(1)
|
||||
expect(toast.title).toBe('Backup Reminder')
|
||||
expect(toast.description).toBe('Don\'t forget to backup!')
|
||||
expect(toast.action).toBeTruthy()
|
||||
expect(toast.open).toBe(true)
|
||||
})
|
||||
it('should dismiss a toast using returned dismiss function', () => {
|
||||
const { result } = renderHook(() => useToast())
|
||||
vi.useFakeTimers()
|
||||
const toast = result.current.toasts[0];
|
||||
expect(result.current.toasts.length).toBe(1);
|
||||
expect(toast.title).toBe("Backup Reminder");
|
||||
expect(toast.description).toBe("Don't forget to backup!");
|
||||
expect(toast.action).toBeTruthy();
|
||||
expect(toast.open).toBe(true);
|
||||
});
|
||||
it("should dismiss a toast using returned dismiss function", () => {
|
||||
const { result } = renderHook(() => useToast());
|
||||
vi.useFakeTimers();
|
||||
|
||||
let toastRef: { id: string, dismiss: () => void }
|
||||
let toastRef: { id: string; dismiss: () => void };
|
||||
|
||||
act(() => {
|
||||
toastRef = result.current.toast({ title: 'Dismiss Me' })
|
||||
vi.runAllTimers() // Flush ADD_TOAST
|
||||
})
|
||||
toastRef = result.current.toast({ title: "Dismiss Me" });
|
||||
vi.runAllTimers(); // Flush ADD_TOAST
|
||||
});
|
||||
|
||||
act(() => {
|
||||
toastRef.dismiss()
|
||||
})
|
||||
toastRef.dismiss();
|
||||
});
|
||||
|
||||
const toast = result.current.toasts.find(t => t.id === toastRef.id)
|
||||
expect(toast?.open).toBe(false)
|
||||
const toast = result.current.toasts.find((t) => t.id === toastRef.id);
|
||||
expect(toast?.open).toBe(false);
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should allow dismiss via hook dismiss function", () => {
|
||||
const { result } = renderHook(() => useToast());
|
||||
vi.useFakeTimers();
|
||||
|
||||
it('should allow dismiss via hook dismiss function', () => {
|
||||
const { result } = renderHook(() => useToast())
|
||||
vi.useFakeTimers()
|
||||
|
||||
let toastRef: { id: string }
|
||||
let toastRef: { id: string };
|
||||
|
||||
act(() => {
|
||||
toastRef = result.current.toast({ title: 'Manual Dismiss' })
|
||||
vi.runAllTimers()
|
||||
})
|
||||
toastRef = result.current.toast({ title: "Manual Dismiss" });
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.dismiss(toastRef.id)
|
||||
})
|
||||
result.current.dismiss(toastRef.id);
|
||||
});
|
||||
|
||||
const toast = result.current.toasts.find(t => t.id === toastRef.id)
|
||||
expect(toast?.open).toBe(false)
|
||||
const toast = result.current.toasts.find((t) => t.id === toastRef.id);
|
||||
expect(toast?.open).toBe(false);
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
})
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
+115
-39
@@ -17,7 +17,7 @@ export type DialogVariant = keyof Device["dialog"];
|
||||
type NodeError = {
|
||||
node: number;
|
||||
error: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Device {
|
||||
id: number;
|
||||
@@ -92,7 +92,6 @@ export interface Device {
|
||||
) => Protobuf.Mesh.NodeInfo[];
|
||||
getNodesLength: () => number;
|
||||
getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined;
|
||||
|
||||
}
|
||||
|
||||
export interface DeviceState {
|
||||
@@ -110,7 +109,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
remoteDevices: new Map(),
|
||||
|
||||
addDevice: (id: number) => {
|
||||
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
draft.devices.set(id, {
|
||||
@@ -164,14 +162,37 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
const device = draft.devices.get(id);
|
||||
if (device) {
|
||||
switch (config.payloadVariant.case) {
|
||||
case "device": { device.config.device = config.payloadVariant.value; break; }
|
||||
case "position": { device.config.position = config.payloadVariant.value; break; }
|
||||
case "power": { device.config.power = config.payloadVariant.value; break; }
|
||||
case "network": { device.config.network = config.payloadVariant.value; break; }
|
||||
case "display": { device.config.display = config.payloadVariant.value; break; }
|
||||
case "lora": { device.config.lora = config.payloadVariant.value; break; }
|
||||
case "bluetooth": { device.config.bluetooth = config.payloadVariant.value; break; }
|
||||
case "security": { device.config.security = config.payloadVariant.value; }
|
||||
case "device": {
|
||||
device.config.device = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "position": {
|
||||
device.config.position = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "power": {
|
||||
device.config.power = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "network": {
|
||||
device.config.network = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "display": {
|
||||
device.config.display = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "lora": {
|
||||
device.config.lora = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "bluetooth": {
|
||||
device.config.bluetooth = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "security": {
|
||||
device.config.security = config.payloadVariant.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -183,18 +204,63 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
const device = draft.devices.get(id);
|
||||
if (device) {
|
||||
switch (config.payloadVariant.case) {
|
||||
case "mqtt": { device.moduleConfig.mqtt = config.payloadVariant.value; break; }
|
||||
case "serial": { device.moduleConfig.serial = config.payloadVariant.value; break; }
|
||||
case "externalNotification": { device.moduleConfig.externalNotification = config.payloadVariant.value; break; }
|
||||
case "storeForward": { device.moduleConfig.storeForward = config.payloadVariant.value; break; }
|
||||
case "rangeTest": { device.moduleConfig.rangeTest = config.payloadVariant.value; break; }
|
||||
case "telemetry": { device.moduleConfig.telemetry = config.payloadVariant.value; break; }
|
||||
case "cannedMessage": { device.moduleConfig.cannedMessage = config.payloadVariant.value; break; }
|
||||
case "audio": { device.moduleConfig.audio = config.payloadVariant.value; break; }
|
||||
case "neighborInfo": { device.moduleConfig.neighborInfo = config.payloadVariant.value; break; }
|
||||
case "ambientLighting": { device.moduleConfig.ambientLighting = config.payloadVariant.value; break; }
|
||||
case "detectionSensor": { device.moduleConfig.detectionSensor = config.payloadVariant.value; break; }
|
||||
case "paxcounter": { device.moduleConfig.paxcounter = config.payloadVariant.value; break; }
|
||||
case "mqtt": {
|
||||
device.moduleConfig.mqtt = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "serial": {
|
||||
device.moduleConfig.serial = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "externalNotification": {
|
||||
device.moduleConfig.externalNotification =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "storeForward": {
|
||||
device.moduleConfig.storeForward =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "rangeTest": {
|
||||
device.moduleConfig.rangeTest =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "telemetry": {
|
||||
device.moduleConfig.telemetry =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "cannedMessage": {
|
||||
device.moduleConfig.cannedMessage =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "audio": {
|
||||
device.moduleConfig.audio = config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "neighborInfo": {
|
||||
device.moduleConfig.neighborInfo =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "ambientLighting": {
|
||||
device.moduleConfig.ambientLighting =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "detectionSensor": {
|
||||
device.moduleConfig.detectionSensor =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "paxcounter": {
|
||||
device.moduleConfig.paxcounter =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -216,13 +282,17 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
}),
|
||||
);
|
||||
},
|
||||
setWorkingModuleConfig: (moduleConfig: Protobuf.ModuleConfig.ModuleConfig) => {
|
||||
setWorkingModuleConfig: (
|
||||
moduleConfig: Protobuf.ModuleConfig.ModuleConfig,
|
||||
) => {
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) return;
|
||||
const index = device.workingModuleConfig.findIndex(
|
||||
(wmc) => wmc.payloadVariant.case === moduleConfig.payloadVariant.case,
|
||||
(wmc) =>
|
||||
wmc.payloadVariant.case ===
|
||||
moduleConfig.payloadVariant.case,
|
||||
);
|
||||
if (index !== -1) {
|
||||
device.workingModuleConfig[index] = moduleConfig;
|
||||
@@ -277,7 +347,9 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
produce<DeviceState>((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;
|
||||
} else {
|
||||
@@ -288,8 +360,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
);
|
||||
},
|
||||
addNodeInfo: (nodeInfo) => {
|
||||
console.log("Node Info", nodeInfo);
|
||||
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
@@ -316,11 +386,12 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const currentNode = device.nodesMap.get(user.from) ?? create(Protobuf.Mesh.NodeInfoSchema);
|
||||
const currentNode = device.nodesMap.get(user.from) ??
|
||||
create(Protobuf.Mesh.NodeInfoSchema);
|
||||
currentNode.user = user.data;
|
||||
currentNode.num = user.from;
|
||||
device.nodesMap.set(user.from, currentNode);
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
addPosition: (position) => {
|
||||
@@ -330,11 +401,12 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const currentNode = device.nodesMap.get(position.from) ?? create(Protobuf.Mesh.NodeInfoSchema);
|
||||
const currentNode = device.nodesMap.get(position.from) ??
|
||||
create(Protobuf.Mesh.NodeInfoSchema);
|
||||
currentNode.position = position.data;
|
||||
currentNode.num = position.from;
|
||||
device.nodesMap.set(position.from, currentNode);
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
addConnection: (connection) => {
|
||||
@@ -373,10 +445,11 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
device.nodesMap.delete(nodeNum);
|
||||
}))
|
||||
}),
|
||||
);
|
||||
},
|
||||
setDialogOpen: (dialog: DialogVariant, open: boolean) => {
|
||||
set(
|
||||
@@ -463,7 +536,7 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
if (!device) return;
|
||||
const currentCount = device.unreadCounts.get(nodeNum) ?? 0;
|
||||
device.unreadCounts.set(nodeNum, currentCount + 1);
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
resetUnread: (nodeNum: number) => {
|
||||
@@ -475,16 +548,19 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
if (device.unreadCounts.get(nodeNum) === 0) {
|
||||
device.unreadCounts.delete(nodeNum);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
getNodes: (filter?: (node: Protobuf.Mesh.NodeInfo) => boolean): Protobuf.Mesh.NodeInfo[] => {
|
||||
getNodes: (
|
||||
filter?: (node: Protobuf.Mesh.NodeInfo) => boolean,
|
||||
): Protobuf.Mesh.NodeInfo[] => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return [];
|
||||
}
|
||||
const allNodes = Array.from(device.nodesMap.values()).filter(
|
||||
(node) => node.num !== get().devices.get(id)?.hardware.myNodeNum);
|
||||
(node) => node.num !== get().devices.get(id)?.hardware.myNodeNum,
|
||||
);
|
||||
if (filter) {
|
||||
return allNodes.filter(filter);
|
||||
}
|
||||
@@ -505,7 +581,7 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
if (!device) {
|
||||
return 0;
|
||||
}
|
||||
return device.nodesMap.size
|
||||
return device.nodesMap.size;
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
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 { ChannelId, ClearMessageParams, ConversationId, GetMessagesParams, Message, MessageId, MessageLogMap, NodeNum, SetMessageStateParams } from "@core/stores/messageStore/types.ts";
|
||||
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 {
|
||||
ChannelId,
|
||||
ClearMessageParams,
|
||||
ConversationId,
|
||||
GetMessagesParams,
|
||||
Message,
|
||||
MessageId,
|
||||
MessageLogMap,
|
||||
NodeNum,
|
||||
SetMessageStateParams,
|
||||
} from "@core/stores/messageStore/types.ts";
|
||||
|
||||
export enum MessageState {
|
||||
Ack = "ack",
|
||||
@@ -16,8 +26,11 @@ export enum MessageType {
|
||||
Broadcast = "broadcast",
|
||||
}
|
||||
|
||||
export function getConversationId(node1: NodeNum, node2: NodeNum): ConversationId {
|
||||
return [node1, node2].sort((a, b) => a - b).join(':');
|
||||
export function getConversationId(
|
||||
node1: NodeNum,
|
||||
node2: NodeNum,
|
||||
): ConversationId {
|
||||
return [node1, node2].sort((a, b) => a - b).join(":");
|
||||
}
|
||||
|
||||
export interface MessageStore {
|
||||
@@ -25,9 +38,9 @@ export interface MessageStore {
|
||||
direct: Map<ConversationId, MessageLogMap>;
|
||||
broadcast: Map<ChannelId, MessageLogMap>;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface MessageStore {
|
||||
messages: MessageStore['messages'];
|
||||
messages: MessageStore["messages"];
|
||||
draft: Map<Types.Destination, string>;
|
||||
nodeNum: number; // This device's node number
|
||||
activeChat: number; // Represents otherNodeNum for Direct, or channel for Broadcast
|
||||
@@ -47,7 +60,7 @@ export interface MessageStore {
|
||||
clearDraft: (key: Types.Destination) => void;
|
||||
}
|
||||
|
||||
const CURRENT_STORE_VERSION = 0;
|
||||
// const CURRENT_STORE_VERSION = 0;
|
||||
|
||||
export const useMessageStore = create<MessageStore>()(
|
||||
// persist(
|
||||
@@ -82,17 +95,29 @@ export const useMessageStore = create<MessageStore>()(
|
||||
if (message.type === MessageType.Direct) {
|
||||
const conversationId = getConversationId(message.from, message.to);
|
||||
if (!state.messages.direct.has(conversationId)) {
|
||||
state.messages.direct.set(conversationId, new Map<MessageId, Message>());
|
||||
state.messages.direct.set(
|
||||
conversationId,
|
||||
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)) {
|
||||
state.messages.broadcast.set(channelId, new Map<MessageId, Message>());
|
||||
state.messages.broadcast.set(
|
||||
channelId,
|
||||
new Map<MessageId, Message>(),
|
||||
);
|
||||
}
|
||||
state.messages.broadcast.get(channelId)!.set(message.messageId, message);
|
||||
state.messages.broadcast.get(channelId)!.set(
|
||||
message.messageId,
|
||||
message,
|
||||
);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -103,7 +128,10 @@ export const useMessageStore = create<MessageStore>()(
|
||||
let targetMessage: Message | undefined;
|
||||
|
||||
if (params.type === MessageType.Direct) {
|
||||
const conversationId = getConversationId(params.nodeA, params.nodeB);
|
||||
const conversationId = getConversationId(
|
||||
params.nodeA,
|
||||
params.nodeB,
|
||||
);
|
||||
messageLog = state.messages.direct.get(conversationId);
|
||||
if (messageLog) {
|
||||
targetMessage = messageLog.get(params.messageId);
|
||||
@@ -118,9 +146,13 @@ export const useMessageStore = create<MessageStore>()(
|
||||
if (targetMessage) {
|
||||
targetMessage.state = params.newState ?? MessageState.Ack;
|
||||
} else {
|
||||
console.warn(`Message or conversation/channel not found for state update. Params: ${JSON.stringify(params)}`);
|
||||
console.warn(
|
||||
`Message or conversation/channel not found for state update. Params: ${
|
||||
JSON.stringify(params)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
getMessages: (params: GetMessagesParams): Message[] => {
|
||||
@@ -130,7 +162,6 @@ export const useMessageStore = create<MessageStore>()(
|
||||
if (params.type === MessageType.Direct) {
|
||||
const conversationId = getConversationId(params.nodeA, params.nodeB);
|
||||
messageMap = state.messages.direct.get(conversationId);
|
||||
|
||||
} else {
|
||||
messageMap = state.messages.broadcast.get(params.channelId);
|
||||
}
|
||||
@@ -165,23 +196,29 @@ export const useMessageStore = create<MessageStore>()(
|
||||
const deleted = messageLog.delete(params.messageId);
|
||||
|
||||
if (deleted) {
|
||||
console.log(`Deleted message ${params.messageId} from ${params.type} message ${parentKey}`);
|
||||
console.log(
|
||||
`Deleted message ${params.messageId} from ${params.type} message ${parentKey}`,
|
||||
);
|
||||
// Clean up empty MessageLogMap and its entry in the parent map
|
||||
if (messageLog.size === 0) {
|
||||
parentMap.delete(parentKey);
|
||||
console.log(`Cleaned up empty message entry for ${parentKey}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Message ${params.messageId} not found in ${params.type} chat ${parentKey} for deletion.`);
|
||||
console.warn(
|
||||
`Message ${params.messageId} not found in ${params.type} chat ${parentKey} for deletion.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Message entry ${parentKey} not found for message deletion.`);
|
||||
console.warn(
|
||||
`Message entry ${parentKey} not found for message deletion.`,
|
||||
);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
getDraft: (key) => {
|
||||
return get().draft.get(key) ?? '';
|
||||
return get().draft.get(key) ?? "";
|
||||
},
|
||||
setDraft: (key, message) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
@@ -198,7 +235,7 @@ export const useMessageStore = create<MessageStore>()(
|
||||
state.messages.direct = new Map<ConversationId, MessageLogMap>();
|
||||
state.messages.broadcast = new Map<ChannelId, MessageLogMap>();
|
||||
}));
|
||||
}
|
||||
},
|
||||
}),
|
||||
// {
|
||||
// name: 'meshtastic-message-store',
|
||||
@@ -209,4 +246,4 @@ export const useMessageStore = create<MessageStore>()(
|
||||
// nodeNum: state.nodeNum,
|
||||
// }),
|
||||
// })
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
useMessageStore,
|
||||
MessageType,
|
||||
MessageState,
|
||||
getConversationId,
|
||||
} from './index.ts';
|
||||
import type { ConversationId, ChannelId, MessageLogMap, Message } from './types.ts';
|
||||
import { Types } from '@meshtastic/core';
|
||||
MessageState,
|
||||
MessageType,
|
||||
useMessageStore,
|
||||
} from "./index.ts";
|
||||
import type {
|
||||
ChannelId,
|
||||
ConversationId,
|
||||
Message,
|
||||
MessageLogMap,
|
||||
} from "./types.ts";
|
||||
import { Types } from "@meshtastic/core";
|
||||
|
||||
vi.mock('../storage/indexDB.ts', () => {
|
||||
let memoryStorage: Record<string, string> = {};
|
||||
vi.mock("../storage/indexDB.ts", () => {
|
||||
const memoryStorage: Record<string, string> = {};
|
||||
return {
|
||||
storageWithMapSupport: {
|
||||
getItem: vi.fn(async (name: string): Promise<string | null> => {
|
||||
return memoryStorage[name] ?? null;
|
||||
return await memoryStorage[name] ?? null;
|
||||
}),
|
||||
setItem: vi.fn(async (name: string, value: string): Promise<void> => {
|
||||
memoryStorage[name] = value;
|
||||
memoryStorage[name] = await value;
|
||||
}),
|
||||
removeItem: vi.fn(async (name: string): Promise<void> => {
|
||||
delete memoryStorage[name];
|
||||
await delete memoryStorage[name];
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -38,7 +43,7 @@ const directMessageToOther1: Message = {
|
||||
date: Date.now(),
|
||||
messageId: 101,
|
||||
state: MessageState.Waiting,
|
||||
message: 'Hello other 1 from me',
|
||||
message: "Hello other 1 from me",
|
||||
};
|
||||
|
||||
const directMessageFromOther1: Message = {
|
||||
@@ -49,7 +54,7 @@ const directMessageFromOther1: Message = {
|
||||
date: Date.now() + 1000,
|
||||
messageId: 102,
|
||||
state: MessageState.Waiting,
|
||||
message: 'Hello me from other 1',
|
||||
message: "Hello me from other 1",
|
||||
};
|
||||
|
||||
const directMessageToOther2: Message = {
|
||||
@@ -60,7 +65,7 @@ const directMessageToOther2: Message = {
|
||||
date: Date.now() + 2000,
|
||||
messageId: 103,
|
||||
state: MessageState.Waiting,
|
||||
message: 'Hello other 2 from me',
|
||||
message: "Hello other 2 from me",
|
||||
};
|
||||
|
||||
const broadcastMessage1: Message = {
|
||||
@@ -71,7 +76,7 @@ const broadcastMessage1: Message = {
|
||||
date: Date.now() + 3000,
|
||||
messageId: 201,
|
||||
state: MessageState.Waiting,
|
||||
message: 'Broadcast message 1',
|
||||
message: "Broadcast message 1",
|
||||
};
|
||||
|
||||
const broadcastMessage2: Message = {
|
||||
@@ -82,10 +87,10 @@ const broadcastMessage2: Message = {
|
||||
date: Date.now() + 4000,
|
||||
messageId: 202,
|
||||
state: MessageState.Waiting,
|
||||
message: 'Broadcast message 2',
|
||||
message: "Broadcast message 2",
|
||||
};
|
||||
|
||||
describe('useMessageStore', () => {
|
||||
describe("useMessageStore", () => {
|
||||
const initialState = useMessageStore.getState();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -97,10 +102,9 @@ describe('useMessageStore', () => {
|
||||
},
|
||||
draft: new Map<Types.Destination, string>(),
|
||||
}, true);
|
||||
|
||||
});
|
||||
|
||||
it('should have correct initial state', () => {
|
||||
it("should have correct initial state", () => {
|
||||
const state = useMessageStore.getState();
|
||||
expect(state.messages.direct).toBeInstanceOf(Map);
|
||||
expect(state.messages.direct.size).toBe(0);
|
||||
@@ -113,23 +117,26 @@ describe('useMessageStore', () => {
|
||||
expect(state.chatType).toBe(MessageType.Broadcast);
|
||||
});
|
||||
|
||||
it('should set nodeNum', () => {
|
||||
it("should set nodeNum", () => {
|
||||
useMessageStore.getState().setNodeNum(myNodeNum);
|
||||
expect(useMessageStore.getState().nodeNum).toBe(myNodeNum);
|
||||
});
|
||||
|
||||
it('should set activeChat and chatType', () => {
|
||||
it("should set activeChat and chatType", () => {
|
||||
useMessageStore.getState().setActiveChat(otherNodeNum1);
|
||||
useMessageStore.getState().setChatType(MessageType.Direct);
|
||||
expect(useMessageStore.getState().activeChat).toBe(otherNodeNum1);
|
||||
expect(useMessageStore.getState().chatType).toBe(MessageType.Direct);
|
||||
});
|
||||
|
||||
describe('saveMessage', () => {
|
||||
it('should save a direct message with correct Map structure', () => {
|
||||
describe("saveMessage", () => {
|
||||
it("should save a direct message with correct Map structure", () => {
|
||||
useMessageStore.getState().saveMessage(directMessageToOther1);
|
||||
const state = useMessageStore.getState();
|
||||
const conversationId = getConversationId(directMessageToOther1.from, directMessageToOther1.to);
|
||||
const conversationId = getConversationId(
|
||||
directMessageToOther1.from,
|
||||
directMessageToOther1.to,
|
||||
);
|
||||
|
||||
// Check if the conversation Map exists
|
||||
expect(state.messages.direct.has(conversationId)).toBe(true);
|
||||
@@ -139,10 +146,12 @@ describe('useMessageStore', () => {
|
||||
// Check if the message exists within the inner Map
|
||||
expect(conversationLog?.has(directMessageToOther1.messageId)).toBe(true);
|
||||
// Check the message content
|
||||
expect(conversationLog?.get(directMessageToOther1.messageId)).toEqual(directMessageToOther1);
|
||||
expect(conversationLog?.get(directMessageToOther1.messageId)).toEqual(
|
||||
directMessageToOther1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should save a broadcast message with correct Map structure', () => {
|
||||
it("should save a broadcast message with correct Map structure", () => {
|
||||
useMessageStore.getState().saveMessage(broadcastMessage1);
|
||||
const state = useMessageStore.getState();
|
||||
const channelId = broadcastMessage1.channel;
|
||||
@@ -151,10 +160,12 @@ describe('useMessageStore', () => {
|
||||
const channelLog = state.messages.broadcast.get(channelId);
|
||||
expect(channelLog).toBeInstanceOf(Map);
|
||||
expect(channelLog?.has(broadcastMessage1.messageId)).toBe(true);
|
||||
expect(channelLog?.get(broadcastMessage1.messageId)).toEqual(broadcastMessage1);
|
||||
expect(channelLog?.get(broadcastMessage1.messageId)).toEqual(
|
||||
broadcastMessage1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should save multiple messages correctly', () => {
|
||||
it("should save multiple messages correctly", () => {
|
||||
useMessageStore.getState().saveMessage(directMessageToOther1);
|
||||
useMessageStore.getState().saveMessage(directMessageFromOther1);
|
||||
useMessageStore.getState().saveMessage(broadcastMessage1);
|
||||
@@ -162,16 +173,28 @@ describe('useMessageStore', () => {
|
||||
const state = useMessageStore.getState();
|
||||
|
||||
const convId1 = getConversationId(myNodeNum, otherNodeNum1);
|
||||
expect(state.messages.direct.get(convId1)?.get(directMessageToOther1.messageId)).toEqual(directMessageToOther1);
|
||||
expect(
|
||||
state.messages.direct.get(convId1)?.get(
|
||||
directMessageToOther1.messageId,
|
||||
),
|
||||
).toEqual(directMessageToOther1);
|
||||
|
||||
expect(state.messages.direct.get(convId1)?.get(directMessageFromOther1.messageId)).toEqual(directMessageFromOther1);
|
||||
expect(
|
||||
state.messages.direct.get(convId1)?.get(
|
||||
directMessageFromOther1.messageId,
|
||||
),
|
||||
).toEqual(directMessageFromOther1);
|
||||
|
||||
const channelId = broadcastMessage1.channel;
|
||||
expect(state.messages.broadcast.get(channelId)?.get(broadcastMessage1.messageId)).toEqual(broadcastMessage1);
|
||||
expect(
|
||||
state.messages.broadcast.get(channelId)?.get(
|
||||
broadcastMessage1.messageId,
|
||||
),
|
||||
).toEqual(broadcastMessage1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessages', () => {
|
||||
describe("getMessages", () => {
|
||||
beforeEach(() => {
|
||||
useMessageStore.getState().setNodeNum(myNodeNum);
|
||||
useMessageStore.getState().saveMessage(directMessageToOther1);
|
||||
@@ -181,56 +204,56 @@ describe('useMessageStore', () => {
|
||||
useMessageStore.getState().saveMessage(broadcastMessage2);
|
||||
});
|
||||
|
||||
it('should return broadcast messages for a channel, sorted by date', () => {
|
||||
it("should return broadcast messages for a channel, sorted by date", () => {
|
||||
const messages = useMessageStore.getState().getMessages({
|
||||
type: MessageType.Broadcast,
|
||||
channelId: broadcastChannel
|
||||
channelId: broadcastChannel,
|
||||
});
|
||||
expect(messages).toHaveLength(2);
|
||||
expect(messages[0]).toEqual(broadcastMessage1);
|
||||
expect(messages[1]).toEqual(broadcastMessage2);
|
||||
});
|
||||
|
||||
it('should return empty array for broadcast if channel has no messages', () => {
|
||||
it("should return empty array for broadcast if channel has no messages", () => {
|
||||
const messages = useMessageStore.getState().getMessages({
|
||||
type: MessageType.Broadcast,
|
||||
channelId: Types.ChannelNumber.Channel1
|
||||
channelId: Types.ChannelNumber.Channel1,
|
||||
});
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return combined direct messages for a specific chat pair, sorted by date', () => {
|
||||
it("should return combined direct messages for a specific chat pair, sorted by date", () => {
|
||||
const messages = useMessageStore.getState().getMessages({
|
||||
type: MessageType.Direct,
|
||||
nodeA: myNodeNum,
|
||||
nodeB: otherNodeNum1
|
||||
nodeB: otherNodeNum1,
|
||||
});
|
||||
expect(messages).toHaveLength(2);
|
||||
expect(messages[0]).toEqual(directMessageToOther1);
|
||||
expect(messages[1]).toEqual(directMessageFromOther1);
|
||||
});
|
||||
|
||||
it('should return only relevant direct messages for a different chat pair', () => {
|
||||
it("should return only relevant direct messages for a different chat pair", () => {
|
||||
const messages = useMessageStore.getState().getMessages({
|
||||
type: MessageType.Direct,
|
||||
nodeA: myNodeNum,
|
||||
nodeB: otherNodeNum2
|
||||
nodeB: otherNodeNum2,
|
||||
});
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toEqual(directMessageToOther2);
|
||||
});
|
||||
|
||||
it('should return empty array for direct chat if no messages exist', () => {
|
||||
it("should return empty array for direct chat if no messages exist", () => {
|
||||
const messages = useMessageStore.getState().getMessages({
|
||||
type: MessageType.Direct,
|
||||
nodeA: myNodeNum,
|
||||
nodeB: 999
|
||||
nodeB: 999,
|
||||
});
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMessageState', () => {
|
||||
describe("setMessageState", () => {
|
||||
beforeEach(() => {
|
||||
useMessageStore.getState().setNodeNum(myNodeNum);
|
||||
useMessageStore.getState().saveMessage(directMessageToOther1);
|
||||
@@ -238,7 +261,7 @@ describe('useMessageStore', () => {
|
||||
useMessageStore.getState().saveMessage(broadcastMessage1);
|
||||
});
|
||||
|
||||
it('should update state for a direct message', () => {
|
||||
it("should update state for a direct message", () => {
|
||||
useMessageStore.getState().setMessageState({
|
||||
type: MessageType.Direct,
|
||||
nodeA: directMessageToOther1.from,
|
||||
@@ -246,12 +269,17 @@ describe('useMessageStore', () => {
|
||||
messageId: directMessageToOther1.messageId,
|
||||
newState: MessageState.Ack,
|
||||
});
|
||||
const conversationId = getConversationId(directMessageToOther1.from, directMessageToOther1.to);
|
||||
const message = useMessageStore.getState().messages.direct.get(conversationId)?.get(directMessageToOther1.messageId);
|
||||
const conversationId = getConversationId(
|
||||
directMessageToOther1.from,
|
||||
directMessageToOther1.to,
|
||||
);
|
||||
const message = useMessageStore.getState().messages.direct.get(
|
||||
conversationId,
|
||||
)?.get(directMessageToOther1.messageId);
|
||||
expect(message?.state).toBe(MessageState.Ack);
|
||||
});
|
||||
|
||||
it('should update state for another direct message in the same conversation', () => {
|
||||
it("should update state for another direct message in the same conversation", () => {
|
||||
useMessageStore.getState().setMessageState({
|
||||
type: MessageType.Direct,
|
||||
nodeA: directMessageFromOther1.from,
|
||||
@@ -259,24 +287,31 @@ describe('useMessageStore', () => {
|
||||
messageId: directMessageFromOther1.messageId,
|
||||
newState: MessageState.Failed,
|
||||
});
|
||||
const conversationId = getConversationId(directMessageFromOther1.from, directMessageFromOther1.to);
|
||||
const message = useMessageStore.getState().messages.direct.get(conversationId)?.get(directMessageFromOther1.messageId);
|
||||
const conversationId = getConversationId(
|
||||
directMessageFromOther1.from,
|
||||
directMessageFromOther1.to,
|
||||
);
|
||||
const message = useMessageStore.getState().messages.direct.get(
|
||||
conversationId,
|
||||
)?.get(directMessageFromOther1.messageId);
|
||||
expect(message?.state).toBe(MessageState.Failed);
|
||||
});
|
||||
|
||||
it('should update state for a broadcast message', () => {
|
||||
it("should update state for a broadcast message", () => {
|
||||
useMessageStore.getState().setMessageState({
|
||||
type: MessageType.Broadcast,
|
||||
channelId: broadcastChannel,
|
||||
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);
|
||||
});
|
||||
|
||||
it('should warn if message is not found (direct)', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
it("should warn if message is not found (direct)", () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
useMessageStore.getState().setMessageState({
|
||||
type: MessageType.Direct,
|
||||
nodeA: myNodeNum,
|
||||
@@ -284,24 +319,32 @@ describe('useMessageStore', () => {
|
||||
messageId: 999,
|
||||
newState: MessageState.Ack,
|
||||
});
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Message or conversation/channel not found for state update'));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Message or conversation/channel not found for state update",
|
||||
),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should warn if message is not found (broadcast)', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
it("should warn if message is not found (broadcast)", () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
useMessageStore.getState().setMessageState({
|
||||
type: MessageType.Broadcast,
|
||||
channelId: broadcastChannel,
|
||||
messageId: 999,
|
||||
newState: MessageState.Ack,
|
||||
});
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Message or conversation/channel not found for state update'));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Message or conversation/channel not found for state update",
|
||||
),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should warn if conversation/channel is not found', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
it("should warn if conversation/channel is not found", () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
useMessageStore.getState().setMessageState({
|
||||
type: MessageType.Direct,
|
||||
nodeA: myNodeNum,
|
||||
@@ -309,22 +352,30 @@ describe('useMessageStore', () => {
|
||||
messageId: 101,
|
||||
newState: MessageState.Ack,
|
||||
});
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Message or conversation/channel not found for state update'));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Message or conversation/channel not found for state update",
|
||||
),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearMessageByMessageId', () => {
|
||||
describe("clearMessageByMessageId", () => {
|
||||
const extraDirectMessageId = 1011;
|
||||
beforeEach(() => {
|
||||
useMessageStore.getState().setNodeNum(myNodeNum);
|
||||
useMessageStore.getState().saveMessage(directMessageToOther1);
|
||||
useMessageStore.getState().saveMessage(directMessageFromOther1);
|
||||
useMessageStore.getState().saveMessage(broadcastMessage1);
|
||||
useMessageStore.getState().saveMessage({ ...directMessageToOther1, messageId: extraDirectMessageId, date: Date.now() + 50 });
|
||||
useMessageStore.getState().saveMessage({
|
||||
...directMessageToOther1,
|
||||
messageId: extraDirectMessageId,
|
||||
date: Date.now() + 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a specific direct message', () => {
|
||||
it("should delete a specific direct message", () => {
|
||||
const messageIdToDelete = directMessageToOther1.messageId;
|
||||
const nodeA = directMessageToOther1.from;
|
||||
const nodeB = directMessageToOther1.to;
|
||||
@@ -334,19 +385,20 @@ describe('useMessageStore', () => {
|
||||
type: MessageType.Direct,
|
||||
nodeA: nodeA,
|
||||
nodeB: nodeB,
|
||||
messageId: messageIdToDelete
|
||||
messageId: messageIdToDelete,
|
||||
});
|
||||
|
||||
const state = useMessageStore.getState();
|
||||
const conversationLog = state.messages.direct.get(conversationId);
|
||||
expect(conversationLog?.has(messageIdToDelete)).toBe(false);
|
||||
expect(conversationLog?.has(extraDirectMessageId)).toBe(true);
|
||||
expect(conversationLog?.has(directMessageFromOther1.messageId)).toBe(true);
|
||||
expect(conversationLog?.has(directMessageFromOther1.messageId)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(state.messages.direct.has(conversationId)).toBe(true);
|
||||
|
||||
});
|
||||
|
||||
it('should delete another specific direct message', () => {
|
||||
it("should delete another specific direct message", () => {
|
||||
const messageIdToDelete = directMessageFromOther1.messageId;
|
||||
const nodeA = directMessageFromOther1.from;
|
||||
const nodeB = directMessageFromOther1.to;
|
||||
@@ -356,7 +408,7 @@ describe('useMessageStore', () => {
|
||||
type: MessageType.Direct,
|
||||
nodeA: nodeA,
|
||||
nodeB: nodeB,
|
||||
messageId: messageIdToDelete
|
||||
messageId: messageIdToDelete,
|
||||
});
|
||||
|
||||
const state = useMessageStore.getState();
|
||||
@@ -366,38 +418,63 @@ describe('useMessageStore', () => {
|
||||
expect(conversationLog?.has(extraDirectMessageId)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should delete a specific broadcast message', () => {
|
||||
it("should delete a specific broadcast message", () => {
|
||||
const messageIdToDelete = broadcastMessage1.messageId;
|
||||
const channelId = broadcastMessage1.channel;
|
||||
|
||||
useMessageStore.getState().clearMessageByMessageId({
|
||||
type: MessageType.Broadcast,
|
||||
channelId: channelId,
|
||||
messageId: messageIdToDelete
|
||||
messageId: messageIdToDelete,
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const directConvId = getConversationId(directMessageFromOther1.from, directMessageFromOther1.to);
|
||||
it("should clean up empty conversation/channel Maps", () => {
|
||||
const directConvId = getConversationId(
|
||||
directMessageFromOther1.from,
|
||||
directMessageFromOther1.to,
|
||||
);
|
||||
const broadcastChanId = broadcastMessage1.channel;
|
||||
|
||||
useMessageStore.getState().clearMessageByMessageId({ type: MessageType.Direct, nodeA: directMessageToOther1.from, nodeB: directMessageToOther1.to, messageId: directMessageToOther1.messageId });
|
||||
useMessageStore.getState().clearMessageByMessageId({ type: MessageType.Direct, nodeA: directMessageFromOther1.from, nodeB: directMessageFromOther1.to, messageId: directMessageFromOther1.messageId });
|
||||
useMessageStore.getState().clearMessageByMessageId({ type: MessageType.Direct, nodeA: directMessageToOther1.from, nodeB: directMessageToOther1.to, messageId: extraDirectMessageId });
|
||||
useMessageStore.getState().clearMessageByMessageId({
|
||||
type: MessageType.Direct,
|
||||
nodeA: directMessageToOther1.from,
|
||||
nodeB: directMessageToOther1.to,
|
||||
messageId: directMessageToOther1.messageId,
|
||||
});
|
||||
useMessageStore.getState().clearMessageByMessageId({
|
||||
type: MessageType.Direct,
|
||||
nodeA: directMessageFromOther1.from,
|
||||
nodeB: directMessageFromOther1.to,
|
||||
messageId: directMessageFromOther1.messageId,
|
||||
});
|
||||
useMessageStore.getState().clearMessageByMessageId({
|
||||
type: MessageType.Direct,
|
||||
nodeA: directMessageToOther1.from,
|
||||
nodeB: directMessageToOther1.to,
|
||||
messageId: extraDirectMessageId,
|
||||
});
|
||||
|
||||
expect(useMessageStore.getState().messages.direct.has(directConvId)).toBe(false);
|
||||
expect(useMessageStore.getState().messages.direct.has(directConvId)).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
useMessageStore.getState().clearMessageByMessageId({ type: MessageType.Broadcast, channelId: broadcastChanId, messageId: broadcastMessage1.messageId });
|
||||
useMessageStore.getState().clearMessageByMessageId({
|
||||
type: MessageType.Broadcast,
|
||||
channelId: broadcastChanId,
|
||||
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', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
it("should not error when trying to delete non-existent message", () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const conversationId = getConversationId(myNodeNum, otherNodeNum1);
|
||||
|
||||
expect(() => {
|
||||
@@ -405,30 +482,34 @@ describe('useMessageStore', () => {
|
||||
type: MessageType.Direct,
|
||||
nodeA: myNodeNum,
|
||||
nodeB: otherNodeNum1,
|
||||
messageId: 9999
|
||||
messageId: 9999,
|
||||
});
|
||||
}).not.toThrow();
|
||||
|
||||
const state = useMessageStore.getState();
|
||||
const conversationLog = state.messages.direct.get(conversationId);
|
||||
expect(conversationLog?.size).toBe(3); // 101, 102, 1011
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('not found in direct chat'));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("not found in direct chat"),
|
||||
);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not error when trying to delete from non-existent conversation/channel', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
||||
it("should not error when trying to delete from non-existent conversation/channel", () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
expect(() => {
|
||||
useMessageStore.getState().clearMessageByMessageId({
|
||||
type: MessageType.Direct,
|
||||
nodeA: myNodeNum,
|
||||
nodeB: 9998,
|
||||
messageId: 101
|
||||
messageId: 101,
|
||||
});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Message entry"));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Message entry"),
|
||||
);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -436,43 +517,54 @@ describe('useMessageStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drafts', () => {
|
||||
describe("Drafts", () => {
|
||||
const draftKeyDirect = otherNodeNum1;
|
||||
const draftKeyBroadcast = broadcastChannel;
|
||||
const draftMessage = 'This is a draft';
|
||||
const draftMessage = "This is a draft";
|
||||
|
||||
it('should set and get a draft for direct chat', () => {
|
||||
it("should set and get a draft for direct chat", () => {
|
||||
useMessageStore.getState().setDraft(draftKeyDirect, draftMessage);
|
||||
expect(useMessageStore.getState().draft.get(draftKeyDirect)).toBe(draftMessage);
|
||||
expect(useMessageStore.getState().getDraft(draftKeyDirect)).toBe(draftMessage);
|
||||
expect(useMessageStore.getState().draft.get(draftKeyDirect)).toBe(
|
||||
draftMessage,
|
||||
);
|
||||
expect(useMessageStore.getState().getDraft(draftKeyDirect)).toBe(
|
||||
draftMessage,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set and get a draft for broadcast chat', () => {
|
||||
it("should set and get a draft for broadcast chat", () => {
|
||||
useMessageStore.getState().setDraft(draftKeyBroadcast, draftMessage);
|
||||
expect(useMessageStore.getState().draft.get(draftKeyBroadcast)).toBe(draftMessage);
|
||||
expect(useMessageStore.getState().getDraft(draftKeyBroadcast)).toBe(draftMessage);
|
||||
expect(useMessageStore.getState().draft.get(draftKeyBroadcast)).toBe(
|
||||
draftMessage,
|
||||
);
|
||||
expect(useMessageStore.getState().getDraft(draftKeyBroadcast)).toBe(
|
||||
draftMessage,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty string for non-existent draft', () => {
|
||||
expect(useMessageStore.getState().getDraft(999)).toBe('');
|
||||
it("should return empty string for non-existent draft", () => {
|
||||
expect(useMessageStore.getState().getDraft(999)).toBe("");
|
||||
});
|
||||
|
||||
it('should clear a draft', () => {
|
||||
it("should clear a draft", () => {
|
||||
useMessageStore.getState().setDraft(draftKeyDirect, draftMessage);
|
||||
expect(useMessageStore.getState().draft.has(draftKeyDirect)).toBe(true);
|
||||
useMessageStore.getState().clearDraft(draftKeyDirect);
|
||||
expect(useMessageStore.getState().draft.has(draftKeyDirect)).toBe(false);
|
||||
expect(useMessageStore.getState().getDraft(draftKeyDirect)).toBe('');
|
||||
expect(useMessageStore.getState().getDraft(draftKeyDirect)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAllMessages', () => {
|
||||
it('should clear all direct and broadcast messages, leaving empty Maps', () => {
|
||||
describe("deleteAllMessages", () => {
|
||||
it("should clear all direct and broadcast messages, leaving empty Maps", () => {
|
||||
useMessageStore.getState().saveMessage(directMessageToOther1);
|
||||
useMessageStore.getState().saveMessage(broadcastMessage1);
|
||||
|
||||
expect(useMessageStore.getState().messages.direct.size).toBeGreaterThan(0);
|
||||
expect(useMessageStore.getState().messages.broadcast.size).toBeGreaterThan(0);
|
||||
expect(useMessageStore.getState().messages.direct.size).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(useMessageStore.getState().messages.broadcast.size)
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
useMessageStore.getState().deleteAllMessages();
|
||||
|
||||
@@ -483,4 +575,4 @@ describe('useMessageStore', () => {
|
||||
expect(state.messages.broadcast.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,14 +21,14 @@ interface GenericMessage<T extends MessageType> extends MessageBase {
|
||||
type: T;
|
||||
}
|
||||
|
||||
type Message = GenericMessage<MessageType.Direct> | GenericMessage<MessageType.Broadcast>;
|
||||
|
||||
type Message =
|
||||
| GenericMessage<MessageType.Direct>
|
||||
| GenericMessage<MessageType.Broadcast>;
|
||||
|
||||
type GetMessagesParams =
|
||||
| { type: MessageType.Direct; nodeA: NodeNum; nodeB: NodeNum }
|
||||
| { type: MessageType.Broadcast; channelId: ChannelId };
|
||||
|
||||
|
||||
type SetMessageStateParams =
|
||||
| {
|
||||
type: MessageType.Direct;
|
||||
@@ -58,13 +58,13 @@ type ClearMessageParams =
|
||||
};
|
||||
|
||||
export type {
|
||||
Message,
|
||||
ConversationId,
|
||||
NodeNum,
|
||||
MessageLogMap,
|
||||
ChannelId,
|
||||
MessageId,
|
||||
GetMessagesParams,
|
||||
SetMessageStateParams,
|
||||
ClearMessageParams,
|
||||
}
|
||||
ConversationId,
|
||||
GetMessagesParams,
|
||||
Message,
|
||||
MessageId,
|
||||
MessageLogMap,
|
||||
NodeNum,
|
||||
SetMessageStateParams,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useState, useContext, useMemo } from 'react';
|
||||
import React, { createContext, useContext, useMemo, useState } from "react";
|
||||
|
||||
interface SidebarContextProps {
|
||||
isCollapsed: boolean;
|
||||
@@ -6,13 +6,17 @@ interface SidebarContextProps {
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextProps | undefined>(undefined);
|
||||
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);
|
||||
setIsCollapsed((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
@@ -22,7 +26,7 @@ export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||
}), [isCollapsed, toggleSidebar]);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={value} >
|
||||
<SidebarContext.Provider value={value}>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
@@ -31,7 +35,7 @@ export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||
export const useSidebar = (): SidebarContextProps => {
|
||||
const context = useContext(SidebarContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSidebar must be used within a SidebarProvider');
|
||||
throw new Error("useSidebar must be used within a SidebarProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PersistStorage, StateStorage, } from "zustand/middleware"; // Added StorageValue for clarity, though not strictly needed in the final signature here
|
||||
import { get, set, del } from "idb-keyval";
|
||||
import { PersistStorage, StateStorage } from "zustand/middleware";
|
||||
import { del, get, set } from "idb-keyval";
|
||||
import { ChannelId, MessageLogMap } from "@core/stores/messageStore/types.ts";
|
||||
|
||||
type PersistedMessageState = {
|
||||
@@ -22,30 +22,31 @@ export const zustandIndexDBStorage: StateStorage = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
type SerializedMap<K = unknown, V = unknown> = {
|
||||
__dataType: 'Map';
|
||||
__dataType: "Map";
|
||||
value: Array<[K, V]>;
|
||||
};
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type JsonReplacer = (this: any, key: string, value: unknown) => unknown;
|
||||
const replacer: JsonReplacer = (_, value) => {
|
||||
if (value instanceof Map) {
|
||||
const map = value as Map<unknown, unknown>;
|
||||
const serialized: SerializedMap = {
|
||||
__dataType: 'Map',
|
||||
__dataType: "Map",
|
||||
value: Array.from(map.entries()),
|
||||
};
|
||||
return serialized;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type JsonReviver = (this: any, key: string, value: unknown) => unknown;
|
||||
function isSerializedMap(value: unknown): value is SerializedMap {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const potentialMap = value as Partial<SerializedMap>;
|
||||
return potentialMap.__dataType === 'Map' && Array.isArray(potentialMap.value);
|
||||
return potentialMap.__dataType === "Map" && Array.isArray(potentialMap.value);
|
||||
}
|
||||
const reviver: JsonReviver = (_, value) => {
|
||||
if (isSerializedMap(value)) {
|
||||
@@ -54,11 +55,10 @@ const reviver: JsonReviver = (_, value) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
|
||||
export const storageWithMapSupport: PersistStorage<PersistedMessageState> = {
|
||||
getItem: async (name): Promise<PersistedMessageState | null> => {
|
||||
const str = await zustandIndexDBStorage.getItem(name);
|
||||
if (!str) { return null; }
|
||||
if (!str) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(str, reviver) as PersistedMessageState;
|
||||
return parsed;
|
||||
@@ -72,10 +72,13 @@ export const storageWithMapSupport: PersistStorage<PersistedMessageState> = {
|
||||
const str = JSON.stringify(newValue, replacer);
|
||||
await zustandIndexDBStorage.setItem(name, str);
|
||||
} catch (error) {
|
||||
console.error(`Error stringifying or setting persisted state (${name}):`, error);
|
||||
console.error(
|
||||
`Error stringifying or setting persisted state (${name}):`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
removeItem: async (name): Promise<void> => {
|
||||
await zustandIndexDBStorage.removeItem(name);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { Device } from "@core/stores/deviceStore.ts";
|
||||
import { MeshDevice, Protobuf } from "@meshtastic/core";
|
||||
import { MessageState, MessageType, type MessageStore } from "./stores/messageStore/index.ts";
|
||||
import { type MessageStore, MessageType } from "./stores/messageStore/index.ts";
|
||||
import PacketToMessageDTO from "@core/dto/PacketToMessageDTO.ts";
|
||||
import NodeInfoFactory from "@core/dto/NodeNumToNodeInfoDTO.ts";
|
||||
|
||||
export const subscribeAll = (
|
||||
device: Device,
|
||||
connection: MeshDevice,
|
||||
messageStore: MessageStore
|
||||
messageStore: MessageStore,
|
||||
) => {
|
||||
let myNodeNum = 0;
|
||||
|
||||
@@ -79,7 +79,6 @@ export const subscribeAll = (
|
||||
device.setModuleConfig(moduleConfig);
|
||||
});
|
||||
|
||||
|
||||
connection.events.onMessagePacket.subscribe((messagePacket) => {
|
||||
// incoming and outgoing messages are handled by this event listener
|
||||
console.log("Message Packet", messagePacket);
|
||||
@@ -117,7 +116,6 @@ export const subscribeAll = (
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||
if (routingPacket.data.variant.case === "errorReason") {
|
||||
switch (routingPacket.data.variant.value) {
|
||||
@@ -126,19 +124,24 @@ export const subscribeAll = (
|
||||
break;
|
||||
case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
|
||||
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||
device.setNodeError(routingPacket.from, Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value]);
|
||||
device.setNodeError(
|
||||
routingPacket.from,
|
||||
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value],
|
||||
);
|
||||
device.setDialogOpen("refreshKeys", true);
|
||||
break;
|
||||
case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY:
|
||||
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||
device.setNodeError(routingPacket.from, Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value]);
|
||||
device.setNodeError(
|
||||
routingPacket.from,
|
||||
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value],
|
||||
);
|
||||
device.setDialogOpen("refreshKeys", true);
|
||||
break;
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
|
||||
describe("EventBus", () => {
|
||||
beforeEach(() => {
|
||||
// Reset event listeners before each test
|
||||
(eventBus as any).listeners = {};
|
||||
eventBus.listeners = {};
|
||||
});
|
||||
|
||||
it("should register an event listener and trigger it on emit", () => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export type EventMap = {
|
||||
'dialog:unsafeRoles': {
|
||||
action: 'confirm' | 'dismiss';
|
||||
"dialog:unsafeRoles": {
|
||||
action: "confirm" | "dismiss";
|
||||
};
|
||||
// add more events as required
|
||||
};
|
||||
|
||||
export type EventName = keyof EventMap;
|
||||
@@ -11,12 +10,15 @@ export type EventCallback<T extends EventName> = (data: EventMap[T]) => void;
|
||||
class EventBus {
|
||||
private listeners: { [K in EventName]?: Array<EventCallback<K>> } = {};
|
||||
|
||||
public on<T extends EventName>(event: T, callback: EventCallback<T>): () => void {
|
||||
public on<T extends EventName>(
|
||||
event: T,
|
||||
callback: EventCallback<T>,
|
||||
): () => void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
|
||||
this.listeners[event]?.push(callback as any);
|
||||
this.listeners[event]?.push(callback);
|
||||
|
||||
return () => {
|
||||
this.off(event, callback);
|
||||
@@ -26,7 +28,7 @@ class EventBus {
|
||||
public off<T extends EventName>(event: T, callback: EventCallback<T>): void {
|
||||
if (!this.listeners[event]) return;
|
||||
|
||||
const callbackIndex = this.listeners[event]?.indexOf(callback as any);
|
||||
const callbackIndex = this.listeners[event]?.indexOf(callback);
|
||||
if (callbackIndex !== undefined && callbackIndex > -1) {
|
||||
this.listeners[event]?.splice(callbackIndex, 1);
|
||||
}
|
||||
@@ -35,10 +37,10 @@ class EventBus {
|
||||
public emit<T extends EventName>(event: T, data: EventMap[T]): void {
|
||||
if (!this.listeners[event]) return;
|
||||
|
||||
this.listeners[event]?.forEach(callback => {
|
||||
this.listeners[event]?.forEach((callback) => {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const eventBus = new EventBus();
|
||||
export const eventBus = new EventBus();
|
||||
|
||||
@@ -63,7 +63,6 @@ export default function newGithubIssueUrl(
|
||||
|
||||
function validateOptions(options: GithubIssueUrlOptions): ValidatedOptions {
|
||||
const repoUrl = options.repoUrl ??
|
||||
|
||||
(options.user && options.repo
|
||||
? `https://github.com/${options.user}/${options.repo}`
|
||||
: undefined);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export function convertIntToIpAddress(int: number): string {
|
||||
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff
|
||||
}`;
|
||||
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${
|
||||
(int >> 24) & 0xff
|
||||
}`;
|
||||
}
|
||||
|
||||
export function convertIpAddressToInt(ip: string): number | undefined {
|
||||
|
||||
@@ -35,10 +35,18 @@ export interface LengthValidationResult {
|
||||
currentLength: number | null;
|
||||
}
|
||||
|
||||
export function validateMaxByteLength(value: string | null | undefined, maxByteLength: number): LengthValidationResult {
|
||||
export function validateMaxByteLength(
|
||||
value: string | null | undefined,
|
||||
maxByteLength: number,
|
||||
): LengthValidationResult {
|
||||
// Ensure maxByteLength is valid
|
||||
if (typeof maxByteLength !== 'number' || !Number.isInteger(maxByteLength) || maxByteLength < 0) {
|
||||
console.warn('validateMaxByteLength: maxByteLength must be a non-negative integer.');
|
||||
if (
|
||||
typeof maxByteLength !== "number" || !Number.isInteger(maxByteLength) ||
|
||||
maxByteLength < 0
|
||||
) {
|
||||
console.warn(
|
||||
"validateMaxByteLength: maxByteLength must be a non-negative integer.",
|
||||
);
|
||||
return { isValid: false, currentLength: null }; // Cannot validate with invalid limit
|
||||
}
|
||||
|
||||
@@ -48,8 +56,10 @@ export function validateMaxByteLength(value: string | null | undefined, maxByteL
|
||||
}
|
||||
|
||||
// Check for TextEncoder availability
|
||||
if (typeof TextEncoder === 'undefined') {
|
||||
console.error('validateMaxByteLength: TextEncoder API is not available in this environment.');
|
||||
if (typeof TextEncoder === "undefined") {
|
||||
console.error(
|
||||
"validateMaxByteLength: TextEncoder API is not available in this environment.",
|
||||
);
|
||||
return { isValid: false, currentLength: null }; // Cannot determine byte length
|
||||
}
|
||||
|
||||
@@ -64,8 +74,7 @@ export function validateMaxByteLength(value: string | null | undefined, maxByteL
|
||||
return { isValid, currentLength };
|
||||
} catch (error) {
|
||||
// Handle potential errors during encoding
|
||||
console.error('validateMaxByteLength: Error encoding string:', error);
|
||||
console.error("validateMaxByteLength: Error encoding string:", error);
|
||||
return { isValid: false, currentLength: null }; // Encoding failed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user