Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist message history to local storage #346

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/components/PageComponents/Messages/ChannelChat.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import {
type MessageWithState,
useDevice,
} from "@app/core/stores/deviceStore.ts";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import { MessageStore } from "@app/core/stores/messageStore.ts";
import { Message } from "@components/PageComponents/Messages/Message.tsx";
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
import type { Protobuf, Types } from "@meshtastic/js";
import { InboxIcon } from "lucide-react";
import { useState, useEffect, useRef } from 'react';

export interface ChannelChatProps {
messages?: MessageWithState[];
messageStore: MessageStore;
channel: Types.ChannelNumber;
to: Types.Destination;
traceroutes?: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[];
}

export const ChannelChat = ({
messages,
messageStore,
channel,
to,
traceroutes,
}: ChannelChatProps): JSX.Element => {
const { nodes } = useDevice();
const messages = messageStore.messages;

return (
<div className="flex flex-grow flex-col">
Expand Down
2 changes: 2 additions & 0 deletions src/components/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const PageLayout = ({
noPadding,
actions,
children,
onScroll,
}: PageLayoutProps): JSX.Element => {
return (
<>
Expand Down Expand Up @@ -52,6 +53,7 @@ export const PageLayout = ({
"flex h-full w-full flex-col overflow-y-auto",
!noPadding && "pl-3 pr-3 ",
)}
onScroll={onScroll}
>
{children}
<Footer />
Expand Down
56 changes: 29 additions & 27 deletions src/core/stores/deviceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { createContext, useContext } from "react";
import { produce } from "immer";
import { create } from "zustand";

import MessageStore from "@core/stores/messageStore.ts";

import { Protobuf, Types } from "@meshtastic/js";

export type Page = "messages" | "map" | "config" | "channels" | "nodes";
Expand Down Expand Up @@ -38,10 +40,11 @@ export interface Device {
hardware: Protobuf.Mesh.MyNodeInfo;
nodes: Map<number, Protobuf.Mesh.NodeInfo>;
metadata: Map<number, Protobuf.Mesh.DeviceMetadata>;
messages: {
direct: Map<number, MessageWithState[]>;
broadcast: Map<Types.ChannelNumber, MessageWithState[]>;
};
messageStores: {
direct: Map<number, MessageStore>;
broadcast: Map<Types.ChannelNumber, MessageStore>;
semaphore: boolean;
}
traceroutes: Map<
number,
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
Expand Down Expand Up @@ -125,9 +128,10 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
hardware: new Protobuf.Mesh.MyNodeInfo(),
nodes: new Map(),
metadata: new Map(),
messages: {
messageStores: {
direct: new Map(),
broadcast: new Map(),
semaphore: false,
},
traceroutes: new Map(),
connection: undefined,
Expand Down Expand Up @@ -392,6 +396,9 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
return;
}
device.channels.set(channel.index, channel);

const messageStoreGroup = device.messageStores.broadcast;
messageStoreGroup.set(channel.index, new MessageStore(device.hardware.myNodeNum, channel.index))
}),
);
},
Expand Down Expand Up @@ -421,6 +428,9 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
return;
}
device.nodes.set(nodeInfo.num, nodeInfo);

const messageStoreGroup = device.messageStores.direct;
messageStoreGroup.set(nodeInfo.num, new MessageStore(device.hardware.myNodeNum, nodeInfo.num))
}),
);
},
Expand Down Expand Up @@ -480,21 +490,19 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
if (!device) {
return;
}
const messageGroup = device.messages[message.type];
const messageIndex =

const messageStoreGroup = device.messageStores[message.type];
const messageStoreIndex =
message.type === "direct"
? message.from === device.hardware.myNodeNum
? message.to
: message.from
: message.channel;
const messages = messageGroup.get(messageIndex);
const messageStore = messageStoreGroup.get(messageStoreIndex);

if (messages) {
messages.push(message);
messageGroup.set(messageIndex, messages);
} else {
messageGroup.set(messageIndex, [message]);
}
messageStore.addMessage(message);

device.messageStores.semaphore = !device.messageStores.semaphore;
}),
);
},
Expand Down Expand Up @@ -557,30 +565,24 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
console.log("no device found for id");
return;
}
const messageGroup = device.messages[type];

const messageIndex =
const messageStoreGroup = device.messageStores[type];
const messageStoreIndex =
type === "direct"
? from === device.hardware.myNodeNum
? to
: from
: channelIndex;
const messages = messageGroup.get(messageIndex);
const messageStore = messageStoreGroup.get(messageStoreIndex);

if (!messages) {
if (!messageStore) {
console.log("no messages found for id");
return;
}

messageGroup.set(
messageIndex,
messages.map((msg) => {
if (msg.id === messageId) {
msg.state = state;
}
return msg;
}),
);
messageStore.setMessageState(messageId, state);

device.messageStores.semaphore = !device.messageStores.semaphore;
}),
);
},
Expand Down
117 changes: 117 additions & 0 deletions src/core/stores/messageStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Protobuf, Types } from "@meshtastic/js";

export interface MessageWithState extends Types.PacketMetadata<string> {
state: MessageState;
}

export type MessageState = "ack" | "waiting" | Protobuf.Mesh.Routing_Error;

export default class MessageStore {
constructor(deviceNumber: number, channelNumber: number) {
this.deviceNumber = deviceNumber;
this.channelNumber = channelNumber;
this.messageIdsKey = `device/${this.deviceNumber}/group/${this.channelNumber}/message_ids`;
this.cursor = 0;
this.loaded = false;
this.messageIds = [];
this.messages = [];

this.loadMessageIds();
this.loadMessages();
}

messageKey(messageId: number): string {
return `device/${this.deviceNumber}/message/${messageId}`;
}

addMessage(message: MessageWithState): void {
this.setMessage(message);

this.messageIds.unshift(message.id);
localStorage.setItem(this.messageIdsKey, JSON.stringify(this.messageIds));

this.messages.push(message);
}

getMessage(messageId: number): MessageWithState {
const messageKey = this.messageKey(messageId);
const messageJSON = localStorage.getItem(messageKey);

if (messageJSON === null) {
return null;
}

const message = JSON.parse(messageJSON);

message.rxTime = new Date(message.rxTime);

return message;
}

setMessage(message: MessageWithState): void {
const messageKey = this.messageKey(message.id);

localStorage.setItem(messageKey, JSON.stringify(message));
}

setMessageState(messageId: number, state: MessageState): void {
const message = this.getMessage(messageId);

message.state = state;

this.setMessage(message);

this.messages.forEach((m, i) => {
if (m.id === messageId) {
this.messages[i] = message
return;
}
});
}

deleteMessage(messageId: number): void {
const messageKey = this.messageKey(messageId);
localStorage.removeItem(messageKey);
}

loadMessageIds(): void {
const messageIdsJSON = localStorage.getItem(this.messageIdsKey);

if (messageIdsJSON === null) {
this.messageIds = [];
localStorage.setItem(this.messageIdsKey, JSON.stringify(this.messageIds));
return;
}

this.messageIds = JSON.parse(messageIdsJSON);
}

loadMessages(): void {
if (this.loaded) return;

let message;

for (let i = this.cursor; i <= 50 + this.cursor; i++) {
message = this.getMessage(this.messageIds[i]);

if (message === null) {
this.loaded = true;
return;
}

this.cursor = i;
this.messages.unshift(message);
};
}

clear() {
this.messageIds.forEach((messageId) => {
this.deleteMessage(messageId);
});

localStorage.removeItem(this.messageIdsKey);

this.messageIds = [];
this.messages = [];
}
}
48 changes: 43 additions & 5 deletions src/pages/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { getChannelName } from "@pages/Channels.tsx";
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon, TrashIcon } from "lucide-react";
import { useState } from "react";

export const MessagesPage = (): JSX.Element => {
const { channels, nodes, hardware, messages, traceroutes, connection } =
const { channels, nodes, hardware, messageStores, traceroutes, connection } =
useDevice();
const [chatType, setChatType] =
useState<Types.PacketDestination>("broadcast");
Expand All @@ -32,6 +32,20 @@ export const MessagesPage = (): JSX.Element => {
const node = nodes.get(activeChat);
const nodeHex = node?.num ? numberToHexUnpadded(node.num) : "Unknown";

const [isLoading, setIsLoading] = useState(false);

const handleScroll = (event) => {
if (!isLoading && activeChat && event.currentTarget.scrollTop < 10) {
setIsLoading(true);

const messageStore = messageStores[chatType].get(activeChat);

messageStore.loadMessages();

setIsLoading(false);
}
};

return (
<>
<Sidebar>
Expand Down Expand Up @@ -72,6 +86,7 @@ export const MessagesPage = (): JSX.Element => {
</Sidebar>
<div className="flex flex-col flex-grow">
<PageLayout
onScroll={handleScroll}
label={`Messages: ${
chatType === "broadcast" && currentChannel
? getChannelName(currentChannel)
Expand Down Expand Up @@ -114,8 +129,31 @@ export const MessagesPage = (): JSX.Element => {
);
},
},
{
icon: TrashIcon,
onClick() {
const targetNode = nodes.get(activeChat)?.num;
if (targetNode === undefined) return;
messageStores.direct.get(targetNode).clear()
toast({
title: "Message history cleared.",
});
}
},
]
: [
{
icon: TrashIcon,
onClick() {
const targetChannel = channels.get(activeChat)?.index;
if (targetChannel === undefined) return;
messageStores.broadcast.get(targetChannel).clear()
toast({
title: "Message history cleared.",
});
}
},
]
: []
}
>
{allChannels.map(
Expand All @@ -124,7 +162,7 @@ export const MessagesPage = (): JSX.Element => {
<ChannelChat
key={channel.index}
to="broadcast"
messages={messages.broadcast.get(channel.index)}
messageStore={messageStores.broadcast.get(channel.index)}
channel={channel.index}
/>
),
Expand All @@ -135,7 +173,7 @@ export const MessagesPage = (): JSX.Element => {
<ChannelChat
key={node.num}
to={activeChat}
messages={messages.direct.get(node.num)}
messageStore={messageStores.direct.get(node.num)}
channel={Types.ChannelNumber.Primary}
traceroutes={traceroutes.get(node.num)}
/>
Expand Down