Browse Source

Merge branch 'meshtastic:main' into main

pull/720/head
Mictronics 12 months ago
committed by GitHub
parent
commit
7116c5c1f3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      README.md
  2. 24
      bun.lock
  3. 22
      packages/core/src/meshDevice.ts
  4. 10
      packages/core/src/utils/queue.ts
  5. 4
      packages/transport-http/src/transport.ts
  6. 8
      packages/web/package.json
  7. 13
      packages/web/src/components/DeviceInfoPanel.tsx
  8. 4
      packages/web/src/components/Dialog/TracerouteResponseDialog.tsx
  9. 4
      packages/web/src/components/PageComponents/Connect/BLE.tsx
  10. 6
      packages/web/src/components/PageComponents/Connect/Serial.tsx
  11. 2
      packages/web/src/components/PageComponents/Messages/TraceRoute.tsx
  12. 19
      packages/web/src/components/ThemeSwitcher.tsx
  13. 16
      packages/web/src/components/UI/Footer.tsx
  14. 2
      packages/web/src/components/UI/Switch.tsx
  15. 32
      packages/web/src/core/hooks/useToggleVisiblility.ts
  16. 21
      packages/web/src/routes.tsx
  17. 16
      packages/web/tsconfig.json
  18. 1
      packages/web/vite-env.d.ts
  19. 10
      packages/web/vite.config.ts
  20. 19
      tsconfig.base.json
  21. 13
      tsconfig.json

2
README.md

@ -63,7 +63,7 @@ Follow the installation instructions on their home page.
1. **Clone the repository:**
```bash
git clone [https://github.com/meshtastic/meshtastic-web.git](https://github.com/meshtastic/meshtastic-web.git)
git clone https://github.com/meshtastic/meshtastic-web.git
cd meshtastic-web
```
2. **Install dependencies for all packages:**

24
bun.lock

@ -5,6 +5,7 @@
"name": "meshtastic-web",
"dependencies": {
"@bufbuild/protobuf": "^2.6.1",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs",
"ste-simple-events": "^3.0.11",
"tslog": "^4.9.3",
},
@ -18,7 +19,6 @@
"name": "@meshtastic/core",
"version": "2.6.5",
"dependencies": {
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs",
"crc": "npm:crc@^4.3.2",
},
},
@ -28,7 +28,7 @@
},
"packages/transport-http": {
"name": "@meshtastic/transport-http",
"version": "0.2.1",
"version": "0.2.2",
},
"packages/transport-node": {
"name": "@meshtastic/transport-node",
@ -36,14 +36,14 @@
},
"packages/transport-web-bluetooth": {
"name": "@meshtastic/transport-web-bluetooth",
"version": "0.1.2",
"version": "0.1.3",
"devDependencies": {
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20",
},
},
"packages/transport-web-serial": {
"name": "@meshtastic/transport-web-serial",
"version": "0.2.1",
"version": "0.2.2",
"dependencies": {
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7",
},
@ -54,7 +54,7 @@
"dependencies": {
"@bufbuild/protobuf": "^2.6.0",
"@hookform/resolvers": "^5.1.1",
"@meshtastic/core": "npm:@jsr/[email protected]",
"@meshtastic/core": "workspace:*",
"@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http",
"@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth",
"@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial",
@ -296,6 +296,8 @@
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
@ -1296,6 +1298,8 @@
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["[email protected]", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"splaytree-ts": ["[email protected]", "", {}, "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA=="],
"split-string": ["[email protected]", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="],
@ -1336,6 +1340,8 @@
"tar": ["[email protected]", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"terser": ["[email protected]", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg=="],
"testing-library": ["[email protected]", "", { "dependencies": { "tslib": "^1.9.0" }, "peerDependencies": { "@angular/common": "^6.0.0-rc.0 || ^6.0.0", "@angular/core": "^6.0.0-rc.0 || ^6.0.0" } }, "sha512-KCbqCCllbgiCXOgmh9MdsgdJ05pmimXGuggtC78pzpxpq/40A3bS+NJoqwCIqZbNnMr6KIZ2mlMZoZCkWVnaWw=="],
"through2": ["[email protected]", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="],
@ -1696,8 +1702,6 @@
"lru-cache/yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"meshtastic-web/@meshtastic/core": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__core/2.6.4.tgz", { "dependencies": { "@bufbuild/protobuf": "^2.2.3", "@jsr/meshtastic__protobufs": "^2.6.2", "crc": "^4.3.2", "ste-simple-events": "^3.0.11", "tslog": "^4.9.3" } }, "sha512-1Kz5DK6peFxluHOJR38vFwfgeJzMXTz+3p6TvibjILVhSQC2U1nu8aJbn6w5zhRqS+j79OmtrRvdzL6VNsTkkQ=="],
"meshtastic-web/@meshtastic/transport-http": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-http/0.2.1.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.0" } }, "sha512-lmQKr3aIINKvtGROU4HchmSVqbZSbkIHqajowRRC8IAjsnR0zNTyxz210QyY4pFUF9hpcW3GRjwq5h/VO2JuGg=="],
"meshtastic-web/@meshtastic/transport-web-bluetooth": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-web-bluetooth/0.1.2.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.4" } }, "sha512-Z+5pv9RXNgY0/crKExOH3pZ6LT0HIXFmnBL7NX5AO2knOFRn+4lmxQEhhmiTTlkUfqyEfAvbjuY5u4mq9TPTdQ=="],
@ -1724,6 +1728,8 @@
"recast/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"source-map-support/source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"split-string/extend-shallow": ["[email protected]", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
"string_decoder/safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@ -1732,6 +1738,8 @@
"sweepline-intersections/tinyqueue": ["[email protected]", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="],
"terser/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"topojson-client/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"topojson-server/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
@ -1748,8 +1756,6 @@
"geojson-polygon-self-intersections/rbush/quickselect": ["[email protected]", "", {}, "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ=="],
"meshtastic-web/@meshtastic/core/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg=="],
"meshtastic-web/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"peek-stream/through2/readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],

22
packages/core/src/meshDevice.ts

@ -1,7 +1,6 @@
import { Logger } from "tslog";
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
import * as Protobuf from "@meshtastic/protobufs";
import { Logger } from "tslog";
import { Constants } from "./constants.ts";
import type { Destination, PacketMetadata, Transport } from "./types.ts";
@ -40,6 +39,8 @@ export class MeshDevice {
public xModem: Xmodem;
private _heartbeatIntervalId: ReturnType<typeof setInterval> | undefined;
constructor(transport: Transport, configId?: number) {
this.log = new Logger({
name: "iMeshDevice",
@ -741,6 +742,18 @@ export class MeshDevice {
return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio));
}
/**
* Initializes the heartbeat interval, which sends a heartbeat ping every interval milliseconds.
*/
public setHeartbeatInterval(interval: number): void {
if (this._heartbeatIntervalId !== undefined) {
clearInterval(this._heartbeatIntervalId);
}
this._heartbeatIntervalId = setInterval(() => {
this.heartbeat();
}, interval);
}
/**
* Sends a trace route packet to the designated node
*/
@ -798,6 +811,11 @@ export class MeshDevice {
/** Disconnects from the device **/
public async disconnect(): Promise<void> {
this.log.debug(Emitter[Emitter.Disconnect], "🔌 Disconnecting from device");
if (this._heartbeatIntervalId !== undefined) {
clearInterval(this._heartbeatIntervalId);
}
this.complete();
await this.transport.toDevice.close();
}

10
packages/core/src/utils/queue.ts

@ -44,6 +44,16 @@ export class Queue {
if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) {
this.remove(item.id);
const decoded = fromBinary(Protobuf.Mesh.ToRadioSchema, item.data);
if (
decoded.payloadVariant.case === "heartbeat" ||
decoded.payloadVariant.case === "wantConfigId"
) {
// heartbeat and wantConfigId packets are not acknowledged by the device, assume success after timeout
resolve(item.id);
return;
}
console.warn(
`Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`,
);

4
packages/transport-http/src/transport.ts

@ -12,7 +12,9 @@ export class TransportHTTP implements Types.Transport {
tls?: boolean,
): Promise<TransportHTTP> {
const connectionUrl = `${tls ? "https" : "http"}://${address}`;
await fetch(`${connectionUrl}/json/report`);
await fetch(`${connectionUrl}/api/v1/toradio`, {
method: "OPTIONS",
});
await Promise.resolve();
return new TransportHTTP(connectionUrl);
}

8
packages/web/package.json

@ -14,14 +14,14 @@
"homepage": "https://meshtastic.org",
"scripts": {
"build": "bunx --bun vite build",
"build": "vite build",
"build:analyze": "BUNDLE_ANALYZE=true bun run build",
"check": "biome check src/",
"check:fix": "biome check --write src/",
"dev": "bunx --bun vite",
"test": "bunx --bun vitest",
"dev": "vite",
"test": "vitest",
"ts:check": "bun run tsc --noEmit",
"preview": "bunx --bun vite preview",
"preview": "vite preview",
"generate:routes": "bun @tanstack/router-cli generate --outDir src/ routes --rootRoutePath /",
"package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
},

13
packages/web/src/components/DeviceInfoPanel.tsx

@ -85,6 +85,12 @@ export const DeviceInfoPanel = ({
];
const actionButtons: ActionButtonConfig[] = [
{
id: "theme",
label: t("theme.changeTheme"),
icon: Palette,
render: () => <ThemeSwitcher />,
},
{
id: "changeName",
label: t("sidebar.deviceInfo.deviceName.changeName"),
@ -97,12 +103,7 @@ export const DeviceInfoPanel = ({
icon: SearchIcon,
onClick: setCommandPaletteOpen,
},
{
id: "theme",
label: t("theme.changeTheme"),
icon: Palette,
render: () => <ThemeSwitcher />,
},
{
id: "language",
label: t("language.changeLanguage"),

4
packages/web/src/components/Dialog/TracerouteResponseDialog.tsx

@ -30,7 +30,7 @@ export const TracerouteResponseDialog = ({
const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
const from = getNode(traceroute?.from ?? 0);
const from = getNode(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet
const fromLongName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
@ -40,7 +40,7 @@ export const TracerouteResponseDialog = ({
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName"));
const toUser = getNode(traceroute?.to ?? 0);
const toUser = getNode(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet
if (!toUser || !from) {
return null;

4
packages/web/src/components/PageComponents/Connect/BLE.tsx

@ -36,6 +36,10 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
setSelectedDevice(id);
device.addConnection(connection);
subscribeAll(device, connection, messageStore);
const HEARTBEAT_INTERVAL = 5*60*1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);
closeDialog();
};

6
packages/web/src/components/PageComponents/Connect/Serial.tsx

@ -43,6 +43,9 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
device.addConnection(connection);
subscribeAll(device, connection, messageStore);
const HEARTBEAT_INTERVAL = 5*60*1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);
closeDialog();
};
@ -52,7 +55,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
disabled={connectionInProgress}
>
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
{serialPorts.map((port) => {
{serialPorts.map((port, idx) => {
const { usbProductId, usbVendorId } = port.getInfo();
const vendor = usbVendorId ?? t("unknown.shortName");
const product = usbProductId ?? t("unknown.shortName");
@ -70,6 +73,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
{t("newDeviceDialog.serialConnection.deviceIdentifier", {
vendorId: vendor,
productId: product,
index: idx,
})}
</Button>
);

2
packages/web/src/components/PageComponents/Messages/TraceRoute.tsx

@ -41,7 +41,7 @@ const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => {
<span key={getNode(hop)?.num ?? hop}>
<p>
{getNode(hop)?.user?.longName ??
`${t("traceRoute.nodeUnknownPrefix")}${numberToHexUnpadded(hop)}`}
`${t("unknown.longName")} (!${numberToHexUnpadded(hop)})`}
</p>
<p>
{snr?.[i + 1] ?? t("unknown.num")}

19
packages/web/src/components/ThemeSwitcher.tsx

@ -1,7 +1,8 @@
import { useTheme } from "@core/hooks/useTheme.ts";
import { cn } from "@core/utils/cn.ts";
import { Monitor, Moon, Sun } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "../core/hooks/useTheme.ts";
import { useToggleVisibility } from "../core/hooks/useToggleVisiblility.ts";
import { cn } from "../core/utils/cn.ts";
import { Button } from "./UI/Button.tsx";
import { Subtle } from "./UI/Typography/Subtle.tsx";
@ -12,10 +13,16 @@ interface ThemeSwitcherProps {
disableHover?: boolean;
}
const TOOLTIP_TIMEOUT = 2000; // 2 seconds
export default function ThemeSwitcher({
className: passedClassName = "",
disableHover = false,
}: ThemeSwitcherProps) {
const [showTooltip, toggleShowTooltip] = useToggleVisibility({
timeout: TOOLTIP_TIMEOUT,
});
const { preference, setPreference } = useTheme();
const { t } = useTranslation("ui");
@ -35,8 +42,10 @@ export default function ThemeSwitcher({
const toggleTheme = () => {
const preferences: ThemePreference[] = ["light", "dark", "system"];
const currentIndex = preferences.indexOf(preference);
const nextPreference = preferences[(currentIndex + 1) % preferences.length];
const nextPreference =
preferences[(currentIndex + 1) % preferences.length] ?? "system";
setPreference(nextPreference);
toggleShowTooltip();
};
const preferenceDisplayMap: Record<ThemePreference, string> = {
@ -65,12 +74,12 @@ export default function ThemeSwitcher({
<span
data-label="theme-preference-tooltip"
className={cn(
"transition-opacity duration-150",
"transition-opacity duration-150 hidden",
"block absolute w-max max-w-xs",
"p-1 text-xs text-white dark:text-black bg-black dark:bg-white",
"rounded-md shadow-lg",
"left-1/2 -translate-x-1/2 -top-8",
"opacity-0",
showTooltip ? "visible" : "hidden opacity-0",
)}
>
{currentDisplayPreference}

16
packages/web/src/components/UI/Footer.tsx

@ -1,13 +1,11 @@
import { cn } from "@core/utils/cn.ts";
import { Trans, useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
type FooterProps = {
className?: string;
};
const Footer = ({ className, ...props }: FooterProps) => {
const { t } = useTranslation();
return (
<footer
className={cn(
@ -16,11 +14,15 @@ const Footer = ({ className, ...props }: FooterProps) => {
)}
{...props}
>
<div className="justify-start px-2">
<div className="px-2">
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40">
{String(import.meta.env.VITE_VERSION)?.toUpperCase()}
</span>
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40 mx-2">
-
</span>
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40">
{t("footer.commitSha", {
sha: String(import.meta.env.VITE_COMMIT_HASH)?.toUpperCase(),
})}
{`#${String(import.meta.env.VITE_COMMIT_HASH)?.toUpperCase()}`}
</span>
</div>
<p className="ml-auto mr-auto text-gray-500 dark:text-gray-400">

2
packages/web/src/components/UI/Switch.tsx

@ -8,7 +8,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-slate-900 data-[state=unchecked]:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=checked]:bg-slate-600 dark:data-[state=unchecked]:bg-slate-300",
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-500 data-[state=unchecked]:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=checked]:bg-green-600 dark:data-[state=unchecked]:bg-slate-300",
className,
)}
{...props}

32
packages/web/src/core/hooks/useToggleVisiblility.ts

@ -0,0 +1,32 @@
import { useCallback, useEffect, useRef, useState } from "react";
export function useToggleVisibility({ timeout }: { timeout?: number } = {}) {
const [isVisible, setIsVisible] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const show = useCallback(() => {
setIsVisible(true);
if (timeout) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setIsVisible(false);
timeoutRef.current = null;
}, timeout);
}
}, [timeout]);
// Clear timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return [isVisible, show] as const;
}

21
packages/web/src/routes.tsx

@ -14,7 +14,7 @@ import {
redirect,
} from "@tanstack/react-router";
import type { useTranslation } from "react-i18next";
import { z } from "zod";
import { z } from "zod/v4";
import { App } from "./App.tsx";
interface AppContext {
@ -55,23 +55,6 @@ const messagesRoute = createRoute({
},
});
const chatIdSchema = z.string().refine(
(val) => {
const num = Number(val);
if (Number.isNaN(num) || !Number.isInteger(num)) {
return false;
}
const isChannelId = num >= 0 && num <= 10;
const isNodeId = num >= 1000000000 && num <= 9999999999;
return isChannelId || isNodeId;
},
{
message: "Chat ID must be a channel (0-10) or a valid node ID.",
},
);
export const messagesWithParamsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/messages/$type/$chatId",
@ -83,7 +66,7 @@ export const messagesWithParamsRoute = createRoute({
message: 'Type must be "direct" or "broadcast".',
})
.parse(params.type),
chatId: chatIdSchema.parse(params.chatId),
chatId: z.coerce.number().int().min(0).max(4294967294).parse(params.chatId), // max is 0xffffffff - 1
}),
});

16
packages/web/tsconfig.json

@ -1,20 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,

1
packages/web/vite-env.d.ts

@ -3,6 +3,7 @@
interface ImportMetaEnv {
readonly env: {
readonly VITE_COMMIT_HASH: string;
readonly VITE_VERSION: string;
};
}

10
packages/web/vite.config.ts

@ -6,6 +6,7 @@ import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
let hash = "";
let version = "v0.0.0";
try {
hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim();
} catch (error) {
@ -13,6 +14,14 @@ try {
hash = "DEV";
}
try {
version = execSync("git describe --tags --abbrev=0", {
encoding: "utf8",
}).trim();
} catch (error) {
console.error("Error getting git version:", error);
}
const CONTENT_SECURITY_POLICY =
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' data: https://rsms.me https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' data: https://rsms.me https://cdn.jsdelivr.net; worker-src 'self' blob:; object-src 'none'; base-uri 'self';";
@ -37,6 +46,7 @@ export default defineConfig({
},
define: {
"import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash),
"import.meta.env.VITE_VERSION": JSON.stringify(version),
},
build: {
emptyOutDir: true,

19
tsconfig.base.json

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "."
}
}

13
tsconfig.json

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.base.json",
"files": [],
"references": [
{ "path": "packages/web" },
{ "path": "packages/transport-deno" },
{ "path": "packages/transport-node" },
{ "path": "packages/transport-web-serial" },
{ "path": "packages/transport-web-bluetooth" },
{ "path": "packages/transport-http" }
]
}
Loading…
Cancel
Save