refactor: switch to using Bun (#718)

This commit is contained in:
Dan Ditomaso
2025-07-16 22:30:10 -04:00
committed by GitHub
parent acfd7ebd2d
commit 704d06cfe7
401 changed files with 35294 additions and 39107 deletions
@@ -1,5 +1,6 @@
import { makeChannelSchema } from "@app/validation/channel.ts";
import { create } from "@bufbuild/protobuf";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
@@ -8,8 +9,7 @@ import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { infer as zodInfer } from "zod/v4";
import type { infer as zodInfer } from "zod/v4";
export interface SettingsPanelProps {
channel: Protobuf.Channel.Channel;
@@ -20,9 +20,8 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
const { t } = useTranslation(["channels", "ui", "dialog"]);
const { toast } = useToast();
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(
false,
);
const [preSharedDialogOpen, setPreSharedDialogOpen] =
useState<boolean>(false);
const [pass, setPass] = useState<string>(
fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
);
@@ -30,12 +29,9 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
channel?.settings?.psk.length ?? 16,
);
const ChannelValidationSchema = useMemo(
() => {
return makeChannelSchema(byteCount);
},
[byteCount],
);
const ChannelValidationSchema = useMemo(() => {
return makeChannelSchema(byteCount);
}, [byteCount]);
type ChannelValidation = zodInfer<typeof ChannelValidationSchema>;
@@ -52,10 +48,12 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
},
});
connection?.setChannel(channel).then(() => {
console.debug(t("toast.savedChannel.title", {
ns: "ui",
channelName: channel.settings?.name,
}));
console.debug(
t("toast.savedChannel.title", {
ns: "ui",
channelName: channel.settings?.name,
}),
);
toast({
title: t("toast.savedChannel.title", {
ns: "ui",
@@ -108,7 +106,7 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
...channel?.settings?.moduleSettings,
positionPrecision:
channel?.settings?.moduleSettings?.positionPrecision ===
undefined
undefined
? 10
: channel?.settings?.moduleSettings?.positionPrecision,
},
@@ -127,12 +125,13 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
disabled: channel.index === 0,
description: t("role.description"),
properties: {
enumValue: channel.index === 0
? { [t("role.options.primary")]: 1 }
: {
[t("role.options.disabled")]: 0,
[t("role.options.secondary")]: 2,
},
enumValue:
channel.index === 0
? { [t("role.options.primary")]: 1 }
: {
[t("role.options.disabled")]: 0,
[t("role.options.secondary")]: 2,
},
},
},
{
@@ -182,79 +181,36 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
label: t("positionPrecision.label"),
description: t("positionPrecision.description"),
properties: {
enumValue: config.display?.units === 0
? {
[t("positionPrecision.options.none")]: 0,
[
t("positionPrecision.options.metric_km23")
]: 10,
[
t("positionPrecision.options.metric_km12")
]: 11,
[
t("positionPrecision.options.metric_km5_8")
]: 12,
[
t("positionPrecision.options.metric_km2_9")
]: 13,
[
t("positionPrecision.options.metric_km1_5")
]: 14,
[
t("positionPrecision.options.metric_m700")
]: 15,
[
t("positionPrecision.options.metric_m350")
]: 16,
[
t("positionPrecision.options.metric_m200")
]: 17,
[
t("positionPrecision.options.metric_m90")
]: 18,
[
t("positionPrecision.options.metric_m50")
]: 19,
[
t("positionPrecision.options.precise")
]: 32,
}
: {
[t("positionPrecision.options.none")]: 0,
[
t("positionPrecision.options.imperial_mi15")
]: 10,
[
t("positionPrecision.options.imperial_mi7_3")
]: 11,
[
t("positionPrecision.options.imperial_mi3_6")
]: 12,
[
t("positionPrecision.options.imperial_mi1_8")
]: 13,
[
t("positionPrecision.options.imperial_mi0_9")
]: 14,
[
t("positionPrecision.options.imperial_mi0_5")
]: 15,
[
t("positionPrecision.options.imperial_mi0_2")
]: 16,
[
t("positionPrecision.options.imperial_ft600")
]: 17,
[
t("positionPrecision.options.imperial_ft300")
]: 18,
[
t("positionPrecision.options.imperial_ft150")
]: 19,
[
t("positionPrecision.options.precise")
]: 32,
},
enumValue:
config.display?.units === 0
? {
[t("positionPrecision.options.none")]: 0,
[t("positionPrecision.options.metric_km23")]: 10,
[t("positionPrecision.options.metric_km12")]: 11,
[t("positionPrecision.options.metric_km5_8")]: 12,
[t("positionPrecision.options.metric_km2_9")]: 13,
[t("positionPrecision.options.metric_km1_5")]: 14,
[t("positionPrecision.options.metric_m700")]: 15,
[t("positionPrecision.options.metric_m350")]: 16,
[t("positionPrecision.options.metric_m200")]: 17,
[t("positionPrecision.options.metric_m90")]: 18,
[t("positionPrecision.options.metric_m50")]: 19,
[t("positionPrecision.options.precise")]: 32,
}
: {
[t("positionPrecision.options.none")]: 0,
[t("positionPrecision.options.imperial_mi15")]: 10,
[t("positionPrecision.options.imperial_mi7_3")]: 11,
[t("positionPrecision.options.imperial_mi3_6")]: 12,
[t("positionPrecision.options.imperial_mi1_8")]: 13,
[t("positionPrecision.options.imperial_mi0_9")]: 14,
[t("positionPrecision.options.imperial_mi0_5")]: 15,
[t("positionPrecision.options.imperial_mi0_2")]: 16,
[t("positionPrecision.options.imperial_ft600")]: 17,
[t("positionPrecision.options.imperial_ft300")]: 18,
[t("positionPrecision.options.imperial_ft150")]: 19,
[t("positionPrecision.options.precise")]: 32,
},
},
},
],
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type BluetoothValidation,
BluetoothValidationSchema,
@@ -8,14 +9,16 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface BluetoothConfigProps {
onFormInit: DynamicFormFormInit<BluetoothValidation>;
}
export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => {
useWaitForConfig({ configCase: "bluetooth" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
@@ -1,37 +0,0 @@
import {
useDevice,
ValidConfigType,
ValidModuleConfigType,
} from "@core/stores/deviceStore.ts";
import { useEffect, useState } from "react";
export function ConfigSuspender({
configCase,
moduleConfigCase,
children,
}: {
configCase?: ValidConfigType;
moduleConfigCase?: ValidModuleConfigType;
children: React.ReactNode;
}) {
const { config, moduleConfig } = useDevice();
let cfg = undefined;
if (configCase) {
cfg = config[configCase];
} else if (moduleConfigCase) {
cfg = moduleConfig[moduleConfigCase];
} else {
return children;
}
const [ready, setReady] = useState(() => cfg !== undefined);
useEffect(() => {
if (cfg !== undefined) setReady(true);
}, [cfg]);
if (!ready) throw new Promise(() => {}); // triggers suspense fallback
return children;
}
@@ -1,22 +1,25 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type DeviceValidation,
DeviceValidationSchema,
} from "@app/validation/config/device.ts";
import { create } from "@bufbuild/protobuf";
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
interface DeviceConfigProps {
onFormInit: DynamicFormFormInit<DeviceValidation>;
}
export const Device = ({ onFormInit }: DeviceConfigProps) => {
useWaitForConfig({ configCase: "device" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
@@ -113,8 +116,8 @@ export const Device = ({ onFormInit }: DeviceConfigProps) => {
properties: {
fieldLength: {
max: 64,
currentValueLength: getEffectiveConfig("device")?.tzdef
?.length,
currentValueLength:
getEffectiveConfig("device")?.tzdef?.length,
showCharacterCount: true,
},
},
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type DisplayValidation,
DisplayValidationSchema,
@@ -8,14 +9,15 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface DisplayConfigProps {
onFormInit: DynamicFormFormInit<DisplayValidation>;
}
export const Display = ({ onFormInit }: DisplayConfigProps) => {
useWaitForConfig({ configCase: "display" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type LoRaValidation,
LoRaValidationSchema,
@@ -8,14 +9,16 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface LoRaConfigProps {
onFormInit: DynamicFormFormInit<LoRaValidation>;
}
export const LoRa = ({ onFormInit }: LoRaConfigProps) => {
useWaitForConfig({ configCase: "lora" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type NetworkValidation,
NetworkValidationSchema,
@@ -8,18 +9,20 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import {
convertIntToIpAddress,
convertIpAddressToInt,
} from "@core/utils/ip.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface NetworkConfigProps {
onFormInit: DynamicFormFormInit<NetworkValidation>;
}
export const Network = ({ onFormInit }: NetworkConfigProps) => {
useWaitForConfig({ configCase: "network" });
const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
@@ -72,24 +75,28 @@ export const Network = ({ onFormInit }: NetworkConfigProps) => {
),
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
},
enabledProtocols: config.network?.enabledProtocols ??
enabledProtocols:
config.network?.enabledProtocols ??
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
}}
values={{
...networkConfig,
ipv4Config: {
ip: convertIntToIpAddress(networkConfig?.ipv4Config?.ip ?? 0),
gateway: convertIntToIpAddress(
networkConfig?.ipv4Config?.gateway ?? 0,
),
subnet: convertIntToIpAddress(
networkConfig?.ipv4Config?.subnet ?? 0,
),
dns: convertIntToIpAddress(networkConfig?.ipv4Config?.dns ?? 0),
},
enabledProtocols: networkConfig?.enabledProtocols ??
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
} as NetworkValidation}
values={
{
...networkConfig,
ipv4Config: {
ip: convertIntToIpAddress(networkConfig?.ipv4Config?.ip ?? 0),
gateway: convertIntToIpAddress(
networkConfig?.ipv4Config?.gateway ?? 0,
),
subnet: convertIntToIpAddress(
networkConfig?.ipv4Config?.subnet ?? 0,
),
dns: convertIntToIpAddress(networkConfig?.ipv4Config?.dns ?? 0),
},
enabledProtocols:
networkConfig?.enabledProtocols ??
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
} as NetworkValidation
}
fieldGroups={[
{
label: t("network.title"),
@@ -1,7 +1,4 @@
import {
type FlagName,
usePositionFlags,
} from "@core/hooks/usePositionFlags.ts";
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type PositionValidation,
PositionValidationSchema,
@@ -11,16 +8,22 @@ import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import {
type FlagName,
usePositionFlags,
} from "@core/hooks/usePositionFlags.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface PositionConfigProps {
onFormInit: DynamicFormFormInit<PositionValidation>;
}
export const Position = ({ onFormInit }: PositionConfigProps) => {
useWaitForConfig({ configCase: "position" });
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type PowerValidation,
PowerValidationSchema,
@@ -8,14 +9,16 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface PowerConfigProps {
onFormInit: DynamicFormFormInit<PowerValidation>;
}
export const Power = ({ onFormInit }: PowerConfigProps) => {
useWaitForConfig({ configCase: "power" });
const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } =
useDevice();
const { t } = useTranslation("deviceConfig");
@@ -1,30 +1,33 @@
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type ParsedSecurity,
type RawSecurity,
RawSecuritySchema,
} from "@app/validation/config/security.ts";
import { useEffect, useState } from "react";
import { create } from "@bufbuild/protobuf";
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 { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
import { Protobuf } from "@meshtastic/core";
import { fromByteArray, toByteArray } from "base64-js";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { type DefaultValues, useForm } from "react-hook-form";
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { useTranslation } from "react-i18next";
interface SecurityConfigProps {
onFormInit: DynamicFormFormInit<RawSecurity>;
}
export const Security = ({ onFormInit }: SecurityConfigProps) => {
useWaitForConfig({ configCase: "security" });
const {
config,
setWorkingConfig,
@@ -43,19 +46,11 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
privateKey: fromByteArray(
securityConfig?.privateKey ?? new Uint8Array(0),
),
publicKey: fromByteArray(
securityConfig?.publicKey ?? new Uint8Array(0),
),
publicKey: fromByteArray(securityConfig?.publicKey ?? new Uint8Array(0)),
adminKey: [
fromByteArray(
securityConfig?.adminKey?.at(0) ?? new Uint8Array(0),
),
fromByteArray(
securityConfig?.adminKey?.at(1) ?? new Uint8Array(0),
),
fromByteArray(
securityConfig?.adminKey?.at(2) ?? new Uint8Array(0),
),
fromByteArray(securityConfig?.adminKey?.at(0) ?? new Uint8Array(0)),
fromByteArray(securityConfig?.adminKey?.at(1) ?? new Uint8Array(0)),
fromByteArray(securityConfig?.adminKey?.at(2) ?? new Uint8Array(0)),
],
},
};
@@ -73,15 +68,15 @@ 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) return;
if (!formState.isReady) {
return;
}
const payload: ParsedSecurity = {
...data,
@@ -1,19 +1,17 @@
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { Button } from "@components/UI/Button.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { TransportWebBluetooth } from "@meshtastic/transport-web-bluetooth";
import { MeshDevice } from "@meshtastic/core";
import { TransportWebBluetooth } from "@meshtastic/transport-web-bluetooth";
import { useCallback, useEffect, useState } from "react";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { useTranslation } from "react-i18next";
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
export const BLE = (
{ closeDialog }: TabElementProps,
) => {
export const BLE = ({ closeDialog }: TabElementProps) => {
const [connectionInProgress, setConnectionInProgress] = useState(false);
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
const { addDevice } = useDeviceStore();
@@ -77,10 +75,12 @@ export const BLE = (
if (exists === -1) {
setBleDevices(bleDevices.concat(device));
}
}).catch((error: Error) => {
})
.catch((error: Error) => {
console.error("Error requesting device:", error);
setConnectionInProgress(false);
}).finally(() => {
})
.finally(() => {
setConnectionInProgress(false);
});
}}
@@ -1,7 +1,7 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { MeshDevice } from "@meshtastic/core";
import { TransportHTTP } from "@meshtastic/transport-http";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("@core/stores/appStore.ts", () => ({
@@ -35,8 +35,9 @@ describe("HTTP Component", () => {
render(<HTTP closeDialog={vi.fn()} />);
expect(screen.getByText("IP Address/Hostname")).toBeInTheDocument();
expect(screen.getByRole("textbox")).toBeInTheDocument();
expect(screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"))
.toBeInTheDocument();
expect(
screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"),
).toBeInTheDocument();
expect(screen.getByText("Use HTTPS")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Connect" })).toBeInTheDocument();
});
@@ -45,8 +46,9 @@ describe("HTTP Component", () => {
render(<HTTP closeDialog={vi.fn()} />);
const inputField = screen.getByRole("textbox");
fireEvent.change(inputField, { target: { value: "meshtastic.local" } });
expect(screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"))
.toBeInTheDocument();
expect(
screen.getByPlaceholderText("000.000.000.000 / meshtastic.local"),
).toBeInTheDocument();
});
it("toggles HTTPS switch and updates prefix", () => {
@@ -1,19 +1,19 @@
import type { TabElementProps } from "@components/Dialog/NewDeviceDialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Link } from "@components/UI/Typography/Link.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Switch } from "@components/UI/Switch.tsx";
import { Link } from "@components/UI/Typography/Link.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core";
import { TransportHTTP } from "@meshtastic/transport-http";
import { AlertTriangle } from "lucide-react";
import { useState } from "react";
import { useController, useForm } from "react-hook-form";
import { AlertTriangle } from "lucide-react";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { useTranslation } from "react-i18next";
interface FormData {
@@ -21,9 +21,7 @@ interface FormData {
tls: boolean;
}
export const HTTP = (
{ closeDialog }: TabElementProps,
) => {
export const HTTP = ({ closeDialog }: TabElementProps) => {
const { t } = useTranslation("dialog");
const [connectionInProgress, setConnectionInProgress] = useState(false);
const isURLHTTPS = location.protocol === "https:";
@@ -35,11 +33,11 @@ export const HTTP = (
const { control, handleSubmit, register } = useForm<FormData>({
defaultValues: {
ip: ["client.meshtastic.org", "localhost"].includes(
globalThis.location.hostname,
)
globalThis.location.hostname,
)
? "meshtastic.local"
: globalThis.location.host,
tls: isURLHTTPS ? true : false,
tls: !!isURLHTTPS,
},
});
@@ -47,9 +45,10 @@ export const HTTP = (
field: { value: tlsValue, onChange: setTLS },
} = useController({ name: "tls", control });
const [connectionError, setConnectionError] = useState<
{ host: string; secure: boolean } | null
>(null);
const [connectionError, setConnectionError] = useState<{
host: string;
secure: boolean;
} | null>(null);
const onSubmit = handleSubmit(async (data) => {
setConnectionInProgress(true);
@@ -85,9 +84,11 @@ export const HTTP = (
<div>
<Label>{t("newDeviceDialog.httpConnection.label")}</Label>
<Input
prefix={tlsValue
? `${t("newDeviceDialog.https")}://`
: `${t("newDeviceDialog.http")}://`}
prefix={
tlsValue
? `${t("newDeviceDialog.https")}://`
: `${t("newDeviceDialog.http")}://`
}
placeholder={t("newDeviceDialog.httpConnection.placeholder")}
className="text-slate-900 dark:text-slate-100"
{...register("ip")}
@@ -136,9 +137,10 @@ export const HTTP = (
{t("newDeviceDialog.connectionFailedAlert.openLinkSuffix")}
{connectionError.secure
? t(
"newDeviceDialog.connectionFailedAlert.acceptTlsWarningSuffix",
)
: ""}.{" "}
"newDeviceDialog.connectionFailedAlert.acceptTlsWarningSuffix",
)
: ""}
.{" "}
<Link
href="https://meshtastic.org/docs/software/web-client/#http"
className="underline font-medium text-amber-800 dark:text-amber-800"
@@ -151,10 +153,7 @@ export const HTTP = (
</div>
)}
</fieldset>
<Button
type="submit"
variant="default"
>
<Button type="submit" variant="default">
<span>
{connectionInProgress
? t("newDeviceDialog.connecting")
@@ -1,19 +1,17 @@
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { Button } from "@components/UI/Button.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core";
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
export const Serial = (
{ closeDialog }: TabElementProps,
) => {
export const Serial = ({ closeDialog }: TabElementProps) => {
const [connectionInProgress, setConnectionInProgress] = useState(false);
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
const { addDevice } = useDeviceStore();
@@ -54,13 +52,13 @@ export const Serial = (
disabled={connectionInProgress}
>
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
{serialPorts.map((port, index) => {
{serialPorts.map((port) => {
const { usbProductId, usbVendorId } = port.getInfo();
const vendor = usbVendorId ?? t("unknown.shortName");
const product = usbProductId ?? t("unknown.shortName");
return (
<Button
key={`${vendor}-${product}-${index}`}
key={`${vendor}-${product}`}
disabled={port.readable !== null}
variant="default"
onClick={async () => {
@@ -70,7 +68,6 @@ export const Serial = (
}}
>
{t("newDeviceDialog.serialConnection.deviceIdentifier", {
index: index,
vendorId: vendor,
productId: product,
})}
@@ -86,14 +83,18 @@ export const Serial = (
<Button
variant="default"
onClick={async () => {
await navigator.serial.requestPort().then((port) => {
setSerialPorts(serialPorts.concat(port));
// No need to setConnectionInProgress(false) here if requestPort is quick
}).catch((error: Error) => {
console.error("Error requesting port:", error);
}).finally(() => {
setConnectionInProgress(false);
});
await navigator.serial
.requestPort()
.then((port) => {
setSerialPorts(serialPorts.concat(port));
// No need to setConnectionInProgress(false) here if requestPort is quick
})
.catch((error: Error) => {
console.error("Error requesting port:", error);
})
.finally(() => {
setConnectionInProgress(false);
});
}}
>
<span>{t("newDeviceDialog.serialConnection.newDeviceButton")}</span>
@@ -1,12 +1,21 @@
import BatteryStatus from "@components/BatteryStatus.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Avatar } from "@components/UI/Avatar.tsx";
import { Separator } from "@components/UI/Seperator.tsx";
import { Heading } from "@components/UI/Typography/Heading.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { formatQuantity } from "@core/utils/string.ts";
import { Avatar } from "@components/UI/Avatar.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Protobuf } from "@meshtastic/core";
import type { Protobuf as ProtobufType } from "@meshtastic/core";
import { Protobuf } from "@meshtastic/core";
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { useNavigate } from "@tanstack/react-router";
import {
Dot,
LockIcon,
@@ -15,16 +24,7 @@ import {
MountainSnow,
Star,
} from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import BatteryStatus from "@components/BatteryStatus.tsx";
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
export interface NodeDetailProps {
node: ProtobufType.Mesh.NodeInfo;
@@ -60,23 +60,21 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
e.stopPropagation();
}}
>
{node.user?.publicKey && node.user?.publicKey.length > 0
? (
<LockIcon
className="text-green-600 mb-1.5"
size={12}
strokeWidth={3}
aria-label={t("node_detail_public_key_enabled_aria_label")}
/>
)
: (
<LockOpenIcon
className="text-yellow-500 mb-1.5"
size={12}
strokeWidth={3}
aria-label={t("node_detail_no_public_key_aria_label")}
/>
)}
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
<LockIcon
className="text-green-600 mb-1.5"
size={12}
strokeWidth={3}
aria-label={t("node_detail_public_key_enabled_aria_label")}
/>
) : (
<LockOpenIcon
className="text-yellow-500 mb-1.5"
size={12}
strokeWidth={3}
aria-label={t("node_detail_no_public_key_aria_label")}
/>
)}
<TooltipProvider>
<Tooltip>
@@ -105,9 +103,11 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<Star
fill={node.isFavorite ? "black" : "none"}
size={15}
aria-label={node.isFavorite
? t("nodeDetail.favorite.label")
: t("nodeDetail.notFavorite.label")}
aria-label={
node.isFavorite
? t("nodeDetail.favorite.label")
: t("nodeDetail.notFavorite.label")
}
/>
</div>
</div>
@@ -1,6 +1,6 @@
import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx";
import type { Message } from "@core/stores/messageStore/types.ts";
import { InboxIcon } from "lucide-react";
import { Message } from "@core/stores/messageStore/types.ts";
import { useTranslation } from "react-i18next";
export interface ChannelChatProps {
@@ -41,10 +41,7 @@ export const MessageActionsMenu = ({
const iconSizeClass = "size-4";
return (
<div
className={cn(hoverIconBarClass)}
onClick={(e) => e.stopPropagation()}
>
<div className={cn(hoverIconBarClass)}>
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
@@ -1,3 +1,4 @@
import type { Types } from "@meshtastic/core";
import {
act,
fireEvent,
@@ -6,13 +7,10 @@ import {
waitFor,
} from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MessageInput, MessageInputProps } from "./MessageInput.tsx";
import { Types } from "@meshtastic/core";
import { MessageInput, type MessageInputProps } from "./MessageInput.tsx";
vi.mock("@components/UI/Button.tsx", () => ({
Button: vi.fn((
{ type, className, children, onClick, onSubmit, ...rest },
) => (
Button: vi.fn(({ type, className, children, onClick, onSubmit, ...rest }) => (
<button
type={type}
className={className}
@@ -26,11 +24,8 @@ vi.mock("@components/UI/Button.tsx", () => ({
}));
vi.mock("@components/UI/Input.tsx", () => ({
Input: vi.fn((
{ autoFocus, minLength, name, placeholder, value, onChange },
) => (
Input: vi.fn(({ minLength, name, placeholder, value, onChange }) => (
<input
autoFocus={autoFocus}
minLength={minLength}
name={name}
placeholder={placeholder}
@@ -225,16 +220,14 @@ describe("MessageInput", () => {
it("should work with broadcast destination for drafts", () => {
const broadcastDest: Types.Destination = "broadcast";
mockGetDraft.mockImplementation((key) =>
key === broadcastDest ? "Broadcast draft" : ""
key === broadcastDest ? "Broadcast draft" : "",
);
renderComponent({ to: broadcastDest });
expect(mockGetDraft).toHaveBeenCalledWith(broadcastDest);
expect(
(screen.getByTestId(
"message-input-field",
) as HTMLInputElement).value,
(screen.getByTestId("message-input-field") as HTMLInputElement).value,
).toBe("Broadcast draft");
const inputElement = screen.getByTestId(
@@ -1,9 +1,9 @@
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import type { Types } from "@meshtastic/core";
import { SendIcon } from "lucide-react";
import { startTransition, useState } from "react";
import { useMessageStore } from "@core/stores/messageStore/index.ts";
import { useTranslation } from "react-i18next";
export interface MessageInputProps {
@@ -12,11 +12,7 @@ export interface MessageInputProps {
maxBytes: number;
}
export const MessageInput = ({
onSend,
to,
maxBytes,
}: MessageInputProps) => {
export const MessageInput = ({ onSend, to, maxBytes }: MessageInputProps) => {
const { setDraft, getDraft, clearDraft } = useMessageStore();
const { t } = useTranslation("messages");
@@ -25,7 +21,7 @@ export const MessageInput = ({
const initialDraft = getDraft(to);
const [localDraft, setLocalDraft] = useState(initialDraft);
const [messageBytes, setMessageBytes] = useState(() =>
calculateBytes(initialDraft)
calculateBytes(initialDraft),
);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -41,7 +37,9 @@ export const MessageInput = ({
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!localDraft.trim()) return;
if (!localDraft.trim()) {
return;
}
// Reset bytes *before* sending (consider if onSend failure needs different handling)
setMessageBytes(0);
@@ -56,9 +54,8 @@ export const MessageInput = ({
<div className="flex gap-2">
<form className="w-full" name="messageInput" onSubmit={handleSubmit}>
<div className="flex grow gap-1">
<label className="w-full">
<label className="w-full" htmlFor="messageInput">
<Input
autoFocus
minLength={1}
name="messageInput"
placeholder={t("sendMessage.placeholder")}
@@ -70,15 +67,13 @@ export const MessageInput = ({
<label
data-testid="byte-counter"
htmlFor="messageInput"
className="flex items-center w-20 p-1 text-sm place-content-end"
>
{messageBytes}/{maxBytes}
</label>
<Button
type="submit"
variant="default"
>
<Button type="submit" variant="default">
<SendIcon size={16} />
</Button>
</div>
@@ -1,3 +1,4 @@
import { Avatar } from "@components/UI/Avatar.tsx";
import {
Tooltip,
TooltipArrow,
@@ -6,18 +7,18 @@ import {
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { cn } from "@core/utils/cn.ts";
import { Avatar } from "@components/UI/Avatar.tsx";
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { ReactNode, useMemo } from "react";
import {
MessageState,
useMessageStore,
} from "@core/stores/messageStore/index.ts";
import { Protobuf, Types } from "@meshtastic/core";
import { Message } from "@core/stores/messageStore/types.ts";
import type { Message } from "@core/stores/messageStore/types.ts";
import { cn } from "@core/utils/cn.ts";
import { type Protobuf, Types } from "@meshtastic/core";
import type { LucideIcon } from "lucide-react";
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
import { type ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
// import { MessageActionsMenu } from "@components/PageComponents/Messages/MessageActionsMenu.tsx"; // TODO: Uncomment when actions menu is implemented
interface MessageStatusInfo {
@@ -27,12 +28,13 @@ interface MessageStatusInfo {
iconClassName?: string;
}
const StatusTooltip = (
{ statusInfo, children }: {
statusInfo: MessageStatusInfo;
children: ReactNode;
},
) => (
const StatusTooltip = ({
statusInfo,
children,
}: {
statusInfo: MessageStatusInfo;
children: ReactNode;
}) => (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
@@ -77,16 +79,20 @@ export const MessageItem = ({ message }: MessageItemProps) => {
[t],
);
const UNKNOWN_STATUS = useMemo((): MessageStatusInfo => ({
displayText: t("deliveryStatus.unknown.displayText"),
icon: AlertCircle,
ariaLabel: t("deliveryStatus.unknown.label"),
iconClassName: "text-red-500 dark:text-red-400",
}), [t]);
const UNKNOWN_STATUS = useMemo(
(): MessageStatusInfo => ({
displayText: t("deliveryStatus.unknown.displayText"),
icon: AlertCircle,
ariaLabel: t("deliveryStatus.unknown.label"),
iconClassName: "text-red-500 dark:text-red-400",
}),
[t],
);
const getMessageStatusInfo = useMemo(
() => (state: MessageState): MessageStatusInfo =>
MESSAGE_STATUS_MAP[state] ?? UNKNOWN_STATUS,
() =>
(state: MessageState): MessageStatusInfo =>
MESSAGE_STATUS_MAP[state] ?? UNKNOWN_STATUS,
[MESSAGE_STATUS_MAP, UNKNOWN_STATUS],
);
@@ -103,8 +109,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,
@@ -116,7 +122,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
const StatusIconComponent = messageStatusInfo.icon;
const messageDate = useMemo(
() => message.date ? new Date(message.date) : null,
() => (message.date ? new Date(message.date) : null),
[message.date],
);
const locale = i18n.language;
@@ -199,11 +205,9 @@ export const MessageItem = ({ message }: MessageItemProps) => {
</div>
</div>
{/* Actions Menu Placeholder */}
{
/* <div className="absolute top-1 right-1">
{/* <div className="absolute top-1 right-1">
<MessageActionsMenu onReply={() => console.log("Reply")} />
</div> */
}
</div> */}
</li>
);
};
@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { mockDeviceStore } from "@core/stores/deviceStore.mock.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@core/stores/deviceStore");
@@ -18,7 +18,7 @@ describe("TraceRoute", () => {
macaddr: new Uint8Array([0x01, 0x02, 0x03, 0x04]),
id: "source-node",
isLicensed: false,
role: Protobuf.Config.Config_DeviceConfig_Role["CLIENT"],
role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
} as Protobuf.Mesh.NodeInfo["user"],
};
@@ -32,7 +32,7 @@ describe("TraceRoute", () => {
macaddr: new Uint8Array([0x05, 0x06, 0x07, 0x08]),
id: "destination-node",
isLicensed: false,
role: Protobuf.Config.Config_DeviceConfig_Role["CLIENT"],
role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
} as Protobuf.Mesh.NodeInfo["user"],
};
@@ -123,13 +123,7 @@ describe("TraceRoute", () => {
});
it("renders '??' for missing SNR values", () => {
render(
<TraceRoute
from={fromUser}
to={toUser}
route={[1]}
/>,
);
render(<TraceRoute from={fromUser} to={toUser} route={[1]} />);
expect(screen.getByText("Node A")).toBeInTheDocument();
// Check for translated '??' placeholder
@@ -22,9 +22,7 @@ interface RoutePathProps {
snr?: number[];
}
const RoutePath = (
{ title, from, to, path, snr }: RoutePathProps,
) => {
const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => {
const { getNode } = useDevice();
const { t } = useTranslation();
@@ -1,4 +1,4 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type AmbientLightingValidation,
AmbientLightingValidationSchema,
@@ -8,17 +8,19 @@ import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface AmbientLightingModuleConfigProps {
onFormInit: DynamicFormFormInit<AmbientLightingValidation>;
}
export const AmbientLighting = (
{ onFormInit }: AmbientLightingModuleConfigProps,
) => {
export const AmbientLighting = ({
onFormInit,
}: AmbientLightingModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "ambientLighting" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type AudioValidation,
AudioValidationSchema,
@@ -8,15 +9,16 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface AudioModuleConfigProps {
onFormInit: DynamicFormFormInit<AudioValidation>;
}
export const Audio = ({ onFormInit }: AudioModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "audio" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type CannedMessageValidation,
CannedMessageValidationSchema,
@@ -8,17 +9,19 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface CannedMessageModuleConfigProps {
onFormInit: DynamicFormFormInit<CannedMessageValidation>;
}
export const CannedMessage = (
{ onFormInit }: CannedMessageModuleConfigProps,
) => {
export const CannedMessage = ({
onFormInit,
}: CannedMessageModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "cannedMessage" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -92,8 +95,9 @@ export const CannedMessage = (
label: t("cannedMessage.inputbrokerEventCw.label"),
description: t("cannedMessage.inputbrokerEventCw.description"),
properties: {
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -102,8 +106,9 @@ export const CannedMessage = (
label: t("cannedMessage.inputbrokerEventCcw.label"),
description: t("cannedMessage.inputbrokerEventCcw.description"),
properties: {
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -112,8 +117,9 @@ export const CannedMessage = (
label: t("cannedMessage.inputbrokerEventPress.label"),
description: t("cannedMessage.inputbrokerEventPress.description"),
properties: {
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@@ -1,4 +1,4 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type DetectionSensorValidation,
DetectionSensorValidationSchema,
@@ -8,17 +8,20 @@ import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface DetectionSensorModuleConfigProps {
onFormInit: DynamicFormFormInit<DetectionSensorValidation>;
}
export const DetectionSensor = (
{ onFormInit }: DetectionSensorModuleConfigProps,
) => {
export const DetectionSensor = ({
onFormInit,
}: DetectionSensorModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "detectionSensor" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -135,8 +138,9 @@ export const DetectionSensor = (
},
],
properties: {
enumValue: Protobuf.ModuleConfig
.ModuleConfig_DetectionSensorConfig_TriggerType,
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_DetectionSensorConfig_TriggerType,
},
},
{
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type ExternalNotificationValidation,
ExternalNotificationValidationSchema,
@@ -8,17 +9,19 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface ExternalNotificationModuleConfigProps {
onFormInit: DynamicFormFormInit<ExternalNotificationValidation>;
}
export const ExternalNotification = (
{ onFormInit }: ExternalNotificationModuleConfigProps,
) => {
export const ExternalNotification = ({
onFormInit,
}: ExternalNotificationModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "externalNotification" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -1,4 +1,4 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type MqttValidation,
MqttValidationSchema,
@@ -8,15 +8,18 @@ import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface MqttModuleConfigProps {
onFormInit: DynamicFormFormInit<MqttValidation>;
}
export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "mqtt" });
const {
config,
moduleConfig,
@@ -55,10 +58,12 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
) => {
return cfg
? {
...cfg,
mapReportSettings: cfg.mapReportSettings ??
{ publishIntervalSecs: 0, positionPrecision: 10 },
}
...cfg,
mapReportSettings: cfg.mapReportSettings ?? {
publishIntervalSecs: 0,
positionPrecision: 10,
},
}
: undefined;
};
@@ -202,78 +207,77 @@ export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => {
{
type: "select",
name: "mapReportSettings.positionPrecision",
label: t(
"mqtt.mapReportSettings.positionPrecision.label",
),
label: t("mqtt.mapReportSettings.positionPrecision.label"),
description: t(
"mqtt.mapReportSettings.positionPrecision.description",
),
properties: {
enumValue: 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_m350")
]: 16,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m200")
]: 17,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m90")
]: 18,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m50")
]: 19,
}
: {
[
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_mi0_2")
]: 16,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft600")
]: 17,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft300")
]: 18,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft150")
]: 19,
},
enumValue:
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_m350",
)]: 16,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m200",
)]: 17,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m90",
)]: 18,
[t(
"mqtt.mapReportSettings.positionPrecision.options.metric_m50",
)]: 19,
}
: {
[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_mi0_2",
)]: 16,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_ft600",
)]: 17,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_ft300",
)]: 18,
[t(
"mqtt.mapReportSettings.positionPrecision.options.imperial_ft150",
)]: 19,
},
},
disabledBy: [
{
@@ -1,4 +1,4 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type NeighborInfoValidation,
NeighborInfoValidationSchema,
@@ -8,15 +8,18 @@ import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface NeighborInfoModuleConfigProps {
onFormInit: DynamicFormFormInit<NeighborInfoValidation>;
}
export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "neighborInfo" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type PaxcounterValidation,
PaxcounterValidationSchema,
@@ -8,15 +9,17 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface PaxcounterModuleConfigProps {
onFormInit: DynamicFormFormInit<PaxcounterValidation>;
}
export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "paxcounter" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type RangeTestValidation,
RangeTestValidationSchema,
@@ -8,21 +9,24 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface RangeTestModuleConfigProps {
onFormInit: DynamicFormFormInit<RangeTestValidation>;
}
export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "rangeTest" });
const {
moduleConfig,
setWorkingModuleConfig,
getEffectiveModuleConfig,
removeWorkingModuleConfig,
} = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: RangeTestValidation) => {
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type SerialValidation,
SerialValidationSchema,
@@ -8,15 +9,17 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface SerialModuleConfigProps {
onFormInit: DynamicFormFormInit<SerialValidation>;
}
export const Serial = ({ onFormInit }: SerialModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "serial" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type StoreForwardValidation,
StoreForwardValidationSchema,
@@ -8,15 +9,17 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface StoreForwardModuleConfigProps {
onFormInit: DynamicFormFormInit<StoreForwardValidation>;
}
export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "storeForward" });
const {
moduleConfig,
setWorkingModuleConfig,
@@ -1,3 +1,4 @@
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
import {
type TelemetryValidation,
TelemetryValidationSchema,
@@ -8,15 +9,17 @@ import {
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
interface TelemetryModuleConfigProps {
onFormInit: DynamicFormFormInit<TelemetryValidation>;
}
export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => {
useWaitForConfig({ moduleConfigCase: "telemetry" });
const {
moduleConfig,
setWorkingModuleConfig,