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" : ""}