diff --git a/src/components/PageComponents/Messages/MessageInput.tsx b/src/components/PageComponents/Messages/MessageInput.tsx index 45fdc212..a2383d0a 100644 --- a/src/components/PageComponents/Messages/MessageInput.tsx +++ b/src/components/PageComponents/Messages/MessageInput.tsx @@ -13,7 +13,6 @@ export interface MessageInputProps { maxBytes: number; } - export const MessageInput = ({ to, channel, diff --git a/src/components/PageComponents/Messages/MessageItem.tsx b/src/components/PageComponents/Messages/MessageItem.tsx index 549cd678..f448bb01 100644 --- a/src/components/PageComponents/Messages/MessageItem.tsx +++ b/src/components/PageComponents/Messages/MessageItem.tsx @@ -90,11 +90,7 @@ export const MessageItem = ({ lastMsgSameUser, message }: MessageProps) => { const messageUser: Protobuf.Mesh.NodeInfo | null = useMemo(() => { if (message?.from === null || message?.from === undefined) return null; for (const device of getDevices()) { - console.log("MessageItem: getDevices", { device }); - if (device.nodes.has(message.from)) { - console.log("MessageItem hasNode", { device, message }); - return device.nodes.get(message.from) ?? null; } } diff --git a/src/core/stores/messageStore.test.ts b/src/core/stores/messageStore.test.ts index e9a456c7..e8fc70c7 100644 --- a/src/core/stores/messageStore.test.ts +++ b/src/core/stores/messageStore.test.ts @@ -1,337 +1,372 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useMessageStore, Message, MessageState, MessageType } from './messageStore.ts'; -import { Types } from '@meshtastic/core'; - -vi.mock('./storage/indexDB.ts', () => ({ - zustandIndexDBStorage: { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - }, -})); - -beforeEach(() => { - useMessageStore.setState({ - messages: { direct: {}, broadcast: {} }, - draft: new Map(), - nodeNum: 0, - activeChat: 0, - chatType: MessageType.Broadcast, - }); +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + useMessageStore, + MessageType, + MessageState, + type Message, +} from './messageStore.ts'; + +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]; + }), + }, + }; }); +const myNodeNum = 111; +const otherNodeNum1 = 222; +const otherNodeNum2 = 333; +const broadcastChannel = 0; + + + +const directMessageToOther1: Message = { + type: MessageType.Direct, + from: myNodeNum, + to: otherNodeNum1, + channel: 0, + date: Date.now(), + messageId: 101, + state: MessageState.Waiting, + message: 'Hello other 1 from me', +}; + +const directMessageFromOther1: Message = { + type: MessageType.Direct, + from: otherNodeNum1, + to: myNodeNum, + channel: 0, + date: Date.now() + 1000, + messageId: 102, + state: MessageState.Waiting, + message: 'Hello me from other 1', +}; + +const directMessageToOther2: Message = { + type: MessageType.Direct, + from: myNodeNum, + to: otherNodeNum2, + channel: 0, + date: Date.now() + 2000, + messageId: 103, + state: MessageState.Waiting, + message: 'Hello other 2 from me', +}; + +const broadcastMessage1: Message = { + type: MessageType.Broadcast, + from: otherNodeNum1, + to: 0xffffffff, + channel: broadcastChannel, + date: Date.now() + 3000, + messageId: 201, + state: MessageState.Waiting, + message: 'Broadcast message 1', +}; + +const broadcastMessage2: Message = { + type: MessageType.Broadcast, + from: myNodeNum, + to: 0xffffffff, + channel: broadcastChannel, + date: Date.now() + 4000, + messageId: 202, + state: MessageState.Waiting, + message: 'Broadcast message 2', +}; + describe('useMessageStore', () => { - it('sets and gets nodeNum', () => { - useMessageStore.getState().setNodeNum(42); - expect(useMessageStore.getState().getNodeNum()).toBe(42); + const initialState = useMessageStore.getState(); + + beforeEach(() => { + useMessageStore.setState(initialState, true); }); - it('sets activeChat', () => { - useMessageStore.getState().setActiveChat(123); - expect(useMessageStore.getState().activeChat).toBe(123); + it('should have correct initial state', () => { + const state = useMessageStore.getState(); + expect(state.messages.direct).toEqual({}); + expect(state.messages.broadcast).toEqual({}); + expect(state.draft).toBeInstanceOf(Map); + expect(state.draft.size).toBe(0); + expect(state.nodeNum).toBe(0); + expect(state.activeChat).toBe(0); + expect(state.chatType).toBe(MessageType.Broadcast); }); - it('sets chatType', () => { + it('should set nodeNum', () => { + useMessageStore.getState().setNodeNum(myNodeNum); + expect(useMessageStore.getState().nodeNum).toBe(myNodeNum); + }); + + 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('saves a direct message', () => { - const message: Message = { - type: MessageType.Direct, - channel: 0, - to: 101, - from: 202, - date: Date.now(), - messageId: 1, - state: MessageState.Waiting, - message: 'Hello Direct', - }; - useMessageStore.getState().saveMessage(message); - expect(useMessageStore.getState().messages.direct[101]?.[1]).toEqual(message); + it('should save a direct message with correct structure', () => { + useMessageStore.getState().saveMessage(directMessageToOther1); + const state = useMessageStore.getState(); + expect(state.messages.direct[myNodeNum]).toBeDefined(); + expect(state.messages.direct[myNodeNum][otherNodeNum1]).toBeDefined(); + expect( + state.messages.direct[myNodeNum][otherNodeNum1][directMessageToOther1.messageId], + ).toEqual(directMessageToOther1); }); - it('saves a broadcast message', () => { - const message: Message = { - type: MessageType.Broadcast, - channel: 5, - to: 0, - from: 303, - date: Date.now(), - messageId: 100, - state: MessageState.Waiting, - message: 'Broadcast Message', - }; - useMessageStore.getState().saveMessage(message); - expect(useMessageStore.getState().messages.broadcast[5]?.[100]).toEqual(message); + it('should save a broadcast message with correct structure', () => { + useMessageStore.getState().saveMessage(broadcastMessage1); + const state = useMessageStore.getState(); + expect(state.messages.broadcast[broadcastChannel]).toBeDefined(); + expect( + state.messages.broadcast[broadcastChannel][broadcastMessage1.messageId], + ).toEqual(broadcastMessage1); }); - it('ensures date is stored as milliseconds', () => { - const now = Date.now(); - const message: Message = { - type: MessageType.Direct, - channel: 0, - to: 101, - from: 202, - date: now, - messageId: 1, - state: MessageState.Waiting, - message: 'Hello Direct', - }; - useMessageStore.getState().saveMessage(message); - expect(useMessageStore.getState().messages.direct[101]?.[1]?.date).toBe(new Date(now).getTime()); + it('should save multiple messages correctly', () => { + useMessageStore.getState().saveMessage(directMessageToOther1); + useMessageStore.getState().saveMessage(directMessageFromOther1); + useMessageStore.getState().saveMessage(broadcastMessage1); + + const state = useMessageStore.getState(); + + // Direct msg 1 (me -> other1) + expect(state.messages.direct[myNodeNum]?.[otherNodeNum1]?.[directMessageToOther1.messageId]).toEqual(directMessageToOther1); + // Direct msg 2 (other1 -> me) + expect(state.messages.direct[otherNodeNum1]?.[myNodeNum]?.[directMessageFromOther1.messageId]).toEqual(directMessageFromOther1); + // Broadcast msg 1 + expect(state.messages.broadcast[broadcastChannel]?.[broadcastMessage1.messageId]).toEqual(broadcastMessage1); }); }); - describe('setMessageState', () => { - it('updates the state of an existing direct message', () => { - const message: Message = { - type: MessageType.Direct, - channel: 0, - to: 101, - from: 202, - date: Date.now(), - messageId: 1, - state: MessageState.Waiting, - message: 'Change me', - }; - useMessageStore.getState().saveMessage(message); + describe('getMessages', () => { + beforeEach(() => { + useMessageStore.getState().setNodeNum(myNodeNum); + useMessageStore.getState().saveMessage(directMessageToOther1); + useMessageStore.getState().saveMessage(directMessageFromOther1); + useMessageStore.getState().saveMessage(directMessageToOther2); + useMessageStore.getState().saveMessage(broadcastMessage1); + useMessageStore.getState().saveMessage(broadcastMessage2); + }); - useMessageStore.getState().setMessageState({ - type: MessageType.Direct, - key: 101, - messageId: 1, - newState: MessageState.Ack, + it('should return broadcast messages for a channel, sorted by date', () => { + const messages = useMessageStore.getState().getMessages(MessageType.Broadcast, { + myNodeNum: myNodeNum, // Not strictly needed for broadcast, but good practice + channel: broadcastChannel }); + expect(messages).toHaveLength(2); + expect(messages[0]).toEqual(broadcastMessage1); + expect(messages[1]).toEqual(broadcastMessage2); + }); - expect(useMessageStore.getState().messages.direct[101]?.[1]?.state).toBe(MessageState.Ack); + it('should return empty array for broadcast if channel has no messages', () => { + const messages = useMessageStore.getState().getMessages(MessageType.Broadcast, { + myNodeNum: myNodeNum, + channel: 99 + }); + expect(messages).toEqual([]); }); - it('updates the state of an existing broadcast message', () => { - const message: Message = { - type: MessageType.Broadcast, - channel: 5, - to: 0, - from: 303, - date: Date.now(), - messageId: 100, - state: MessageState.Waiting, - message: 'Broadcast Message', - }; - useMessageStore.getState().saveMessage(message); + it('should return combined direct messages for a specific chat (pair), sorted by date', () => { + const messages = useMessageStore.getState().getMessages(MessageType.Direct, { + myNodeNum: myNodeNum, + otherNodeNum: otherNodeNum1 + }); + expect(messages).toHaveLength(2); + expect(messages[0]).toEqual(directMessageToOther1); + expect(messages[1]).toEqual(directMessageFromOther1); + }); - useMessageStore.getState().setMessageState({ - type: MessageType.Broadcast, - key: 5, - messageId: 100, - newState: MessageState.Failed, + it('should return only relevant direct messages for a different chat pair', () => { + const messages = useMessageStore.getState().getMessages(MessageType.Direct, { + myNodeNum: myNodeNum, + otherNodeNum: otherNodeNum2 + }); + expect(messages).toHaveLength(1); + expect(messages[0]).toEqual(directMessageToOther2); + }); + + it('should return empty array for direct chat if no messages exist', () => { + const messages = useMessageStore.getState().getMessages(MessageType.Direct, { + myNodeNum: myNodeNum, + otherNodeNum: 999 }); + expect(messages).toEqual([]); + }); - expect(useMessageStore.getState().messages.broadcast[5]?.[100]?.state).toBe(MessageState.Failed); + it('should return empty array if myNodeNum is not provided for direct messages', () => { + const messages = useMessageStore.getState().getMessages(MessageType.Direct, { + otherNodeNum: otherNodeNum1 + }); + expect(messages).toEqual([]); }); + }); - it('does not update if the message is not found and logs a warning', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { }); + describe('setMessageState', () => { + beforeEach(() => { + useMessageStore.getState().setNodeNum(myNodeNum); + useMessageStore.getState().saveMessage(directMessageToOther1); + useMessageStore.getState().saveMessage(directMessageFromOther1); + useMessageStore.getState().saveMessage(broadcastMessage1); + }); + it('should update state for a direct message sent BY ME', () => { useMessageStore.getState().setMessageState({ type: MessageType.Direct, - key: 999, - messageId: 99, + key: otherNodeNum1, + messageId: directMessageToOther1.messageId, newState: MessageState.Ack, }); - - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Message not found - type: direct, key: 999, messageId: 99', - ); - - consoleWarnSpy.mockRestore(); + const message = useMessageStore.getState().messages.direct[myNodeNum]?.[otherNodeNum1]?.[directMessageToOther1.messageId]; + expect(message?.state).toBe(MessageState.Ack); }); - }); - it('clears all messages', () => { - useMessageStore.getState().saveMessage({ - type: MessageType.Broadcast, - channel: 5, - to: 0, - from: 303, - date: Date.now(), - messageId: 100, - state: MessageState.Waiting, - message: 'Broadcast Message', - }); - useMessageStore.getState().saveMessage({ - type: MessageType.Direct, - channel: 0, - to: 101, - from: 202, - date: Date.now(), - messageId: 1, - state: MessageState.Waiting, - message: 'Hello Direct', + it('should update state for a direct message received FROM OTHER', () => { + useMessageStore.getState().setMessageState({ + type: MessageType.Direct, + key: otherNodeNum1, + messageId: directMessageFromOther1.messageId, + newState: MessageState.Failed, + }); + const message = useMessageStore.getState().messages.direct[otherNodeNum1]?.[myNodeNum]?.[directMessageFromOther1.messageId]; + expect(message?.state).toBe(MessageState.Failed); }); - useMessageStore.getState().clearAllMessages(); - expect(useMessageStore.getState().messages.direct).toEqual({}); - expect(useMessageStore.getState().messages.broadcast).toEqual({}); - }); - describe('getMessages', () => { - it('retrieves sorted broadcast messages for a channel', () => { - const now = Date.now(); - const earlier = now - 10000; - const later = now; - - useMessageStore.getState().saveMessage({ - type: MessageType.Broadcast, - channel: 4, - to: 0, - from: 404, - date: later, - messageId: 2, - state: MessageState.Waiting, - message: 'Second', - }); - useMessageStore.getState().saveMessage({ + it('should update state for a broadcast message', () => { + useMessageStore.getState().setMessageState({ type: MessageType.Broadcast, - channel: 4, - to: 0, - from: 404, - date: earlier, - messageId: 1, - state: MessageState.Waiting, - message: 'First', + key: broadcastChannel, + messageId: broadcastMessage1.messageId, + newState: MessageState.Ack, }); - - const messages = useMessageStore.getState().getMessages(MessageType.Broadcast, { channel: 4 }); - expect(messages.map((m) => m.message)).toEqual(['First', 'Second']); - expect(messages[0]?.date).toBe(earlier); - expect(messages[1]?.date).toBe(later); + const message = useMessageStore.getState().messages.broadcast[broadcastChannel]?.[broadcastMessage1.messageId]; + expect(message?.state).toBe(MessageState.Ack); }); - it('returns an empty array for broadcast messages if channel does not exist', () => { - const messages = useMessageStore.getState().getMessages(MessageType.Broadcast, { channel: 99 }); - expect(messages).toEqual([]); + it('should warn if message is not found', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { }); + useMessageStore.getState().setMessageState({ + type: MessageType.Direct, + key: otherNodeNum1, + messageId: 999, + newState: MessageState.Ack, + }); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Message not found for state update')); + warnSpy.mockRestore(); }); + }); - it('merges and sorts direct messages by date', () => { - const myNodeNum = 1; - const otherNodeNum = 2; - const now = Date.now(); - const earlier = now - 10000; - const later = now + 10000; - const incomingMessage: Message = { - type: MessageType.Direct, - channel: 0, - to: myNodeNum, - from: otherNodeNum, - date: earlier, - messageId: 1, - state: MessageState.Ack, - message: 'Incoming from 2', - }; - useMessageStore.getState().saveMessage(incomingMessage); - - const outgoingMessage: Message = { + describe('clearMessageByMessageId', () => { + beforeEach(() => { + useMessageStore.getState().setNodeNum(myNodeNum); + useMessageStore.getState().saveMessage(directMessageToOther1); + useMessageStore.getState().saveMessage(directMessageFromOther1); + useMessageStore.getState().saveMessage(broadcastMessage1); + useMessageStore.getState().saveMessage({ ...directMessageToOther1, messageId: 1011, date: Date.now() + 50 }); + }); + + it('should delete a specific direct message (sent by me)', () => { + const messageIdToDelete = directMessageToOther1.messageId; + useMessageStore.getState().clearMessageByMessageId({ type: MessageType.Direct, - channel: 0, - to: otherNodeNum, - from: myNodeNum, - date: later, - messageId: 2, - state: MessageState.Waiting, - message: 'Outgoing from 1', - }; - useMessageStore.getState().saveMessage(outgoingMessage); - - const merged = useMessageStore.getState().getMessages(MessageType.Direct, { - myNodeNum: myNodeNum, - otherNodeNum: otherNodeNum, + sender: myNodeNum, + recipient: otherNodeNum1, + messageId: messageIdToDelete }); + const state = useMessageStore.getState(); + expect(state.messages.direct[myNodeNum]?.[otherNodeNum1]?.[messageIdToDelete]).toBeUndefined(); + expect(state.messages.direct[myNodeNum]?.[otherNodeNum1]?.[1011]).toBeDefined(); + expect(state.messages.direct[otherNodeNum1]?.[myNodeNum]?.[directMessageFromOther1.messageId]).toBeDefined(); + }); - expect(merged.length).toBe(2); - expect(merged.map((m) => m.message)).toEqual(['Incoming from 2', 'Outgoing from 1']); - expect(merged[0]?.date).toBe(earlier); - expect(merged[1]?.date).toBe(later); + it('should delete a specific direct message (sent by other)', () => { + const messageIdToDelete = directMessageFromOther1.messageId; + useMessageStore.getState().clearMessageByMessageId({ + type: MessageType.Direct, + sender: otherNodeNum1, + recipient: myNodeNum, + messageId: messageIdToDelete + }); + const state = useMessageStore.getState(); + expect(state.messages.direct[otherNodeNum1]?.[myNodeNum]?.[messageIdToDelete]).toBeUndefined(); + expect(state.messages.direct[myNodeNum]?.[otherNodeNum1]?.[directMessageToOther1.messageId]).toBeDefined(); + expect(state.messages.direct[myNodeNum]?.[otherNodeNum1]?.[1011]).toBeDefined(); }); - it('returns an empty array for direct messages if no messages exist between nodes', () => { - const myNodeNum = 1; - const otherNodeNum = 2; - const messages = useMessageStore.getState().getMessages(MessageType.Direct, { - myNodeNum: myNodeNum, - otherNodeNum: otherNodeNum, + it('should delete a specific broadcast message', () => { + const messageIdToDelete = broadcastMessage1.messageId; + useMessageStore.getState().clearMessageByMessageId({ + type: MessageType.Broadcast, + channel: broadcastChannel, + messageId: messageIdToDelete }); - expect(messages).toEqual([]); + const state = useMessageStore.getState(); + expect(state.messages.broadcast[broadcastChannel]?.[messageIdToDelete]).toBeUndefined(); + }); + + it('should clean up empty recipient/sender/channel objects', () => { + useMessageStore.getState().clearMessageByMessageId({ type: MessageType.Direct, sender: otherNodeNum1, recipient: myNodeNum, messageId: directMessageFromOther1.messageId }); + expect(useMessageStore.getState().messages.direct[otherNodeNum1]?.[myNodeNum]).toBeUndefined(); // Recipient level removed + expect(useMessageStore.getState().messages.direct[otherNodeNum1]).toBeUndefined(); // Sender level removed + + useMessageStore.getState().clearMessageByMessageId({ type: MessageType.Broadcast, channel: broadcastChannel, messageId: broadcastMessage1.messageId }); + expect(useMessageStore.getState().messages.broadcast[broadcastChannel]).toBeUndefined(); // Channel level removed }); }); - describe('draft functionality', () => { - it('sets and gets a draft message', () => { - const key: Types.Destination = 123; - useMessageStore.getState().setDraft(key, 'Draft text'); - expect(useMessageStore.getState().getDraft(key)).toBe('Draft text'); + describe('Drafts', () => { + const draftKey = otherNodeNum1; + const draftMessage = 'This is a draft'; + + it('should set and get a draft', () => { + useMessageStore.getState().setDraft(draftKey, draftMessage); + expect(useMessageStore.getState().draft.get(draftKey)).toBe(draftMessage); + expect(useMessageStore.getState().getDraft(draftKey)).toBe(draftMessage); }); - it('gets an empty string if no draft exists for a key', () => { - const key: Types.Destination = 456; - expect(useMessageStore.getState().getDraft(key)).toBe(''); + it('should return empty string for non-existent draft', () => { + expect(useMessageStore.getState().getDraft(999)).toBe(''); }); - it('clears a draft message', () => { - const key: Types.Destination = 123; - useMessageStore.getState().setDraft(key, 'Draft to clear'); - useMessageStore.getState().clearDraft(key); - expect(useMessageStore.getState().getDraft(key)).toBe(''); + it('should clear a draft', () => { + useMessageStore.getState().setDraft(draftKey, draftMessage); + expect(useMessageStore.getState().draft.has(draftKey)).toBe(true); + useMessageStore.getState().clearDraft(draftKey); + expect(useMessageStore.getState().draft.has(draftKey)).toBe(false); + expect(useMessageStore.getState().getDraft(draftKey)).toBe(''); }); }); - describe('clearMessageByMessageId', () => { - it('clears a direct message by messageId', () => { - const message: Message = { - type: MessageType.Direct, - channel: 0, - to: 111, - from: 222, - date: Date.now(), - messageId: 42, - state: MessageState.Waiting, - message: 'To be deleted', - }; - useMessageStore.getState().saveMessage(message); - expect(useMessageStore.getState().messages.direct[111]?.[42]).toBeDefined(); - - useMessageStore.getState().clearMessageByMessageId(MessageType.Direct, 42); - - expect(useMessageStore.getState().messages.direct[111]?.[42]).toBeUndefined(); - expect(useMessageStore.getState().messages.direct[111]).toBeUndefined(); - }); + describe('clearAllMessages', () => { + it('should clear all direct and broadcast messages', () => { + useMessageStore.getState().saveMessage(directMessageToOther1); + useMessageStore.getState().saveMessage(broadcastMessage1); + expect(Object.keys(useMessageStore.getState().messages.direct).length).toBeGreaterThan(0); + expect(Object.keys(useMessageStore.getState().messages.broadcast).length).toBeGreaterThan(0); - it('clears a broadcast message by messageId', () => { - const message: Message = { - type: MessageType.Broadcast, - channel: 2, - to: 0, - from: 333, - date: Date.now(), - messageId: 77, - state: MessageState.Waiting, - message: 'Broadcast to delete', - }; - useMessageStore.getState().saveMessage(message); - expect(useMessageStore.getState().messages.broadcast[2]?.[77]).toBeDefined(); - - useMessageStore.getState().clearMessageByMessageId(MessageType.Broadcast, 77); - - expect(useMessageStore.getState().messages.broadcast[2]?.[77]).toBeUndefined(); - expect(useMessageStore.getState().messages.broadcast[2]).toBeUndefined(); - }); + useMessageStore.getState().clearAllMessages(); - it('does not throw error if trying to clear a non-existent message', () => { - expect(() => { - useMessageStore.getState().clearMessageByMessageId(MessageType.Direct, 999); - useMessageStore.getState().clearMessageByMessageId(MessageType.Broadcast, 999); - }).not.toThrow(); + expect(useMessageStore.getState().messages.direct).toEqual({}); + expect(useMessageStore.getState().messages.broadcast).toEqual({}); }); }); -}); \ No newline at end of file + +}); diff --git a/src/core/stores/messageStore.ts b/src/core/stores/messageStore.ts index b6198136..fdb9ad9c 100644 --- a/src/core/stores/messageStore.ts +++ b/src/core/stores/messageStore.ts @@ -19,7 +19,7 @@ interface MessageBase { channel: Types.ChannelNumber; to: number; from: number; - date: number; // Unix timestamp in milliseconds + date: number; messageId: number; state: MessageState; message: string; @@ -33,12 +33,12 @@ export type Message = GenericMessage | GenericMessage>; // other_node_num -> messageId -> Message + direct: Record>>; broadcast: Record>; // channel -> messageId -> Message }; draft: Map; - nodeNum: number; - activeChat: number; + nodeNum: number; // This device's node number + activeChat: number; // Represents otherNodeNum for Direct, or channel for Broadcast chatType: MessageType; setNodeNum: (nodeNum: number) => void; @@ -48,24 +48,33 @@ export interface MessageStore { saveMessage: (message: Message) => void; setMessageState: (params: { type: MessageType; + // For Direct: Represents the *other* node number involved in the chat. + // For Broadcast: Represents the channel number. key: number; messageId: number; newState?: MessageState; }) => void; - getMessages: (type: MessageType, options: { myNodeNum?: number; otherNodeNum?: number; channel?: number }) => Message[]; + getMessages: (type: MessageType, options: { myNodeNum: number; otherNodeNum?: number; channel?: number }) => Message[]; getDraft: (key: Types.Destination) => string; setDraft: (key: Types.Destination, message: string) => void; clearAllMessages: () => void; - clearMessageByMessageId: (type: MessageType, messageId: number) => void; + clearMessageByMessageId: (params: { + type: MessageType; + sender?: number; + recipient?: number; + channel?: number; + messageId: number + }) => void; clearDraft: (key: Types.Destination) => void; - } +const CURRENT_STORE_VERSION = 0; + export const useMessageStore = create()( persist( (set, get) => ({ messages: { - direct: {}, + direct: {}, // Record>> broadcast: {}, }, draft: new Map(), @@ -90,15 +99,25 @@ export const useMessageStore = create()( }, saveMessage: (message) => { set(produce((state: MessageStore) => { - const group = state.messages[message.type]; - // Direct messages are keyed by the RECIPIENT's node number (`message.to`) - // Broadcast messages are keyed by the channel number (`message.channel`) - const key = message.type === MessageType.Direct ? Number(message.to) : Number(message.channel); - if (!group[key]) { - group[key] = {}; + if (message.type === MessageType.Direct) { + const sender = Number(message.from); + const recipient = Number(message.to); + + if (!state.messages.direct[sender]) { + state.messages.direct[sender] = {}; + } + if (!state.messages.direct[sender][recipient]) { + state.messages.direct[sender][recipient] = {}; + } + state.messages.direct[sender][recipient][message.messageId] = message; + + } else if (message.type === MessageType.Broadcast) { + const channel = Number(message.channel); + if (!state.messages.broadcast[channel]) { + state.messages.broadcast[channel] = {}; + } + state.messages.broadcast[channel][message.messageId] = message; } - const messageToSave = { ...message }; - group[key][message.messageId] = messageToSave; })); }, setMessageState: ({ @@ -109,21 +128,30 @@ export const useMessageStore = create()( }) => { set( produce((state: MessageStore) => { - const message = state.messages[type]?.[key]?.[messageId]; + let message: Message | undefined; + + if (type === MessageType.Broadcast) { + const channel = key; + message = state.messages.broadcast?.[channel]?.[messageId]; + } else if (type === MessageType.Direct) { + const otherNodeNum = key; + const myNodeNum = state.nodeNum; + + message = state.messages.direct?.[myNodeNum]?.[otherNodeNum]?.[messageId]; + + if (!message) { + message = state.messages.direct?.[otherNodeNum]?.[myNodeNum]?.[messageId]; + } + } + if (message) { message.state = newState; } else { - console.warn(`Message not found - type: ${type}, key: ${key}, messageId: ${messageId}`); + console.warn(`Message not found for state update - type: ${type}, key (otherNode/channel): ${key}, messageId: ${messageId}, myNodeNum: ${state.nodeNum}`); } }), ); }, - clearMessages: () => { - set(produce((state: MessageStore) => { - state.messages.direct = {}; - state.messages.broadcast = {}; - })); - }, getMessages: (type, options) => { const state = get(); @@ -133,25 +161,47 @@ export const useMessageStore = create()( } if (type === MessageType.Direct && options.myNodeNum !== undefined && options.otherNodeNum !== undefined) { - // Messages TO the other node (sent by me) are keyed under their nodeNum - const messagesToOtherNodeMap = state.messages.direct[options.otherNodeNum] ?? {}; - const sentByMe = Object.values(messagesToOtherNodeMap); + const myNodeNum = options.myNodeNum; + const otherNodeNum = options.otherNodeNum; + + // Messages sent BY ME TO OTHER + const sentByMeMap = state.messages.direct?.[myNodeNum]?.[otherNodeNum] ?? {}; + const sentByMe = Object.values(sentByMeMap); - // Messages TO me (potentially from the other node) are keyed under my nodeNum - const messagesToMeMap = state.messages.direct[options.myNodeNum] ?? {}; - // Filter messages TO me to find the ones FROM the specific other node - const sentByOtherNode = Object.values(messagesToMeMap).filter( - (msg) => msg.from === options.otherNodeNum - ); + // Messages sent BY OTHER TO ME + const sentByOtherMap = state.messages.direct?.[otherNodeNum]?.[myNodeNum] ?? {}; + const sentByOther = Object.values(sentByOtherMap); // Merge and sort chronologically - return [...sentByMe, ...sentByOtherNode].sort( - (a, b) => a.date - b.date - ); + return [...sentByMe, ...sentByOther].sort((a, b) => a.date - b.date); } - return []; }, + clearMessageByMessageId: ({ type, sender, recipient, channel, messageId }) => { + set(produce((state: MessageStore) => { + if (type === MessageType.Broadcast && channel !== undefined) { + const messageMap = state.messages.broadcast[channel]; + if (messageMap?.[messageId]) { + delete messageMap[messageId]; + if (Object.keys(messageMap).length === 0) { + delete state.messages.broadcast[channel]; + } + } + } else if (type === MessageType.Direct && sender !== undefined && recipient !== undefined) { + const messageMap = state.messages.direct?.[sender]?.[recipient]; + if (messageMap?.[messageId]) { + delete messageMap[messageId]; + if (Object.keys(messageMap).length === 0) { + delete state.messages.direct[sender][recipient]; + if (Object.keys(state.messages.direct[sender]).length === 0) { + delete state.messages.direct[sender]; + } + } + } + console.warn("clearMessageByMessageId called without sufficient identifiers for type", type); + } + })); + }, getDraft: (key) => { return get().draft.get(key) ?? ''; }, @@ -160,20 +210,6 @@ export const useMessageStore = create()( state.draft.set(key, message); })); }, - clearMessageByMessageId: (type, messageId) => { - set(produce((state: MessageStore) => { - const group = state.messages[type]; - for (const key in group) { - if (group[key][messageId]) { - delete group[key][messageId]; - if (Object.keys(group[key]).length === 0) { - delete group[key]; - } - break; - } - } - })); - }, clearDraft: (key) => { set(produce((state: MessageStore) => { state.draft.delete(key); @@ -189,9 +225,10 @@ export const useMessageStore = create()( { name: 'meshtastic-message-store', storage: createJSONStorage(() => zustandIndexDBStorage), + version: CURRENT_STORE_VERSION, partialize: (state) => ({ messages: state.messages, + nodeNum: state.nodeNum, }), } - ) -); + )); \ No newline at end of file diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index 45d335f5..214b1e96 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -153,6 +153,7 @@ export const MessagesPage = () => {