mirror of
https://github.com/meshtastic/web.git
synced 2026-02-07 06:11:47 -05:00
fix(config): update change registry channel value (#929)
* fix(config): update change registry channel value * format/linting
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> => ({
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user