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

View File

@@ -42,7 +42,9 @@ type DialogState = {
protocol: "http" | "https";
url: string;
testStatus: TestingStatus;
btSelected: { id: string; name?: string; device?: BluetoothDevice } | undefined;
btSelected:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
serialSelected: { vendorId?: number; productId?: number } | undefined;
};
@@ -55,7 +57,9 @@ type DialogAction =
| { type: "SET_TEST_STATUS"; payload: TestingStatus }
| {
type: "SET_BT_SELECTED";
payload: { id: string; name?: string; device?: BluetoothDevice } | undefined;
payload:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
}
| {
type: "SET_SERIAL_SELECTED";
@@ -596,18 +600,21 @@ export default function AddConnectionDialog({
const currentPane = PANES[state.tab];
const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
const submit = (fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) => async () => {
if (!canCreate) {
return;
}
const payload = currentPane.build();
const submit =
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) =>
async () => {
if (!canCreate) {
return;
}
const payload = currentPane.build();
if (!payload) {
return;
}
const btDevice = state.tab === "bluetooth" ? state.btSelected?.device : undefined;
await fn(payload, btDevice);
};
if (!payload) {
return;
}
const btDevice =
state.tab === "bluetooth" ? state.btSelected?.device : undefined;
await fn(payload, btDevice);
};
return (
<DialogWrapper

View File

@@ -123,11 +123,11 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
});
if (deepCompareConfig(channel, payload, true)) {
removeChange({ type: "channels", index: channel.index });
removeChange({ type: "channel", index: channel.index });
return;
}
setChange({ type: "channels", index: channel.index }, payload, channel);
setChange({ type: "channel", index: channel.index }, payload, channel);
};
const preSharedKeyRegenerate = async () => {

View File

@@ -1,9 +1,10 @@
import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx";
import { Separator } from "@components/UI/Separator";
import { Skeleton } from "@components/UI/Skeleton.tsx";
import type { Message } from "@core/stores/messageStore/types.ts";
import type { TFunction } from "i18next";
import { InboxIcon } from "lucide-react";
import { Fragment, useMemo } from "react";
import { Fragment, Suspense, useMemo } from "react";
import { useTranslation } from "react-i18next";
export interface ChannelChatProps {
@@ -75,6 +76,24 @@ const DateDelimiter = ({ label }: { label: string }) => (
</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 { t } = useTranslation("messages");
return (
@@ -130,10 +149,12 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => {
<Fragment key={dayKey}>
{/* Render messages first, then delimiter — with flex-col-reverse this shows the delimiter above that day's messages */}
{items.map((message) => (
<MessageItem
<Suspense
key={message.messageId ?? `${message.from}-${message.date}`}
message={message}
/>
fallback={<MessageSkeleton />}
>
<MessageItem message={message} />
</Suspense>
))}
<DateDelimiter label={label} />
</Fragment>

View File

@@ -1,5 +1,4 @@
import { Avatar } from "@components/UI/Avatar.tsx";
import { Skeleton } from "@components/UI/Skeleton.tsx";
import {
Tooltip,
TooltipArrow,
@@ -7,7 +6,7 @@ import {
TooltipProvider,
TooltipTrigger,
} 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 { cn } from "@core/utils/cn.ts";
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 { 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
interface MessageStatusInfo {
@@ -49,28 +92,12 @@ interface MessageItemProps {
export const MessageItem = ({ message }: MessageItemProps) => {
const { config } = useDevice();
const { getNode, getMyNode } = useNodeDB();
const { getNode } = useNodeDB();
const { t, i18n } = useTranslation("messages");
const myNodeNum = useMemo(() => getMyNode()?.num, [getMyNode]);
// Show loading state when myNodeNum is not yet available
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>
);
}
// This will suspend if myNode is not available yet
const myNode = useSuspendingMyNode();
const myNodeNum = myNode.num;
const MESSAGE_STATUS_MAP = useMemo(
(): Record<MessageState, MessageStatusInfo> => ({

View File

@@ -6,7 +6,10 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<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}
/>
);

View File

@@ -29,7 +29,7 @@ export type ValidModuleConfigType =
export type ConfigChangeKey =
| { type: "config"; variant: ValidConfigType }
| { type: "moduleConfig"; variant: ValidModuleConfigType }
| { type: "channels"; index: Types.ChannelNumber }
| { type: "channel"; index: Types.ChannelNumber }
| { type: "user" };
// Serialized key for Map storage
@@ -57,7 +57,7 @@ export function serializeKey(key: ConfigChangeKey): ConfigChangeKeyString {
return `config:${key.variant}`;
case "moduleConfig":
return `moduleConfig:${key.variant}`;
case "channels":
case "channel":
return `channel:${key.index}`;
case "user":
return "user";
@@ -78,9 +78,9 @@ export function deserializeKey(keyStr: ConfigChangeKeyString): ConfigChangeKey {
type: "moduleConfig",
variant: variant as ValidModuleConfigType,
};
case "channels":
case "channel":
return {
type: "channels",
type: "channel",
index: Number(variant) as Types.ChannelNumber,
};
case "user":
@@ -126,7 +126,7 @@ export function hasChannelChange(
registry: ChangeRegistry,
index: Types.ChannelNumber,
): 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;
for (const keyStr of registry.changes.keys()) {
const key = deserializeKey(keyStr);
if (key.type === "channels") {
if (key.type === "channel") {
count++;
}
}
@@ -212,7 +212,7 @@ export function getAllModuleConfigChanges(
export function getAllChannelChanges(registry: ChangeRegistry): ChangeEntry[] {
const changes: ChangeEntry[] = [];
for (const entry of registry.changes.values()) {
if (entry.key.type === "channels") {
if (entry.key.type === "channel") {
changes.push(entry);
}
}