WIP
This commit is contained in:
+3
-2
@@ -33,7 +33,7 @@
|
||||
]
|
||||
},
|
||||
"homepage": "https://meshtastic.org",
|
||||
"dependencies": {
|
||||
"ddbependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.3",
|
||||
"@meshtastic/core": "npm:@jsr/meshtastic__core@2.6.0-0",
|
||||
"@meshtastic/js": "npm:@jsr/meshtastic__js@2.6.0-0",
|
||||
@@ -61,6 +61,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.477.0",
|
||||
@@ -100,7 +101,7 @@
|
||||
"tar": "^7.4.3",
|
||||
"testing-library": "^0.0.2",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.0",
|
||||
"vite": "^6.2.3",
|
||||
"vitest": "^3.0.7",
|
||||
"vite-plugin-pwa": "^0.21.1"
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import { subscribeAll } from "@core/subscriptions.ts";
|
||||
import { randId } from "@core/utils/randId.ts";
|
||||
import { BleConnection, ServiceUuid } from "@meshtastic/js";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
|
||||
export const BLE = ({ closeDialog }: TabElementProps) => {
|
||||
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
const messageStore = useMessageStore()
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
const updateBleDeviceList = useCallback(async (): Promise<void> => {
|
||||
@@ -30,7 +32,7 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
|
||||
device: bleDevice,
|
||||
});
|
||||
device.addConnection(connection);
|
||||
subscribeAll(device, connection);
|
||||
subscribeAll(device, connection, messageStore);
|
||||
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportHTTP } from "@meshtastic/transport-http";
|
||||
import { useState } from "react";
|
||||
import { useForm, useController } from "react-hook-form";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
|
||||
interface FormData {
|
||||
ip: string;
|
||||
@@ -21,6 +22,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
|
||||
const isURLHTTPS = location.protocol === "https:";
|
||||
|
||||
const { addDevice } = useDeviceStore();
|
||||
const messageStore = useMessageStore();
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
const { control, handleSubmit, register } = useForm<FormData>({
|
||||
@@ -49,7 +51,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
|
||||
connection.configure();
|
||||
setSelectedDevice(id);
|
||||
device.addConnection(connection);
|
||||
subscribeAll(device, connection);
|
||||
subscribeAll(device, connection, messageStore);
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ import { randId } from "@core/utils/randId.ts";
|
||||
import { MeshDevice } from "@meshtastic/core";
|
||||
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
|
||||
export const Serial = ({ closeDialog }: TabElementProps) => {
|
||||
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
const messageStore = useMessageStore()
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
const updateSerialPortList = useCallback(async () => {
|
||||
@@ -36,7 +38,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
|
||||
const connection = new MeshDevice(transport, id);
|
||||
connection.configure();
|
||||
device.addConnection(connection);
|
||||
subscribeAll(device, connection);
|
||||
subscribeAll(device, connection, messageStore);
|
||||
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
@@ -101,10 +101,10 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||
const TimeDisplay = memo(({ date, className }: { date: Date; className?: string }) => (
|
||||
<div className={cn("flex items-center gap-2 shrink-0", className)}>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
||||
{date.toLocaleDateString()}
|
||||
{date?.toLocaleDateString()}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
||||
{date.toLocaleTimeString(undefined, {
|
||||
{date?.toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
@@ -112,7 +112,9 @@ const TimeDisplay = memo(({ date, className }: { date: Date; className?: string
|
||||
</div>
|
||||
));
|
||||
|
||||
export const Message = memo(({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||
export const Message = (({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||
console.log('Message', message);
|
||||
|
||||
const { getDevices } = useDeviceStore();
|
||||
|
||||
const isDeviceUser = useMemo(
|
||||
@@ -162,7 +164,7 @@ export const Message = memo(({ lastMsgSameUser, message, sender }: MessageProps)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TimeDisplay date={message.rxTime} />
|
||||
<TimeDisplay date={message?.rxTime} />
|
||||
<div className="flex place-items-center gap-2 pb-2">
|
||||
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}>
|
||||
{message.data}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import type { Types } from "@meshtastic/core";
|
||||
import { SendIcon } from "lucide-react";
|
||||
import { startTransition, useCallback, useMemo, useState } from "react";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
|
||||
export interface MessageInputProps {
|
||||
to: Types.Destination;
|
||||
@@ -19,13 +20,11 @@ export const MessageInput = ({
|
||||
}: MessageInputProps) => {
|
||||
const {
|
||||
connection,
|
||||
setMessageState,
|
||||
messageDraft,
|
||||
setMessageDraft,
|
||||
isQueueingMessages,
|
||||
queueStatus,
|
||||
hardware,
|
||||
} = useDevice();
|
||||
const { setMessageState } = useMessageStore()
|
||||
const myNodeNum = hardware.myNodeNum;
|
||||
const [localDraft, setLocalDraft] = useState(messageDraft);
|
||||
const [messageBytes, setMessageBytes] = useState(0);
|
||||
@@ -62,7 +61,7 @@ export const MessageInput = ({
|
||||
)
|
||||
);
|
||||
},
|
||||
[channel, connection, myNodeNum, setMessageState, to, queueStatus],
|
||||
[channel, connection, myNodeNum, setMessageState, to],
|
||||
);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -85,12 +84,10 @@ export const MessageInput = ({
|
||||
if (localDraft === "") return;
|
||||
const message = formData.get("messageInput") as string;
|
||||
startTransition(() => {
|
||||
if (!isQueueingMessages) {
|
||||
sendText(message);
|
||||
setLocalDraft("");
|
||||
setMessageDraft("");
|
||||
setMessageBytes(0);
|
||||
}
|
||||
sendText(message);
|
||||
setLocalDraft("");
|
||||
setMessageDraft("");
|
||||
setMessageBytes(0);
|
||||
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { StateStorage } from "zustand/middleware";
|
||||
import { get, set, del } from "idb-keyval";
|
||||
|
||||
export const zustandIDBStorage: StateStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
return (await get(name)) || null;
|
||||
},
|
||||
setItem: async (name: string, value: string): Promise<void> => {
|
||||
await set(name, value);
|
||||
},
|
||||
removeItem: async (name: string): Promise<void> => {
|
||||
await del(name);
|
||||
},
|
||||
};
|
||||
@@ -30,10 +30,6 @@ export type DialogVariant =
|
||||
| "unsafeRoles"
|
||||
| "refreshKeys";
|
||||
|
||||
type QueueStatus = {
|
||||
res: number, free: number, maxlen: number
|
||||
}
|
||||
|
||||
type NodeError = {
|
||||
node: number;
|
||||
error: string;
|
||||
@@ -66,8 +62,6 @@ export interface Device {
|
||||
// currentMetrics: Protobuf.DeviceMetrics;
|
||||
pendingSettingsChanges: boolean;
|
||||
messageDraft: string;
|
||||
queueStatus: QueueStatus,
|
||||
isQueueingMessages: boolean,
|
||||
dialog: {
|
||||
import: boolean;
|
||||
QR: boolean;
|
||||
@@ -116,7 +110,6 @@ export interface Device {
|
||||
getDialogOpen: (dialog: DialogVariant) => boolean;
|
||||
processPacket: (data: ProcessPacketParams) => void;
|
||||
setMessageDraft: (message: string) => void;
|
||||
setQueueStatus: (status: QueueStatus) => void;
|
||||
setNodeError: (nodeNum: number, error: string) => void;
|
||||
clearNodeError: (nodeNum: number) => void;
|
||||
getNodeError: (nodeNum: number) => NodeError | undefined;
|
||||
@@ -160,10 +153,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
activePage: "messages",
|
||||
activeNode: 0,
|
||||
waypoints: [],
|
||||
queueStatus: {
|
||||
res: 0, free: 0, maxlen: 0
|
||||
},
|
||||
isQueueingMessages: false,
|
||||
dialog: {
|
||||
import: false,
|
||||
QR: false,
|
||||
@@ -664,17 +653,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
||||
}),
|
||||
);
|
||||
},
|
||||
setQueueStatus: (status: QueueStatus) => {
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (device) {
|
||||
device.queueStatus = status;
|
||||
device.queueStatus.free >= 10 ? true : false
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
setNodeError: (nodeNum, error) => {
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { produce } from 'immer';
|
||||
import { Types } from '@meshtastic/core';
|
||||
import { zustandIDBStorage } from "@core/services/messaging/db.ts";
|
||||
|
||||
export interface MessageWithState {
|
||||
id: number;
|
||||
from: number;
|
||||
to: number;
|
||||
channel: number;
|
||||
content: string;
|
||||
state: 'ack' | 'waiting' | 'failed';
|
||||
type: 'direct' | 'broadcast';
|
||||
}
|
||||
|
||||
type MessageType = 'direct' | 'broadcast';
|
||||
|
||||
export interface MessageStore {
|
||||
messages: {
|
||||
direct: Record<number, MessageWithState[]>;
|
||||
broadcast: Record<number, MessageWithState[]>;
|
||||
};
|
||||
|
||||
activeChat: number;
|
||||
chatType: MessageType;
|
||||
|
||||
setActiveChat: (chat: number) => void;
|
||||
setChatType: (type: MessageType) => void;
|
||||
addMessage: (message: MessageWithState) => void;
|
||||
getMessages: (type: MessageType, key: number) => MessageWithState[];
|
||||
setMessageState: (
|
||||
type: MessageType,
|
||||
key: number,
|
||||
messageId: number,
|
||||
newState: MessageWithState['state']
|
||||
) => void;
|
||||
clearMessages: () => void;
|
||||
}
|
||||
|
||||
export const useMessageStore = create<MessageStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
messages: {
|
||||
direct: {},
|
||||
broadcast: {},
|
||||
},
|
||||
|
||||
activeChat: Types.ChannelNumber.Primary,
|
||||
chatType: 'broadcast',
|
||||
|
||||
setActiveChat: (chat) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
state.activeChat = chat;
|
||||
}));
|
||||
},
|
||||
|
||||
setChatType: (type) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
state.chatType = type;
|
||||
}));
|
||||
},
|
||||
|
||||
addMessage: (message) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
const group = message.type === 'direct' ? state.messages.direct : state.messages.broadcast;
|
||||
const key = message.type === 'direct' ? message.from : message.channel;
|
||||
if (!group[key]) {
|
||||
group[key] = [];
|
||||
}
|
||||
group[key].push(message);
|
||||
}));
|
||||
},
|
||||
|
||||
getMessages: (type, key) => {
|
||||
const group = type === 'direct' ? get().messages.direct : get().messages.broadcast;
|
||||
return group[key] ?? [];
|
||||
},
|
||||
|
||||
setMessageState: (type, key, messageId, newState) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
const group = type === 'direct' ? state.messages.direct : state.messages.broadcast;
|
||||
const messages = group[key];
|
||||
if (!messages) return;
|
||||
const message = messages.find((msg) => msg.id === messageId);
|
||||
if (message) {
|
||||
message.state = newState;
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
clearMessages: () => {
|
||||
set(produce((state: MessageStore) => {
|
||||
state.messages.direct = {};
|
||||
state.messages.broadcast = {};
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'mesh-messages',
|
||||
storage: createJSONStorage(() => zustandIDBStorage),
|
||||
// ✅ No need for partialize magic — simple object storage
|
||||
partialize: (state) => ({
|
||||
activeChat: state.activeChat,
|
||||
chatType: state.chatType,
|
||||
messages: state.messages,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Device } from "@core/stores/deviceStore.ts";
|
||||
import { MeshDevice, Protobuf } from "@meshtastic/core";
|
||||
import type { MessageStore } from "@core/stores/messageStore.ts";
|
||||
|
||||
export const subscribeAll = (
|
||||
device: Device,
|
||||
connection: MeshDevice,
|
||||
messageStore: MessageStore
|
||||
) => {
|
||||
let myNodeNum = 0;
|
||||
|
||||
@@ -15,6 +17,8 @@ export const subscribeAll = (
|
||||
});
|
||||
|
||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||
console.log("routingPacket", routingPacket);
|
||||
|
||||
switch (routingPacket.data.variant.case) {
|
||||
case "errorReason": {
|
||||
if (
|
||||
@@ -81,8 +85,10 @@ export const subscribeAll = (
|
||||
|
||||
|
||||
connection.events.onMessagePacket.subscribe((messagePacket) => {
|
||||
device.addMessage({
|
||||
console.log("messagePacket", messagePacket);
|
||||
messageStore.addMessage({
|
||||
...messagePacket,
|
||||
|
||||
state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
|
||||
});
|
||||
});
|
||||
@@ -105,9 +111,9 @@ export const subscribeAll = (
|
||||
});
|
||||
});
|
||||
|
||||
connection.events.onQueueStatus.subscribe((queueStatus) => {
|
||||
device.setQueueStatus(queueStatus);
|
||||
});
|
||||
// connection.events.onQueueStatus.subscribe((queueStatus) => {
|
||||
// device.setQueueStatus(queueStatus);
|
||||
// });
|
||||
|
||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||
if (routingPacket.data.variant.case === "errorReason") {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useAppStore } from "../core/stores/appStore.ts";
|
||||
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
|
||||
import { PageLayout } from "@components/PageLayout.tsx";
|
||||
import { Sidebar } from "@components/Sidebar.tsx";
|
||||
@@ -14,11 +13,14 @@ import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { useMessageStore } from "@core/stores/messageStore.ts";
|
||||
|
||||
export const MessagesPage = () => {
|
||||
const { channels, nodes, hardware, messages, hasNodeError } = useDevice();
|
||||
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
|
||||
const { channels, nodes, hardware, hasNodeError } = useDevice();
|
||||
const { getMessages, setActiveChat, chatType, activeChat, setChatType } = useMessageStore()
|
||||
const { toast } = useToast();
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
|
||||
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||
if (node.num === hardware.myNodeNum) return false;
|
||||
const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`;
|
||||
@@ -29,7 +31,6 @@ export const MessagesPage = () => {
|
||||
(ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED,
|
||||
);
|
||||
const currentChannel = channels.get(activeChat);
|
||||
const { toast } = useToast();
|
||||
const node = nodes.get(activeChat);
|
||||
const nodeHex = node?.num ? numberToHexUnpadded(node.num) : "Unknown";
|
||||
|
||||
@@ -130,7 +131,7 @@ export const MessagesPage = () => {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ChannelChat
|
||||
key={currentChannel.index}
|
||||
messages={messages.broadcast.get(currentChannel.index)}
|
||||
messages={getMessages('broadcast', currentChannel?.index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +142,7 @@ export const MessagesPage = () => {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ChannelChat
|
||||
key={node.num}
|
||||
messages={messages.direct.get(node.num)}
|
||||
messages={getMessages('direct', node?.num)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user