WIP updates

This commit is contained in:
Sacha Weatherstone
2022-08-09 08:33:54 +10:00
parent ae5ff4f9de
commit ed9ee28745
13 changed files with 239 additions and 264 deletions

View File

@@ -24,7 +24,7 @@
"@emeraldpay/hashicon-react": "^0.5.2",
"@hookform/resolvers": "^2.9.7",
"@meshtastic/eslint-config": "^1.0.8",
"@meshtastic/meshtasticjs": "^0.6.84",
"@meshtastic/meshtasticjs": "^0.6.87",
"base64-js": "^1.5.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
@@ -49,7 +49,7 @@
"@types/chrome": "^0.0.193",
"@types/geodesy": "^2.2.3",
"@types/node": "^18.6.4",
"@types/react": "^18.0.15",
"@types/react": "^18.0.16",
"@types/react-dom": "^18.0.6",
"@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.15",

42
pnpm-lock.yaml generated
View File

@@ -5,11 +5,11 @@ specifiers:
'@emeraldpay/hashicon-react': ^0.5.2
'@hookform/resolvers': ^2.9.7
'@meshtastic/eslint-config': ^1.0.8
'@meshtastic/meshtasticjs': ^0.6.84
'@meshtastic/meshtasticjs': ^0.6.87
'@types/chrome': ^0.0.193
'@types/geodesy': ^2.2.3
'@types/node': ^18.6.4
'@types/react': ^18.0.15
'@types/react': ^18.0.16
'@types/react-dom': ^18.0.6
'@types/w3c-web-serial': ^1.0.2
'@types/web-bluetooth': ^0.0.15
@@ -47,7 +47,7 @@ dependencies:
'@emeraldpay/hashicon-react': 0.5.2
'@hookform/resolvers': 2.9.7_react-hook-form@7.34.0
'@meshtastic/eslint-config': 1.0.8
'@meshtastic/meshtasticjs': 0.6.84
'@meshtastic/meshtasticjs': 0.6.87
base64-js: 1.5.1
class-transformer: 0.5.1
class-validator: 0.13.2
@@ -72,7 +72,7 @@ devDependencies:
'@types/chrome': 0.0.193
'@types/geodesy': 2.2.3
'@types/node': 18.6.4
'@types/react': 18.0.15
'@types/react': 18.0.16
'@types/react-dom': 18.0.6
'@types/w3c-web-serial': 1.0.2
'@types/web-bluetooth': 0.0.15
@@ -584,8 +584,8 @@ packages:
- supports-color
dev: false
/@meshtastic/meshtasticjs/0.6.84:
resolution: {integrity: sha512-ydylXdjfTzxLh1sapDpSlkpkFR2izq9bOLP8KH4eV/4bmogk+Uc/bizLR6qJ0jQyUVcl2wFJcnOHyYwKz0jCzA==}
/@meshtastic/meshtasticjs/0.6.87:
resolution: {integrity: sha512-vehcqwhHMlUm9C/UnhmmhwVZzBjFTkYSrerwbAzJrvLC4E6tRIHCgeyPYtJZaqJoZ90gE+PD+haH2sc86mLgZQ==}
dependencies:
'@meshtastic/eslint-config': 1.0.8
'@protobuf-ts/runtime': 2.7.0
@@ -836,25 +836,25 @@ packages:
/@types/react-dom/18.0.6:
resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==}
dependencies:
'@types/react': 18.0.15
'@types/react': 18.0.16
dev: true
/@types/react-transition-group/4.4.5:
resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==}
dependencies:
'@types/react': 18.0.15
'@types/react': 18.0.16
dev: false
/@types/react/16.14.29:
resolution: {integrity: sha512-I5IwEaefGZbpmmK1J7zHwZe3JkGxcRkc5WJUDWmNySVVovueViRTEUWV7spTvpe96l3nbKD/K6+GxoN69CYb/w==}
/@types/react/16.14.30:
resolution: {integrity: sha512-tG+xGtDDSuIl1l63mN0LnaROAc99knkYyN4YTheE80iPzYvSy0U8LVie+OBZkrgjVrpkQV6bMCkSphPBnVNk6g==}
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2
csstype: 3.1.0
dev: false
/@types/react/18.0.15:
resolution: {integrity: sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==}
/@types/react/18.0.16:
resolution: {integrity: sha512-3vX1dzVucqc2nhXtzyaParTIIRZeNbisRqLE7QdeLomVybEyeiuAouzZXgz71P+2kbJOqj3dy0fzoATg2I06GQ==}
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2
@@ -1421,7 +1421,7 @@ packages:
isarray: 2.0.5
object-is: 1.1.5
object-keys: 1.1.1
object.assign: 4.1.2
object.assign: 4.1.3
regexp.prototype.flags: 1.4.3
side-channel: 1.0.4
which-boxed-primitive: 1.0.2
@@ -1556,7 +1556,7 @@ packages:
is-weakref: 1.0.2
object-inspect: 1.12.2
object-keys: 1.1.1
object.assign: 4.1.2
object.assign: 4.1.3
regexp.prototype.flags: 1.4.3
string.prototype.trimend: 1.0.5
string.prototype.trimstart: 1.0.5
@@ -2131,7 +2131,7 @@ packages:
dependencies:
'@babel/runtime': 7.18.9
'@segment/react-tiny-virtual-list': 2.2.1_react@18.2.0
'@types/react': 16.14.29
'@types/react': 16.14.30
'@types/react-transition-group': 4.4.5
arrify: 1.0.1
classnames: 2.3.1
@@ -2768,7 +2768,7 @@ packages:
engines: {node: '>=4.0'}
dependencies:
array-includes: 3.1.5
object.assign: 4.1.2
object.assign: 4.1.3
dev: false
/levn/0.4.1:
@@ -3009,8 +3009,8 @@ packages:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
/object.assign/4.1.2:
resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==}
/object.assign/4.1.3:
resolution: {integrity: sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
@@ -3218,8 +3218,8 @@ packages:
node-modules-regexp: 1.0.0
dev: true
/postcss/8.4.14:
resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==}
/postcss/8.4.16:
resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.4
@@ -4055,7 +4055,7 @@ packages:
optional: true
dependencies:
esbuild: 0.14.53
postcss: 8.4.14
postcss: 8.4.16
resolve: 1.22.1
rollup: 2.77.2
optionalDependencies:

View File

@@ -1,8 +1,8 @@
import type React from "react";
import { useEffect, useState } from "react";
import { SelectField, TextInputField } from "evergreen-ui";
import { useForm } from "react-hook-form";
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
import { Controller, useForm } from "react-hook-form";
import { DisplayValidation } from "@app/validation/config/display.js";
import { Form } from "@components/form/Form";
@@ -19,6 +19,7 @@ export const Display = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<Protobuf.Config_DisplayConfig>({
defaultValues: config.display,
resolver: classValidatorResolver(DisplayValidation),
@@ -67,6 +68,20 @@ export const Display = (): JSX.Element => {
>
{renderOptions(Protobuf.Config_DisplayConfig_GpsCoordinateFormat)}
</SelectField>
<FormField
label="Compass North Top"
description="Description"
isInvalid={!!errors.compassNorthTop?.message}
validationMessage={errors.compassNorthTop?.message}
>
<Controller
name="compassNorthTop"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form>
);
};

View File

@@ -1,94 +0,0 @@
import type React from "react";
import { useEffect, useState } from "react";
import { FormField, Switch, TextInputField, toaster } from "evergreen-ui";
import { Controller, useForm } from "react-hook-form";
import { WiFiValidation } from "@app/validation/config/wifi.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const WiFi = (): JSX.Element => {
const { config, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
control,
reset,
} = useForm<WiFiValidation>({
defaultValues: config.wifi,
resolver: classValidatorResolver(WiFiValidation),
});
useEffect(() => {
reset(config.wifi);
}, [reset, config.wifi]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "wifi",
wifi: data,
},
},
async () => {
toaster.success("Your source is now sending data");
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<TextInputField
label="SSID"
description="This is a description."
isInvalid={!!errors.ssid?.message}
validationMessage={errors.ssid?.message}
{...register("ssid", { valueAsNumber: true })}
/>
<TextInputField
label="PSK"
description="This is a description."
type="password"
isInvalid={!!errors.psk?.message}
validationMessage={errors.psk?.message}
{...register("psk", { valueAsNumber: true })}
/>
<FormField
label="Enable WiFi AP"
description="Description"
isInvalid={!!errors.apMode?.message}
validationMessage={errors.apMode?.message}
>
<Controller
name="apMode"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<FormField
label="Don't broadcast SSID"
description="Description"
isInvalid={!!errors.apHidden?.message}
validationMessage={errors.apHidden?.message}
>
<Controller
name="apHidden"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form>
);
};

View File

@@ -1,13 +1,21 @@
import type React from "react";
import { useEffect, useState } from "react";
import { FormField, Switch, TextInputField, toaster } from "evergreen-ui";
import { Controller, useForm } from "react-hook-form";
import {
FormField,
SelectField,
Switch,
TextInputField,
toaster,
} from "evergreen-ui";
import { Controller, useForm, useWatch } from "react-hook-form";
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
import { WiFiValidation } from "@app/validation/config/wifi.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const WiFi = (): JSX.Element => {
const { config, connection } = useDevice();
@@ -23,6 +31,12 @@ export const WiFi = (): JSX.Element => {
resolver: classValidatorResolver(WiFiValidation),
});
const wifiEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
});
useEffect(() => {
reset(config.wifi);
}, [reset, config.wifi]);
@@ -46,6 +60,27 @@ export const WiFi = (): JSX.Element => {
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<FormField
label="WiFi Enabled"
description="Description"
isInvalid={!!errors.enabled?.message}
validationMessage={errors.enabled?.message}
>
<Controller
name="enabled"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<SelectField
label="WiFi Mode"
description="This is a description."
{...register("mode", { valueAsNumber: true })}
>
{renderOptions(Protobuf.Config_WiFiConfig_WiFiMode)}
</SelectField>
<TextInputField
label="SSID"
description="This is a description."
@@ -53,7 +88,6 @@ export const WiFi = (): JSX.Element => {
validationMessage={errors.ssid?.message}
{...register("ssid")}
/>
<TextInputField
label="PSK"
type="password"
@@ -62,35 +96,6 @@ export const WiFi = (): JSX.Element => {
validationMessage={errors.psk?.message}
{...register("psk")}
/>
<FormField
label="Enable WiFi AP"
description="Description"
isInvalid={!!errors.apMode?.message}
validationMessage={errors.apMode?.message}
>
<Controller
name="apMode"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<FormField
label="Don't broadcast SSID"
description="Description"
isInvalid={!!errors.apHidden?.message}
validationMessage={errors.apHidden?.message}
>
<Controller
name="apHidden"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form>
);
};

View File

@@ -82,6 +82,7 @@ export const Sidebar = (): JSX.Element => {
{navLinks.map((Link) => (
<Tab
key={Link.name}
userSelect="none"
gap={majorScale(2)}
disabled={Link.disabled}
direction="vertical"
@@ -95,6 +96,7 @@ export const Sidebar = (): JSX.Element => {
</Tab>
))}
<Tab
userSelect="none"
gap={5}
direction="vertical"
isSelected={PeersDialogOpen}

View File

@@ -14,7 +14,7 @@ export type Page =
| "info";
export interface MessageWithAck {
message: Types.TextPacket;
message: Types.MessagePacket;
ack: boolean;
received: Date;
}
@@ -26,8 +26,8 @@ export interface Channel {
}
export interface Node {
deviceMetrics: Protobuf.DeviceMetrics;
environmentMetrics: Protobuf.EnvironmentMetrics;
deviceMetrics: Protobuf.DeviceMetrics[];
environmentMetrics: Protobuf.EnvironmentMetrics[];
data: Protobuf.NodeInfo;
}
@@ -40,8 +40,9 @@ export interface Device {
hardware: Protobuf.MyNodeInfo;
nodes: Node[];
connection?: IConnection;
activeChat: number;
activePage: Page;
peerInfoOpen: boolean;
activePeer: number;
setReady(ready: boolean): void;
setStatus: (status: Types.DeviceStatusEnum) => void;
@@ -50,8 +51,9 @@ export interface Device {
setModuleConfig: (config: Protobuf.ModuleConfig) => void;
setHardware: (hardware: Protobuf.MyNodeInfo) => void;
setMetrics: (metrics: Types.TelemetryPacket) => void;
setActiveChat: (chatIndex: number) => void;
setActivePage: (page: Page) => void;
setPeerInfoOpen: (open: boolean) => void;
setActivePeer: (peer: number) => void;
addNodeInfo: (nodeInfo: Types.NodeInfoPacket) => void;
addUser: (user: Types.UserPacket) => void;
addPosition: (position: Types.PositionPacket) => void;
@@ -85,8 +87,9 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
hardware: Protobuf.MyNodeInfo.create(),
nodes: [],
connection: undefined,
activeChat: 0,
activePage: "messages",
peerInfoOpen: false,
activePeer: 0,
setReady: (ready: boolean) => {
set(
@@ -219,8 +222,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
snr: metrics.packet.rxSnr,
lastHeard: new Date().getSeconds(),
}),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
deviceMetrics: [],
environmentMetrics: [],
};
device.nodes.push(node);
@@ -228,27 +231,20 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
if (node) {
switch (metrics.data.variant.oneofKind) {
case "deviceMetrics":
node.deviceMetrics = metrics.data.variant.deviceMetrics;
node.deviceMetrics.push(
metrics.data.variant.deviceMetrics
);
break;
case "environmentMetrics":
node.environmentMetrics =
metrics.data.variant.environmentMetrics;
node.environmentMetrics.push(
metrics.data.variant.environmentMetrics
);
break;
}
}
})
);
},
setActiveChat: (chatIndex) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.activeChat = chatIndex;
}
})
);
},
setActivePage: (page) => {
set(
produce<DeviceState>((draft) => {
@@ -277,14 +273,34 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
} else {
device.nodes.push({
data: Protobuf.NodeInfo.create(nodeInfo.data),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
deviceMetrics: [],
environmentMetrics: [],
});
}
}
})
);
},
setPeerInfoOpen: (open) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.peerInfoOpen = open;
}
})
);
},
setActivePeer: (peer) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.activePeer = peer;
}
})
);
},
addUser: (user) => {
set(
produce<DeviceState>((draft) => {
@@ -307,8 +323,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
snr: user.packet.rxSnr,
user: user.data,
}),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
deviceMetrics: [],
environmentMetrics: [],
});
}
}
@@ -336,8 +352,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
num: position.packet.from,
position: position.data,
}),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
deviceMetrics: [],
environmentMetrics: [],
});
}
}
@@ -369,6 +385,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
);
},
ackMessage: (channelIndex: number, messageId: number) => {
console.log("ack called");
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);

View File

@@ -12,14 +12,14 @@ export const subscribeAll = (device: Device, connection: IConnection) => {
// onLogEvent
// onMeshHeartbeat
// onRoutingPacket
// onTelemetryPacket
connection.onRoutingPacket.subscribe((routingPacket) => {
console.log(routingPacket);
});
connection.onTelemetryPacket.subscribe((telemetryPacket) => {
console.log(telemetryPacket.data.variant);
device.setMetrics(telemetryPacket);
});
@@ -63,12 +63,12 @@ export const subscribeAll = (device: Device, connection: IConnection) => {
device.setModuleConfig(moduleConfig.data);
});
connection.onTextPacket.subscribe((message) => {
connection.onMessagePacket.subscribe((messagePacket) => {
device.addMessage({
message: message,
ack: message.packet.from !== device.hardware.myNodeNum,
received: message.packet.rxTime
? new Date(message.packet.rxTime * 1000)
message: messagePacket,
ack: messagePacket.packet.from !== device.hardware.myNodeNum,
received: messagePacket.packet.rxTime
? new Date(messagePacket.packet.rxTime * 1000)
: new Date(),
});
});

View File

@@ -1,6 +1,7 @@
import type React from "react";
import { Pane } from "evergreen-ui";
import JSONPretty from "react-json-pretty";
import { useDevice } from "@app/core/stores/deviceStore.js";
@@ -11,8 +12,8 @@ export const InfoPage = (): JSX.Element => {
return (
<Pane>
{JSON.stringify(myNode)}
{JSON.stringify(hardware)}
<JSONPretty data={myNode} />
<JSONPretty data={hardware} />
</Pane>
);
};

View File

@@ -0,0 +1,33 @@
import type React from "react";
import { LocateIcon, majorScale, minorScale, Pane, Text } from "evergreen-ui";
import { toMGRS } from "@app/core/utils/toMGRS.js";
import type { Protobuf } from "@meshtastic/meshtasticjs";
export interface LocationMessageProps {
location: Protobuf.Location;
}
export const LocationMessage = ({
location,
}: LocationMessageProps): JSX.Element => {
return (
<Pane
marginLeft={majorScale(2)}
paddingLeft={majorScale(1)}
borderLeft="3px solid #e6e6e6"
>
<Pane
gap={majorScale(1)}
display="flex"
borderRadius={majorScale(1)}
elevation={1}
padding={minorScale(1)}
>
<LocateIcon color="#474d66" marginY="auto" />
<Text>{toMGRS(location.latitudeI, location.longitudeI)}</Text>
</Pane>
</Pane>
);
};

View File

@@ -1,21 +1,23 @@
import type React from "react";
import {
CircleIcon,
FullCircleIcon,
majorScale,
Pane,
Pulsar,
Small,
Strong,
Text,
TimeIcon,
} from "evergreen-ui";
import { Hashicon } from "@emeraldpay/hashicon-react";
import type { Protobuf } from "@meshtastic/meshtasticjs";
import type { Protobuf, Types } from "@meshtastic/meshtasticjs";
import { LocationMessage } from "./LocationMessage.js";
export interface MessageProps {
lastMsgSameUser: boolean;
message: string;
messagePacket: Types.MessagePacket;
ack: boolean;
rxTime: Date;
sender?: Protobuf.NodeInfo;
@@ -23,78 +25,66 @@ export interface MessageProps {
export const Message = ({
lastMsgSameUser,
message,
messagePacket,
ack,
rxTime,
sender,
}: MessageProps): JSX.Element => {
return (
<Pane marginBottom={majorScale(1)} className="group hover:bg-gray-200">
{lastMsgSameUser ? (
<Pane
marginX={majorScale(2)}
display="flex"
justifyContent="space-between"
marginTop={-majorScale(1)}
className={`${lastMsgSameUser ? "" : "py-1"}`}
>
<Pane
display="flex"
position="relative"
gap={majorScale(1)}
className="gap-2"
>
<Small
marginY="auto"
marginLeft="auto"
width={majorScale(3)}
paddingTop={majorScale(1)}
className="pt-1 text-transparent group-hover:text-gray-500"
>
{rxTime
.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})
.replace("AM", "")
.replace("PM", "")}
</Small>
<Text marginY="auto" className={`${ack ? "" : "animate-pulse"}`}>
{message}
</Text>
<Pulsar />
</Pane>
<Pane
display="flex"
gap={majorScale(1)}
paddingTop={majorScale(1)}
className="text-transparent group-hover:text-gray-500"
>
<TimeIcon />
<Small>25s</Small>
</Pane>
</Pane>
return lastMsgSameUser ? (
<Pane display="flex" marginLeft={majorScale(3)}>
{ack ? (
<FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
) : (
<Pane display="flex" marginX={majorScale(2)} gap={majorScale(1)}>
<Pane marginY="auto" width={majorScale(3)}>
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
</Pane>
<Pane>
<Pane display="flex" gap={majorScale(1)}>
<Strong cursor="default" size={500} className="hover:underline">
{sender?.user?.longName ?? "UNK"}
</Strong>
<Small marginY="auto">
{rxTime.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})}
</Small>
</Pane>
<Text className={`${ack ? "" : "animate-pulse"}`}>{message}</Text>
</Pane>
</Pane>
<CircleIcon color="#9c9fab" marginY="auto" size={8} />
)}
{messagePacket.location ? (
<LocationMessage location={messagePacket.location} />
) : (
<Text
color={ack ? "#474d66" : "#9c9fab"}
marginLeft={majorScale(2)}
paddingLeft={majorScale(1)}
borderLeft="3px solid #e6e6e6"
>
{messagePacket.text}
</Text>
)}
</Pane>
) : (
<Pane marginX={majorScale(2)} gap={majorScale(1)} marginTop={majorScale(1)}>
<Pane display="flex" gap={majorScale(1)}>
<Pane width={majorScale(3)}>
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
</Pane>
<Strong cursor="default" size={500}>
{sender?.user?.longName ?? "UNK"}
</Strong>
<Small>
{rxTime.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})}
</Small>
</Pane>
<Pane display="flex" marginLeft={majorScale(1)}>
{ack ? (
<FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
) : (
<CircleIcon color="#9c9fab" marginY="auto" size={8} />
)}
{messagePacket.location ? (
<LocationMessage location={messagePacket.location} />
) : (
<Text
color={ack ? "#474d66" : "#9c9fab"}
marginLeft={majorScale(2)}
paddingLeft={majorScale(1)}
borderLeft="3px solid #e6e6e6"
>
{messagePacket.text}
</Text>
)}
</Pane>
</Pane>
);
};

View File

@@ -1,4 +1,4 @@
import { IsEnum, IsInt } from "class-validator";
import { IsBoolean, IsEnum, IsInt } from "class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
@@ -11,4 +11,7 @@ export class DisplayValidation implements Protobuf.Config_DisplayConfig {
@IsInt()
autoScreenCarouselSecs: number;
@IsBoolean()
compassNorthTop: boolean;
}

View File

@@ -1,15 +1,17 @@
import { Length } from "class-validator";
import { IsBoolean, IsEnum, Length } from "class-validator";
import type { Protobuf } from "@meshtastic/meshtasticjs";
import { Protobuf } from "@meshtastic/meshtasticjs";
export class WiFiValidation implements Protobuf.Config_WiFiConfig {
@IsBoolean()
enabled: boolean;
@IsEnum(Protobuf.Config_WiFiConfig_WiFiMode)
mode: Protobuf.Config_WiFiConfig_WiFiMode;
@Length(1, 33)
ssid: string;
@Length(8, 64)
psk: string;
apMode: boolean;
apHidden: boolean;
}