refactor: switch to using Bun (#718)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user