fix(config): update change registry channel value (#929)

* fix(config): update change registry channel value

* format/linting
This commit is contained in:
Dan Ditomaso
2025-11-04 15:12:35 -05:00
committed by GitHub
parent 2e60af1e29
commit d8ad0efcdf
6 changed files with 107 additions and 49 deletions
@@ -42,7 +42,9 @@ type DialogState = {
protocol: "http" | "https"; protocol: "http" | "https";
url: string; url: string;
testStatus: TestingStatus; testStatus: TestingStatus;
btSelected: { id: string; name?: string; device?: BluetoothDevice } | undefined; btSelected:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
serialSelected: { vendorId?: number; productId?: number } | undefined; serialSelected: { vendorId?: number; productId?: number } | undefined;
}; };
@@ -55,7 +57,9 @@ type DialogAction =
| { type: "SET_TEST_STATUS"; payload: TestingStatus } | { type: "SET_TEST_STATUS"; payload: TestingStatus }
| { | {
type: "SET_BT_SELECTED"; type: "SET_BT_SELECTED";
payload: { id: string; name?: string; device?: BluetoothDevice } | undefined; payload:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
} }
| { | {
type: "SET_SERIAL_SELECTED"; type: "SET_SERIAL_SELECTED";
@@ -596,18 +600,21 @@ export default function AddConnectionDialog({
const currentPane = PANES[state.tab]; const currentPane = PANES[state.tab];
const canCreate = useMemo(() => currentPane.validate(), [currentPane]); const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
const submit = (fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) => async () => { const submit =
if (!canCreate) { (fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) =>
return; async () => {
} if (!canCreate) {
const payload = currentPane.build(); return;
}
const payload = currentPane.build();
if (!payload) { if (!payload) {
return; return;
} }
const btDevice = state.tab === "bluetooth" ? state.btSelected?.device : undefined; const btDevice =
await fn(payload, btDevice); state.tab === "bluetooth" ? state.btSelected?.device : undefined;
}; await fn(payload, btDevice);
};
return ( return (
<DialogWrapper <DialogWrapper
@@ -123,11 +123,11 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
}); });
if (deepCompareConfig(channel, payload, true)) { if (deepCompareConfig(channel, payload, true)) {
removeChange({ type: "channels", index: channel.index }); removeChange({ type: "channel", index: channel.index });
return; return;
} }
setChange({ type: "channels", index: channel.index }, payload, channel); setChange({ type: "channel", index: channel.index }, payload, channel);
}; };
const preSharedKeyRegenerate = async () => { const preSharedKeyRegenerate = async () => {
@@ -1,9 +1,10 @@
import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx"; import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx";
import { Separator } from "@components/UI/Separator"; import { Separator } from "@components/UI/Separator";
import { Skeleton } from "@components/UI/Skeleton.tsx";
import type { Message } from "@core/stores/messageStore/types.ts"; import type { Message } from "@core/stores/messageStore/types.ts";
import type { TFunction } from "i18next"; import type { TFunction } from "i18next";
import { InboxIcon } from "lucide-react"; import { InboxIcon } from "lucide-react";
import { Fragment, useMemo } from "react"; import { Fragment, Suspense, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export interface ChannelChatProps { export interface ChannelChatProps {
@@ -75,6 +76,24 @@ const DateDelimiter = ({ label }: { label: string }) => (
</li> </li>
); );
const MessageSkeleton = () => {
console.log("[ChannelChat] Showing MessageSkeleton (Suspense fallback)");
return (
<li className="group w-full py-2 relative list-none rounded-md">
<div className="grid grid-cols-[auto_1fr] gap-x-2">
<Skeleton className="size-8 rounded-full" />
<div className="flex flex-col gap-1.5 min-w-0">
<div className="flex items-center gap-1.5">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-12" />
</div>
<Skeleton className="h-4 w-full" />
</div>
</div>
</li>
);
};
const EmptyState = () => { const EmptyState = () => {
const { t } = useTranslation("messages"); const { t } = useTranslation("messages");
return ( return (
@@ -130,10 +149,12 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => {
<Fragment key={dayKey}> <Fragment key={dayKey}>
{/* Render messages first, then delimiter — with flex-col-reverse this shows the delimiter above that day's messages */} {/* Render messages first, then delimiter — with flex-col-reverse this shows the delimiter above that day's messages */}
{items.map((message) => ( {items.map((message) => (
<MessageItem <Suspense
key={message.messageId ?? `${message.from}-${message.date}`} key={message.messageId ?? `${message.from}-${message.date}`}
message={message} fallback={<MessageSkeleton />}
/> >
<MessageItem message={message} />
</Suspense>
))} ))}
<DateDelimiter label={label} /> <DateDelimiter label={label} />
</Fragment> </Fragment>
@@ -1,5 +1,4 @@
import { Avatar } from "@components/UI/Avatar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx";
import { Skeleton } from "@components/UI/Skeleton.tsx";
import { import {
Tooltip, Tooltip,
TooltipArrow, TooltipArrow,
@@ -7,7 +6,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@components/UI/Tooltip.tsx"; } from "@components/UI/Tooltip.tsx";
import { MessageState, useDevice, useNodeDB } from "@core/stores"; import { MessageState, useAppStore, useDevice, useNodeDB } from "@core/stores";
import type { Message } from "@core/stores/messageStore/types.ts"; import type { Message } from "@core/stores/messageStore/types.ts";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { type Protobuf, Types } from "@meshtastic/core"; import { type Protobuf, Types } from "@meshtastic/core";
@@ -16,6 +15,50 @@ import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
import { type ReactNode, useMemo } from "react"; import { type ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
// Cache for pending promises
const myNodePromises = new Map<string, Promise<Protobuf.Mesh.NodeInfo>>();
// Hook that suspends when myNode is not available
function useSuspendingMyNode() {
const { getMyNode } = useNodeDB();
const selectedDeviceId = useAppStore((s) => s.selectedDeviceId);
const myNode = getMyNode();
if (!myNode) {
// Use the selected device ID to cache promises per device
const deviceKey = `device-${selectedDeviceId}`;
if (!myNodePromises.has(deviceKey)) {
const promise = new Promise<Protobuf.Mesh.NodeInfo>((resolve) => {
// Poll for myNode to become available
const checkInterval = setInterval(() => {
const node = getMyNode();
if (node) {
console.log(
"[MessageItem] myNode now available, resolving promise",
);
clearInterval(checkInterval);
myNodePromises.delete(deviceKey);
resolve(node);
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
myNodePromises.delete(deviceKey);
}, 10000);
});
myNodePromises.set(deviceKey, promise);
}
// Throw the promise to trigger Suspense
throw myNodePromises.get(deviceKey);
}
return myNode;
}
// import { MessageActionsMenu } from "@components/PageComponents/Messages/MessageActionsMenu.tsx"; // TODO: Uncomment when actions menu is implemented // import { MessageActionsMenu } from "@components/PageComponents/Messages/MessageActionsMenu.tsx"; // TODO: Uncomment when actions menu is implemented
interface MessageStatusInfo { interface MessageStatusInfo {
@@ -49,28 +92,12 @@ interface MessageItemProps {
export const MessageItem = ({ message }: MessageItemProps) => { export const MessageItem = ({ message }: MessageItemProps) => {
const { config } = useDevice(); const { config } = useDevice();
const { getNode, getMyNode } = useNodeDB(); const { getNode } = useNodeDB();
const { t, i18n } = useTranslation("messages"); const { t, i18n } = useTranslation("messages");
const myNodeNum = useMemo(() => getMyNode()?.num, [getMyNode]); // This will suspend if myNode is not available yet
const myNode = useSuspendingMyNode();
// Show loading state when myNodeNum is not yet available const myNodeNum = myNode.num;
if (myNodeNum === undefined) {
return (
<li className="group w-full py-2 relative list-none rounded-md">
<div className="grid grid-cols-[auto_1fr] gap-x-2">
<Skeleton className="size-8 rounded-full" />
<div className="flex flex-col gap-1.5 min-w-0">
<div className="flex items-center gap-1.5">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-12" />
</div>
<Skeleton className="h-4 w-full" />
</div>
</div>
</li>
);
}
const MESSAGE_STATUS_MAP = useMemo( const MESSAGE_STATUS_MAP = useMemo(
(): Record<MessageState, MessageStatusInfo> => ({ (): Record<MessageState, MessageStatusInfo> => ({
+4 -1
View File
@@ -6,7 +6,10 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) { }: React.HTMLAttributes<HTMLDivElement>) {
return ( return (
<div <div
className={cn("animate-pulse rounded-md bg-slate-200 dark:bg-slate-700", className)} className={cn(
"animate-pulse rounded-md bg-slate-200 dark:bg-slate-700",
className,
)}
{...props} {...props}
/> />
); );
@@ -29,7 +29,7 @@ export type ValidModuleConfigType =
export type ConfigChangeKey = export type ConfigChangeKey =
| { type: "config"; variant: ValidConfigType } | { type: "config"; variant: ValidConfigType }
| { type: "moduleConfig"; variant: ValidModuleConfigType } | { type: "moduleConfig"; variant: ValidModuleConfigType }
| { type: "channels"; index: Types.ChannelNumber } | { type: "channel"; index: Types.ChannelNumber }
| { type: "user" }; | { type: "user" };
// Serialized key for Map storage // Serialized key for Map storage
@@ -57,7 +57,7 @@ export function serializeKey(key: ConfigChangeKey): ConfigChangeKeyString {
return `config:${key.variant}`; return `config:${key.variant}`;
case "moduleConfig": case "moduleConfig":
return `moduleConfig:${key.variant}`; return `moduleConfig:${key.variant}`;
case "channels": case "channel":
return `channel:${key.index}`; return `channel:${key.index}`;
case "user": case "user":
return "user"; return "user";
@@ -78,9 +78,9 @@ export function deserializeKey(keyStr: ConfigChangeKeyString): ConfigChangeKey {
type: "moduleConfig", type: "moduleConfig",
variant: variant as ValidModuleConfigType, variant: variant as ValidModuleConfigType,
}; };
case "channels": case "channel":
return { return {
type: "channels", type: "channel",
index: Number(variant) as Types.ChannelNumber, index: Number(variant) as Types.ChannelNumber,
}; };
case "user": case "user":
@@ -126,7 +126,7 @@ export function hasChannelChange(
registry: ChangeRegistry, registry: ChangeRegistry,
index: Types.ChannelNumber, index: Types.ChannelNumber,
): boolean { ): boolean {
return registry.changes.has(serializeKey({ type: "channels", index })); return registry.changes.has(serializeKey({ type: "channel", index }));
} }
/** /**
@@ -171,7 +171,7 @@ export function getChannelChangeCount(registry: ChangeRegistry): number {
let count = 0; let count = 0;
for (const keyStr of registry.changes.keys()) { for (const keyStr of registry.changes.keys()) {
const key = deserializeKey(keyStr); const key = deserializeKey(keyStr);
if (key.type === "channels") { if (key.type === "channel") {
count++; count++;
} }
} }
@@ -212,7 +212,7 @@ export function getAllModuleConfigChanges(
export function getAllChannelChanges(registry: ChangeRegistry): ChangeEntry[] { export function getAllChannelChanges(registry: ChangeRegistry): ChangeEntry[] {
const changes: ChangeEntry[] = []; const changes: ChangeEntry[] = [];
for (const entry of registry.changes.values()) { for (const entry of registry.changes.values()) {
if (entry.key.type === "channels") { if (entry.key.type === "channel") {
changes.push(entry); changes.push(entry);
} }
} }