Browse Source

keyed conversations against from/to, updated tests

pull/536/head
Dan Ditomaso 1 year ago
parent
commit
eadadb5d1d
  1. 1
      src/components/PageComponents/Messages/MessageInput.tsx
  2. 4
      src/components/PageComponents/Messages/MessageItem.tsx
  3. 579
      src/core/stores/messageStore.test.ts
  4. 143
      src/core/stores/messageStore.ts
  5. 1
      src/pages/Messages.tsx

1
src/components/PageComponents/Messages/MessageInput.tsx

@ -13,7 +13,6 @@ export interface MessageInputProps {
maxBytes: number;
}
export const MessageInput = ({
to,
channel,

4
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;
}
}

579
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<Types.Destination, string>(),
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<string, string> = {};
vi.mock('./storage/indexDB.ts', () => {
console.log("Mocking zustandIndexDBStorage...");
return {
zustandIndexDBStorage: {
getItem: vi.fn(async (name: string): Promise<string | null> => {
console.log(`Mock getItem: ${name}`, memoryStorage[name] ?? null);
return memoryStorage[name] ?? null;
}),
setItem: vi.fn(async (name: string, value: string): Promise<void> => {
console.log(`Mock setItem: ${name}`, value);
memoryStorage[name] = value;
}),
removeItem: vi.fn(async (name: string): Promise<void> => {
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({});
});
});
});
});

143
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<MessageType.Direct> | GenericMessage<Messag
export interface MessageStore {
messages: {
direct: Record<number, Record<number, Message>>; // other_node_num -> messageId -> Message
direct: Record<number, Record<number, Record<number, Message>>>;
broadcast: Record<number, Record<number, Message>>; // channel -> messageId -> Message
};
draft: Map<Types.Destination, string>;
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<MessageStore>()(
persist(
(set, get) => ({
messages: {
direct: {},
direct: {}, // Record<sender, Record<recipient, Record<messageId, Message>>>
broadcast: {},
},
draft: new Map<number, string>(),
@ -90,15 +99,25 @@ export const useMessageStore = create<MessageStore>()(
},
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<MessageStore>()(
}) => {
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<MessageStore>()(
}
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<MessageStore>()(
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<MessageStore>()(
{
name: 'meshtastic-message-store',
storage: createJSONStorage(() => zustandIndexDBStorage),
version: CURRENT_STORE_VERSION,
partialize: (state) => ({
messages: state.messages,
nodeNum: state.nodeNum,
}),
}
)
);
));

1
src/pages/Messages.tsx

@ -153,6 +153,7 @@ export const MessagesPage = () => {
<div className="shrink-0 p-4 w-full dark:bg-slate-900">
<MessageInput
from={activeChat}
to={currentChat.type === MessageType.Direct ? activeChat : MessageType.Broadcast}
channel={currentChat.type === MessageType.Direct ? Types.ChannelNumber.Primary : currentChat.id}
maxBytes={200}

Loading…
Cancel
Save