Skip to content

Commit d8ad0ef

Browse files
authored
fix(config): update change registry channel value (#929)
* fix(config): update change registry channel value * format/linting
1 parent 2e60af1 commit d8ad0ef

File tree

6 files changed

+107
-49
lines changed

6 files changed

+107
-49
lines changed

packages/web/src/components/Dialog/AddConnectionDialog/AddConnectionDialog.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ type DialogState = {
4242
protocol: "http" | "https";
4343
url: string;
4444
testStatus: TestingStatus;
45-
btSelected: { id: string; name?: string; device?: BluetoothDevice } | undefined;
45+
btSelected:
46+
| { id: string; name?: string; device?: BluetoothDevice }
47+
| undefined;
4648
serialSelected: { vendorId?: number; productId?: number } | undefined;
4749
};
4850

@@ -55,7 +57,9 @@ type DialogAction =
5557
| { type: "SET_TEST_STATUS"; payload: TestingStatus }
5658
| {
5759
type: "SET_BT_SELECTED";
58-
payload: { id: string; name?: string; device?: BluetoothDevice } | undefined;
60+
payload:
61+
| { id: string; name?: string; device?: BluetoothDevice }
62+
| undefined;
5963
}
6064
| {
6165
type: "SET_SERIAL_SELECTED";
@@ -596,18 +600,21 @@ export default function AddConnectionDialog({
596600
const currentPane = PANES[state.tab];
597601
const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
598602

599-
const submit = (fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) => async () => {
600-
if (!canCreate) {
601-
return;
602-
}
603-
const payload = currentPane.build();
603+
const submit =
604+
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) =>
605+
async () => {
606+
if (!canCreate) {
607+
return;
608+
}
609+
const payload = currentPane.build();
604610

605-
if (!payload) {
606-
return;
607-
}
608-
const btDevice = state.tab === "bluetooth" ? state.btSelected?.device : undefined;
609-
await fn(payload, btDevice);
610-
};
611+
if (!payload) {
612+
return;
613+
}
614+
const btDevice =
615+
state.tab === "bluetooth" ? state.btSelected?.device : undefined;
616+
await fn(payload, btDevice);
617+
};
611618

612619
return (
613620
<DialogWrapper

packages/web/src/components/PageComponents/Channels/Channel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,11 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
123123
});
124124

125125
if (deepCompareConfig(channel, payload, true)) {
126-
removeChange({ type: "channels", index: channel.index });
126+
removeChange({ type: "channel", index: channel.index });
127127
return;
128128
}
129129

130-
setChange({ type: "channels", index: channel.index }, payload, channel);
130+
setChange({ type: "channel", index: channel.index }, payload, channel);
131131
};
132132

133133
const preSharedKeyRegenerate = async () => {

packages/web/src/components/PageComponents/Messages/ChannelChat.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx";
22
import { Separator } from "@components/UI/Separator";
3+
import { Skeleton } from "@components/UI/Skeleton.tsx";
34
import type { Message } from "@core/stores/messageStore/types.ts";
45
import type { TFunction } from "i18next";
56
import { InboxIcon } from "lucide-react";
6-
import { Fragment, useMemo } from "react";
7+
import { Fragment, Suspense, useMemo } from "react";
78
import { useTranslation } from "react-i18next";
89

910
export interface ChannelChatProps {
@@ -75,6 +76,24 @@ const DateDelimiter = ({ label }: { label: string }) => (
7576
</li>
7677
);
7778

79+
const MessageSkeleton = () => {
80+
console.log("[ChannelChat] Showing MessageSkeleton (Suspense fallback)");
81+
return (
82+
<li className="group w-full py-2 relative list-none rounded-md">
83+
<div className="grid grid-cols-[auto_1fr] gap-x-2">
84+
<Skeleton className="size-8 rounded-full" />
85+
<div className="flex flex-col gap-1.5 min-w-0">
86+
<div className="flex items-center gap-1.5">
87+
<Skeleton className="h-4 w-24" />
88+
<Skeleton className="h-3 w-12" />
89+
</div>
90+
<Skeleton className="h-4 w-full" />
91+
</div>
92+
</div>
93+
</li>
94+
);
95+
};
96+
7897
const EmptyState = () => {
7998
const { t } = useTranslation("messages");
8099
return (
@@ -130,10 +149,12 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => {
130149
<Fragment key={dayKey}>
131150
{/* Render messages first, then delimiter — with flex-col-reverse this shows the delimiter above that day's messages */}
132151
{items.map((message) => (
133-
<MessageItem
152+
<Suspense
134153
key={message.messageId ?? `${message.from}-${message.date}`}
135-
message={message}
136-
/>
154+
fallback={<MessageSkeleton />}
155+
>
156+
<MessageItem message={message} />
157+
</Suspense>
137158
))}
138159
<DateDelimiter label={label} />
139160
</Fragment>

packages/web/src/components/PageComponents/Messages/MessageItem.tsx

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { Avatar } from "@components/UI/Avatar.tsx";
2-
import { Skeleton } from "@components/UI/Skeleton.tsx";
32
import {
43
Tooltip,
54
TooltipArrow,
65
TooltipContent,
76
TooltipProvider,
87
TooltipTrigger,
98
} from "@components/UI/Tooltip.tsx";
10-
import { MessageState, useDevice, useNodeDB } from "@core/stores";
9+
import { MessageState, useAppStore, useDevice, useNodeDB } from "@core/stores";
1110
import type { Message } from "@core/stores/messageStore/types.ts";
1211
import { cn } from "@core/utils/cn.ts";
1312
import { type Protobuf, Types } from "@meshtastic/core";
@@ -16,6 +15,50 @@ import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
1615
import { type ReactNode, useMemo } from "react";
1716
import { useTranslation } from "react-i18next";
1817

18+
// Cache for pending promises
19+
const myNodePromises = new Map<string, Promise<Protobuf.Mesh.NodeInfo>>();
20+
21+
// Hook that suspends when myNode is not available
22+
function useSuspendingMyNode() {
23+
const { getMyNode } = useNodeDB();
24+
const selectedDeviceId = useAppStore((s) => s.selectedDeviceId);
25+
const myNode = getMyNode();
26+
27+
if (!myNode) {
28+
// Use the selected device ID to cache promises per device
29+
const deviceKey = `device-${selectedDeviceId}`;
30+
31+
if (!myNodePromises.has(deviceKey)) {
32+
const promise = new Promise<Protobuf.Mesh.NodeInfo>((resolve) => {
33+
// Poll for myNode to become available
34+
const checkInterval = setInterval(() => {
35+
const node = getMyNode();
36+
if (node) {
37+
console.log(
38+
"[MessageItem] myNode now available, resolving promise",
39+
);
40+
clearInterval(checkInterval);
41+
myNodePromises.delete(deviceKey);
42+
resolve(node);
43+
}
44+
}, 100);
45+
46+
setTimeout(() => {
47+
clearInterval(checkInterval);
48+
myNodePromises.delete(deviceKey);
49+
}, 10000);
50+
});
51+
52+
myNodePromises.set(deviceKey, promise);
53+
}
54+
55+
// Throw the promise to trigger Suspense
56+
throw myNodePromises.get(deviceKey);
57+
}
58+
59+
return myNode;
60+
}
61+
1962
// import { MessageActionsMenu } from "@components/PageComponents/Messages/MessageActionsMenu.tsx"; // TODO: Uncomment when actions menu is implemented
2063

2164
interface MessageStatusInfo {
@@ -49,28 +92,12 @@ interface MessageItemProps {
4992

5093
export const MessageItem = ({ message }: MessageItemProps) => {
5194
const { config } = useDevice();
52-
const { getNode, getMyNode } = useNodeDB();
95+
const { getNode } = useNodeDB();
5396
const { t, i18n } = useTranslation("messages");
5497

55-
const myNodeNum = useMemo(() => getMyNode()?.num, [getMyNode]);
56-
57-
// Show loading state when myNodeNum is not yet available
58-
if (myNodeNum === undefined) {
59-
return (
60-
<li className="group w-full py-2 relative list-none rounded-md">
61-
<div className="grid grid-cols-[auto_1fr] gap-x-2">
62-
<Skeleton className="size-8 rounded-full" />
63-
<div className="flex flex-col gap-1.5 min-w-0">
64-
<div className="flex items-center gap-1.5">
65-
<Skeleton className="h-4 w-24" />
66-
<Skeleton className="h-3 w-12" />
67-
</div>
68-
<Skeleton className="h-4 w-full" />
69-
</div>
70-
</div>
71-
</li>
72-
);
73-
}
98+
// This will suspend if myNode is not available yet
99+
const myNode = useSuspendingMyNode();
100+
const myNodeNum = myNode.num;
74101

75102
const MESSAGE_STATUS_MAP = useMemo(
76103
(): Record<MessageState, MessageStatusInfo> => ({

packages/web/src/components/UI/Skeleton.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ function Skeleton({
66
}: React.HTMLAttributes<HTMLDivElement>) {
77
return (
88
<div
9-
className={cn("animate-pulse rounded-md bg-slate-200 dark:bg-slate-700", className)}
9+
className={cn(
10+
"animate-pulse rounded-md bg-slate-200 dark:bg-slate-700",
11+
className,
12+
)}
1013
{...props}
1114
/>
1215
);

packages/web/src/core/stores/deviceStore/changeRegistry.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type ValidModuleConfigType =
2929
export type ConfigChangeKey =
3030
| { type: "config"; variant: ValidConfigType }
3131
| { type: "moduleConfig"; variant: ValidModuleConfigType }
32-
| { type: "channels"; index: Types.ChannelNumber }
32+
| { type: "channel"; index: Types.ChannelNumber }
3333
| { type: "user" };
3434

3535
// Serialized key for Map storage
@@ -57,7 +57,7 @@ export function serializeKey(key: ConfigChangeKey): ConfigChangeKeyString {
5757
return `config:${key.variant}`;
5858
case "moduleConfig":
5959
return `moduleConfig:${key.variant}`;
60-
case "channels":
60+
case "channel":
6161
return `channel:${key.index}`;
6262
case "user":
6363
return "user";
@@ -78,9 +78,9 @@ export function deserializeKey(keyStr: ConfigChangeKeyString): ConfigChangeKey {
7878
type: "moduleConfig",
7979
variant: variant as ValidModuleConfigType,
8080
};
81-
case "channels":
81+
case "channel":
8282
return {
83-
type: "channels",
83+
type: "channel",
8484
index: Number(variant) as Types.ChannelNumber,
8585
};
8686
case "user":
@@ -126,7 +126,7 @@ export function hasChannelChange(
126126
registry: ChangeRegistry,
127127
index: Types.ChannelNumber,
128128
): boolean {
129-
return registry.changes.has(serializeKey({ type: "channels", index }));
129+
return registry.changes.has(serializeKey({ type: "channel", index }));
130130
}
131131

132132
/**
@@ -171,7 +171,7 @@ export function getChannelChangeCount(registry: ChangeRegistry): number {
171171
let count = 0;
172172
for (const keyStr of registry.changes.keys()) {
173173
const key = deserializeKey(keyStr);
174-
if (key.type === "channels") {
174+
if (key.type === "channel") {
175175
count++;
176176
}
177177
}
@@ -212,7 +212,7 @@ export function getAllModuleConfigChanges(
212212
export function getAllChannelChanges(registry: ChangeRegistry): ChangeEntry[] {
213213
const changes: ChangeEntry[] = [];
214214
for (const entry of registry.changes.values()) {
215-
if (entry.key.type === "channels") {
215+
if (entry.key.type === "channel") {
216216
changes.push(entry);
217217
}
218218
}

0 commit comments

Comments
 (0)