From d8ad0efcdf78fe2d0e903fbead32dc6080cd7ed0 Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Tue, 4 Nov 2025 15:12:35 -0500 Subject: [PATCH] fix(config): update change registry channel value (#929) * fix(config): update change registry channel value * format/linting --- .../AddConnectionDialog.tsx | 33 +++++---- .../PageComponents/Channels/Channel.tsx | 4 +- .../PageComponents/Messages/ChannelChat.tsx | 29 ++++++-- .../PageComponents/Messages/MessageItem.tsx | 71 +++++++++++++------ packages/web/src/components/UI/Skeleton.tsx | 5 +- .../core/stores/deviceStore/changeRegistry.ts | 14 ++-- 6 files changed, 107 insertions(+), 49 deletions(-) diff --git a/packages/web/src/components/Dialog/AddConnectionDialog/AddConnectionDialog.tsx b/packages/web/src/components/Dialog/AddConnectionDialog/AddConnectionDialog.tsx index 9ccc2860..7135acd8 100644 --- a/packages/web/src/components/Dialog/AddConnectionDialog/AddConnectionDialog.tsx +++ b/packages/web/src/components/Dialog/AddConnectionDialog/AddConnectionDialog.tsx @@ -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) => async () => { - if (!canCreate) { - return; - } - const payload = currentPane.build(); + const submit = + (fn: (p: NewConnection, device?: BluetoothDevice) => Promise) => + 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 ( { }); 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 () => { diff --git a/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx b/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx index 5a2ee286..f44cb626 100644 --- a/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx +++ b/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx @@ -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 }) => ( ); +const MessageSkeleton = () => { + console.log("[ChannelChat] Showing MessageSkeleton (Suspense fallback)"); + return ( +
  • +
    + +
    +
    + + +
    + +
    +
    +
  • + ); +}; + const EmptyState = () => { const { t } = useTranslation("messages"); return ( @@ -130,10 +149,12 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => { {/* Render messages first, then delimiter — with flex-col-reverse this shows the delimiter above that day's messages */} {items.map((message) => ( - + fallback={} + > + + ))} diff --git a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx index c45e8f5d..27fe2d97 100644 --- a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx +++ b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx @@ -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>(); + +// 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((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 ( -
  • -
    - -
    -
    - - -
    - -
    -
    -
  • - ); - } + // This will suspend if myNode is not available yet + const myNode = useSuspendingMyNode(); + const myNodeNum = myNode.num; const MESSAGE_STATUS_MAP = useMemo( (): Record => ({ diff --git a/packages/web/src/components/UI/Skeleton.tsx b/packages/web/src/components/UI/Skeleton.tsx index dc8e3744..07237aca 100644 --- a/packages/web/src/components/UI/Skeleton.tsx +++ b/packages/web/src/components/UI/Skeleton.tsx @@ -6,7 +6,10 @@ function Skeleton({ }: React.HTMLAttributes) { return (
    ); diff --git a/packages/web/src/core/stores/deviceStore/changeRegistry.ts b/packages/web/src/core/stores/deviceStore/changeRegistry.ts index 7dbaae12..76de523e 100644 --- a/packages/web/src/core/stores/deviceStore/changeRegistry.ts +++ b/packages/web/src/core/stores/deviceStore/changeRegistry.ts @@ -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); } }