Browse Source

WIP

pull/536/head
Dan Ditomaso 1 year ago
parent
commit
0d6c5878fc
  1. 4322
      deno.lock
  2. 5
      package.json
  3. 4
      src/components/PageComponents/Connect/BLE.tsx
  4. 4
      src/components/PageComponents/Connect/HTTP.tsx
  5. 4
      src/components/PageComponents/Connect/Serial.tsx
  6. 10
      src/components/PageComponents/Messages/Message.tsx
  7. 17
      src/components/PageComponents/Messages/MessageInput.tsx
  8. 14
      src/core/services/messaging/db.ts
  9. 22
      src/core/stores/deviceStore.ts
  10. 110
      src/core/stores/messageStore.ts
  11. 14
      src/core/subscriptions.ts
  12. 13
      src/pages/Messages.tsx

4322
deno.lock

File diff suppressed because it is too large

5
package.json

@ -33,7 +33,7 @@
] ]
}, },
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"dependencies": { "ddbependencies": {
"@bufbuild/protobuf": "^2.2.3", "@bufbuild/protobuf": "^2.2.3",
"@meshtastic/core": "npm:@jsr/[email protected]", "@meshtastic/core": "npm:@jsr/[email protected]",
"@meshtastic/js": "npm:@jsr/[email protected]", "@meshtastic/js": "npm:@jsr/[email protected]",
@ -61,6 +61,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.4", "cmdk": "^1.0.4",
"crypto-random-string": "^5.0.0", "crypto-random-string": "^5.0.0",
"idb-keyval": "^6.2.1",
"immer": "^10.1.1", "immer": "^10.1.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
@ -100,7 +101,7 @@
"tar": "^7.4.3", "tar": "^7.4.3",
"testing-library": "^0.0.2", "testing-library": "^0.0.2",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite": "^6.2.0", "vite": "^6.2.3",
"vitest": "^3.0.7", "vitest": "^3.0.7",
"vite-plugin-pwa": "^0.21.1" "vite-plugin-pwa": "^0.21.1"
} }

4
src/components/PageComponents/Connect/BLE.tsx

@ -7,10 +7,12 @@ import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts"; import { randId } from "@core/utils/randId.ts";
import { BleConnection, ServiceUuid } from "@meshtastic/js"; import { BleConnection, ServiceUuid } from "@meshtastic/js";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useMessageStore } from "@core/stores/messageStore.ts";
export const BLE = ({ closeDialog }: TabElementProps) => { export const BLE = ({ closeDialog }: TabElementProps) => {
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]); const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
const { addDevice } = useDeviceStore(); const { addDevice } = useDeviceStore();
const messageStore = useMessageStore()
const { setSelectedDevice } = useAppStore(); const { setSelectedDevice } = useAppStore();
const updateBleDeviceList = useCallback(async (): Promise<void> => { const updateBleDeviceList = useCallback(async (): Promise<void> => {
@ -30,7 +32,7 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
device: bleDevice, device: bleDevice,
}); });
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection); subscribeAll(device, connection, messageStore);
closeDialog(); closeDialog();
}; };

4
src/components/PageComponents/Connect/HTTP.tsx

@ -11,6 +11,7 @@ import { MeshDevice } from "@meshtastic/core";
import { TransportHTTP } from "@meshtastic/transport-http"; import { TransportHTTP } from "@meshtastic/transport-http";
import { useState } from "react"; import { useState } from "react";
import { useForm, useController } from "react-hook-form"; import { useForm, useController } from "react-hook-form";
import { useMessageStore } from "@core/stores/messageStore.ts";
interface FormData { interface FormData {
ip: string; ip: string;
@ -21,6 +22,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
const isURLHTTPS = location.protocol === "https:"; const isURLHTTPS = location.protocol === "https:";
const { addDevice } = useDeviceStore(); const { addDevice } = useDeviceStore();
const messageStore = useMessageStore();
const { setSelectedDevice } = useAppStore(); const { setSelectedDevice } = useAppStore();
const { control, handleSubmit, register } = useForm<FormData>({ const { control, handleSubmit, register } = useForm<FormData>({
@ -49,7 +51,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
connection.configure(); connection.configure();
setSelectedDevice(id); setSelectedDevice(id);
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection); subscribeAll(device, connection, messageStore);
closeDialog(); closeDialog();
}); });

4
src/components/PageComponents/Connect/Serial.tsx

@ -8,10 +8,12 @@ import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core"; import { MeshDevice } from "@meshtastic/core";
import { TransportWebSerial } from "@meshtastic/transport-web-serial"; import { TransportWebSerial } from "@meshtastic/transport-web-serial";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useMessageStore } from "@core/stores/messageStore.ts";
export const Serial = ({ closeDialog }: TabElementProps) => { export const Serial = ({ closeDialog }: TabElementProps) => {
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]); const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
const { addDevice } = useDeviceStore(); const { addDevice } = useDeviceStore();
const messageStore = useMessageStore()
const { setSelectedDevice } = useAppStore(); const { setSelectedDevice } = useAppStore();
const updateSerialPortList = useCallback(async () => { const updateSerialPortList = useCallback(async () => {
@ -36,7 +38,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
const connection = new MeshDevice(transport, id); const connection = new MeshDevice(transport, id);
connection.configure(); connection.configure();
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection); subscribeAll(device, connection, messageStore);
closeDialog(); closeDialog();
}; };

10
src/components/PageComponents/Messages/Message.tsx

@ -101,10 +101,10 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
const TimeDisplay = memo(({ date, className }: { date: Date; className?: string }) => ( const TimeDisplay = memo(({ date, className }: { date: Date; className?: string }) => (
<div className={cn("flex items-center gap-2 shrink-0", className)}> <div className={cn("flex items-center gap-2 shrink-0", className)}>
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono"> <span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
{date.toLocaleDateString()} {date?.toLocaleDateString()}
</span> </span>
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono"> <span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
{date.toLocaleTimeString(undefined, { {date?.toLocaleTimeString(undefined, {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
})} })}
@ -112,7 +112,9 @@ const TimeDisplay = memo(({ date, className }: { date: Date; className?: string
</div> </div>
)); ));
export const Message = memo(({ lastMsgSameUser, message, sender }: MessageProps) => { export const Message = (({ lastMsgSameUser, message, sender }: MessageProps) => {
console.log('Message', message);
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
const isDeviceUser = useMemo( const isDeviceUser = useMemo(
@ -162,7 +164,7 @@ export const Message = memo(({ lastMsgSameUser, message, sender }: MessageProps)
</div> </div>
)} )}
</div> </div>
<TimeDisplay date={message.rxTime} /> <TimeDisplay date={message?.rxTime} />
<div className="flex place-items-center gap-2 pb-2"> <div className="flex place-items-center gap-2 pb-2">
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}> <div className={cn(isDeviceUser && "pl-11", messageTextClass)}>
{message.data} {message.data}

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

@ -5,6 +5,7 @@ import { useDevice } from "@core/stores/deviceStore.ts";
import type { Types } from "@meshtastic/core"; import type { Types } from "@meshtastic/core";
import { SendIcon } from "lucide-react"; import { SendIcon } from "lucide-react";
import { startTransition, useCallback, useMemo, useState } from "react"; import { startTransition, useCallback, useMemo, useState } from "react";
import { useMessageStore } from "@core/stores/messageStore.ts";
export interface MessageInputProps { export interface MessageInputProps {
to: Types.Destination; to: Types.Destination;
@ -19,13 +20,11 @@ export const MessageInput = ({
}: MessageInputProps) => { }: MessageInputProps) => {
const { const {
connection, connection,
setMessageState,
messageDraft, messageDraft,
setMessageDraft, setMessageDraft,
isQueueingMessages,
queueStatus,
hardware, hardware,
} = useDevice(); } = useDevice();
const { setMessageState } = useMessageStore()
const myNodeNum = hardware.myNodeNum; const myNodeNum = hardware.myNodeNum;
const [localDraft, setLocalDraft] = useState(messageDraft); const [localDraft, setLocalDraft] = useState(messageDraft);
const [messageBytes, setMessageBytes] = useState(0); 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>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -85,12 +84,10 @@ export const MessageInput = ({
if (localDraft === "") return; if (localDraft === "") return;
const message = formData.get("messageInput") as string; const message = formData.get("messageInput") as string;
startTransition(() => { startTransition(() => {
if (!isQueueingMessages) { sendText(message);
sendText(message); setLocalDraft("");
setLocalDraft(""); setMessageDraft("");
setMessageDraft(""); setMessageBytes(0);
setMessageBytes(0);
}
}); });
}} }}

14
src/core/services/messaging/db.ts

@ -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);
},
};

22
src/core/stores/deviceStore.ts

@ -30,10 +30,6 @@ export type DialogVariant =
| "unsafeRoles" | "unsafeRoles"
| "refreshKeys"; | "refreshKeys";
type QueueStatus = {
res: number, free: number, maxlen: number
}
type NodeError = { type NodeError = {
node: number; node: number;
error: string; error: string;
@ -66,8 +62,6 @@ export interface Device {
// currentMetrics: Protobuf.DeviceMetrics; // currentMetrics: Protobuf.DeviceMetrics;
pendingSettingsChanges: boolean; pendingSettingsChanges: boolean;
messageDraft: string; messageDraft: string;
queueStatus: QueueStatus,
isQueueingMessages: boolean,
dialog: { dialog: {
import: boolean; import: boolean;
QR: boolean; QR: boolean;
@ -116,7 +110,6 @@ export interface Device {
getDialogOpen: (dialog: DialogVariant) => boolean; getDialogOpen: (dialog: DialogVariant) => boolean;
processPacket: (data: ProcessPacketParams) => void; processPacket: (data: ProcessPacketParams) => void;
setMessageDraft: (message: string) => void; setMessageDraft: (message: string) => void;
setQueueStatus: (status: QueueStatus) => void;
setNodeError: (nodeNum: number, error: string) => void; setNodeError: (nodeNum: number, error: string) => void;
clearNodeError: (nodeNum: number) => void; clearNodeError: (nodeNum: number) => void;
getNodeError: (nodeNum: number) => NodeError | undefined; getNodeError: (nodeNum: number) => NodeError | undefined;
@ -160,10 +153,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
activePage: "messages", activePage: "messages",
activeNode: 0, activeNode: 0,
waypoints: [], waypoints: [],
queueStatus: {
res: 0, free: 0, maxlen: 0
},
isQueueingMessages: false,
dialog: { dialog: {
import: false, import: false,
QR: 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) => { setNodeError: (nodeNum, error) => {
set( set(
produce<DeviceState>((draft) => { produce<DeviceState>((draft) => {

110
src/core/stores/messageStore.ts

@ -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,
}),
}
)
);

14
src/core/subscriptions.ts

@ -1,9 +1,11 @@
import type { Device } from "@core/stores/deviceStore.ts"; import type { Device } from "@core/stores/deviceStore.ts";
import { MeshDevice, Protobuf } from "@meshtastic/core"; import { MeshDevice, Protobuf } from "@meshtastic/core";
import type { MessageStore } from "@core/stores/messageStore.ts";
export const subscribeAll = ( export const subscribeAll = (
device: Device, device: Device,
connection: MeshDevice, connection: MeshDevice,
messageStore: MessageStore
) => { ) => {
let myNodeNum = 0; let myNodeNum = 0;
@ -15,6 +17,8 @@ export const subscribeAll = (
}); });
connection.events.onRoutingPacket.subscribe((routingPacket) => { connection.events.onRoutingPacket.subscribe((routingPacket) => {
console.log("routingPacket", routingPacket);
switch (routingPacket.data.variant.case) { switch (routingPacket.data.variant.case) {
case "errorReason": { case "errorReason": {
if ( if (
@ -81,8 +85,10 @@ export const subscribeAll = (
connection.events.onMessagePacket.subscribe((messagePacket) => { connection.events.onMessagePacket.subscribe((messagePacket) => {
device.addMessage({ console.log("messagePacket", messagePacket);
messageStore.addMessage({
...messagePacket, ...messagePacket,
state: messagePacket.from !== myNodeNum ? "ack" : "waiting", state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
}); });
}); });
@ -105,9 +111,9 @@ export const subscribeAll = (
}); });
}); });
connection.events.onQueueStatus.subscribe((queueStatus) => { // connection.events.onQueueStatus.subscribe((queueStatus) => {
device.setQueueStatus(queueStatus); // device.setQueueStatus(queueStatus);
}); // });
connection.events.onRoutingPacket.subscribe((routingPacket) => { connection.events.onRoutingPacket.subscribe((routingPacket) => {
if (routingPacket.data.variant.case === "errorReason") { if (routingPacket.data.variant.case === "errorReason") {

13
src/pages/Messages.tsx

@ -1,4 +1,3 @@
import { useAppStore } from "../core/stores/appStore.ts";
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx"; import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
import { PageLayout } from "@components/PageLayout.tsx"; import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx"; import { Sidebar } from "@components/Sidebar.tsx";
@ -14,11 +13,14 @@ import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx"; import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { useMessageStore } from "@core/stores/messageStore.ts";
export const MessagesPage = () => { export const MessagesPage = () => {
const { channels, nodes, hardware, messages, hasNodeError } = useDevice(); const { channels, nodes, hardware, hasNodeError } = useDevice();
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore(); const { getMessages, setActiveChat, chatType, activeChat, setChatType } = useMessageStore()
const { toast } = useToast();
const [searchTerm, setSearchTerm] = useState<string>(""); const [searchTerm, setSearchTerm] = useState<string>("");
const filteredNodes = Array.from(nodes.values()).filter((node) => { const filteredNodes = Array.from(nodes.values()).filter((node) => {
if (node.num === hardware.myNodeNum) return false; if (node.num === hardware.myNodeNum) return false;
const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`; const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`;
@ -29,7 +31,6 @@ export const MessagesPage = () => {
(ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED, (ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED,
); );
const currentChannel = channels.get(activeChat); const currentChannel = channels.get(activeChat);
const { toast } = useToast();
const node = nodes.get(activeChat); const node = nodes.get(activeChat);
const nodeHex = node?.num ? numberToHexUnpadded(node.num) : "Unknown"; const nodeHex = node?.num ? numberToHexUnpadded(node.num) : "Unknown";
@ -130,7 +131,7 @@ export const MessagesPage = () => {
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<ChannelChat <ChannelChat
key={currentChannel.index} key={currentChannel.index}
messages={messages.broadcast.get(currentChannel.index)} messages={getMessages('broadcast', currentChannel?.index)}
/> />
</div> </div>
</div> </div>
@ -141,7 +142,7 @@ export const MessagesPage = () => {
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<ChannelChat <ChannelChat
key={node.num} key={node.num}
messages={messages.direct.get(node.num)} messages={getMessages('direct', node?.num)}
/> />
</div> </div>
</div> </div>

Loading…
Cancel
Save