From cad590f9938dfdfa30d7340907cfa70f4f60504a Mon Sep 17 00:00:00 2001 From: Hunter275 Date: Fri, 7 Mar 2025 22:01:34 -0500 Subject: [PATCH 01/14] wip --- src/components/UI/Sidebar/sidebarButton.tsx | 3 +++ src/core/stores/appStore.ts | 20 +++++++++++++++++++- src/core/stores/deviceStore.ts | 14 ++++++++++++++ src/core/subscriptions.ts | 2 ++ src/index.css | 8 ++++++++ src/pages/Messages.tsx | 6 +++++- 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/components/UI/Sidebar/sidebarButton.tsx b/src/components/UI/Sidebar/sidebarButton.tsx index b6444bb8..bbd99725 100644 --- a/src/components/UI/Sidebar/sidebarButton.tsx +++ b/src/components/UI/Sidebar/sidebarButton.tsx @@ -3,6 +3,7 @@ import type { LucideIcon } from "lucide-react"; export interface SidebarButtonProps { label: string; + count?: number; active?: boolean; Icon?: LucideIcon; element?; @@ -13,6 +14,7 @@ export const SidebarButton = ({ label, active, Icon, + count, element, onClick, }: SidebarButtonProps) => ( @@ -25,5 +27,6 @@ export const SidebarButton = ({ {Icon && } {element && element} {label} + {count > 0 &&
{count}
} ); diff --git a/src/core/stores/appStore.ts b/src/core/stores/appStore.ts index 4b286e6f..42143426 100644 --- a/src/core/stores/appStore.ts +++ b/src/core/stores/appStore.ts @@ -9,6 +9,7 @@ export interface RasterSource { tileSize: number; } + interface ErrorState { field: string; message: string; @@ -19,7 +20,12 @@ interface ErrorState { message: string; } -interface AppState { +export interface App { + unreadCounts: Map; + setUnread: (id: number, count: number) => void; +} + +export interface AppState { selectedDevice: number; devices: { id: number; @@ -34,6 +40,7 @@ interface AppState { activeChat: number; chatType: "broadcast" | "direct"; errors: ErrorState[]; + unreadCounts: Map; setRasterSources: (sources: RasterSource[]) => void; addRasterSource: (source: RasterSource) => void; @@ -56,6 +63,9 @@ interface AppState { removeError: (field: string) => void; clearErrors: () => void; setNewErrors: (newErrors: ErrorState[]) => void; + + // unread counts + setUnread: (id: number, count: number) => void; } export const useAppStore = create()((set, get) => ({ @@ -70,6 +80,7 @@ export const useAppStore = create()((set, get) => ({ activeChat: Types.ChannelNumber.Primary, chatType: "broadcast", errors: [], + unreadCounts: new Map([[0, 100],[2718471552, 1]]), setRasterSources: (sources: RasterSource[]) => { set( @@ -178,4 +189,11 @@ export const useAppStore = create()((set, get) => ({ }), ); }, + setUnread: (id: number, count: number) => { + set( + produce((draft) => { + draft.unreadCounts.set(id, count); + }) + ); + } })); diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index 0822473f..e2bde391 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -64,6 +64,7 @@ export interface Device { pkiBackup: boolean; nodeDetails: boolean; }; + unreadCounts: Map; setStatus: (status: Types.DeviceStatusEnum) => void; setConfig: (config: Protobuf.Config.Config) => void; @@ -98,6 +99,7 @@ export interface Device { setDialogOpen: (dialog: DialogVariant, open: boolean) => void; processPacket: (data: ProcessPacketParams) => void; setMessageDraft: (message: string) => void; + setUnread: (id: number, count: number) => void; } export interface DeviceState { @@ -149,6 +151,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, pendingSettingsChanges: false, messageDraft: "", + unreadCounts: new Map([[0, 100],[2718471552, 1]]), setStatus: (status: Types.DeviceStatusEnum) => { set( @@ -631,6 +634,17 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, + setUnread: (id: number, count: number) => { + set( + produce((draft) => { + console.log(id, count); + const device = draft.devices.get(id); + if (device) { + device.unreadCounts.set(id, count); + } + }) + ); + } }); }), ); diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index 8f1c1a09..7a1a5b95 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -1,6 +1,7 @@ import type { Device } from "@core/stores/deviceStore.ts"; import { Protobuf, type Types } from "@meshtastic/core"; + export const subscribeAll = ( device: Device, connection: Types.ConnectionType, @@ -84,6 +85,7 @@ export const subscribeAll = ( ...messagePacket, state: messagePacket.from !== myNodeNum ? "ack" : "waiting", }); + device.unreadCounts.set(messagePacket.from, 1); }); connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => { diff --git a/src/index.css b/src/index.css index 7c1ffc76..429dffe1 100644 --- a/src/index.css +++ b/src/index.css @@ -110,3 +110,11 @@ img { .animate-spin-slow { animation: spin-slower 2s linear infinite; } + +.notification-count { + color: white; + border-radius: 20%; + padding-left: 2%; + padding-right: 2%; + background-color: rgb(195,0,0); +} \ No newline at end of file diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index b520f906..c5ca6170 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -14,7 +14,7 @@ import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react"; import { useState } from "react"; export const MessagesPage = () => { - const { channels, nodes, hardware, messages } = useDevice(); + const { channels, nodes, hardware, messages, unreadCounts } = useDevice(); const { activeChat, chatType, setActiveChat, setChatType } = useAppStore(); const [searchTerm, setSearchTerm] = useState(""); const filteredNodes = Array.from(nodes.values()).filter((node) => { @@ -38,6 +38,7 @@ export const MessagesPage = () => { {filteredChannels.map((channel) => ( { onClick={() => { setChatType("broadcast"); setActiveChat(channel.index); + unreadCounts.set(channel.index, 0); }} element={} /> @@ -66,12 +68,14 @@ export const MessagesPage = () => { {filteredNodes.map((node) => ( { setChatType("direct"); setActiveChat(node.num); + unreadCounts.set(node.num, 1) }} element={ Date: Fri, 7 Mar 2025 23:59:52 -0500 Subject: [PATCH 02/14] wip --- src/core/stores/deviceStore.ts | 11 +++++++---- src/core/subscriptions.ts | 2 +- src/pages/Messages.tsx | 9 +++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index e2bde391..1e0496f0 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -151,7 +151,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, pendingSettingsChanges: false, messageDraft: "", - unreadCounts: new Map([[0, 100],[2718471552, 1]]), + unreadCounts: new Map([[0, 100],[3663106604, 1]]), setStatus: (status: Types.DeviceStatusEnum) => { set( @@ -634,13 +634,16 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - setUnread: (id: number, count: number) => { + setUnread: (unread_id: number, count?: number) => { set( produce((draft) => { - console.log(id, count); const device = draft.devices.get(id); if (device) { - device.unreadCounts.set(id, count); + if (count == null) { + let currentCount = device.unreadCounts.get(unread_id) ?? 0; + count = currentCount + 1; + } + device.unreadCounts.set(unread_id, count); } }) ); diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index 7a1a5b95..aecaa343 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -85,7 +85,7 @@ export const subscribeAll = ( ...messagePacket, state: messagePacket.from !== myNodeNum ? "ack" : "waiting", }); - device.unreadCounts.set(messagePacket.from, 1); + device.setUnread(messagePacket.from); }); connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => { diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index c5ca6170..3d63e05e 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -14,14 +14,15 @@ import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react"; import { useState } from "react"; export const MessagesPage = () => { - const { channels, nodes, hardware, messages, unreadCounts } = useDevice(); + const { channels, nodes, hardware, messages, unreadCounts, setUnread } = useDevice(); const { activeChat, chatType, setActiveChat, setChatType } = useAppStore(); const [searchTerm, setSearchTerm] = useState(""); const filteredNodes = Array.from(nodes.values()).filter((node) => { if (node.num === hardware.myNodeNum) return false; const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`; return nodeName.toLowerCase().includes(searchTerm.toLowerCase()); - }); + }).map((node) => { node.unreadCount = unreadCounts.get(node.num) ?? 0; return node;}) + .sort((a, b) => b.unreadCount - a.unreadCount); const allChannels = Array.from(channels.values()); const filteredChannels = allChannels.filter( (ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED, @@ -48,7 +49,7 @@ export const MessagesPage = () => { onClick={() => { setChatType("broadcast"); setActiveChat(channel.index); - unreadCounts.set(channel.index, 0); + setUnread(channel.index, 0); }} element={} /> @@ -75,7 +76,7 @@ export const MessagesPage = () => { onClick={() => { setChatType("direct"); setActiveChat(node.num); - unreadCounts.set(node.num, 1) + setUnread(node.num, 0) }} element={ Date: Sun, 9 Mar 2025 00:57:52 -0500 Subject: [PATCH 03/14] spread on the map --- src/pages/Messages.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index 3d63e05e..1559453d 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -21,7 +21,10 @@ export const MessagesPage = () => { if (node.num === hardware.myNodeNum) return false; const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`; return nodeName.toLowerCase().includes(searchTerm.toLowerCase()); - }).map((node) => { node.unreadCount = unreadCounts.get(node.num) ?? 0; return node;}) + }).map((node) => { + node = {...node, unreadCount: unreadCounts.get(node.num) ?? 0} + return node; + }) .sort((a, b) => b.unreadCount - a.unreadCount); const allChannels = Array.from(channels.values()); const filteredChannels = allChannels.filter( From c103d7012bf4773e0537c799f1130e9dc0309b09 Mon Sep 17 00:00:00 2001 From: Hunter275 Date: Mon, 10 Mar 2025 20:54:42 -0400 Subject: [PATCH 04/14] tests --- src/pages/Messages.test.tsx | 79 +++++++++++++++++++++++++++++++++++++ src/pages/Messages.tsx | 2 +- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/pages/Messages.test.tsx diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx new file mode 100644 index 00000000..df3b59b0 --- /dev/null +++ b/src/pages/Messages.test.tsx @@ -0,0 +1,79 @@ +import { describe, it, vi, expect } from "vitest"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { MessagesPage } from "./Messages.tsx"; +import { useDevice } from "../core/stores/deviceStore"; +import { Protobuf } from "@meshtastic/core"; + +// Mock the store +vi.mock("../core/stores/deviceStore", () => ({ + useDevice: vi.fn() +})); + +const mockUseDevice = { + channels: new Map([ + [0, { + index: 0, + settings: { name: "Primary" }, + role: Protobuf.Channel.Channel_Role.PRIMARY + }] + ]), + nodes: new Map([ + [1111, { + num: 1111, + user: { longName: "Test Node", shortName: "TN", publicKey: "12345" } + }], + [2222, { + num: 2222, + user: { longName: "Test Node 2", shortName: "TN2", publicKey: "67890" } + }], + [3333, { + num: 3333, + user: { longName: "Test Node 3", shortName: "TN3", publicKey: "11111" } + }] + ]), + hardware: { myNodeNum: 1 }, + messages: { broadcast: new Map(), direct: new Map() }, + metadata: new Map(), + unreadCounts: new Map([[1111, 3], [2222, 10]]), + setUnread: vi.fn() +}; + + +describe("Messages Page", () => { + beforeEach(() => { + vi.mocked(useDevice).mockReturnValue(mockUseDevice); + }); + + it("shows unread count correctly", () => { + render(); + const unreadCount = screen.getByText("3"); + expect(unreadCount).toBeInTheDocument(); + }); + + it("updates unread when active chat changes",() => { + render(); + const nodeButton = screen.getByRole("button", { name: "TN Test Node 3" }); + fireEvent.click(nodeButton); + + expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); + expect(mockUseDevice.unreadCounts.get(1111)).toBe(0); + const unreadCount = screen.getByText("3"); + expect(unreadCount).not.toBeInTheDocument(); + }); + + it("does not update the incorrect node",() => { + render(); + const nodeButton = screen.getByRole("button", { name: "TN Test Node 3" }); + fireEvent.click(nodeButton); + + expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); + expect(mockUseDevice.unreadCounts.get(2222)).toBe(10); + const unreadCount = screen.getByText("10"); + expect(unreadCount).toBeInTheDocument(); + }); + + it("sorts unreads to the top", () => { + const container = render(); + const buttonOrder = screen.getAllByRole("button") + }); +}); \ No newline at end of file diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index 1559453d..ee5690d6 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -79,7 +79,7 @@ export const MessagesPage = () => { onClick={() => { setChatType("direct"); setActiveChat(node.num); - setUnread(node.num, 0) + setUnread(node.num, 0); }} element={ Date: Mon, 10 Mar 2025 23:33:39 -0400 Subject: [PATCH 05/14] tests --- src/pages/Messages.test.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx index df3b59b0..80d1a0d4 100644 --- a/src/pages/Messages.test.tsx +++ b/src/pages/Messages.test.tsx @@ -3,6 +3,7 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { MessagesPage } from "./Messages.tsx"; import { useDevice } from "../core/stores/deviceStore"; import { Protobuf } from "@meshtastic/core"; +import { b } from "vitest/dist/chunks/suite.qtkXWc6R.js"; // Mock the store vi.mock("../core/stores/deviceStore", () => ({ @@ -20,7 +21,7 @@ const mockUseDevice = { nodes: new Map([ [1111, { num: 1111, - user: { longName: "Test Node", shortName: "TN", publicKey: "12345" } + user: { longName: "Test Node 1", shortName: "TN1", publicKey: "12345" } }], [2222, { num: 2222, @@ -63,7 +64,7 @@ describe("Messages Page", () => { it("does not update the incorrect node",() => { render(); - const nodeButton = screen.getByRole("button", { name: "TN Test Node 3" }); + const nodeButton = screen.getAllByRole("button").filter(b => b.textContent.includes("TN1Test Node 13"))[0]; fireEvent.click(nodeButton); expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); @@ -74,6 +75,9 @@ describe("Messages Page", () => { it("sorts unreads to the top", () => { const container = render(); - const buttonOrder = screen.getAllByRole("button") + const buttonOrder = screen.getAllByRole("button").filter(b => b.textContent.includes("Test Node")); + expect(buttonOrder[0].textContent).toContain("TN2Test Node 210"); + expect(buttonOrder[1].textContent).toContain("TN1Test Node 13"); + expect(buttonOrder[2].textContent).toContain("TN3Test Node 3"); }); }); \ No newline at end of file From ce8fcd22691cc484ef5795f4db5b3f2503a3d83a Mon Sep 17 00:00:00 2001 From: Hunter Thornsberry Date: Tue, 11 Mar 2025 16:20:22 -0400 Subject: [PATCH 06/14] simplify device list and tests --- src/pages/Messages.test.tsx | 18 ++++++++---------- src/pages/Messages.tsx | 7 ++++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx index 80d1a0d4..ae90b5ca 100644 --- a/src/pages/Messages.test.tsx +++ b/src/pages/Messages.test.tsx @@ -19,6 +19,10 @@ const mockUseDevice = { }] ]), nodes: new Map([ + [0, { + num: 0, + user: { longName: "Test Node 0", shortName: "TN0", publicKey: "0000" } + }], [1111, { num: 1111, user: { longName: "Test Node 1", shortName: "TN1", publicKey: "12345" } @@ -53,24 +57,17 @@ describe("Messages Page", () => { it("updates unread when active chat changes",() => { render(); - const nodeButton = screen.getByRole("button", { name: "TN Test Node 3" }); + const nodeButton = screen.getAllByRole("button").filter(b => b.textContent.includes("TN1Test Node 13"))[0]; fireEvent.click(nodeButton); - expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); - expect(mockUseDevice.unreadCounts.get(1111)).toBe(0); - const unreadCount = screen.getByText("3"); - expect(unreadCount).not.toBeInTheDocument(); }); - it("does not update the incorrect node",() => { + it("does not update the incorrect node", async () => { render(); const nodeButton = screen.getAllByRole("button").filter(b => b.textContent.includes("TN1Test Node 13"))[0]; fireEvent.click(nodeButton); - expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); expect(mockUseDevice.unreadCounts.get(2222)).toBe(10); - const unreadCount = screen.getByText("10"); - expect(unreadCount).toBeInTheDocument(); }); it("sorts unreads to the top", () => { @@ -78,6 +75,7 @@ describe("Messages Page", () => { const buttonOrder = screen.getAllByRole("button").filter(b => b.textContent.includes("Test Node")); expect(buttonOrder[0].textContent).toContain("TN2Test Node 210"); expect(buttonOrder[1].textContent).toContain("TN1Test Node 13"); - expect(buttonOrder[2].textContent).toContain("TN3Test Node 3"); + expect(buttonOrder[2].textContent).toContain("TN0Test Node 0"); + expect(buttonOrder[3].textContent).toContain("TN3Test Node 3"); }); }); \ No newline at end of file diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index ee5690d6..778c304b 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -21,10 +21,11 @@ export const MessagesPage = () => { if (node.num === hardware.myNodeNum) return false; const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`; return nodeName.toLowerCase().includes(searchTerm.toLowerCase()); - }).map((node) => { - node = {...node, unreadCount: unreadCounts.get(node.num) ?? 0} - return node; }) + .map((node) => ({ + ...node, + unreadCount: unreadCounts.get(node.num) ?? 0 + })) .sort((a, b) => b.unreadCount - a.unreadCount); const allChannels = Array.from(channels.values()); const filteredChannels = allChannels.filter( From 7b77b7f5e9058c8b77b546273fc243a18c9b8e17 Mon Sep 17 00:00:00 2001 From: Hunter Thornsberry Date: Tue, 11 Mar 2025 16:21:44 -0400 Subject: [PATCH 07/14] remove test values --- src/core/stores/deviceStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index 1e0496f0..20e7939d 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -151,7 +151,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, pendingSettingsChanges: false, messageDraft: "", - unreadCounts: new Map([[0, 100],[3663106604, 1]]), + unreadCounts: new Map(), setStatus: (status: Types.DeviceStatusEnum) => { set( From f80bb6c42d35e438af9d336b5c4473ce63be5580 Mon Sep 17 00:00:00 2001 From: Hunter Thornsberry Date: Fri, 14 Mar 2025 15:28:45 -0400 Subject: [PATCH 08/14] DM vs Channel message detection --- src/core/subscriptions.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index 0db53173..db52de3e 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -90,7 +90,14 @@ export const subscribeAll = ( ...messagePacket, state: messagePacket.from !== myNodeNum ? "ack" : "waiting", }); - device.setUnread(messagePacket.from); + if (messagePacket.type == "direct") + { + device.setUnread(messagePacket.from); + } + else + { + device.setUnread(messagePacket.channel); + } }); connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => { From 0bef82ec32b54f333d6ca1f27a775484ba3254b0 Mon Sep 17 00:00:00 2001 From: Hunter Thornsberry Date: Fri, 14 Mar 2025 15:44:02 -0400 Subject: [PATCH 09/14] don't update unred if the channel/dm is active --- src/components/UI/Sidebar/sidebarButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UI/Sidebar/sidebarButton.tsx b/src/components/UI/Sidebar/sidebarButton.tsx index bbd99725..83042f3d 100644 --- a/src/components/UI/Sidebar/sidebarButton.tsx +++ b/src/components/UI/Sidebar/sidebarButton.tsx @@ -27,6 +27,6 @@ export const SidebarButton = ({ {Icon && } {element && element} {label} - {count > 0 &&
{count}
} + {count > 0 && !active &&
{count}
} ); From 7e1ba428732844f88495a8a0ea5119f91d1e5894 Mon Sep 17 00:00:00 2001 From: Hunter Thornsberry Date: Sun, 16 Mar 2025 19:45:27 -0400 Subject: [PATCH 10/14] remove defined css class and just use tailwind --- src/components/UI/Sidebar/sidebarButton.tsx | 2 +- src/index.css | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/UI/Sidebar/sidebarButton.tsx b/src/components/UI/Sidebar/sidebarButton.tsx index 83042f3d..e85f3cb7 100644 --- a/src/components/UI/Sidebar/sidebarButton.tsx +++ b/src/components/UI/Sidebar/sidebarButton.tsx @@ -27,6 +27,6 @@ export const SidebarButton = ({ {Icon && } {element && element} {label} - {count > 0 && !active &&
{count}
} + {count > 0 && !active &&
{count}
} ); diff --git a/src/index.css b/src/index.css index 217ddb0a..d81c2967 100644 --- a/src/index.css +++ b/src/index.css @@ -122,12 +122,4 @@ img { .animate-spin-slow { animation: spin-slower 2s linear infinite; -} - -.notification-count { - color: white; - border-radius: 20%; - padding-left: 2%; - padding-right: 2%; - background-color: rgb(195,0,0); } \ No newline at end of file From d1c19d9d3ed96e03dad251f3fdff41a48f147670 Mon Sep 17 00:00:00 2001 From: Hunter Thornsberry Date: Fri, 21 Mar 2025 14:20:27 -0400 Subject: [PATCH 11/14] add hasNodeError for tests --- src/pages/Messages.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx index ae90b5ca..cd6c84e3 100644 --- a/src/pages/Messages.test.tsx +++ b/src/pages/Messages.test.tsx @@ -40,7 +40,8 @@ const mockUseDevice = { messages: { broadcast: new Map(), direct: new Map() }, metadata: new Map(), unreadCounts: new Map([[1111, 3], [2222, 10]]), - setUnread: vi.fn() + setUnread: vi.fn(), + hasNodeError: vi.fn() }; @@ -64,7 +65,7 @@ describe("Messages Page", () => { it("does not update the incorrect node", async () => { render(); - const nodeButton = screen.getAllByRole("button").filter(b => b.textContent.includes("TN1Test Node 13"))[0]; + const nodeButton = screen.getAllByRole("button").filter(b => b.textContent.includes("TN1Test Node 1"))[0]; fireEvent.click(nodeButton); expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); expect(mockUseDevice.unreadCounts.get(2222)).toBe(10); From dab76df1315c8554df7fa45b636145adf5208543 Mon Sep 17 00:00:00 2001 From: Hunter275 Date: Fri, 21 Mar 2025 23:43:57 -0400 Subject: [PATCH 12/14] reorder tests so they don't step on each other --- src/pages/Messages.test.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx index cd6c84e3..ea50cc41 100644 --- a/src/pages/Messages.test.tsx +++ b/src/pages/Messages.test.tsx @@ -3,7 +3,6 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { MessagesPage } from "./Messages.tsx"; import { useDevice } from "../core/stores/deviceStore"; import { Protobuf } from "@meshtastic/core"; -import { b } from "vitest/dist/chunks/suite.qtkXWc6R.js"; // Mock the store vi.mock("../core/stores/deviceStore", () => ({ @@ -50,10 +49,13 @@ describe("Messages Page", () => { vi.mocked(useDevice).mockReturnValue(mockUseDevice); }); - it("shows unread count correctly", () => { + it("sorts unreads to the top", () => { render(); - const unreadCount = screen.getByText("3"); - expect(unreadCount).toBeInTheDocument(); + const buttonOrder = screen.getAllByRole("button").filter(b => b.textContent.includes("Test Node")); + expect(buttonOrder[0].textContent).toContain("TN2Test Node 210"); + expect(buttonOrder[1].textContent).toContain("TN1Test Node 13"); + expect(buttonOrder[2].textContent).toContain("TN0Test Node 0"); + expect(buttonOrder[3].textContent).toContain("TN3Test Node 3"); }); it("updates unread when active chat changes",() => { @@ -70,13 +72,4 @@ describe("Messages Page", () => { expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); expect(mockUseDevice.unreadCounts.get(2222)).toBe(10); }); - - it("sorts unreads to the top", () => { - const container = render(); - const buttonOrder = screen.getAllByRole("button").filter(b => b.textContent.includes("Test Node")); - expect(buttonOrder[0].textContent).toContain("TN2Test Node 210"); - expect(buttonOrder[1].textContent).toContain("TN1Test Node 13"); - expect(buttonOrder[2].textContent).toContain("TN0Test Node 0"); - expect(buttonOrder[3].textContent).toContain("TN3Test Node 3"); - }); }); \ No newline at end of file From 4755c0eeb9aadff774d5a33f2af6cc293c98a88b Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Fri, 4 Apr 2025 22:22:35 -0400 Subject: [PATCH 13/14] refactor to integrate messageStore and unreadCounts --- .../useRefreshKeysDialog.test.ts | 4 +- .../RefreshKeysDialog/useRefreshKeysDialog.ts | 4 +- src/core/stores/appStore.ts | 20 +--- src/core/stores/deviceStore.ts | 49 ++++++--- src/core/stores/messageStore.test.ts | 11 +- src/core/subscriptions.ts | 23 ++-- src/pages/Messages.tsx | 103 ++++++++++-------- src/pages/Nodes.tsx | 19 ++-- 8 files changed, 118 insertions(+), 115 deletions(-) diff --git a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts index b26d0165..9e8fa30e 100644 --- a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts +++ b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts @@ -3,8 +3,8 @@ import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { useDevice } from "@core/stores/deviceStore.ts"; -vi.mock("@core/stores/appStore.ts", () => ({ - useAppStore: vi.fn(() => ({ activeChat: "chat-123" })), +vi.mock("@core/stores/messageStore.ts", () => ({ + useMessageStore: vi.fn(() => ({ activeChat: "chat-123" })), })); vi.mock("@core/stores/deviceStore.ts", () => ({ diff --git a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts index 821aade7..4a291eea 100644 --- a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts +++ b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts @@ -1,10 +1,10 @@ import { useCallback } from "react"; -import { useAppStore } from "@core/stores/appStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts"; +import { useMessageStore } from "@core/stores/messageStore.ts"; export function useRefreshKeysDialog() { const { removeNode, setDialogOpen, clearNodeError, getNodeError } = useDevice(); - const { activeChat } = useAppStore(); + const { activeChat } = useMessageStore(); const handleNodeRemove = useCallback(() => { const nodeWithError = getNodeError(activeChat); diff --git a/src/core/stores/appStore.ts b/src/core/stores/appStore.ts index 6b3ab022..d7cc67fb 100644 --- a/src/core/stores/appStore.ts +++ b/src/core/stores/appStore.ts @@ -8,7 +8,6 @@ export interface RasterSource { tileSize: number; } - interface ErrorState { field: string; message: string; @@ -19,12 +18,7 @@ interface ErrorState { message: string; } -export interface App { - unreadCounts: Map; - setUnread: (id: number, count: number) => void; -} - -export interface AppState { +interface AppState { selectedDevice: number; devices: { id: number; @@ -36,7 +30,6 @@ export interface AppState { connectDialogOpen: boolean; nodeNumDetails: number; errors: ErrorState[]; - unreadCounts: Map; setRasterSources: (sources: RasterSource[]) => void; addRasterSource: (source: RasterSource) => void; @@ -57,9 +50,6 @@ export interface AppState { removeError: (field: string) => void; clearErrors: () => void; setNewErrors: (newErrors: ErrorState[]) => void; - - // unread counts - setUnread: (id: number, count: number) => void; } export const useAppStore = create()((set, get) => ({ @@ -72,7 +62,6 @@ export const useAppStore = create()((set, get) => ({ nodeNumToBeRemoved: 0, nodeNumDetails: 0, errors: [], - unreadCounts: new Map([[0, 100],[2718471552, 1]]), setRasterSources: (sources: RasterSource[]) => { set( @@ -173,11 +162,4 @@ export const useAppStore = create()((set, get) => ({ }), ); }, - setUnread: (id: number, count: number) => { - set( - produce((draft) => { - draft.unreadCounts.set(id, count); - }) - ); - } })); diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index 2912f9da..68f34e51 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -74,7 +74,6 @@ export interface Device { refreshKeys: boolean; clearMessages: boolean; }; - unreadCounts: Map; setStatus: (status: Types.DeviceStatusEnum) => void; @@ -102,11 +101,12 @@ export interface Device { getDialogOpen: (dialog: DialogVariant) => boolean; processPacket: (data: ProcessPacketParams) => void; setMessageDraft: (message: string) => void; - setUnread: (id: number, count: number) => void; setNodeError: (nodeNum: number, error: string) => void; clearNodeError: (nodeNum: number) => void; getNodeError: (nodeNum: number) => NodeError | undefined; hasNodeError: (nodeNum: number) => boolean + incrementUnread: (nodeNum: number) => void; + resetUnread: (nodeNum: number) => void; } export interface DeviceState { @@ -154,12 +154,13 @@ export const useDeviceStore = createStore((set, get) => ({ unsafeRoles: false, refreshKeys: false, rebootOTA: false, + clearMessages: false, }, pendingSettingsChanges: false, messageDraft: "", - unreadCounts: new Map(), nodeErrors: new Map(), + setStatus: (status: Types.DeviceStatusEnum) => { set( produce((draft) => { @@ -581,20 +582,6 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - setUnread: (unread_id: number, count?: number) => { - set( - produce((draft) => { - const device = draft.devices.get(id); - if (device) { - if (count == null) { - let currentCount = device.unreadCounts.get(unread_id) ?? 0; - count = currentCount + 1; - } - device.unreadCounts.set(unread_id, count); - } - }) - ); - }, setNodeError: (nodeNum, error) => { set( produce((draft) => { @@ -629,7 +616,35 @@ export const useDeviceStore = createStore((set, get) => ({ } return device.nodeErrors.has(nodeNum); }, + incrementUnread: (nodeNum: number) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) { + console.warn(`incrementUnread: Device with ID ${id} not found.`); + return; + } + const currentCount = device.unreadCounts.get(nodeNum) ?? 0; + device.unreadCounts.set(nodeNum, currentCount + 1); + }) + ); + }, + resetUnread: (nodeNum: number) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) { + console.warn(`resetUnread: Device with ID ${id} not found.`); + return; + } + device.unreadCounts.set(nodeNum, 0); + if (device.unreadCounts.get(nodeNum) === 0) { + device.unreadCounts.delete(nodeNum); + } + }) + ); + }, }); }), ); diff --git a/src/core/stores/messageStore.test.ts b/src/core/stores/messageStore.test.ts index e8fc70c7..a8dc9880 100644 --- a/src/core/stores/messageStore.test.ts +++ b/src/core/stores/messageStore.test.ts @@ -9,19 +9,15 @@ import { let memoryStorage: Record = {}; vi.mock('./storage/indexDB.ts', () => { - console.log("Mocking zustandIndexDBStorage..."); return { zustandIndexDBStorage: { getItem: vi.fn(async (name: string): Promise => { - console.log(`Mock getItem: ${name}`, memoryStorage[name] ?? null); return memoryStorage[name] ?? null; }), setItem: vi.fn(async (name: string, value: string): Promise => { - console.log(`Mock setItem: ${name}`, value); memoryStorage[name] = value; }), removeItem: vi.fn(async (name: string): Promise => { - console.log(`Mock removeItem: ${name}`); delete memoryStorage[name]; }), }, @@ -211,11 +207,14 @@ describe('useMessageStore', () => { expect(messages).toEqual([]); }); - it('should return empty array if myNodeNum is not provided for direct messages', () => { + it('should return combined direct messages when myNodeNum and otherNodeNum are provided', () => { const messages = useMessageStore.getState().getMessages(MessageType.Direct, { + myNodeNum: myNodeNum, // Keep this otherNodeNum: otherNodeNum1 }); - expect(messages).toEqual([]); + expect(messages).toHaveLength(2); + expect(messages[0]).toEqual(directMessageToOther1); + expect(messages[1]).toEqual(directMessageFromOther1); }); }); diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index 9dd2bd32..279e5058 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -1,10 +1,8 @@ import type { Device } from "@core/stores/deviceStore.ts"; import { MeshDevice, Protobuf } from "@meshtastic/core"; -import type { MessageStore, MessageType } from "@core/stores/messageStore.ts"; -import { MessageType } from "@core/stores/messageStore.ts"; +import type { MessageStore } from "@core/stores/messageStore.ts"; import PacketToMessageDTO from "@core/dto/PacketToMessageDTO.ts"; - export const subscribeAll = ( device: Device, connection: MeshDevice, @@ -87,15 +85,20 @@ export const subscribeAll = ( connection.events.onMessagePacket.subscribe((messagePacket) => { + // incoming and outgoing messages are handled by this event listener const dto = new PacketToMessageDTO(messagePacket, myNodeNum); const message = dto.toMessage(); - messsageStore.saveMessage(message); - - message.type == MessageType.Direct - ? - device.setUnread(messagePacket.from); - : - device.setUnread(messagePacket.channel); + messageStore.saveMessage(message); + + if (message.type == MessageType.Direct) { + if (message.to === myNodeNum) { + device.incrementUnread(messagePacket.from); + } + } else if (message.type == MessageType.Broadcast) { + if (message.from !== myNodeNum) { + device.incrementUnread(message.channel); + } + } }); connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => { diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index ef11ff9d..a191044b 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -15,23 +15,27 @@ import { MessageInput } from "@components/PageComponents/Messages/MessageInput.t import { cn } from "@core/utils/cn.ts"; import { MessageType, useMessageStore } from "@core/stores/messageStore.ts"; +type NodeInfoWithUnread = Protobuf.Mesh.NodeInfo & { unreadCount: number }; + export const MessagesPage = () => { - const { channels, nodes, hardware, hasNodeError, unreadCounts, setUnread } = useDevice(); + const { channels, nodes, hardware, hasNodeError, unreadCounts, resetUnread } = useDevice(); const { getNodeNum, getMessages, setActiveChat, chatType, activeChat, setChatType } = useMessageStore() const { toast } = useToast(); - const [searchTerm, setSearchTerm] = useState(""); - const filteredNodes = Array.from(nodes.values()).filter((node) => { - if (node.num === hardware.myNodeNum) return false; - const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`; - return nodeName.toLowerCase().includes(searchTerm.toLowerCase()); - }) - .map((node) => ({ - ...node, - unreadCount: unreadCounts.get(node.num) ?? 0 - })) - .sort((a, b) => b.unreadCount - a.unreadCount); + const filteredNodes: NodeInfoWithUnread[] = Array.from(nodes.values()) + .filter((node) => node.num !== hardware.myNodeNum) + .map((node) => ({ + ...node, + unreadCount: unreadCounts.get(node.num) ?? 0, + })) + .filter((node) => { + const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`; + return nodeName.toLowerCase().includes(searchTerm.toLowerCase()); + }) + .sort((a, b) => b.unreadCount - a.unreadCount); + + const allChannels = Array.from(channels.values()); const filteredChannels = allChannels.filter( (ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED, @@ -55,47 +59,44 @@ export const MessagesPage = () => { { setChatType(MessageType.Broadcast); setActiveChat(channel.index); - setUnread(channel.index, 0); + resetUnread(channel.index); }} element={} /> ))} -
+
setSearchTerm(e.target.value)} - className="w-full p-2 border border-slate-300 rounded-sm bg-white text-slate-900" + className="w-full p-2 border border-slate-300 rounded-sm bg-white text-slate-900 dark:bg-slate-700 dark:border-slate-600 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500" />
-
- {filteredNodes.map((otherNode) => ( +
+ {filteredNodes.map((node) => ( 0 ? node.unreadCount : undefined} + active={activeChat === node.num && chatType === MessageType.Direct} onClick={() => { setChatType(MessageType.Direct); - setActiveChat(otherNode.num); - setUnread(otherNode.num, 0); + setActiveChat(node.num); + resetUnread(node.num); }} element={ } @@ -107,26 +108,22 @@ export const MessagesPage = () => {
{
)} + + {!isBroadcast && !isDirect && ( +
+ Select a channel or node to start messaging. +
+ )}
- + {(isBroadcast || isDirect) ? ( + + ) : ( +
Select a chat to send a message.
+ )}
diff --git a/src/pages/Nodes.tsx b/src/pages/Nodes.tsx index 389bf0d0..371e53c3 100644 --- a/src/pages/Nodes.tsx +++ b/src/pages/Nodes.tsx @@ -24,10 +24,9 @@ function shortNameFromNode( ): string { const shortNameOfNode = node.user?.shortName ?? (node.user?.macaddr - ? `${ - base16 - .stringify(node.user?.macaddr.subarray(4, 6) ?? []) - .toLowerCase() + ? `${base16 + .stringify(node.user?.macaddr.subarray(4, 6) ?? []) + .toLowerCase() }` : `${numberToHexUnpadded(node.num).slice(-4)}`); return String(shortNameOfNode); @@ -93,7 +92,7 @@ const NodesPage = (): JSX.Element => { placeholder="Search nodes..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - className="w-full p-2 border border-slate-300 rounded-sm bg-white text-slate-900" + className="w-full p-2 border border-slate-300 rounded-sm bg-white text-slate-900 dark:bg-slate-700 dark:border-slate-600 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500" />
@@ -124,10 +123,9 @@ const NodesPage = (): JSX.Element => { > {node.user?.longName ?? (node.user?.macaddr - ? `Meshtastic ${ - base16 - .stringify(node.user?.macaddr.subarray(4, 6) ?? []) - .toLowerCase() + ? `Meshtastic ${base16 + .stringify(node.user?.macaddr.subarray(4, 6) ?? []) + .toLowerCase() }` : `!${numberToHexUnpadded(node.num)}`)} , @@ -135,8 +133,7 @@ const NodesPage = (): JSX.Element => { {node.lastHeard !== 0 ? node.viaMqtt === false && node.hopsAway === 0 ? "Direct" - : `${node.hopsAway?.toString()} ${ - node.hopsAway > 1 ? "hops" : "hop" + : `${node.hopsAway?.toString()} ${node.hopsAway > 1 ? "hops" : "hop" } away` : "-"} {node.viaMqtt === true ? ", via MQTT" : ""} From 818bbb4a302c7042abe7695197d2bc801b7f6f55 Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Sat, 5 Apr 2025 08:39:27 -0400 Subject: [PATCH 14/14] fix broken test --- src/pages/Messages.test.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx index ea50cc41..1ad82b4d 100644 --- a/src/pages/Messages.test.tsx +++ b/src/pages/Messages.test.tsx @@ -4,7 +4,6 @@ import { MessagesPage } from "./Messages.tsx"; import { useDevice } from "../core/stores/deviceStore"; import { Protobuf } from "@meshtastic/core"; -// Mock the store vi.mock("../core/stores/deviceStore", () => ({ useDevice: vi.fn() })); @@ -39,7 +38,7 @@ const mockUseDevice = { messages: { broadcast: new Map(), direct: new Map() }, metadata: new Map(), unreadCounts: new Map([[1111, 3], [2222, 10]]), - setUnread: vi.fn(), + resetUnread: vi.fn(), hasNodeError: vi.fn() }; @@ -62,14 +61,14 @@ describe("Messages Page", () => { render(); const nodeButton = screen.getAllByRole("button").filter(b => b.textContent.includes("TN1Test Node 13"))[0]; fireEvent.click(nodeButton); - expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); + expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0); }); it("does not update the incorrect node", async () => { render(); const nodeButton = screen.getAllByRole("button").filter(b => b.textContent.includes("TN1Test Node 1"))[0]; fireEvent.click(nodeButton); - expect(mockUseDevice.setUnread).toHaveBeenCalledWith(1111, 0); + expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0); expect(mockUseDevice.unreadCounts.get(2222)).toBe(10); }); }); \ No newline at end of file