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";
|
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> => ({
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user