Persistent message store (#814)

* Fix default filter behaviour

* Persist message store

* messageStore tests, node PKI validation

Implement node validation and improve merging logic

- Added `validateIncomingNode` function to validate new nodes against existing nodes, checking for public key conflicts and ensuring proper handling of node updates.
- Updated `nodeDBFactory` to utilize the new validation function when adding nodes.
- Enhanced `getNodes` method to optionally include the current node in the results.
- Removed the `mergeNodeInfo` utility as its functionality is now integrated into the validation and merging process.
- Updated tests to cover new validation logic and ensure correct behavior during node addition and merging.
- Cleaned up unused utility functions related to key comparison.

* refactor: reuse eviction logic for message and node stores

* Update format, move hooks

* Improve test performance

* Update imports

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
This commit is contained in:
Jeremy Gallant
2025-09-03 21:00:48 -04:00
committed by GitHub
co-authored by philon- Dan Ditomaso
parent 799283fd46
commit dcb44d27fe
24 changed files with 1092 additions and 353 deletions
@@ -1,10 +1,13 @@
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
import { useMessageStore } from "@core/stores";
import { type MessageStore, useMessages } from "@core/stores";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@core/stores", () => ({
useMessageStore: vi.fn(() => ({
CurrentDeviceContext: {
_currentValue: { deviceId: 1234 },
},
useMessages: vi.fn(() => ({
deleteAllMessages: vi.fn(),
})),
}));
@@ -17,11 +20,14 @@ describe("DeleteMessagesDialog", () => {
mockOnOpenChange.mockClear();
mockClearAllMessages.mockClear();
const mockedUseMessageStore = vi.mocked(useMessageStore);
mockedUseMessageStore.mockImplementation(() => ({
deleteAllMessages: mockClearAllMessages,
}));
mockedUseMessageStore.mockClear();
const mockedUseMessages = vi.mocked(useMessages);
mockedUseMessages.mockImplementation(
() =>
({
deleteAllMessages: mockClearAllMessages,
}) as unknown as MessageStore,
);
mockedUseMessages.mockClear();
});
it("calls onOpenChange with false when the close button (X) is clicked", () => {
@@ -8,7 +8,7 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { useMessageStore } from "@core/stores";
import { useMessages } from "@core/stores";
import { AlertTriangleIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
@@ -22,7 +22,7 @@ export const DeleteMessagesDialog = ({
onOpenChange,
}: DeleteMessagesDialogProps) => {
const { t } = useTranslation("dialog");
const { deleteAllMessages } = useMessageStore();
const { deleteAllMessages } = useMessages();
const handleCloseDialog = () => {
onOpenChange(false);
};
@@ -6,7 +6,7 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { useMessageStore, useNodeDB } from "@core/stores";
import { useMessages, useNodeDB } from "@core/stores";
import { LockKeyholeOpenIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
@@ -21,7 +21,7 @@ export const RefreshKeysDialog = ({
onOpenChange,
}: RefreshKeysDialogProps) => {
const { t } = useTranslation("dialog");
const { activeChat } = useMessageStore();
const { activeChat } = useMessages();
const { nodeErrors, getNode } = useNodeDB();
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
@@ -1,10 +1,10 @@
import { useDevice, useMessageStore, useNodeDB } from "@core/stores";
import { useDevice, useMessages, useNodeDB } from "@core/stores";
import { useCallback } from "react";
export function useRefreshKeysDialog() {
const { setDialogOpen } = useDevice();
const { removeNode, clearNodeError, getNodeError } = useNodeDB();
const { activeChat } = useMessageStore();
const { activeChat } = useMessages();
const handleCloseDialog = useCallback(() => {
setDialogOpen("refreshKeys", false);
@@ -20,7 +20,7 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore();
const { addMessageStore } = useMessageStore();
const { setSelectedDevice } = useAppStore();
const { t } = useTranslation();
@@ -37,6 +37,7 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
const transport = await TransportWebBluetooth.createFromDevice(bleDevice);
const device = addDevice(id);
const nodeDB = addNodeDB(id);
const messageStore = addMessageStore(id);
const connection = new MeshDevice(transport, id);
connection.configure();
@@ -9,7 +9,9 @@ vi.mock("@core/stores", () => ({
useDeviceStore: vi.fn(() => ({
addDevice: vi.fn(() => ({ addConnection: vi.fn() })),
})),
useMessageStore: vi.fn(),
useMessageStore: vi.fn(() => ({
addMessageStore: vi.fn(),
})),
useNodeDBStore: vi.fn(() => ({
addNodeDB: vi.fn(),
})),
@@ -31,7 +31,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore();
const { addMessageStore } = useMessageStore();
const { setSelectedDevice } = useAppStore();
const { control, handleSubmit, register } = useForm<FormData>({
@@ -63,6 +63,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
const transport = await TransportHTTP.create(data.ip, data.tls);
const device = addDevice(id);
const nodeDB = addNodeDB(id);
const messageStore = addMessageStore(id);
const connection = new MeshDevice(transport, id);
connection.configure();
@@ -20,7 +20,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore();
const { addMessageStore } = useMessageStore();
const { setSelectedDevice } = useAppStore();
const { t } = useTranslation();
@@ -42,6 +42,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
const id = randId();
const device = addDevice(id);
const nodeDB = addNodeDB(id);
const messageStore = addMessageStore(id);
setSelectedDevice(id);
const transport = await TransportWebSerial.createFromPort(port);
@@ -40,8 +40,11 @@ const mockSetDraft = vi.fn();
const mockGetDraft = vi.fn();
const mockClearDraft = vi.fn();
vi.mock("@core/stores/messageStore", () => ({
useMessageStore: vi.fn(() => ({
vi.mock("@core/stores", () => ({
CurrentDeviceContext: {
_currentValue: { deviceId: 1234 },
},
useMessages: vi.fn(() => ({
setDraft: mockSetDraft,
getDraft: mockGetDraft,
clearDraft: mockClearDraft,
@@ -1,6 +1,6 @@
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import { useMessageStore } from "@core/stores";
import { useMessages } from "@core/stores";
import type { Types } from "@meshtastic/core";
import { SendIcon } from "lucide-react";
import { startTransition, useState } from "react";
@@ -13,7 +13,7 @@ export interface MessageInputProps {
}
export const MessageInput = ({ onSend, to, maxBytes }: MessageInputProps) => {
const { setDraft, getDraft, clearDraft } = useMessageStore();
const { setDraft, getDraft, clearDraft } = useMessages();
const { t } = useTranslation("messages");
const calculateBytes = (text: string) => new Blob([text]).size;
@@ -6,12 +6,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import {
MessageState,
useDevice,
useMessageStore,
useNodeDB,
} from "@core/stores";
import { MessageState, useDevice, useNodeDB } from "@core/stores";
import type { Message } from "@core/stores/messageStore/types.ts";
import { cn } from "@core/utils/cn.ts";
import { type Protobuf, Types } from "@meshtastic/core";
@@ -53,8 +48,7 @@ interface MessageItemProps {
export const MessageItem = ({ message }: MessageItemProps) => {
const { config } = useDevice();
const { getNode } = useNodeDB();
const { getMyNodeNum } = useMessageStore();
const { getNode, getMyNode } = useNodeDB();
const { t, i18n } = useTranslation("messages");
const MESSAGE_STATUS_MAP = useMemo(
@@ -102,7 +96,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
return message.from != null ? getNode(message.from) : null;
}, [getNode, message.from]);
const myNodeNum = useMemo(() => getMyNodeNum(), [getMyNodeNum]);
const myNodeNum = useMemo(() => getMyNode().num, [getMyNode]);
const { displayName, shortName, isFavorite } = useMemo(() => {
const userIdHex = message.from.toString(16).toUpperCase().padStart(2, "0");