Browse Source

feat: added message persistance

pull/536/head
Dan Ditomaso 1 year ago
parent
commit
ed2ab36ed4
  1. 3726
      deno.lock
  2. 2
      package.json
  3. 16
      src/components/PageComponents/Messages/ChannelChat.tsx
  4. 177
      src/components/PageComponents/Messages/Message.tsx
  5. 109
      src/components/PageComponents/Messages/MessageInput.tsx
  6. 17
      src/components/PageComponents/Messages/MessageItem.tsx
  7. 41
      src/core/dto/PacketToMessageDTO.ts
  8. 2
      src/core/services/messaging/db.ts
  9. 2
      src/core/stores/deviceStore.ts
  10. 135
      src/core/stores/messageStore.ts
  11. 18
      src/core/subscriptions.ts
  12. 54
      src/pages/Messages.tsx

3726
deno.lock

File diff suppressed because it is too large

2
package.json

@ -33,7 +33,7 @@
] ]
}, },
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"ddbependencies": { "dependencies": {
"@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]",

16
src/components/PageComponents/Messages/ChannelChat.tsx

@ -1,10 +1,10 @@
import { type MessageWithState, useDevice } from "@core/stores/deviceStore.ts"; import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx";
import { Message } from "@components/PageComponents/Messages/Message.tsx"; import type { Message as Message } from "@core/stores/messageStore.ts";
import { InboxIcon } from "lucide-react"; import { InboxIcon } from "lucide-react";
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
export interface ChannelChatProps { export interface ChannelChatProps {
messages?: MessageWithState[]; messages?: Message[];
} }
const EmptyState = () => ( const EmptyState = () => (
@ -17,8 +17,6 @@ const EmptyState = () => (
export const ChannelChat = ({ export const ChannelChat = ({
messages = [], messages = [],
}: ChannelChatProps) => { }: ChannelChatProps) => {
const { nodes } = useDevice();
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -59,12 +57,12 @@ export const ChannelChat = ({
> >
<div className="flex flex-col justify-end min-h-full"> <div className="flex flex-col justify-end min-h-full">
{messages?.map((message, index) => ( {messages?.map((message, index) => (
<Message <MessageItem
key={message.id} key={message.messageId + index}
message={message} message={message}
sender={nodes.get(message.from)}
lastMsgSameUser={ lastMsgSameUser={
index > 0 && messages[index - 1].from === message.from index > 0 &&
messages[index - 1].from === message.from
} }
/> />
))} ))}

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

@ -1,177 +0,0 @@
import { memo, useMemo } from "react";
import {
Tooltip,
TooltipArrow,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import {
type MessageWithState,
useDeviceStore,
} from "@core/stores/deviceStore.ts";
import { cn } from "@core/utils/cn.ts";
import { Avatar } from "@components/UI/Avatar.tsx";
import type { Protobuf } from "@meshtastic/core";
import { AlertCircle, CheckCircle2, CircleEllipsis, LucideIcon } from "lucide-react";
type MessageStateValue = {
state: string;
icon: LucideIcon;
displayText: string;
}
type MessageState = MessageWithState["state"];
interface MessageProps {
lastMsgSameUser: boolean;
message: MessageWithState;
sender: Protobuf.Mesh.NodeInfo;
}
interface StatusTooltipProps {
state: MessageState;
children: React.ReactNode;
}
interface StatusIconProps {
state: MessageState;
className?: string;
}
const MESSAGE_STATES: Record<string, MessageStateValue> = {
ACK: { state: 'ack', icon: CheckCircle2, displayText: "Message delivered" },
WAITING: { state: 'waiting', icon: CircleEllipsis, displayText: "Waiting for delivery" },
FAILED: { state: 'failed', icon: AlertCircle, displayText: "Delivery failed" },
};
const getMessageState = (state: MessageState): MessageStateValue => {
switch (state) {
case MESSAGE_STATES.ACK.state:
return MESSAGE_STATES.ACK;
case MESSAGE_STATES.WAITING.state:
return MESSAGE_STATES.WAITING;
case MESSAGE_STATES.FAILED.state:
return MESSAGE_STATES.FAILED;
default:
return MESSAGE_STATES.FAILED;
}
}
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white dark:text-white shadow-md animate-in fade-in-0 zoom-in-95"
side="top"
align="center"
sideOffset={5}
>
{getMessageState(state).displayText ?? "An unknown error occurred"};
<TooltipArrow className="fill-slate-800" />
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
const msgState = getMessageState(state);
const isFailed = msgState.state === 'failed'
const iconClass = cn(
className,
"text-slate-500 dark:text-slate-400 size-5 shrink-0"
);
const Icon = msgState.icon;
return (
<StatusTooltip state={state}>
<Icon
className={iconClass}
{...otherProps}
color={isFailed ? "red" : "currentColor"}
/>
</StatusTooltip>
);
};
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()}
</span>
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
{date?.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
));
export const Message = (({ lastMsgSameUser, message, sender }: MessageProps) => {
console.log('Message', message);
const { getDevices } = useDeviceStore();
const isDeviceUser = useMemo(
() =>
getDevices()
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
.includes(message.from),
[getDevices, message.from]
);
const messageUser = sender?.user;
const getMessageTextStyles = (state: MessageState) => {
const msgState = getMessageState(state);
const isAcknowledged = msgState.state === 'ack'
const isFailed = msgState.state === 'failed'
return cn(
"break-words overflow-hidden",
isAcknowledged
? "text-slate-900 dark:text-white"
: "text-slate-900 dark:text-slate-400",
isFailed && "text-red-500 dark:text-red-500",
);
};
const messageTextClass = useMemo(() => getMessageTextStyles(message.state), [message.state]);
return (
<div className="flex flex-col w-full px-4 justify-start">
<div
className={cn(
"flex flex-col flex-wrap items-start py-1",
isDeviceUser && "items-end"
)}
>
<div className="flex items-center gap-2 mb-2">
{!lastMsgSameUser && (
<div className="flex place-items-center gap-2 mb-1">
<Avatar text={messageUser?.shortName ?? "UNK"} />
<div className="flex flex-col">
<span className="font-medium text-slate-900 dark:text-white truncate">
{messageUser?.longName}
</span>
</div>
</div>
)}
</div>
<TimeDisplay date={message?.rxTime} />
<div className="flex place-items-center gap-2 pb-2">
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}>
{message.data}
</div>
<StatusIcon state={message.state} />
</div>
</div>
</div>
);
});

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

@ -5,7 +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"; import { ChatTypes, useMessageStore } from "@core/stores/messageStore.ts";
export interface MessageInputProps { export interface MessageInputProps {
to: Types.Destination; to: Types.Destination;
@ -18,55 +18,40 @@ export const MessageInput = ({
channel, channel,
maxBytes, maxBytes,
}: MessageInputProps) => { }: MessageInputProps) => {
const { const { connection, messageDraft, setMessageDraft } = useDevice();
connection, const { setMessageState, activeChat } = useMessageStore();
messageDraft,
setMessageDraft,
hardware,
} = useDevice();
const { setMessageState } = useMessageStore()
const myNodeNum = hardware.myNodeNum;
const [localDraft, setLocalDraft] = useState(messageDraft); const [localDraft, setLocalDraft] = useState(messageDraft);
const [messageBytes, setMessageBytes] = useState(0); const [messageBytes, setMessageBytes] = useState(0);
const debouncedSetMessageDraft = useMemo( const debouncedSetMessageDraft = useMemo(
() => debounce(setMessageDraft, 300), () => debounce((value: string) => setMessageDraft(value), 300),
[setMessageDraft], [setMessageDraft]
); );
// sends the message to the selected destination const calculateBytes = (text: string) => new Blob([text]).size;
const sendText = useCallback(
async (message: string) => {
await connection const chatType = to === 'broadcast' ? ChatTypes.BROADCAST : ChatTypes.DIRECT;
?.sendText(message, to, true, channel)
.then((id: number) => const sendText = useCallback(async (message: string) => {
setMessageState( try {
to === "broadcast" ? "broadcast" : "direct", const messageId = await connection?.sendText(message, to, true, channel);
channel, if (messageId !== undefined) {
to as number, setMessageState({ type: chatType, key: activeChat, messageId, newState: 'ack' });
myNodeNum, }
id, } catch (e: any) {
"ack", setMessageState({
) type: chatType,
) key: activeChat,
.catch((e: Types.PacketError) => messageId: e?.id,
setMessageState( newState: 'failed',
to === "broadcast" ? "broadcast" : "direct", });
channel, }
to as number, }, [channel, connection, setMessageState, to, activeChat, chatType]);
myNodeNum,
e.id,
e.error,
)
);
},
[channel, connection, myNodeNum, setMessageState, to],
);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value; const newValue = e.target.value;
const byteLength = new Blob([newValue]).size; const byteLength = calculateBytes(newValue);
if (byteLength <= maxBytes) { if (byteLength <= maxBytes) {
setLocalDraft(newValue); setLocalDraft(newValue);
@ -75,24 +60,29 @@ export const MessageInput = ({
} }
}; };
const handleBeforeInput = (e: React.FormEvent<HTMLInputElement>) => {
const nextValue = localDraft + (e.nativeEvent as InputEvent).data;
if (calculateBytes(nextValue) > maxBytes) {
e.preventDefault();
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!localDraft.trim()) return;
startTransition(() => {
sendText(localDraft.trim());
setLocalDraft("");
setMessageDraft("");
setMessageBytes(0);
});
};
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
<form <form className="w-full" onSubmit={handleSubmit}>
className="w-full" <div className="flex grow gap-2">
action={(formData: FormData) => {
// prevent user from sending blank/empty message
if (localDraft === "") return;
const message = formData.get("messageInput") as string;
startTransition(() => {
sendText(message);
setLocalDraft("");
setMessageDraft("");
setMessageBytes(0);
});
}}
>
<div className="flex grow gap-2 ">
<label className="w-full"> <label className="w-full">
<Input <Input
autoFocus autoFocus
@ -101,13 +91,18 @@ export const MessageInput = ({
placeholder="Enter Message" placeholder="Enter Message"
value={localDraft} value={localDraft}
onChange={handleInputChange} onChange={handleInputChange}
onBeforeInput={handleBeforeInput}
/> />
</label> </label>
<label data-testid="byte-counter" className="flex items-center w-24 p-2 place-content-end"> <label data-testid="byte-counter" className="flex items-center w-24 p-2 place-content-end">
{messageBytes}/{maxBytes} {messageBytes}/{maxBytes}
</label> </label>
<Button type="submit" className="dark:bg-white dark:text-slate-900 dark:hover:bg-slate-400 dark:hover:text-white"> <Button
type="submit"
className="dark:bg-white dark:text-slate-900 dark:hover:bg-slate-400 dark:hover:text-white"
>
<SendIcon size={16} /> <SendIcon size={16} />
</Button> </Button>
</div> </div>

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

@ -5,13 +5,13 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@components/UI/Tooltip.tsx"; } from "@components/UI/Tooltip.tsx";
import { useDeviceStore } from "@core/stores/deviceStore.ts"; import { MessageState, useDeviceStore } from "@core/stores/deviceStore.ts";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { Avatar } from "@components/UI/Avatar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx";
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react"; import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import { ReactNode, useMemo } from "react"; import { ReactNode, useMemo } from "react";
import { Message, MessageState } from "@core/services/types.ts"; import { Message } from "@core/stores/messageStore.ts";
interface MessageProps { interface MessageProps {
lastMsgSameUser: boolean; lastMsgSameUser: boolean;
@ -73,14 +73,15 @@ const getMessageTextStyles = (status: MessageStatus) => {
); );
}; };
const TimeDisplay = ({ date, className }: { date: Date; className?: string }) => ( const TimeDisplay = ({ date, className }: { date: Date; className?: string }) => {
<div className={cn("flex items-center gap-2 shrink-0", className)}> const _date = new Date(date);
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">{date.toLocaleDateString()}</span> return (<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()}</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, { hour: "2-digit", minute: "2-digit" })} {_date?.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}
</span> </span>
</div> </div>)
); };
export const MessageItem = ({ lastMsgSameUser, message }: MessageProps) => { export const MessageItem = ({ lastMsgSameUser, message }: MessageProps) => {
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();

41
src/core/dto/PacketToMessageDTO.ts

@ -0,0 +1,41 @@
import type { Types } from "@meshtastic/js";
import { Message, MessageType, MessageState } from "@core/stores/messageStore.ts";
class PacketToMessageDTO {
channel: Types.ChannelNumber;
to: number;
from: number;
date: string;
messageId: number;
state: MessageState;
message: string;
type: MessageType;
constructor(data: Types.PacketMetadata<string>, nodeNum: number) {
const payload = data
this.channel = payload.channel
this.to = payload.to;
this.from = payload.from;
this.date = new Date(payload.rxTime).toISOString();
this.messageId = payload.id;
this.state = payload.from !== nodeNum ? "ack" : "waiting";
this.message = payload.data;
this.type = payload.type;
}
toMessage(): Message {
return {
channel: this.channel,
to: this.to,
from: this.from,
date: this.date,
messageId: this.messageId,
state: this.state,
message: this.message,
type: this.type,
} as Message;
}
}
export default PacketToMessageDTO;

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

@ -1,7 +1,7 @@
import { StateStorage } from "zustand/middleware"; import { StateStorage } from "zustand/middleware";
import { get, set, del } from "idb-keyval"; import { get, set, del } from "idb-keyval";
export const zustandIDBStorage: StateStorage = { export const zustandIndexDBStorage: StateStorage = {
getItem: async (name: string): Promise<string | null> => { getItem: async (name: string): Promise<string | null> => {
return (await get(name)) || null; return (await get(name)) || null;
}, },

2
src/core/stores/deviceStore.ts

@ -10,7 +10,7 @@ export interface MessageWithState extends Types.PacketMetadata<string> {
state: MessageState; state: MessageState;
} }
export type MessageState = "ack" | "waiting" | Protobuf.Mesh.Routing_Error; export type MessageState = "ack" | "waiting" | 'failed';
export interface ProcessPacketParams { export interface ProcessPacketParams {
from: number; from: number;

135
src/core/stores/messageStore.ts

@ -2,40 +2,60 @@ import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'; import { persist, createJSONStorage } from 'zustand/middleware';
import { produce } from 'immer'; import { produce } from 'immer';
import { Types } from '@meshtastic/core'; import { Types } from '@meshtastic/core';
import { zustandIDBStorage } from "@core/services/messaging/db.ts"; import { zustandIndexDBStorage } from "@core/services/messaging/db.ts";
export interface MessageWithState { const MESSAGE_STATES = {
id: number; ack: "ack",
from: number; waiting: "waiting",
failed: 'failed',
};
export type MessageState = keyof typeof MESSAGE_STATES;
export const ChatTypes = {
DIRECT: "direct",
BROADCAST: "broadcast",
} as const;
export type MessageType = "broadcast" | "direct";
interface MessageBase {
channel: Types.ChannelNumber;
to: number; to: number;
channel: number; from: number;
content: string; date: string;
state: 'ack' | 'waiting' | 'failed'; messageId: number;
type: 'direct' | 'broadcast'; state: MessageState;
message: string;
}
interface GenericMessage<T extends MessageType> extends MessageBase {
type: T;
} }
type MessageType = 'direct' | 'broadcast'; export type Message = GenericMessage<'direct'> | GenericMessage<'broadcast'>;
export interface MessageStore { export interface MessageStore {
messages: { messages: {
direct: Record<number, MessageWithState[]>; direct: Record<number, Record<number, Message>>; // node -> messageId -> Message
broadcast: Record<number, MessageWithState[]>; broadcast: Record<number, Record<number, Message>>; // channel -> messageId -> Message
}; };
nodeNum: number;
activeChat: number; activeChat: number;
chatType: MessageType; chatType: MessageType;
setNodeNum: (nodeNum: number) => void;
getNodeNum: () => number;
setActiveChat: (chat: number) => void; setActiveChat: (chat: number) => void;
setChatType: (type: MessageType) => void; setChatType: (type: MessageType) => void;
addMessage: (message: MessageWithState) => void; saveMessage: (message: Message) => void;
getMessages: (type: MessageType, key: number) => MessageWithState[]; setMessageState: (params: {
setMessageState: ( type: MessageType;
type: MessageType, key: number;
key: number, messageId: number;
messageId: number, newState?: MessageState;
newState: MessageWithState['state'] }) => void;
) => void;
clearMessages: () => void; clearMessages: () => void;
getMessages: (type: MessageType, options: { myNodeNum?: number; otherNodeNum?: number; channel?: number }) => Message[];
} }
export const useMessageStore = create<MessageStore>()( export const useMessageStore = create<MessageStore>()(
@ -45,9 +65,16 @@ export const useMessageStore = create<MessageStore>()(
direct: {}, direct: {},
broadcast: {}, broadcast: {},
}, },
activeChat: 0,
activeChat: Types.ChannelNumber.Primary,
chatType: 'broadcast', chatType: 'broadcast',
nodeNum: 0,
setNodeNum: (nodeNum) => {
set(produce((state: MessageStore) => {
state.nodeNum = nodeNum;
}));
},
getNodeNum: () => get().nodeNum,
setActiveChat: (chat) => { setActiveChat: (chat) => {
set(produce((state: MessageStore) => { set(produce((state: MessageStore) => {
@ -61,48 +88,62 @@ export const useMessageStore = create<MessageStore>()(
})); }));
}, },
addMessage: (message) => { saveMessage: (message) => {
set(produce((state: MessageStore) => { set(produce((state: MessageStore) => {
const group = message.type === 'direct' ? state.messages.direct : state.messages.broadcast; const group = state.messages[message.type];
const key = message.type === 'direct' ? message.from : message.channel; const key = message.type === 'direct' ? Number(message.from) : Number(message.channel);
if (!group[key]) { if (!group[key]) {
group[key] = []; group[key] = {};
} }
group[key].push(message); group[key][message.messageId] = message;
})); }));
}, },
setMessageState: ({ type, key, messageId, newState = 'ack' }) => {
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) => { set(produce((state: MessageStore) => {
const group = type === 'direct' ? state.messages.direct : state.messages.broadcast; const group = state.messages[type];
const messages = group[key]; const messageMap = group[key];
if (!messages) return; if (!messageMap || !messageMap[messageId]) return;
const message = messages.find((msg) => msg.id === messageId); messageMap[messageId].state = newState;
if (message) {
message.state = newState;
}
})); }));
}, },
clearMessages: () => { clearMessages: () => {
set(produce((state: MessageStore) => { set(produce((state: MessageStore) => {
state.messages.direct = {}; state.messages.direct = {};
state.messages.broadcast = {}; state.messages.broadcast = {};
})); }));
}, },
getMessages: (type, options) => {
const state = get();
if (type === 'broadcast' && options.channel !== undefined) {
const messageMap = state.messages.broadcast[options.channel] ?? {};
return Object.values(messageMap).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
}
if (type === 'direct' && options.myNodeNum !== undefined && options.otherNodeNum !== undefined) {
const receivedMap = state.messages.direct[options.otherNodeNum] ?? {};
const sentMap = state.messages.direct[options.myNodeNum] ?? {};
// Pull messages where I am the sender and otherNode is the receiver
const sentMessages = Object.values(sentMap).filter(msg => msg.to === options.otherNodeNum);
// Pull messages received from otherNode
const receivedMessages = Object.values(receivedMap);
// Merge and sort chronologically
return [...receivedMessages, ...sentMessages].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
}
return [];
},
}), }),
{ {
name: 'mesh-messages', name: 'meshtastic-message-store',
storage: createJSONStorage(() => zustandIDBStorage), storage: createJSONStorage(() => zustandIndexDBStorage),
// ✅ No need for partialize magic — simple object storage
partialize: (state) => ({ partialize: (state) => ({
activeChat: state.activeChat,
chatType: state.chatType,
messages: state.messages, messages: state.messages,
}), }),
} }

18
src/core/subscriptions.ts

@ -1,6 +1,7 @@
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"; import type { MessageStore } from "@core/stores/messageStore.ts";
import PacketToMessageDTO from "@core/dto/PacketToMessageDTO.ts";
export const subscribeAll = ( export const subscribeAll = (
device: Device, device: Device,
@ -17,7 +18,7 @@ export const subscribeAll = (
}); });
connection.events.onRoutingPacket.subscribe((routingPacket) => { connection.events.onRoutingPacket.subscribe((routingPacket) => {
console.log("routingPacket", routingPacket); console.log("Routing Packet", routingPacket);
switch (routingPacket.data.variant.case) { switch (routingPacket.data.variant.case) {
case "errorReason": { case "errorReason": {
@ -55,6 +56,7 @@ export const subscribeAll = (
connection.events.onMyNodeInfo.subscribe((nodeInfo) => { connection.events.onMyNodeInfo.subscribe((nodeInfo) => {
device.setHardware(nodeInfo); device.setHardware(nodeInfo);
messageStore.setNodeNum(nodeInfo.myNodeNum);
myNodeNum = nodeInfo.myNodeNum; myNodeNum = nodeInfo.myNodeNum;
}); });
@ -85,12 +87,13 @@ export const subscribeAll = (
connection.events.onMessagePacket.subscribe((messagePacket) => { connection.events.onMessagePacket.subscribe((messagePacket) => {
console.log("messagePacket", messagePacket); console.log("before Message Packet", messagePacket);
messageStore.addMessage({
...messagePacket,
state: messagePacket.from !== myNodeNum ? "ack" : "waiting", // incoming and outgoing messages are handled by this event listener
}); const dto = new PacketToMessageDTO(messagePacket, myNodeNum);
const message = dto.toMessage();
console.log("after Message Packet", message);
messageStore.saveMessage(message);
}); });
connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => { connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => {
@ -111,9 +114,6 @@ export const subscribeAll = (
}); });
}); });
// connection.events.onQueueStatus.subscribe((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") {

54
src/pages/Messages.tsx

@ -13,11 +13,11 @@ 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"; import { ChatTypes, useMessageStore } from "@core/stores/messageStore.ts";
export const MessagesPage = () => { export const MessagesPage = () => {
const { channels, nodes, hardware, hasNodeError } = useDevice(); const { channels, nodes, hardware, hasNodeError } = useDevice();
const { getMessages, setActiveChat, chatType, activeChat, setChatType } = useMessageStore() const { nodeNum, getMessages, setActiveChat, chatType, activeChat, setChatType } = useMessageStore()
const { toast } = useToast(); const { toast } = useToast();
const [searchTerm, setSearchTerm] = useState<string>(""); const [searchTerm, setSearchTerm] = useState<string>("");
@ -31,13 +31,15 @@ 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 node = nodes.get(activeChat);
const nodeHex = node?.num ? numberToHexUnpadded(node.num) : "Unknown";
const messageDestination = chatType === "direct" ? activeChat : "broadcast"; const otherNode = nodes.get(activeChat);
const messageChannel = chatType === "direct"
? Types.ChannelNumber.Primary const nodeHex = otherNode?.num ? numberToHexUnpadded(otherNode.num) : "Unknown";
: activeChat;
const isDirect = chatType === ChatTypes.DIRECT;
const isBroadcast = chatType === ChatTypes.BROADCAST;
const currentChat = { type: chatType, id: activeChat };
return ( return (
<> <>
@ -51,7 +53,6 @@ export const MessagesPage = () => {
: channel.index === 0 : channel.index === 0
? "Primary" ? "Primary"
: `Ch ${channel.index}`} : `Ch ${channel.index}`}
active={activeChat === channel.index && chatType === "broadcast"}
onClick={() => { onClick={() => {
setChatType("broadcast"); setChatType("broadcast");
setActiveChat(channel.index); setActiveChat(channel.index);
@ -71,21 +72,21 @@ export const MessagesPage = () => {
/> />
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{filteredNodes.map((node) => ( {filteredNodes.map((otherNode) => (
<SidebarButton <SidebarButton
key={node.num} key={otherNode.num}
label={node.user?.longName ?? label={otherNode.user?.longName ??
`!${numberToHexUnpadded(node.num)}`} `!${numberToHexUnpadded(otherNode.num)}`}
active={activeChat === node.num && chatType === "direct"} active={activeChat === otherNode.num && chatType === "direct"}
onClick={() => { onClick={() => {
setChatType("direct"); setChatType("direct");
setActiveChat(node.num); setActiveChat(otherNode.num);
}} }}
element={ element={
<Avatar <Avatar
text={node.user?.shortName ?? node.num.toString()} text={otherNode?.user?.shortName ?? otherNode.num.toString()}
className={cn(hasNodeError(node.num) && "text-red-500")} className={cn(hasNodeError(otherNode?.num) && "text-red-500")}
showError={hasNodeError(node.num)} showError={hasNodeError(otherNode?.num)}
size="sm" size="sm"
/> />
} }
@ -126,23 +127,24 @@ export const MessagesPage = () => {
: []} : []}
> >
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{chatType === "broadcast" && currentChannel && ( {isBroadcast && currentChannel && (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<ChannelChat <ChannelChat
key={currentChannel.index} messages={getMessages('broadcast', {
messages={getMessages('broadcast', currentChannel?.index)} myNodeNum: nodeNum,
channel: currentChannel?.index
})}
/> />
</div> </div>
</div> </div>
)} )}
{chatType === "direct" && node && ( {isDirect && otherNode && (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<ChannelChat <ChannelChat
key={node.num} messages={getMessages('direct', { myNodeNum: nodeNum, otherNodeNum: activeChat, })}
messages={getMessages('direct', node?.num)}
/> />
</div> </div>
</div> </div>
@ -151,8 +153,8 @@ export const MessagesPage = () => {
<div className="shrink-0 p-4 w-full dark:bg-slate-900"> <div className="shrink-0 p-4 w-full dark:bg-slate-900">
<MessageInput <MessageInput
to={messageDestination} to={currentChat.type === ChatTypes.DIRECT ? activeChat : ChatTypes.BROADCAST}
channel={messageChannel} channel={currentChat.type === ChatTypes.DIRECT ? Types.ChannelNumber.Primary : currentChat.id}
maxBytes={200} maxBytes={200}
/> />
</div> </div>

Loading…
Cancel
Save