Execute oxfmt (#1166)

Ran
pnpm oxfmt .

This should get the web repo aligned so that we can better enforce oxfmt going forward.
This commit is contained in:
Austin
2026-06-16 20:30:08 -04:00
committed by GitHub
parent c5690d1edb
commit d2cb51d489
378 changed files with 5548 additions and 1974 deletions
@@ -1,8 +1,14 @@
import { type ChannelValidation, makeChannelSchema } from "@app/validation/channel.ts";
import {
type ChannelValidation,
makeChannelSchema,
} from "@app/validation/channel.ts";
import { create } from "@bufbuild/protobuf";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -23,7 +29,10 @@ const EMPTY_CHANNELS_SIGNAL = {
Protobuf.Channel.Channel
>,
peek: () =>
new Map<number, Protobuf.Channel.Channel>() as ReadonlyMap<number, Protobuf.Channel.Channel>,
new Map<number, Protobuf.Channel.Channel>() as ReadonlyMap<
number,
Protobuf.Channel.Channel
>,
subscribe: () => () => {},
} as const;
@@ -43,7 +52,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
moduleSettings: {
...defaultConfig?.settings?.moduleSettings,
positionPrecision:
defaultConfig?.settings?.moduleSettings?.positionPrecision === undefined
defaultConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
? 10
: defaultConfig?.settings?.moduleSettings?.positionPrecision,
},
@@ -62,7 +72,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
moduleSettings: {
...effectiveConfig?.settings?.moduleSettings,
positionPrecision:
effectiveConfig?.settings?.moduleSettings?.positionPrecision === undefined
effectiveConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
? 10
: effectiveConfig?.settings?.moduleSettings?.positionPrecision,
},
@@ -70,8 +81,11 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
},
};
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(false);
const [byteCount, setBytes] = useState<number>(effectiveConfig?.settings?.psk.length ?? 16);
const [preSharedDialogOpen, setPreSharedDialogOpen] =
useState<boolean>(false);
const [byteCount, setBytes] = useState<number>(
effectiveConfig?.settings?.psk.length ?? 16,
);
const ChannelValidationSchema = useMemo(() => {
return makeChannelSchema(byteCount);
}, [byteCount]);
@@ -2,7 +2,12 @@ import { Channel } from "@app/components/PageComponents/Channels/Channel";
import { create } from "@bufbuild/protobuf";
import { Button } from "@components/UI/Button.tsx";
import { Spinner } from "@components/UI/Spinner.tsx";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useChannels, useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -16,7 +21,10 @@ interface ConfigProps {
onFormInit: <T extends object>(methods: UseFormReturn<T>) => void;
}
export const getChannelName = (channel: { index: number; settings?: { name?: string } }) => {
export const getChannelName = (channel: {
index: number;
settings?: { name?: string };
}) => {
return channel.settings?.name?.length
? channel.settings.name
: channel.index === 0
@@ -37,7 +45,9 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
const { setDialogOpen } = useDevice();
const editor = useConfigEditor();
const channels = useChannels();
const dirtyChannels = useSignal(editor?.dirtyChannels ?? EMPTY_DIRTY_CHANNELS_SIGNAL);
const dirtyChannels = useSignal(
editor?.dirtyChannels ?? EMPTY_DIRTY_CHANNELS_SIGNAL,
);
const { t } = useTranslation("channels");
const allChannels = useMemo(
@@ -53,7 +63,12 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
);
const flags = useMemo(
() =>
new Map(allChannels.map((channel) => [channel.index, dirtyChannels.includes(channel.index)])),
new Map(
allChannels.map((channel) => [
channel.index,
dirtyChannels.includes(channel.index),
]),
),
[allChannels, dirtyChannels],
);
@@ -75,7 +90,10 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
)}
</TabsTrigger>
))}
<Button className="ml-auto mr-1 h-8" onClick={() => setDialogOpen("import", true)}>
<Button
className="ml-auto mr-1 h-8"
onClick={() => setDialogOpen("import", true)}
>
<UploadIcon className="mr-2" size={14} />
{t("page.import")}
</Button>
@@ -85,9 +103,16 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
</Button>
</TabsList>
{allChannels.map((channel) => (
<TabsContent key={`channel_${channel.index}`} value={`channel_${channel.index}`}>
<TabsContent
key={`channel_${channel.index}`}
value={`channel_${channel.index}`}
>
<Suspense fallback={<Spinner size="lg" className="my-5" />}>
<Channel key={channel.index} onFormInit={onFormInit} channel={channel} />
<Channel
key={channel.index}
onFormInit={onFormInit}
channel={channel}
/>
</Suspense>
</TabsContent>
))}
@@ -1,7 +1,11 @@
import { Button } from "@app/components/UI/Button";
import type { Connection } from "@app/core/stores/deviceStore/types";
export function ConnectionStatusBadge({ status }: { status: Connection["status"] }) {
export function ConnectionStatusBadge({
status,
}: {
status: Connection["status"];
}) {
let color = "";
let displayStatus = status;
@@ -26,8 +30,13 @@ export function ConnectionStatusBadge({ status }: { status: Connection["status"]
}
return (
<Button variant="subtle" className="inline-flex items-center gap-2">
<span className={`h-2.5 w-2.5 rounded-full ${color}`} aria-hidden="true" />
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">{displayStatus}</span>
<span
className={`h-2.5 w-2.5 rounded-full ${color}`}
aria-hidden="true"
/>
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">
{displayStatus}
</span>
</Button>
);
}
@@ -14,7 +14,12 @@ export interface HeatmapLayerProps {
isVisible: boolean;
}
export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLayerProps) => {
export const HeatmapLayer = ({
id,
filteredNodes,
mode,
isVisible,
}: HeatmapLayerProps) => {
if (!isVisible) return null;
const data: FeatureCollection = useMemo(() => {
const features: Feature[] = filteredNodes
@@ -43,7 +48,9 @@ export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLaye
const paintProps: HeatmapLayerSpecification["paint"] = useMemo(
() => ({
"heatmap-weight":
mode === "density" ? 1 : ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1],
mode === "density"
? 1
: ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1],
"heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 15, 3],
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparancy color
@@ -65,7 +72,17 @@ export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLaye
1,
"rgb(178,24,43)",
],
"heatmap-radius": ["interpolate", ["linear"], ["zoom"], 0, 2, 9, 20, 15, 30],
"heatmap-radius": [
"interpolate",
["linear"],
["zoom"],
0,
2,
9,
20,
15,
30,
],
// Opacity 0.7 to be visible but not blocking
"heatmap-opacity": 0.7,
}),
@@ -45,14 +45,18 @@ export const NodesLayer = ({
const errors = useNodeErrors();
const errorSet = useMemo(() => new Set(errors.map((e) => e.node)), [errors]);
const hasNodeError = useCallback((num: number) => errorSet.has(num), [errorSet]);
const hasNodeError = useCallback(
(num: number) => errorSet.has(num),
[errorSet],
);
const { focusLngLat } = useMapFitting(mapRef);
const selectedNode = useMemo(
() =>
popupState?.type !== "node"
? undefined
: (filteredNodes.find((node) => node.num === popupState.num) ?? undefined),
: (filteredNodes.find((node) => node.num === popupState.num) ??
undefined),
[popupState, filteredNodes],
);
@@ -79,7 +83,9 @@ export const NodesLayer = ({
const isExpanded = expandedCluster === key;
// Precompute pixel offsets for expanded state
const expandedOffsets = isExpanded ? fanOutOffsetsPx(nodes.length, key) : undefined;
const expandedOffsets = isExpanded
? fanOutOffsetsPx(nodes.length, key)
: undefined;
// Always render all node markers in the cluster
for (const [i, node] of nodes.entries()) {
@@ -36,7 +36,10 @@ export function generatePrecisionCircles(
>();
for (const node of filteredNodes) {
if (node.position?.precisionBits === undefined || node.position.precisionBits === 0) {
if (
node.position?.precisionBits === undefined ||
node.position.precisionBits === 0
) {
continue;
}
const [lng, lat] = toLngLat(node.position);
@@ -60,7 +63,9 @@ export function generatePrecisionCircles(
}
}
const items = Array.from(unique.values()).sort((a, b) => a.radiusM - b.radiusM);
const items = Array.from(unique.values()).sort(
(a, b) => a.radiusM - b.radiusM,
);
const features: Feature<Polygon, CircleProps>[] = items.map(
({ lng, lat, radiusM, r, g, b, a }) => {
@@ -91,7 +96,13 @@ export const SourcePrecisionCircles = ({
type="fill"
layout={{ visibility: isVisible ? "visible" : "none" }}
paint={{
"fill-color": ["rgba", ["get", "r"], ["get", "g"], ["get", "b"], ["get", "a"]],
"fill-color": [
"rgba",
["get", "r"],
["get", "g"],
["get", "b"],
["get", "a"],
],
}}
/>
<Layer
@@ -64,7 +64,11 @@ type Pair = {
ba?: number; // SNR b->a
};
function arcSegment(aLngLat: LngLat, bLngLat: LngLat, curved: boolean): LngLat[] | undefined {
function arcSegment(
aLngLat: LngLat,
bLngLat: LngLat,
curved: boolean,
): LngLat[] | undefined {
if (!curved && distanceMeters(aLngLat, bLngLat) < MIN_LEN) {
// Straight line
return [aLngLat, bLngLat];
@@ -176,7 +180,9 @@ function pushIfFeature(
}
}
function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollection {
function generateNeighborLines(
neighborInfos: NeighborInfos[],
): FeatureCollection {
// Collect positions for all referenced nodes, discard pairs with missing positions
const idToLngLat = new Map<number, LngLat>();
const ensure = (node?: Protobuf.Mesh.NodeInfo | NeighborPlus) => {
@@ -240,7 +246,12 @@ function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollectio
return { type: "FeatureCollection", features };
}
export const SNRTooltip = ({ pos, snr, from, to }: Partial<SNRTooltipProps> = {}) => {
export const SNRTooltip = ({
pos,
snr,
from,
to,
}: Partial<SNRTooltipProps> = {}) => {
const { t } = useTranslation();
if (!pos) {
@@ -290,7 +301,9 @@ export const SNRLayer = ({
neighborInfo: {
...neighborInfo,
neighbors: neighborInfo.neighbors.map((n) => {
const node = filteredNodes.find((node) => node.num === n.nodeId);
const node = filteredNodes.find(
(node) => node.num === n.nodeId,
);
return { ...n, num: node?.num, position: node?.position };
}),
},
@@ -312,7 +325,10 @@ export const SNRLayer = ({
}))
: [];
const featureCollection = generateNeighborLines([...remotePairs, ...directPairs]);
const featureCollection = generateNeighborLines([
...remotePairs,
...directPairs,
]);
return (
<Source type="geojson" data={featureCollection}>
@@ -82,7 +82,11 @@ export const WaypointLayer = ({
offset={[0, 25]}
onClose={() => setPopupState(undefined)}
>
<WaypointDetail waypoint={popupState.waypoint} myNode={myNode} onEdit={() => {}} />
<WaypointDetail
waypoint={popupState.waypoint}
myNode={myNode}
onEdit={() => {}}
/>
</PopupWrapper>,
);
}
@@ -8,7 +8,13 @@ type StackBadgeProps = {
onClick: (e: { originalEvent: MouseEvent }) => void;
};
export const StackBadge = ({ lng, lat, count, isVisible = true, onClick }: StackBadgeProps) => {
export const StackBadge = ({
lng,
lat,
count,
isVisible = true,
onClick,
}: StackBadgeProps) => {
return (
<Marker
longitude={lng}
@@ -16,7 +16,14 @@ import {
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { useNavigate } from "@tanstack/react-router";
import { Dot, LockIcon, LockOpenIcon, MessageSquareIcon, MountainSnow, Star } from "lucide-react";
import {
Dot,
LockIcon,
LockOpenIcon,
MessageSquareIcon,
MountainSnow,
Star,
} from "lucide-react";
import { useTranslation } from "react-i18next";
export interface NodeDetailProps {
@@ -38,9 +45,16 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
: rawHardwareType.replaceAll("_", " ")
: `${hwModel}`;
// SNR is in dB; derive a 0–100% quality heuristic (-10 dB → 0%, +10 dB → 100%).
const snrQuality = Math.min(Math.max(Math.round((node.snr + 10) * 5), 0), 100);
const snrQuality = Math.min(
Math.max(Math.round((node.snr + 10) * 5), 0),
100,
);
const snrTone =
snrQuality >= 67 ? "text-green-600" : snrQuality >= 34 ? "text-yellow-600" : "text-red-600";
snrQuality >= 67
? "text-green-600"
: snrQuality >= 34
? "text-yellow-600"
: "text-red-600";
function handleDirectMessage() {
navigate({ to: `/messages/direct/${node.num}` });
}
@@ -101,7 +115,9 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
fill={node.isFavorite ? "black" : "none"}
size={15}
aria-label={
node.isFavorite ? t("nodeDetail.favorite.label") : t("nodeDetail.notFavorite.label")
node.isFavorite
? t("nodeDetail.favorite.label")
: t("nodeDetail.notFavorite.label")
}
/>
</div>
@@ -122,12 +138,15 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div
className="flex gap-1"
title={new Date(node.lastHeard * 1000).toLocaleString(navigator.language)}
title={new Date(node.lastHeard * 1000).toLocaleString(
navigator.language,
)}
>
<div>
{node.lastHeard > 0 && (
<div>
{t("nodeDetail.status.heard")} <TimeAgo timestamp={node.lastHeard * 1000} />
{t("nodeDetail.status.heard")}{" "}
<TimeAgo timestamp={node.lastHeard * 1000} />
</div>
)}
</div>
@@ -145,9 +164,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div className="flex mt-2 text-sm">
<div className="flex items-center grow">
<div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1">
{Number.isNaN(node.hopsAway) ? t("unit.hopsAway.unknown") : node.hopsAway}
{Number.isNaN(node.hopsAway)
? t("unit.hopsAway.unknown")
: node.hopsAway}
</div>
<div>
{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}
</div>
<div>{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}</div>
</div>
{node.position?.altitude && (
<div className="flex items-center grow">
@@ -170,13 +193,17 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
{!!node.deviceMetrics?.channelUtilization && (
<div className="grow">
<div>{t("channelUtilization.short")}</div>
<Mono>{node.deviceMetrics?.channelUtilization.toPrecision(3)}%</Mono>
<Mono>
{node.deviceMetrics?.channelUtilization.toPrecision(3)}%
</Mono>
</div>
)}
{!!node.deviceMetrics?.airUtilTx && (
<div className="grow">
<div>{t("airtimeUtilization.short")}</div>
<Mono className="text-gray-500">{node.deviceMetrics?.airUtilTx.toPrecision(3)}%</Mono>
<Mono className="text-gray-500">
{node.deviceMetrics?.airUtilTx.toPrecision(3)}%
</Mono>
</div>
)}
</div>
@@ -2,7 +2,12 @@ import { TimeAgo } from "@components/generic/TimeAgo";
import { Separator } from "@components/UI/Separator.tsx";
import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import type { WaypointWithMetadata } from "@core/stores";
import { bearingDegrees, distanceMeters, hasPos, toLngLat } from "@core/utils/geo";
import {
bearingDegrees,
distanceMeters,
hasPos,
toLngLat,
} from "@core/utils/geo";
import type { Protobuf } from "@meshtastic/sdk";
import {
ClockFadingIcon,
@@ -83,10 +88,14 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<div className="flex flex-wrap items-start gap-x-3">
<dt className="inline-flex items-center gap-2 text-slate-500 min-w-0">
<ClockPlusIcon size={14} aria-hidden />
<span className="truncate">{t("waypointDetail.createdDate")}</span>
<span className="truncate">
{t("waypointDetail.createdDate")}
</span>
</dt>
<dd className="ms-auto text-right">
<time dateTime={new Date(waypoint.metadata.created).toISOString()}>
<time
dateTime={new Date(waypoint.metadata.created).toISOString()}
>
<TimeAgo timestamp={waypoint.metadata.created} />
</time>
</dd>
@@ -100,7 +109,9 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<span className="truncate">{t("waypointDetail.updated")}</span>
</dt>
<dd className="ms-auto text-right">
<time dateTime={new Date(waypoint.metadata.updated).toISOString()}>
<time
dateTime={new Date(waypoint.metadata.updated).toISOString()}
>
<TimeAgo timestamp={waypoint.metadata.updated} />
</time>
</dd>
@@ -132,7 +143,9 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<dd className="ms-auto text-right">
<data value={Math.round(distance)}>
{Math.round(distance)}{" "}
{distance === 1 ? t("unit.meter.one") : t("unit.meter.plural")}
{distance === 1
? t("unit.meter.one")
: t("unit.meter.plural")}
</data>
</dd>
</div>
@@ -1,6 +1,10 @@
import type { HeatmapMode } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx";
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import { Popover, PopoverContent, PopoverTrigger } from "@components/UI/Popover.tsx";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@components/UI/Popover.tsx";
import { cn } from "@core/utils/cn.ts";
import { LayersIcon } from "lucide-react";
import { type ReactNode, useMemo } from "react";
@@ -40,7 +44,12 @@ interface CheckboxProps {
className?: string;
}
const CheckboxItem = ({ label, checked, onChange, className }: CheckboxProps) => {
const CheckboxItem = ({
label,
checked,
onChange,
className,
}: CheckboxProps) => {
return (
<Checkbox
checked={checked}
@@ -140,7 +149,9 @@ export function MapLayerTool({
<CheckboxItem
label={label}
checked={visibilityState[key as keyof VisibilityState]}
onChange={() => handleCheckboxChange(key as keyof VisibilityState)}
onChange={() =>
handleCheckboxChange(key as keyof VisibilityState)
}
/>
{key === "heatmap" && visibilityState.heatmap && (
<div className="pl-6 pt-2 flex flex-col gap-1">
@@ -151,7 +162,9 @@ export function MapLayerTool({
checked={heatmapMode === "density"}
onChange={() => setHeatmapMode("density")}
/>
<span className="text-sm dark:text-slate-300">{t("layerTool.density")}</span>
<span className="text-sm dark:text-slate-300">
{t("layerTool.density")}
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
@@ -68,7 +68,9 @@ const DateDelimiter = ({ label }: { label: string }) => (
<li aria-label={label}>
<div className="my-2 flex h-3 items-center justify-center">
<Separator className="bg-slate-100 dark:bg-slate-800" />
<div className="mx-5 whitespace-nowrap text-center text-xs text-slate-400">{label}</div>
<div className="mx-5 whitespace-nowrap text-center text-xs text-slate-400">
{label}
</div>
<Separator className="bg-slate-100 dark:bg-slate-800" />
</div>
</li>
@@ -106,7 +108,9 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => {
const { i18n, t } = useTranslation();
const locale = useMemo(
() => i18n.language || (typeof navigator !== "undefined" ? navigator.language : "en-US"),
() =>
i18n.language ||
(typeof navigator !== "undefined" ? navigator.language : "en-US"),
[i18n.language],
);
@@ -14,7 +14,10 @@ interface MessageActionsMenuProps {
onReply?: () => void;
}
export const MessageActionsMenu = ({ onAddReaction, onReply }: MessageActionsMenuProps) => {
export const MessageActionsMenu = ({
onAddReaction,
onReply,
}: MessageActionsMenuProps) => {
const { t } = useTranslation();
const hoverIconBarClass = cn(
"absolute top-2 right-2",
@@ -1,11 +1,23 @@
import type { ConversationKey } from "@meshtastic/sdk";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MessageInput, type MessageInputProps } from "./MessageInput.tsx";
vi.mock("@components/UI/Button.tsx", () => ({
Button: vi.fn(({ type, className, children, onClick, onSubmit, ...rest }) => (
<button type={type} className={className} onClick={onClick} onSubmit={onSubmit} {...rest}>
<button
type={type}
className={className}
onClick={onClick}
onSubmit={onSubmit}
{...rest}
>
{children}
</button>
)),
@@ -127,7 +139,9 @@ describe("MessageInput", () => {
expect(mockOnSend).toHaveBeenCalledTimes(1);
expect(mockOnSend).toHaveBeenCalledWith(testMessage);
expect((inputElement as HTMLInputElement).value).toBe("");
expect(screen.getByTestId("byte-counter")).toHaveTextContent(`0/${defaultProps.maxBytes}`);
expect(screen.getByTestId("byte-counter")).toHaveTextContent(
`0/${defaultProps.maxBytes}`,
);
expect(mockClear).toHaveBeenCalledTimes(1);
});
});
@@ -193,11 +207,13 @@ describe("MessageInput", () => {
mockDraftText = "Broadcast draft";
renderComponent({ conversation: broadcastConv });
expect((screen.getByTestId("message-input-field") as HTMLInputElement).value).toBe(
"Broadcast draft",
);
expect(
(screen.getByTestId("message-input-field") as HTMLInputElement).value,
).toBe("Broadcast draft");
const inputElement = screen.getByTestId("message-input-field") as HTMLInputElement;
const inputElement = screen.getByTestId(
"message-input-field",
) as HTMLInputElement;
const formElement = screen.getByRole("form");
const newMessage = "New broadcast msg";
@@ -12,14 +12,20 @@ export interface MessageInputProps {
maxBytes: number;
}
export const MessageInput = ({ onSend, conversation, maxBytes }: MessageInputProps) => {
export const MessageInput = ({
onSend,
conversation,
maxBytes,
}: MessageInputProps) => {
const { text: persistedDraft, setText, clear } = useDraft(conversation);
const { t } = useTranslation("messages");
const calculateBytes = (value: string) => new Blob([value]).size;
const [localDraft, setLocalDraft] = useState(persistedDraft);
const [messageBytes, setMessageBytes] = useState(() => calculateBytes(persistedDraft));
const [messageBytes, setMessageBytes] = useState(() =>
calculateBytes(persistedDraft),
);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
@@ -6,7 +6,10 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import { useMyNodeAsProto, useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import {
useMyNodeAsProto,
useNodeAsProto,
} from "@core/hooks/useNodesAsProto.ts";
import { useAppStore, useDevice } from "@core/stores";
import { type Message, MessageState } from "@core/stores/messageStore";
import { cn } from "@core/utils/cn.ts";
@@ -151,7 +154,8 @@ export const MessageItem = ({ message }: MessageItemProps) => {
const longName = messageUser?.user?.longName;
const derivedShortName = messageUser?.user?.shortName || fallbackName;
const derivedDisplayName = longName || derivedShortName;
const isFavorite = messageUser?.num !== myNodeNum && messageUser?.isFavorite;
const isFavorite =
messageUser?.num !== myNodeNum && messageUser?.isFavorite;
return {
displayName: derivedDisplayName,
shortName: derivedShortName,
@@ -163,7 +167,10 @@ export const MessageItem = ({ message }: MessageItemProps) => {
const messageStatusInfo = getMessageStatusInfo(message.state);
const StatusIconComponent = messageStatusInfo.icon;
const messageDate = useMemo(() => (message.date ? new Date(message.date) : null), [message.date]);
const messageDate = useMemo(
() => (message.date ? new Date(message.date) : null),
[message.date],
);
const locale = i18n.language;
const formattedTime = useMemo(
@@ -200,7 +207,12 @@ export const MessageItem = ({ message }: MessageItemProps) => {
return (
<li className={messageItemWrapperClass}>
<div className="grid grid-cols-[auto_1fr] gap-x-2">
<Avatar size="sm" nodeNum={nodeNum} className="pt-0.5" showFavorite={isFavorite} />
<Avatar
size="sm"
nodeNum={nodeNum}
className="pt-0.5"
showFavorite={isFavorite}
/>
<div className="flex flex-col gap-0.5 min-w-0">
<div className="flex items-center gap-1.5">
@@ -208,7 +220,10 @@ export const MessageItem = ({ message }: MessageItemProps) => {
{displayName}
</span>
{messageDate && (
<time dateTime={messageDate.toISOString()} className={dateTextStyle}>
<time
dateTime={messageDate.toISOString()}
className={dateTextStyle}
>
<span aria-hidden="true">{formattedTime}</span>
<span className="sr-only">{fullDateTime}</span>
</time>
@@ -217,7 +232,10 @@ export const MessageItem = ({ message }: MessageItemProps) => {
<StatusTooltip statusInfo={messageStatusInfo}>
<span aria-label={messageStatusInfo.ariaLabel} role="img">
<StatusIconComponent
className={cn("size-4 shrink-0", messageStatusInfo.iconClassName)}
className={cn(
"size-4 shrink-0",
messageStatusInfo.iconClassName,
)}
aria-hidden="true"
/>
</span>
@@ -68,7 +68,14 @@ describe("TraceRoute", () => {
});
it("renders the route to destination with SNR values", () => {
render(<TraceRoute from={fromUser} to={toUser} route={[1, 2]} snrTowards={[10, 20, 30]} />);
render(
<TraceRoute
from={fromUser}
to={toUser}
route={[1, 2]}
snrTowards={[10, 20, 30]}
/>,
);
expect(screen.getByText("Source Node")).toBeInTheDocument();
expect(screen.getByText("Destination Node")).toBeInTheDocument();
@@ -3,7 +3,10 @@ import {
type AmbientLightingValidation,
AmbientLightingValidationSchema,
} from "@app/validation/moduleConfig/ambientLighting.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,13 +17,19 @@ interface AmbientLightingModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig },
value: {} as {
ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig;
},
peek: () =>
({}) as { ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig },
({}) as {
ambientLighting?: Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfig;
},
subscribe: () => () => {},
} as const;
export const AmbientLighting = ({ onFormInit }: AmbientLightingModuleConfigProps) => {
export const AmbientLighting = ({
onFormInit,
}: AmbientLightingModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
@@ -1,6 +1,12 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type AudioValidation, AudioValidationSchema } from "@app/validation/moduleConfig/audio.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
type AudioValidation,
AudioValidationSchema,
} from "@app/validation/moduleConfig/audio.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -12,7 +18,8 @@ interface AudioModuleConfigProps {
const EMPTY_MODULES_SIGNAL = {
value: {} as { audio?: Protobuf.ModuleConfig.ModuleConfig_AudioConfig },
peek: () => ({}) as { audio?: Protobuf.ModuleConfig.ModuleConfig_AudioConfig },
peek: () =>
({}) as { audio?: Protobuf.ModuleConfig.ModuleConfig_AudioConfig },
subscribe: () => () => {},
} as const;
@@ -68,7 +75,8 @@ export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
label: t("audio.bitrate.label"),
description: t("audio.bitrate.description"),
properties: {
enumValue: Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud,
enumValue:
Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud,
},
},
{
@@ -3,7 +3,10 @@ import {
type CannedMessageValidation,
CannedMessageValidationSchema,
} from "@app/validation/moduleConfig/cannedMessage.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,12 +17,19 @@ interface CannedMessageModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig },
peek: () => ({}) as { cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig },
value: {} as {
cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig;
},
peek: () =>
({}) as {
cannedMessage?: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig;
},
subscribe: () => () => {},
} as const;
export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) => {
export const CannedMessage = ({
onFormInit,
}: CannedMessageModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
@@ -89,7 +99,9 @@ export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) =>
label: t("cannedMessage.inputbrokerEventPress.label"),
description: t("cannedMessage.inputbrokerEventPress.description"),
properties: {
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -98,7 +110,9 @@ export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) =>
label: t("cannedMessage.inputbrokerEventCw.label"),
description: t("cannedMessage.inputbrokerEventCw.description"),
properties: {
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -107,7 +121,9 @@ export const CannedMessage = ({ onFormInit }: CannedMessageModuleConfigProps) =>
label: t("cannedMessage.inputbrokerEventCcw.label"),
description: t("cannedMessage.inputbrokerEventCcw.description"),
properties: {
enumValue: Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -3,7 +3,10 @@ import {
type DetectionSensorValidation,
DetectionSensorValidationSchema,
} from "@app/validation/moduleConfig/detectionSensor.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,13 +17,19 @@ interface DetectionSensorModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig },
value: {} as {
detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig;
},
peek: () =>
({}) as { detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig },
({}) as {
detectionSensor?: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig;
},
subscribe: () => () => {},
} as const;
export const DetectionSensor = ({ onFormInit }: DetectionSensorModuleConfigProps) => {
export const DetectionSensor = ({
onFormInit,
}: DetectionSensorModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
@@ -64,7 +73,9 @@ export const DetectionSensor = ({ onFormInit }: DetectionSensorModuleConfigProps
type: "number",
name: "minimumBroadcastSecs",
label: t("detectionSensor.minimumBroadcastSecs.label"),
description: t("detectionSensor.minimumBroadcastSecs.description"),
description: t(
"detectionSensor.minimumBroadcastSecs.description",
),
properties: { suffix: t("unit.second.plural") },
},
{
@@ -96,9 +107,13 @@ export const DetectionSensor = ({ onFormInit }: DetectionSensorModuleConfigProps
type: "select",
name: "detectionTriggerType",
label: t("detectionSensor.detectionTriggerType.label"),
description: t("detectionSensor.detectionTriggerType.description"),
description: t(
"detectionSensor.detectionTriggerType.description",
),
properties: {
enumValue: Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfig_TriggerType,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_DetectionSensorConfig_TriggerType,
formatEnumName: true,
},
},
@@ -3,7 +3,10 @@ import {
type ExternalNotificationValidation,
ExternalNotificationValidationSchema,
} from "@app/validation/moduleConfig/externalNotification.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -24,7 +27,9 @@ const EMPTY_MODULES_SIGNAL = {
subscribe: () => () => {},
} as const;
export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleConfigProps) => {
export const ExternalNotification = ({
onFormInit,
}: ExternalNotificationModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "externalNotification" });
const { moduleConfig, getEffectiveModuleConfig } = useDevice();
@@ -56,7 +61,9 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
fieldGroups={[
{
label: t("externalNotification.externalNotificationConfig.label"),
description: t("externalNotification.externalNotificationConfig.description"),
description: t(
"externalNotification.externalNotificationConfig.description",
),
fields: [
{
type: "toggle",
@@ -68,7 +75,9 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
},
{
label: t("externalNotification.notificationsOnMessage.label"),
description: t("externalNotification.notificationsOnMessage.description"),
description: t(
"externalNotification.notificationsOnMessage.description",
),
fields: [
{
type: "toggle",
@@ -80,19 +89,25 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
type: "toggle",
name: "alertMessageBuzzer",
label: t("externalNotification.alertMessageBuzzer.label"),
description: t("externalNotification.alertMessageBuzzer.description"),
description: t(
"externalNotification.alertMessageBuzzer.description",
),
},
{
type: "toggle",
name: "alertMessageVibra",
label: t("externalNotification.alertMessageVibra.label"),
description: t("externalNotification.alertMessageVibra.description"),
description: t(
"externalNotification.alertMessageVibra.description",
),
},
],
},
{
label: t("externalNotification.notificationsOnAlert.label"),
description: t("externalNotification.notificationsOnAlert.description"),
description: t(
"externalNotification.notificationsOnAlert.description",
),
fields: [
{
type: "toggle",
@@ -104,7 +119,9 @@ export const ExternalNotification = ({ onFormInit }: ExternalNotificationModuleC
type: "toggle",
name: "alertBellBuzzer",
label: t("externalNotification.alertBellBuzzer.label"),
description: t("externalNotification.alertBellBuzzer.description"),
description: t(
"externalNotification.alertBellBuzzer.description",
),
},
{
type: "toggle",
@@ -1,7 +1,13 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type MqttValidation, MqttValidationSchema } from "@app/validation/moduleConfig/mqtt.ts";
import {
type MqttValidation,
MqttValidationSchema,
} from "@app/validation/moduleConfig/mqtt.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -17,7 +23,9 @@ const EMPTY_MODULES_SIGNAL = {
subscribe: () => () => {},
} as const;
const populateDefaults = (cfg: Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined) =>
const populateDefaults = (
cfg: Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined,
) =>
cfg
? {
...cfg,
@@ -38,7 +46,9 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
const effectiveMqtt =
modules.mqtt ??
(getEffectiveModuleConfig("mqtt") as Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined);
(getEffectiveModuleConfig("mqtt") as
| Protobuf.ModuleConfig.ModuleConfig_MQTTConfig
| undefined);
const { t } = useTranslation("moduleConfig");
@@ -60,20 +70,37 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
const positionPrecisionOptions =
config.display?.units === 0
? {
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km23")]: 10,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km12")]: 11,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km5_8")]: 12,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km2_9")]: 13,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km1_5")]: 14,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m700")]: 15,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km23")]:
10,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km12")]:
11,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km5_8")]:
12,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km2_9")]:
13,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_km1_5")]:
14,
[t("mqtt.mapReportSettings.positionPrecision.options.metric_m700")]:
15,
}
: {
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi15")]: 10,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3")]: 11,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi3_6")]: 12,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi1_8")]: 13,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_9")]: 14,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_5")]: 15,
[t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi15")]:
10,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3",
)]: 11,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi3_6",
)]: 12,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi1_8",
)]: 13,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_9",
)]: 14,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_5",
)]: 15,
};
return (
@@ -167,14 +194,21 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
type: "toggle",
name: "mapReportSettings.shouldReportLocation",
label: t("mqtt.mapReportSettings.shouldReportLocation.label"),
description: t("mqtt.mapReportSettings.shouldReportLocation.description"),
disabledBy: [{ fieldName: "enabled" }, { fieldName: "mapReportingEnabled" }],
description: t(
"mqtt.mapReportSettings.shouldReportLocation.description",
),
disabledBy: [
{ fieldName: "enabled" },
{ fieldName: "mapReportingEnabled" },
],
},
{
type: "select",
name: "mapReportSettings.positionPrecision",
label: t("mqtt.mapReportSettings.positionPrecision.label"),
description: t("mqtt.mapReportSettings.positionPrecision.description"),
description: t(
"mqtt.mapReportSettings.positionPrecision.description",
),
properties: { enumValue: positionPrecisionOptions },
disabledBy: [
{ fieldName: "enabled" },
@@ -186,7 +220,9 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
type: "number",
name: "mapReportSettings.publishIntervalSecs",
label: t("mqtt.mapReportSettings.publishIntervalSecs.label"),
description: t("mqtt.mapReportSettings.publishIntervalSecs.description"),
description: t(
"mqtt.mapReportSettings.publishIntervalSecs.description",
),
properties: { suffix: t("unit.second.plural") },
disabledBy: [
{ fieldName: "enabled" },
@@ -3,7 +3,10 @@ import {
type NeighborInfoValidation,
NeighborInfoValidationSchema,
} from "@app/validation/moduleConfig/neighborInfo.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,8 +17,13 @@ interface NeighborInfoModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig },
peek: () => ({}) as { neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig },
value: {} as {
neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig;
},
peek: () =>
({}) as {
neighborInfo?: Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfig;
},
subscribe: () => () => {},
} as const;
@@ -3,7 +3,10 @@ import {
type PaxcounterValidation,
PaxcounterValidationSchema,
} from "@app/validation/moduleConfig/paxcounter.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,8 +17,13 @@ interface PaxcounterModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig },
peek: () => ({}) as { paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig },
value: {} as {
paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig;
},
peek: () =>
({}) as {
paxcounter?: Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfig;
},
subscribe: () => () => {},
} as const;
@@ -3,7 +3,10 @@ import {
type RangeTestValidation,
RangeTestValidationSchema,
} from "@app/validation/moduleConfig/rangeTest.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,8 +17,11 @@ interface RangeTestModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig },
peek: () => ({}) as { rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig },
value: {} as {
rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig;
},
peek: () =>
({}) as { rangeTest?: Protobuf.ModuleConfig.ModuleConfig_RangeTestConfig },
subscribe: () => () => {},
} as const;
@@ -3,7 +3,10 @@ import {
type RemoteHardwareValidation,
RemoteHardwareValidationSchema,
} from "@app/validation/moduleConfig/remoteHardware.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -12,10 +15,13 @@ interface RemoteHardwareModuleConfigProps {
onFormInit: DynamicFormFormInit<RemoteHardwareValidation>;
}
export const RemoteHardware = ({ onFormInit }: RemoteHardwareModuleConfigProps) => {
export const RemoteHardware = ({
onFormInit,
}: RemoteHardwareModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "remoteHardware" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: RemoteHardwareValidation) => {
@@ -53,7 +59,9 @@ export const RemoteHardware = ({ onFormInit }: RemoteHardwareModuleConfigProps)
type: "toggle",
name: "allowUndefinedPinAccess",
label: t("remoteHardware.allowUndefinedPinAccess.label"),
description: t("remoteHardware.allowUndefinedPinAccess.description"),
description: t(
"remoteHardware.allowUndefinedPinAccess.description",
),
},
],
},
@@ -3,7 +3,10 @@ import {
type SerialValidation,
SerialValidationSchema,
} from "@app/validation/moduleConfig/serial.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -15,7 +18,8 @@ interface SerialModuleConfigProps {
const EMPTY_MODULES_SIGNAL = {
value: {} as { serial?: Protobuf.ModuleConfig.ModuleConfig_SerialConfig },
peek: () => ({}) as { serial?: Protobuf.ModuleConfig.ModuleConfig_SerialConfig },
peek: () =>
({}) as { serial?: Protobuf.ModuleConfig.ModuleConfig_SerialConfig },
subscribe: () => () => {},
} as const;
@@ -83,7 +87,8 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
label: t("serial.baud.label"),
description: t("serial.baud.description"),
properties: {
enumValue: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud,
enumValue:
Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud,
},
},
{
@@ -99,7 +104,8 @@ export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
label: t("serial.mode.label"),
description: t("serial.mode.description"),
properties: {
enumValue: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode,
enumValue:
Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode,
formatEnumName: true,
},
},
@@ -3,7 +3,10 @@ import {
type StatusMessageValidation,
StatusMessageValidationSchema,
} from "@app/validation/moduleConfig/statusMessage.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -12,10 +15,13 @@ interface StatusMessageModuleConfigProps {
onFormInit: DynamicFormFormInit<StatusMessageValidation>;
}
export const StatusMessage = ({ onFormInit }: StatusMessageModuleConfigProps) => {
export const StatusMessage = ({
onFormInit,
}: StatusMessageModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "statusmessage" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: StatusMessageValidation) => {
@@ -24,7 +30,11 @@ export const StatusMessage = ({ onFormInit }: StatusMessageModuleConfigProps) =>
return;
}
setChange({ type: "moduleConfig", variant: "statusmessage" }, data, moduleConfig.statusmessage);
setChange(
{ type: "moduleConfig", variant: "statusmessage" },
data,
moduleConfig.statusmessage,
);
};
return (
@@ -3,7 +3,10 @@ import {
type StoreForwardValidation,
StoreForwardValidationSchema,
} from "@app/validation/moduleConfig/storeForward.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,8 +17,13 @@ interface StoreForwardModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig },
peek: () => ({}) as { storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig },
value: {} as {
storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig;
},
peek: () =>
({}) as {
storeForward?: Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig;
},
subscribe: () => () => {},
} as const;
@@ -1,6 +1,12 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type TakValidation, TakValidationSchema } from "@app/validation/moduleConfig/tak.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
type TakValidation,
TakValidationSchema,
} from "@app/validation/moduleConfig/tak.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
@@ -13,7 +19,8 @@ interface TakModuleConfigProps {
export const Tak = ({ onFormInit }: TakModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "tak" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: TakValidation) => {
@@ -3,7 +3,10 @@ import {
type TelemetryValidation,
TelemetryValidationSchema,
} from "@app/validation/moduleConfig/telemetry.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -14,8 +17,11 @@ interface TelemetryModuleConfigProps {
}
const EMPTY_MODULES_SIGNAL = {
value: {} as { telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig },
peek: () => ({}) as { telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig },
value: {} as {
telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig;
},
peek: () =>
({}) as { telemetry?: Protobuf.ModuleConfig.ModuleConfig_TelemetryConfig },
subscribe: () => () => {},
} as const;
@@ -56,7 +62,12 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
// device_telemetry_enabled is only writable on firmware ≥ v2.7.12. Hide
// the toggle on older firmware so we don't push a value the device will
// ignore.
const canToggleTelemetry = firmwareAtLeast(metadata.get(0)?.firmwareVersion, 2, 7, 12);
const canToggleTelemetry = firmwareAtLeast(
metadata.get(0)?.firmwareVersion,
2,
7,
12,
);
const { t } = useTranslation("moduleConfig");
@@ -86,7 +97,9 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
type: "toggle" as const,
name: "deviceTelemetryEnabled" as const,
label: t("telemetry.deviceTelemetryEnabled.label"),
description: t("telemetry.deviceTelemetryEnabled.description"),
description: t(
"telemetry.deviceTelemetryEnabled.description",
),
},
]
: []),
@@ -101,7 +114,9 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
type: "toggle",
name: "environmentMeasurementEnabled",
label: t("telemetry.environmentMeasurementEnabled.label"),
description: t("telemetry.environmentMeasurementEnabled.description"),
description: t(
"telemetry.environmentMeasurementEnabled.description",
),
},
{
type: "number",
@@ -120,7 +135,9 @@ export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
type: "toggle",
name: "environmentDisplayFahrenheit",
label: t("telemetry.environmentDisplayFahrenheit.label"),
description: t("telemetry.environmentDisplayFahrenheit.description"),
description: t(
"telemetry.environmentDisplayFahrenheit.description",
),
},
{
type: "toggle",
@@ -3,7 +3,10 @@ import {
type TrafficManagementValidation,
TrafficManagementValidationSchema,
} from "@app/validation/moduleConfig/trafficManagement.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
@@ -12,10 +15,13 @@ interface TrafficManagementModuleConfigProps {
onFormInit: DynamicFormFormInit<TrafficManagementValidation>;
}
export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigProps) => {
export const TrafficManagement = ({
onFormInit,
}: TrafficManagementModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "trafficManagement" });
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = useDevice();
const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } =
useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: TrafficManagementValidation) => {
@@ -53,19 +59,25 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
type: "toggle",
name: "positionDedupEnabled",
label: t("trafficManagement.positionDedupEnabled.label"),
description: t("trafficManagement.positionDedupEnabled.description"),
description: t(
"trafficManagement.positionDedupEnabled.description",
),
},
{
type: "number",
name: "positionPrecisionBits",
label: t("trafficManagement.positionPrecisionBits.label"),
description: t("trafficManagement.positionPrecisionBits.description"),
description: t(
"trafficManagement.positionPrecisionBits.description",
),
},
{
type: "number",
name: "positionMinIntervalSecs",
label: t("trafficManagement.positionMinIntervalSecs.label"),
description: t("trafficManagement.positionMinIntervalSecs.description"),
description: t(
"trafficManagement.positionMinIntervalSecs.description",
),
properties: {
suffix: t("unit.second.plural"),
},
@@ -74,13 +86,17 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
type: "toggle",
name: "nodeinfoDirectResponse",
label: t("trafficManagement.nodeinfoDirectResponse.label"),
description: t("trafficManagement.nodeinfoDirectResponse.description"),
description: t(
"trafficManagement.nodeinfoDirectResponse.description",
),
},
{
type: "number",
name: "nodeinfoDirectResponseMaxHops",
label: t("trafficManagement.nodeinfoDirectResponseMaxHops.label"),
description: t("trafficManagement.nodeinfoDirectResponseMaxHops.description"),
description: t(
"trafficManagement.nodeinfoDirectResponseMaxHops.description",
),
},
{
type: "toggle",
@@ -92,7 +108,9 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
type: "number",
name: "rateLimitWindowSecs",
label: t("trafficManagement.rateLimitWindowSecs.label"),
description: t("trafficManagement.rateLimitWindowSecs.description"),
description: t(
"trafficManagement.rateLimitWindowSecs.description",
),
properties: {
suffix: t("unit.second.plural"),
},
@@ -101,37 +119,49 @@ export const TrafficManagement = ({ onFormInit }: TrafficManagementModuleConfigP
type: "number",
name: "rateLimitMaxPackets",
label: t("trafficManagement.rateLimitMaxPackets.label"),
description: t("trafficManagement.rateLimitMaxPackets.description"),
description: t(
"trafficManagement.rateLimitMaxPackets.description",
),
},
{
type: "toggle",
name: "dropUnknownEnabled",
label: t("trafficManagement.dropUnknownEnabled.label"),
description: t("trafficManagement.dropUnknownEnabled.description"),
description: t(
"trafficManagement.dropUnknownEnabled.description",
),
},
{
type: "number",
name: "unknownPacketThreshold",
label: t("trafficManagement.unknownPacketThreshold.label"),
description: t("trafficManagement.unknownPacketThreshold.description"),
description: t(
"trafficManagement.unknownPacketThreshold.description",
),
},
{
type: "toggle",
name: "exhaustHopTelemetry",
label: t("trafficManagement.exhaustHopTelemetry.label"),
description: t("trafficManagement.exhaustHopTelemetry.description"),
description: t(
"trafficManagement.exhaustHopTelemetry.description",
),
},
{
type: "toggle",
name: "exhaustHopPosition",
label: t("trafficManagement.exhaustHopPosition.label"),
description: t("trafficManagement.exhaustHopPosition.description"),
description: t(
"trafficManagement.exhaustHopPosition.description",
),
},
{
type: "toggle",
name: "routerPreserveHops",
label: t("trafficManagement.routerPreserveHops.label"),
description: t("trafficManagement.routerPreserveHops.description"),
description: t(
"trafficManagement.routerPreserveHops.description",
),
},
],
},
@@ -3,7 +3,10 @@ import {
type BluetoothValidation,
BluetoothValidationSchema,
} from "@app/validation/config/bluetooth.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -27,13 +30,18 @@ export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
const effective =
radio.bluetooth ??
(getEffectiveConfig("bluetooth") as Protobuf.Config.Config_BluetoothConfig | undefined);
(getEffectiveConfig("bluetooth") as
| Protobuf.Config.Config_BluetoothConfig
| undefined);
const { t } = useTranslation("config");
const onSubmit = (data: BluetoothValidation) => {
if (!editor) return;
editor.setRadioSection("bluetooth", data as unknown as Protobuf.Config.Config_BluetoothConfig);
editor.setRadioSection(
"bluetooth",
data as unknown as Protobuf.Config.Config_BluetoothConfig,
);
};
return (
@@ -1,7 +1,13 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type DeviceValidation, DeviceValidationSchema } from "@app/validation/config/device.ts";
import {
type DeviceValidation,
DeviceValidationSchema,
} from "@app/validation/config/device.ts";
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -25,14 +31,19 @@ export const Device = ({ onFormInit }: DeviceConfigProps) => {
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
const effective =
radio.device ??
(getEffectiveConfig("device") as Protobuf.Config.Config_DeviceConfig | undefined);
(getEffectiveConfig("device") as
| Protobuf.Config.Config_DeviceConfig
| undefined);
const { t } = useTranslation("config");
const { validateRoleSelection } = useUnsafeRolesDialog();
const onSubmit = (data: DeviceValidation) => {
if (!editor) return;
editor.setRadioSection("device", data as unknown as Protobuf.Config.Config_DeviceConfig);
editor.setRadioSection(
"device",
data as unknown as Protobuf.Config.Config_DeviceConfig,
);
};
return (
@@ -1,6 +1,12 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type DisplayValidation, DisplayValidationSchema } from "@app/validation/config/display.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
type DisplayValidation,
DisplayValidationSchema,
} from "@app/validation/config/display.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -23,13 +29,18 @@ export const Display = ({ onFormInit }: DisplayConfigProps) => {
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
const effective =
radio.display ??
(getEffectiveConfig("display") as Protobuf.Config.Config_DisplayConfig | undefined);
(getEffectiveConfig("display") as
| Protobuf.Config.Config_DisplayConfig
| undefined);
const { t } = useTranslation("config");
const onSubmit = (data: DisplayValidation) => {
if (!editor) return;
editor.setRadioSection("display", data as unknown as Protobuf.Config.Config_DisplayConfig);
editor.setRadioSection(
"display",
data as unknown as Protobuf.Config.Config_DisplayConfig,
);
};
return (
@@ -129,7 +140,8 @@ export const Display = ({ onFormInit }: DisplayConfigProps) => {
label: t("display.compassOrientation.label"),
description: t("display.compassOrientation.description"),
properties: {
enumValue: Protobuf.Config.Config_DisplayConfig_CompassOrientation,
enumValue:
Protobuf.Config.Config_DisplayConfig_CompassOrientation,
formatEnumName: true,
},
},
@@ -1,6 +1,12 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type LoRaValidation, LoRaValidationSchema } from "@app/validation/config/lora.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
type LoRaValidation,
LoRaValidationSchema,
} from "@app/validation/config/lora.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -24,13 +30,19 @@ export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
const effectiveLora =
radio.lora ?? (getEffectiveConfig("lora") as Protobuf.Config.Config_LoRaConfig | undefined);
radio.lora ??
(getEffectiveConfig("lora") as
| Protobuf.Config.Config_LoRaConfig
| undefined);
const { t } = useTranslation("config");
const onSubmit = (data: LoRaValidation) => {
if (!editor) return;
editor.setRadioSection("lora", data as unknown as Protobuf.Config.Config_LoRaConfig);
editor.setRadioSection(
"lora",
data as unknown as Protobuf.Config.Config_LoRaConfig,
);
};
return (
@@ -1,9 +1,18 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type NetworkValidation, NetworkValidationSchema } from "@app/validation/config/network.ts";
import {
type NetworkValidation,
NetworkValidationSchema,
} from "@app/validation/config/network.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { convertIntToIpAddress, convertIpAddressToInt } from "@core/utils/ip.ts";
import {
convertIntToIpAddress,
convertIpAddressToInt,
} from "@core/utils/ip.ts";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
import { useTranslation } from "react-i18next";
@@ -18,7 +27,9 @@ const EMPTY_RADIO_SIGNAL = {
subscribe: () => () => {},
} as const;
const toFormShape = (cfg: Protobuf.Config.Config_NetworkConfig | undefined) => ({
const toFormShape = (
cfg: Protobuf.Config.Config_NetworkConfig | undefined,
) => ({
...cfg,
ipv4Config: {
ip: convertIntToIpAddress(cfg?.ipv4Config?.ip ?? 0),
@@ -27,7 +38,8 @@ const toFormShape = (cfg: Protobuf.Config.Config_NetworkConfig | undefined) => (
dns: convertIntToIpAddress(cfg?.ipv4Config?.dns ?? 0),
},
enabledProtocols:
cfg?.enabledProtocols ?? Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
cfg?.enabledProtocols ??
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
});
export const Network = ({ onFormInit }: NetworkConfigProps) => {
@@ -38,7 +50,9 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
const effective =
radio.network ??
(getEffectiveConfig("network") as Protobuf.Config.Config_NetworkConfig | undefined);
(getEffectiveConfig("network") as
| Protobuf.Config.Config_NetworkConfig
| undefined);
const { t } = useTranslation("config");
@@ -46,14 +60,20 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
if (!editor) return;
const payload = {
...data,
ipv4Config: create(Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, {
ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
}),
ipv4Config: create(
Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema,
{
ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
},
),
};
editor.setRadioSection("network", payload as unknown as Protobuf.Config.Config_NetworkConfig);
editor.setRadioSection(
"network",
payload as unknown as Protobuf.Config.Config_NetworkConfig,
);
};
return (
@@ -144,7 +164,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -156,7 +177,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -168,7 +190,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -180,7 +203,8 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
disabledBy: [
{
fieldName: "addressMode",
selector: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
selector:
Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
},
],
},
@@ -4,9 +4,15 @@ import {
PositionValidationSchema,
} from "@app/validation/config/position.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { Button } from "@components/UI/Button.tsx";
import { type FlagName, usePositionFlags } from "@core/hooks/usePositionFlags.ts";
import {
type FlagName,
usePositionFlags,
} from "@core/hooks/usePositionFlags.ts";
import { useMyNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores";
@@ -41,16 +47,28 @@ function UseBrowserLocationButton() {
navigator.geolocation.getCurrentPosition(
(pos) => {
setBusy(false);
setValue("latitude", Number(pos.coords.latitude.toFixed(7)), { shouldDirty: true });
setValue("longitude", Number(pos.coords.longitude.toFixed(7)), { shouldDirty: true });
if (pos.coords.altitude !== null && !Number.isNaN(pos.coords.altitude)) {
setValue("altitude", Math.round(pos.coords.altitude), { shouldDirty: true });
setValue("latitude", Number(pos.coords.latitude.toFixed(7)), {
shouldDirty: true,
});
setValue("longitude", Number(pos.coords.longitude.toFixed(7)), {
shouldDirty: true,
});
if (
pos.coords.altitude !== null &&
!Number.isNaN(pos.coords.altitude)
) {
setValue("altitude", Math.round(pos.coords.altitude), {
shouldDirty: true,
});
}
},
(err) => {
setBusy(false);
toast({
title: t("position.useBrowserLocation.failed", "Could not read browser location"),
title: t(
"position.useBrowserLocation.failed",
"Could not read browser location",
),
description: err.message,
});
},
@@ -84,7 +102,9 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
const effectivePosition =
radio.position ??
(getEffectiveConfig("position") as Protobuf.Config.Config_PositionConfig | undefined);
(getEffectiveConfig("position") as
| Protobuf.Config.Config_PositionConfig
| undefined);
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
effectivePosition?.positionFlags ?? 0,
@@ -98,14 +118,23 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
return {
...config.position,
...effectivePosition,
latitude: currentPosition?.latitudeI ? currentPosition.latitudeI / 1e7 : undefined,
longitude: currentPosition?.longitudeI ? currentPosition.longitudeI / 1e7 : undefined,
latitude: currentPosition?.latitudeI
? currentPosition.latitudeI / 1e7
: undefined,
longitude: currentPosition?.longitudeI
? currentPosition.longitudeI / 1e7
: undefined,
altitude: currentPosition?.altitude ?? 0,
} as PositionValidation;
}, [config.position, effectivePosition, currentPosition]);
const onSubmit = (data: PositionValidation) => {
const { latitude: _latitude, longitude: _longitude, altitude: _altitude, ...configData } = data;
const {
latitude: _latitude,
longitude: _longitude,
altitude: _altitude,
...configData
} = data;
const payload = { ...configData, positionFlags: flagsValue };
if (editor) {
@@ -115,7 +144,11 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
);
}
if (data.fixedPosition && data.latitude !== undefined && data.longitude !== undefined) {
if (
data.fixedPosition &&
data.latitude !== undefined &&
data.longitude !== undefined
) {
const message = create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: "setFixedPosition",
@@ -225,14 +258,16 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
label: t("position.fixedPosition.altitude.label"),
description: t("position.fixedPosition.altitude.description", {
unit:
displayUnits === Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
displayUnits ===
Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
? "Feet"
: "Meters",
}),
properties: {
step: 0.0000001,
suffix:
displayUnits === Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
displayUnits ===
Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL
? "Feet"
: "Meters",
},
@@ -266,7 +301,8 @@ export const Position = ({ onFormInit }: PositionConfigProps) => {
type: "multiSelect",
name: "positionFlags",
value: activeFlags,
isChecked: (name: string) => activeFlags?.includes(name as FlagName) ?? false,
isChecked: (name: string) =>
activeFlags?.includes(name as FlagName) ?? false,
onValueChange: onPositonFlagChange,
label: t("position.positionFlags.label"),
placeholder: t("position.flags.placeholder"),
@@ -1,6 +1,12 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import { type PowerValidation, PowerValidationSchema } from "@app/validation/config/power.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
type PowerValidation,
PowerValidationSchema,
} from "@app/validation/config/power.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -23,13 +29,19 @@ export const Power = ({ onFormInit }: PowerConfigProps) => {
const editor = useConfigEditor();
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
const effective =
radio.power ?? (getEffectiveConfig("power") as Protobuf.Config.Config_PowerConfig | undefined);
radio.power ??
(getEffectiveConfig("power") as
| Protobuf.Config.Config_PowerConfig
| undefined);
const { t } = useTranslation("config");
const onSubmit = (data: PowerValidation) => {
if (!editor) return;
editor.setRadioSection("power", data as unknown as Protobuf.Config.Config_PowerConfig);
editor.setRadioSection(
"power",
data as unknown as Protobuf.Config.Config_PowerConfig,
);
};
return (
@@ -7,7 +7,10 @@ import {
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
import { Protobuf } from "@meshtastic/sdk";
@@ -27,7 +30,9 @@ const EMPTY_RADIO_SIGNAL = {
subscribe: () => () => {},
} as const;
const toFormShape = (cfg: Protobuf.Config.Config_SecurityConfig | undefined) => ({
const toFormShape = (
cfg: Protobuf.Config.Config_SecurityConfig | undefined,
) => ({
...cfg,
privateKey: fromByteArray(cfg?.privateKey ?? new Uint8Array(0)),
publicKey: fromByteArray(cfg?.publicKey ?? new Uint8Array(0)),
@@ -46,7 +51,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
const radio = useSignal(editor?.radio ?? EMPTY_RADIO_SIGNAL);
const effective =
radio.security ??
(getEffectiveConfig("security") as Protobuf.Config.Config_SecurityConfig | undefined);
(getEffectiveConfig("security") as
| Protobuf.Config.Config_SecurityConfig
| undefined);
const { t } = useTranslation("config");
@@ -64,8 +71,10 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
onFormInit?.(formMethods);
}, [onFormInit, formMethods]);
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = useState<boolean>(false);
const [managedModeDialogOpen, setManagedModeDialogOpen] = useState<boolean>(false);
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] =
useState<boolean>(false);
const [managedModeDialogOpen, setManagedModeDialogOpen] =
useState<boolean>(false);
const onSubmit = (data: RawSecurity) => {
if (!formState.isReady || !editor) return;
@@ -80,7 +89,10 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
toByteArray(data.adminKey.at(2) ?? ""),
],
};
editor.setRadioSection("security", payload as unknown as Protobuf.Config.Config_SecurityConfig);
editor.setRadioSection(
"security",
payload as unknown as Protobuf.Config.Config_SecurityConfig,
);
};
const pkiRegenerate = () => {
@@ -90,7 +102,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
const updatePublicKey = async (privateKey: string) => {
try {
const publicKey = fromByteArray(getX25519PublicKey(toByteArray(privateKey)));
const publicKey = fromByteArray(
getX25519PublicKey(toByteArray(privateKey)),
);
setValue("privateKey", privateKey, { shouldDirty: true });
setValue("publicKey", publicKey, { shouldDirty: true });
@@ -176,7 +190,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
bits,
devicePSKBitCount: 32,
actionButtons: [],
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
properties: { showCopyButton: true },
},
{
@@ -188,7 +204,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
bits,
devicePSKBitCount: 32,
actionButtons: [],
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
properties: { showCopyButton: true },
},
{
@@ -200,7 +218,9 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
bits,
devicePSKBitCount: 32,
actionButtons: [],
disabledBy: [{ fieldName: "adminChannelEnabled", invert: true }],
disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
properties: { showCopyButton: true },
},
],
@@ -1,6 +1,12 @@
import { type UserValidation, UserValidationSchema } from "@app/validation/config/user.ts";
import {
type UserValidation,
UserValidationSchema,
} from "@app/validation/config/user.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useMyNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import { Protobuf } from "@meshtastic/sdk";
import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
@@ -55,7 +61,9 @@ export const User = ({ onFormInit }: UserConfigProps) => {
editor.setOwner(merged);
};
const hwName = Protobuf.Mesh.HardwareModel[baselineUser.hwModel] ?? String(baselineUser.hwModel);
const hwName =
Protobuf.Mesh.HardwareModel[baselineUser.hwModel] ??
String(baselineUser.hwModel);
return (
<DynamicForm<UserValidation>