Browse Source

Internal rework

pull/2/head
Sacha Weatherstone 5 years ago
parent
commit
0cea0b2165
  1. 13
      package.json
  2. 290
      pnpm-lock.yaml
  3. 151
      src/App.tsx
  4. 269
      src/components/Connection.tsx
  5. 9
      src/components/LoraConfig.tsx
  6. 37
      src/components/chat/MessageBar.tsx
  7. 4
      src/components/generic/Blur.tsx
  8. 6
      src/components/generic/Button.tsx
  9. 2
      src/components/generic/Card.tsx
  10. 37
      src/components/generic/Modal.tsx
  11. 2
      src/components/generic/StatCard.tsx
  12. 38
      src/components/menu/buttons/DeviceStatus.tsx
  13. 4
      src/components/pwa/ReloadPrompt.tsx
  14. 103
      src/core/connection.ts
  15. 10
      src/core/slices/appSlice.ts
  16. 151
      src/core/slices/meshtasticSlice.ts
  17. 5
      src/index.tsx
  18. 34
      src/pages/Messages.tsx
  19. 17
      src/pages/Nodes/Index.tsx
  20. 40
      src/pages/Nodes/Node.tsx
  21. 21
      src/pages/Plugins/ExternalNotification.tsx
  22. 22
      src/pages/Plugins/Files.tsx
  23. 18
      src/pages/Plugins/RangeTest.tsx
  24. 21
      src/pages/Plugins/Serial.tsx
  25. 14
      src/pages/settings/Channels.tsx
  26. 271
      src/pages/settings/Connection.tsx
  27. 8
      src/pages/settings/Index.tsx
  28. 2
      src/pages/settings/Interface.tsx
  29. 12
      src/pages/settings/Position.tsx
  30. 14
      src/pages/settings/Power.tsx
  31. 12
      src/pages/settings/Radio.tsx
  32. 50
      src/pages/settings/User.tsx
  33. 14
      src/pages/settings/WiFi.tsx
  34. 5
      todo.txt

13
package.json

@ -13,15 +13,15 @@
},
"dependencies": {
"@headlessui/react": "^1.4.2",
"@meshtastic/meshtasticjs": "^0.6.28",
"@meshtastic/meshtasticjs": "^0.6.29",
"@reduxjs/toolkit": "^1.6.2",
"boring-avatars": "^1.5.8",
"i18next": "^21.5.1",
"i18next": "^21.5.2",
"i18next-browser-languagedetector": "^6.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-file-icon": "^1.1.0",
"react-hook-form": "^7.19.5",
"react-hook-form": "^7.20.2",
"react-i18next": "^11.14.2",
"react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0",
@ -46,19 +46,20 @@
"babel-plugin-module-resolver": "^4.1.0",
"eslint": "8.2.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-babel-module": "^5.3.1",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.27.0",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react-hooks": "^4.3.0",
"gzipper": "^6.0.0",
"postcss": "^8.3.11",
"prettier": "^2.4.1",
"tailwindcss": "^3.0.0-alpha.2",
"tar": "^6.1.11",
"typescript": "^4.4.4",
"typescript": "^4.5.2",
"vite": "^2.6.14",
"vite-plugin-pwa": "^0.11.5",
"vite-plugin-pwa": "^0.11.6",
"workbox-window": "^6.4.1"
}
}

290
pnpm-lock.yaml

@ -2,7 +2,7 @@ lockfileVersion: 5.3
specifiers:
'@headlessui/react': ^1.4.2
'@meshtastic/meshtasticjs': ^0.6.28
'@meshtastic/meshtasticjs': ^0.6.29
'@reduxjs/toolkit': ^1.6.2
'@types/react': ^17.0.35
'@types/react-dom': ^17.0.11
@ -18,20 +18,21 @@ specifiers:
boring-avatars: ^1.5.8
eslint: 8.2.0
eslint-config-prettier: ^8.3.0
eslint-import-resolver-alias: ^1.1.2
eslint-import-resolver-babel-module: ^5.3.1
eslint-import-resolver-typescript: ^2.5.0
eslint-plugin-import: ^2.25.3
eslint-plugin-react: ^7.27.0
eslint-plugin-react: ^7.27.1
eslint-plugin-react-hooks: ^4.3.0
gzipper: ^6.0.0
i18next: ^21.5.1
i18next: ^21.5.2
i18next-browser-languagedetector: ^6.1.2
postcss: ^8.3.11
prettier: ^2.4.1
react: ^17.0.2
react-dom: ^17.0.2
react-file-icon: ^1.1.0
react-hook-form: ^7.19.5
react-hook-form: ^7.20.2
react-i18next: ^11.14.2
react-icons: ^4.3.1
react-json-pretty: ^2.2.0
@ -42,24 +43,24 @@ specifiers:
tar: ^6.1.11
timeago-react: ^3.0.4
type-route: ^0.6.0
typescript: ^4.4.4
typescript: ^4.5.2
use-breakpoint: ^2.0.2
vite: ^2.6.14
vite-plugin-pwa: ^0.11.5
vite-plugin-pwa: ^0.11.6
workbox-window: ^6.4.1
dependencies:
'@headlessui/react': 1.4[email protected][email protected]
'@meshtastic/meshtasticjs': 0.6.28
'@meshtastic/meshtasticjs': 0.6.29
'@reduxjs/toolkit': 1.6[email protected][email protected]
boring-avatars: 1.5.8
i18next: 21.5.1
i18next: 21.5.2
i18next-browser-languagedetector: 6.1.2
react: 17.0.2
react-dom: 17.0[email protected]
react-file-icon: 1.1[email protected][email protected]
react-hook-form: 7.19.5[email protected]
react-i18next: 11.14[email protected].1[email protected]
react-hook-form: 7.20.2[email protected]
react-i18next: 11.14[email protected].2[email protected]
react-icons: 4.3[email protected]
react-json-pretty: 2.2[email protected][email protected]
react-redux: 7.2[email protected][email protected]
@ -75,38 +76,39 @@ devDependencies:
'@types/react-file-icon': 1.0.1
'@types/w3c-web-serial': 1.0.2
'@types/web-bluetooth': 0.0.11
'@typescript-eslint/eslint-plugin': 5.4.0_b983626bd16070d34b18187cb6bde052
'@typescript-eslint/parser': 5.4[email protected]+typescript@4.4.4
'@verypossible/eslint-config': 1.6.1_typescript@4.4.4
'@typescript-eslint/eslint-plugin': 5.4.0_d6f2571581882eb2d6c9d9867e002185
'@typescript-eslint/parser': 5.4[email protected]+typescript@4.5.2
'@verypossible/eslint-config': 1.6.1_typescript@4.5.2
'@vitejs/plugin-react': 1.0.9
autoprefixer: 10.4[email protected]
babel-plugin-module-resolver: 4.1.0
eslint: 8.2.0
eslint-config-prettier: 8.3[email protected]
eslint-import-resolver-alias: 1.1[email protected]
eslint-import-resolver-babel-module: 5.3.1_e51044130ac762fd207a8cd2109b5344
eslint-import-resolver-typescript: 2.5.0_6e04a54c7bcd7530b1a4c2da0aa486b1
eslint-plugin-import: 2.25[email protected]
eslint-plugin-react: 7.27.0[email protected]
eslint-plugin-react: 7.27.1[email protected]
eslint-plugin-react-hooks: 4.3[email protected]
gzipper: 6.0.0
postcss: 8.3.11
prettier: 2.4.1
tailwindcss: 3.0.0-alpha.2_0c54bdadaf9d9c9c6c134cb2c6c061a3
tar: 6.1.11
typescript: 4.4.4
typescript: 4.5.2
vite: 2.6.14
vite-plugin-pwa: 0.11.5[email protected]
vite-plugin-pwa: 0.11.6[email protected]
workbox-window: 6.4.1
packages:
/@apideck/better-ajv-errors/[email protected].0:
/@apideck/better-ajv-errors/[email protected].1:
resolution: {integrity: sha512-J2dW+EHYudbwI7MGovcHWLBrxasl21uuroc2zT8bH2RxYuv2g5GqsO5jcKUZz4LaMST45xhKjVuyRYkhcWyMhA==}
engines: {node: '>=10'}
peerDependencies:
ajv: '>=8'
dependencies:
ajv: 8.8.0
ajv: 8.8.1
json-schema: 0.3.0
jsonpointer: 5.0.0
leven: 3.1.0
@ -125,8 +127,8 @@ packages:
'@babel/highlight': 7.16.0
dev: true
/@babel/compat-data/7.16.0:
resolution: {integrity: sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==}
/@babel/compat-data/7.16.4:
resolution: {integrity: sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==}
engines: {node: '>=6.9.0'}
dev: true
@ -139,7 +141,7 @@ packages:
'@babel/helper-compilation-targets': 7.16.3_@[email protected]
'@babel/helper-module-transforms': 7.16.0
'@babel/helpers': 7.16.3
'@babel/parser': 7.16.3
'@babel/parser': 7.16.4
'@babel/template': 7.16.0
'@babel/traverse': 7.16.3
'@babel/types': 7.16.0
@ -183,7 +185,7 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/compat-data': 7.16.0
'@babel/compat-data': 7.16.4
'@babel/core': 7.16.0
'@babel/helper-validator-option': 7.14.5
browserslist: 4.18.1
@ -218,8 +220,8 @@ packages:
regexpu-core: 4.8.0
dev: true
/@babel/helper-define-polyfill-provider/0.2.4_@[email protected]:
resolution: {integrity: sha512-OrpPZ97s+aPi6h2n1OXzdhVis1SGSsMU2aMHgLcOKfsp4/v1NWpx3CWT3lBj5eeBq9cDkPkh+YCfdF7O12uNDQ==}
/@babel/helper-define-polyfill-provider/0.3.0_@[email protected]:
resolution: {integrity: sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==}
peerDependencies:
'@babel/core': ^7.4.0-0
dependencies:
@ -308,8 +310,8 @@ packages:
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-remap-async-to-generator/7.16.0:
resolution: {integrity: sha512-MLM1IOMe9aQBqMWxcRw8dcb9jlM86NIw7KA0Wri91Xkfied+dE0QuBFSBjMNvqzmS0OSIDsMNC24dBEkPUi7ew==}
/@babel/helper-remap-async-to-generator/7.16.4:
resolution: {integrity: sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-annotate-as-pure': 7.16.0
@ -394,8 +396,8 @@ packages:
js-tokens: 4.0.0
dev: true
/@babel/parser/7.16.3:
resolution: {integrity: sha512-dcNwU1O4sx57ClvLBVFbEgx0UZWfd0JQX5X6fxFRCLHelFBGXFfSz6Y0FAq2PEwUqlqLkdVjVr4VASEOuUnLJw==}
/@babel/parser/7.16.4:
resolution: {integrity: sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==}
engines: {node: '>=6.0.0'}
hasBin: true
dev: true
@ -422,15 +424,15 @@ packages:
'@babel/plugin-proposal-optional-chaining': 7.16.0_@[email protected]
dev: true
/@babel/plugin-proposal-async-generator-functions/7.16.0_@[email protected]:
resolution: {integrity: sha512-nyYmIo7ZqKsY6P4lnVmBlxp9B3a96CscbLotlsNuktMHahkDwoPYEjXrZHU0Tj844Z9f1IthVxQln57mhkcExw==}
/@babel/plugin-proposal-async-generator-functions/7.16.4_@[email protected]:
resolution: {integrity: sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.16.0
'@babel/helper-plugin-utils': 7.14.5
'@babel/helper-remap-async-to-generator': 7.16.0
'@babel/helper-remap-async-to-generator': 7.16.4
'@babel/plugin-syntax-async-generators': 7.8.4_@[email protected]
transitivePeerDependencies:
- supports-color
@ -535,7 +537,7 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/compat-data': 7.16.0
'@babel/compat-data': 7.16.4
'@babel/core': 7.16.0
'@babel/helper-compilation-targets': 7.16.3_@[email protected]
'@babel/helper-plugin-utils': 7.14.5
@ -763,7 +765,7 @@ packages:
'@babel/core': 7.16.0
'@babel/helper-module-imports': 7.16.0
'@babel/helper-plugin-utils': 7.14.5
'@babel/helper-remap-async-to-generator': 7.16.0
'@babel/helper-remap-async-to-generator': 7.16.4
transitivePeerDependencies:
- supports-color
dev: true
@ -1146,20 +1148,20 @@ packages:
'@babel/helper-plugin-utils': 7.14.5
dev: true
/@babel/preset-env/7.16.0_@[email protected]:
resolution: {integrity: sha512-cdTu/W0IrviamtnZiTfixPfIncr2M1VqRrkjzZWlr1B4TVYimCFK5jkyOdP4qw2MrlKHi+b3ORj6x8GoCew8Dg==}
/@babel/preset-env/7.16.4_@[email protected]:
resolution: {integrity: sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/compat-data': 7.16.0
'@babel/compat-data': 7.16.4
'@babel/core': 7.16.0
'@babel/helper-compilation-targets': 7.16.3_@[email protected]
'@babel/helper-plugin-utils': 7.14.5
'@babel/helper-validator-option': 7.14.5
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.2_@[email protected]
'@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.16.0_@[email protected]
'@babel/plugin-proposal-async-generator-functions': 7.16.0_@[email protected]
'@babel/plugin-proposal-async-generator-functions': 7.16.4_@[email protected]
'@babel/plugin-proposal-class-properties': 7.16.0_@[email protected]
'@babel/plugin-proposal-class-static-block': 7.16.0_@[email protected]
'@babel/plugin-proposal-dynamic-import': 7.16.0_@[email protected]
@ -1222,9 +1224,9 @@ packages:
'@babel/plugin-transform-unicode-regex': 7.16.0_@[email protected]
'@babel/preset-modules': 0.1.5_@[email protected]
'@babel/types': 7.16.0
babel-plugin-polyfill-corejs2: 0.2.3_@[email protected]
babel-plugin-polyfill-corejs3: 0.3.0_@[email protected]
babel-plugin-polyfill-regenerator: 0.2.3_@[email protected]
babel-plugin-polyfill-corejs2: 0.3.0_@[email protected]
babel-plugin-polyfill-corejs3: 0.4.0_@[email protected]
babel-plugin-polyfill-regenerator: 0.3.0_@[email protected]
core-js-compat: 3.19.1
semver: 6.3.0
transitivePeerDependencies:
@ -1255,7 +1257,7 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.16.0
'@babel/parser': 7.16.3
'@babel/parser': 7.16.4
'@babel/types': 7.16.0
dev: true
@ -1268,7 +1270,7 @@ packages:
'@babel/helper-function-name': 7.16.0
'@babel/helper-hoist-variables': 7.16.0
'@babel/helper-split-export-declaration': 7.16.0
'@babel/parser': 7.16.3
'@babel/parser': 7.16.4
'@babel/types': 7.16.0
debug: 4.3.2
globals: 11.12.0
@ -1355,8 +1357,8 @@ packages:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
/@meshtastic/meshtasticjs/0.6.28:
resolution: {integrity: sha512-yoFzIM+fktvYqgRr/IVA41h4JMEv5GJue/xtxWNhWweEX6fiPoy+MoGeDkwU2dRyNzzZ03RLa6xNXqYxiHRlLA==}
/@meshtastic/meshtasticjs/0.6.29:
resolution: {integrity: sha512-XcfHTGlWBLgdfXCK81U/ilDpepyId/OEvXRNiEerg40m9pD7FaNlqPnAKRhvoeOl56VQdKtCpsiWJdeknSI4tw==}
dependencies:
'@protobuf-ts/runtime': 2.0.7
sub-events: 1.8.9
@ -1403,7 +1405,7 @@ packages:
react-redux: 7.2[email protected][email protected]
redux: 4.1.2
redux-thunk: 2.4[email protected]
reselect: 4.1.3
reselect: 4.1.4
dev: false
/@rollup/plugin-babel/5.3.0_@[email protected][email protected]:
@ -1496,8 +1498,8 @@ packages:
resolution: {integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4=}
dev: true
/@types/node/16.11.7:
resolution: {integrity: sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==}
/@types/node/16.11.9:
resolution: {integrity: sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==}
dev: true
/@types/parse-json/4.0.0:
@ -1538,7 +1540,7 @@ packages:
/@types/resolve/1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies:
'@types/node': 16.11.7
'@types/node': 16.11.9
dev: true
/@types/scheduler/0.16.2:
@ -1556,7 +1558,7 @@ packages:
resolution: {integrity: sha512-2CF3Kk2Rcvg/c2QzO7mXUhY7eL9CC3aKzrF+dNWNmp7Q8bmlvjmUM1nFPMSngawdJ+CcIdu8eJlQRytBgAZR9w==}
dev: true
/@typescript-eslint/eslint-plugin/4.33.0_cc617358c89d3f38c52462f6d809db4c:
/@typescript-eslint/eslint-plugin/4.33.0_d00b196ac5df1286ea7e45797bebddbc:
resolution: {integrity: sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@ -1567,8 +1569,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/experimental-utils': 4.33[email protected]+typescript@4.4.4
'@typescript-eslint/parser': 4.33[email protected]+typescript@4.4.4
'@typescript-eslint/experimental-utils': 4.33[email protected]+typescript@4.5.2
'@typescript-eslint/parser': 4.33[email protected]+typescript@4.5.2
'@typescript-eslint/scope-manager': 4.33.0
debug: 4.3.2
eslint: 7.32.0
@ -1576,13 +1578,13 @@ packages:
ignore: 5.1.9
regexpp: 3.2.0
semver: 7.3.5
tsutils: 3.21.0_typescript@4.4.4
typescript: 4.4.4
tsutils: 3.21.0_typescript@4.5.2
typescript: 4.5.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/eslint-plugin/5.4.0_b983626bd16070d34b18187cb6bde052:
/@typescript-eslint/eslint-plugin/5.4.0_d6f2571581882eb2d6c9d9867e002185:
resolution: {integrity: sha512-9/yPSBlwzsetCsGEn9j24D8vGQgJkOTr4oMLas/w886ZtzKIs1iyoqFrwsX2fqYEeUwsdBpC21gcjRGo57u0eg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -1593,8 +1595,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/experimental-utils': 5.4[email protected]+typescript@4.4.4
'@typescript-eslint/parser': 5.4[email protected]+typescript@4.4.4
'@typescript-eslint/experimental-utils': 5.4[email protected]+typescript@4.5.2
'@typescript-eslint/parser': 5.4[email protected]+typescript@4.5.2
'@typescript-eslint/scope-manager': 5.4.0
debug: 4.3.2
eslint: 8.2.0
@ -1602,13 +1604,13 @@ packages:
ignore: 5.1.9
regexpp: 3.2.0
semver: 7.3.5
tsutils: 3.21.0_typescript@4.4.4
typescript: 4.4.4
tsutils: 3.21.0_typescript@4.5.2
typescript: 4.5.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/experimental-utils/[email protected]+typescript@4.4.4:
/@typescript-eslint/experimental-utils/[email protected]+typescript@4.5.2:
resolution: {integrity: sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@ -1617,7 +1619,7 @@ packages:
'@types/json-schema': 7.0.9
'@typescript-eslint/scope-manager': 4.33.0
'@typescript-eslint/types': 4.33.0
'@typescript-eslint/typescript-estree': 4.33.0_typescript@4.4.4
'@typescript-eslint/typescript-estree': 4.33.0_typescript@4.5.2
eslint: 7.32.0
eslint-scope: 5.1.1
eslint-utils: 3.0[email protected]
@ -1626,7 +1628,7 @@ packages:
- typescript
dev: true
/@typescript-eslint/experimental-utils/[email protected]+typescript@4.4.4:
/@typescript-eslint/experimental-utils/[email protected]+typescript@4.5.2:
resolution: {integrity: sha512-Nz2JDIQUdmIGd6p33A+naQmwfkU5KVTLb/5lTk+tLVTDacZKoGQisj8UCxk7onJcrgjIvr8xWqkYI+DbI3TfXg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -1635,7 +1637,7 @@ packages:
'@types/json-schema': 7.0.9
'@typescript-eslint/scope-manager': 5.4.0
'@typescript-eslint/types': 5.4.0
'@typescript-eslint/typescript-estree': 5.4.0_typescript@4.4.4
'@typescript-eslint/typescript-estree': 5.4.0_typescript@4.5.2
eslint: 8.2.0
eslint-scope: 5.1.1
eslint-utils: 3.0[email protected]
@ -1644,7 +1646,7 @@ packages:
- typescript
dev: true
/@typescript-eslint/parser/[email protected]+typescript@4.4.4:
/@typescript-eslint/parser/[email protected]+typescript@4.5.2:
resolution: {integrity: sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@ -1656,15 +1658,15 @@ packages:
dependencies:
'@typescript-eslint/scope-manager': 4.33.0
'@typescript-eslint/types': 4.33.0
'@typescript-eslint/typescript-estree': 4.33.0_typescript@4.4.4
'@typescript-eslint/typescript-estree': 4.33.0_typescript@4.5.2
debug: 4.3.2
eslint: 7.32.0
typescript: 4.4.4
typescript: 4.5.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/parser/[email protected]+typescript@4.4.4:
/@typescript-eslint/parser/[email protected]+typescript@4.5.2:
resolution: {integrity: sha512-JoB41EmxiYpaEsRwpZEYAJ9XQURPFer8hpkIW9GiaspVLX8oqbqNM8P4EP8HOZg96yaALiLEVWllA2E8vwsIKw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -1676,10 +1678,10 @@ packages:
dependencies:
'@typescript-eslint/scope-manager': 5.4.0
'@typescript-eslint/types': 5.4.0
'@typescript-eslint/typescript-estree': 5.4.0_typescript@4.4.4
'@typescript-eslint/typescript-estree': 5.4.0_typescript@4.5.2
debug: 4.3.2
eslint: 8.2.0
typescript: 4.4.4
typescript: 4.5.2
transitivePeerDependencies:
- supports-color
dev: true
@ -1710,7 +1712,7 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@typescript-eslint/typescript-estree/4.33.0_typescript@4.4.4:
/@typescript-eslint/typescript-estree/4.33.0_typescript@4.5.2:
resolution: {integrity: sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@ -1725,13 +1727,13 @@ packages:
globby: 11.0.4
is-glob: 4.0.3
semver: 7.3.5
tsutils: 3.21.0_typescript@4.4.4
typescript: 4.4.4
tsutils: 3.21.0_typescript@4.5.2
typescript: 4.5.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/typescript-estree/5.4.0_typescript@4.4.4:
/@typescript-eslint/typescript-estree/5.4.0_typescript@4.5.2:
resolution: {integrity: sha512-nhlNoBdhKuwiLMx6GrybPT3SFILm5Gij2YBdPEPFlYNFAXUJWX6QRgvi/lwVoadaQEFsizohs6aFRMqsXI2ewA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -1746,8 +1748,8 @@ packages:
globby: 11.0.4
is-glob: 4.0.3
semver: 7.3.5
tsutils: 3.21.0_typescript@4.4.4
typescript: 4.4.4
tsutils: 3.21.0_typescript@4.5.2
typescript: 4.5.2
transitivePeerDependencies:
- supports-color
dev: true
@ -1768,18 +1770,18 @@ packages:
eslint-visitor-keys: 3.1.0
dev: true
/@verypossible/eslint-config/1.6.1_typescript@4.4.4:
/@verypossible/eslint-config/1.6.1_typescript@4.5.2:
resolution: {integrity: sha512-3qf2FSag49zqI6rZlwKcF8RryLX0RJ3W+koJuhDhdQNyelSEeTxiijQ+Y/Xss4ILFzyqpBnzqiphmABGcOgj1Q==}
dependencies:
'@typescript-eslint/eslint-plugin': 4.33.0_cc617358c89d3f38c52462f6d809db4c
'@typescript-eslint/parser': 4.33[email protected]+typescript@4.4.4
'@typescript-eslint/eslint-plugin': 4.33.0_d00b196ac5df1286ea7e45797bebddbc
'@typescript-eslint/parser': 4.33[email protected]+typescript@4.5.2
babel-plugin-module-resolver: 4.1.0
eslint: 7.32.0
eslint-config-prettier: 8.3[email protected]
eslint-import-resolver-babel-module: 5.3.1_e51044130ac762fd207a8cd2109b5344
eslint-import-resolver-typescript: 2.5.0_a820dc868cc8cd66d8297be6779b9035
eslint-plugin-import: 2.25[email protected]
eslint-plugin-react: 7.27.0[email protected]
eslint-plugin-react: 7.27.1[email protected]
eslint-plugin-react-hooks: 4.3[email protected]
prettier: 2.4.1
transitivePeerDependencies:
@ -1812,12 +1814,12 @@ packages:
acorn: 7.4.1
dev: true
/acorn-jsx/5.3.2_acorn@8.5.0:
/acorn-jsx/5.3.2_acorn@8.6.0:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
acorn: 8.5.0
acorn: 8.6.0
dev: true
/acorn-node/1.8.2:
@ -1839,8 +1841,8 @@ packages:
hasBin: true
dev: true
/acorn/8.5.0:
resolution: {integrity: sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==}
/acorn/8.6.0:
resolution: {integrity: sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
@ -1854,8 +1856,8 @@ packages:
uri-js: 4.4.1
dev: true
/ajv/8.8.0:
resolution: {integrity: sha512-L+cJ/+pkdICMueKR6wIx3VP2fjIx3yAhuvadUv/osv9yFD7OVZy442xFF+Oeu3ZvmhBGQzoF6mTSt+LUWBmGQg==}
/ajv/8.8.1:
resolution: {integrity: sha512-6CiMNDrzv0ZR916u2T+iRunnD60uWmNn8SkdB44/6stVORUg0aAkWO7PkOhpCmjmW8f2I/G/xnowD66fxGyQJg==}
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
@ -1965,7 +1967,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.18.1
caniuse-lite: 1.0.30001280
caniuse-lite: 1.0.30001282
fraction.js: 4.1.2
normalize-range: 0.1.2
picocolors: 1.0.0
@ -1991,42 +1993,42 @@ packages:
find-babel-config: 1.2.0
glob: 7.2.0
pkg-up: 3.1.0
reselect: 4.1.3
reselect: 4.1.4
resolve: 1.20.0
dev: true
/babel-plugin-polyfill-corejs2/0.2.3_@[email protected]:
resolution: {integrity: sha512-NDZ0auNRzmAfE1oDDPW2JhzIMXUk+FFe2ICejmt5T4ocKgiQx3e0VCRx9NCAidcMtL2RUZaWtXnmjTCkx0tcbA==}
/babel-plugin-polyfill-corejs2/0.3.0_@[email protected]:
resolution: {integrity: sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/compat-data': 7.16.0
'@babel/compat-data': 7.16.4
'@babel/core': 7.16.0
'@babel/helper-define-polyfill-provider': 0.2.4_@[email protected]
'@babel/helper-define-polyfill-provider': 0.3.0_@[email protected]
semver: 6.3.0
transitivePeerDependencies:
- supports-color
dev: true
/babel-plugin-polyfill-corejs3/0.3.0_@[email protected]:
resolution: {integrity: sha512-JLwi9vloVdXLjzACL80j24bG6/T1gYxwowG44dg6HN/7aTPdyPbJJidf6ajoA3RPHHtW0j9KMrSOLpIZpAnPpg==}
/babel-plugin-polyfill-corejs3/0.4.0_@[email protected]:
resolution: {integrity: sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.16.0
'@babel/helper-define-polyfill-provider': 0.2.4_@[email protected]
'@babel/helper-define-polyfill-provider': 0.3.0_@[email protected]
core-js-compat: 3.19.1
transitivePeerDependencies:
- supports-color
dev: true
/babel-plugin-polyfill-regenerator/0.2.3_@[email protected]:
resolution: {integrity: sha512-JVE78oRZPKFIeUqFGrSORNzQnrDwZR16oiWeGM8ZyjBn2XAT5OjP+wXx5ESuo33nUsFUEJYjtklnsKbxW5L+7g==}
/babel-plugin-polyfill-regenerator/0.3.0_@[email protected]:
resolution: {integrity: sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.16.0
'@babel/helper-define-polyfill-provider': 0.2.4_@[email protected]
'@babel/helper-define-polyfill-provider': 0.3.0_@[email protected]
transitivePeerDependencies:
- supports-color
dev: true
@ -2063,8 +2065,8 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001280
electron-to-chromium: 1.3.899
caniuse-lite: 1.0.30001282
electron-to-chromium: 1.3.904
escalade: 3.1.1
node-releases: 2.0.1
picocolors: 1.0.0
@ -2096,8 +2098,8 @@ packages:
engines: {node: '>= 6'}
dev: true
/caniuse-lite/1.0.30001280:
resolution: {integrity: sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==}
/caniuse-lite/1.0.30001282:
resolution: {integrity: sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==}
dev: true
/chalk/2.4.2:
@ -2167,8 +2169,8 @@ packages:
engines: {node: '>= 10'}
dev: true
/common-tags/1.8.1:
resolution: {integrity: sha512-uOZd85rJqrdEIE/JjhW5YAeatX8iqjjvVzIyfx7JL7G5r9Tep6YpYT9gEJWhWpVyDQEyzukWd6p2qULpJ8tmBw==}
/common-tags/1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
engines: {node: '>=4.0.0'}
dev: true
@ -2339,8 +2341,8 @@ packages:
jake: 10.8.2
dev: true
/electron-to-chromium/1.3.899:
resolution: {integrity: sha512-w16Dtd2zl7VZ4N4Db+FIa7n36sgPGCKjrKvUUmp5ialsikvcQLjcJR9RWnlYNxIyEHLdHaoIZEqKsPxU9MdyBg==}
/electron-to-chromium/1.3.904:
resolution: {integrity: sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw==}
dev: true
/emoji-regex/8.0.0:
@ -2601,6 +2603,15 @@ packages:
eslint: 8.2.0
dev: true
/eslint-import-resolver-alias/[email protected]:
resolution: {integrity: sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==}
engines: {node: '>= 4'}
peerDependencies:
eslint-plugin-import: '>=1.4.0'
dependencies:
eslint-plugin-import: 2.25[email protected]
dev: true
/eslint-import-resolver-babel-module/5.3.1_e51044130ac762fd207a8cd2109b5344:
resolution: {integrity: sha512-WomQAkjO7lUNOdU3FG2zgNgylkoAVUmaw04bHgSpM9QrMWuOLLWa2qcP6CrsBd4VWuLRbUPyzrgBc9ZQIx9agw==}
engines: {node: '>=10.0.0'}
@ -2633,7 +2644,7 @@ packages:
glob: 7.2.0
is-glob: 4.0.3
resolve: 1.20.0
tsconfig-paths: 3.11.0
tsconfig-paths: 3.12.0
transitivePeerDependencies:
- supports-color
dev: true
@ -2651,7 +2662,7 @@ packages:
glob: 7.2.0
is-glob: 4.0.3
resolve: 1.20.0
tsconfig-paths: 3.11.0
tsconfig-paths: 3.12.0
transitivePeerDependencies:
- supports-color
dev: true
@ -2684,7 +2695,7 @@ packages:
minimatch: 3.0.4
object.values: 1.1.5
resolve: 1.20.0
tsconfig-paths: 3.11.0
tsconfig-paths: 3.12.0
dev: true
/eslint-plugin-import/[email protected]:
@ -2706,7 +2717,7 @@ packages:
minimatch: 3.0.4
object.values: 1.1.5
resolve: 1.20.0
tsconfig-paths: 3.11.0
tsconfig-paths: 3.12.0
dev: true
/eslint-plugin-react-hooks/[email protected]:
@ -2727,8 +2738,8 @@ packages:
eslint: 8.2.0
dev: true
/eslint-plugin-react/7.27.0[email protected]:
resolution: {integrity: sha512-0Ut+CkzpppgFtoIhdzi2LpdpxxBvgFf99eFqWxJnUrO7mMe0eOiNpou6rvNYeVVV6lWZvTah0BFne7k5xHjARg==}
/eslint-plugin-react/7.27.1[email protected]:
resolution: {integrity: sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==}
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
@ -2750,8 +2761,8 @@ packages:
string.prototype.matchall: 4.0.6
dev: true
/eslint-plugin-react/7.27.0[email protected]:
resolution: {integrity: sha512-0Ut+CkzpppgFtoIhdzi2LpdpxxBvgFf99eFqWxJnUrO7mMe0eOiNpou6rvNYeVVV6lWZvTah0BFne7k5xHjARg==}
/eslint-plugin-react/7.27.1[email protected]:
resolution: {integrity: sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==}
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
@ -2940,8 +2951,8 @@ packages:
resolution: {integrity: sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
acorn: 8.5.0
acorn-jsx: 5.3.2_acorn@8.5.0
acorn: 8.6.0
acorn-jsx: 5.3.2_acorn@8.6.0
eslint-visitor-keys: 3.1.0
dev: true
@ -3260,8 +3271,8 @@ packages:
'@babel/runtime': 7.16.3
dev: false
/i18next/21.5.1:
resolution: {integrity: sha512-fmpns1dbYYgyOkiATp1rg5gyXzvBdvM0YQFDCM38BoqybG2Rs3looAv+e24ghFeeozD1fteUtDTZ36SQ0a+ycg==}
/i18next/21.5.2:
resolution: {integrity: sha512-Iuztr2+7CPCh5SYQV0utw2HXMx1za18xfznrw/PmgX+98oIpm84bhIM7VUPODjLycwIZ299oP7sEVQ9oCgmzfg==}
dependencies:
'@babel/runtime': 7.16.3
dev: false
@ -3516,7 +3527,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 16.11.7
'@types/node': 16.11.9
merge-stream: 2.0.0
supports-color: 7.2.0
dev: true
@ -4091,15 +4102,16 @@ packages:
tinycolor2: 1.4.2
dev: false
/react-hook-form/[email protected]:
resolution: {integrity: sha512-+M9gd0nWWASuEitf5PQGOGNElnHknzY3rGISrPDXwsOmZb7c/jyvkRUqk4OJXaZit1ZwesSv+EysttdAeFEfmw==}
/react-hook-form/[email protected]:
resolution: {integrity: sha512-J5zkZW0Mf3KuMlk7Tl1tWYXoSjYhfKyEu//NfWTn2xzvv2vnDT5/EE/vHgtNb7kVeYpx6/mHIBd9aOfNnWcOsg==}
engines: {node: '>=12.0'}
peerDependencies:
react: ^16.8.0 || ^17
dependencies:
react: 17.0.2
dev: false
/react-i18next/[email protected].1[email protected]:
/react-i18next/[email protected].2[email protected]:
resolution: {integrity: sha512-fmDhwNA0zDmSEL3BBT5qwNMvxrKu25oXDDAZyHprfB0AHZmWXfBmRLf8MX8i1iBd2I2C2vsA2D9wxYBIwzooEQ==}
peerDependencies:
i18next: '>= 19.0.0'
@ -4107,7 +4119,7 @@ packages:
dependencies:
'@babel/runtime': 7.16.3
html-parse-stringify: 3.0.1
i18next: 21.5.1
i18next: 21.5.2
react: 17.0.2
dev: false
@ -4254,8 +4266,8 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/reselect/4.1.3:
resolution: {integrity: sha512-TVpMknnmdSRNhLPgTDSCQKw32zt1ZIJtEcSxfL/ihtDqShEMUs2X2UY/g96YAVynUXxqLWSXObLGIcqKHQObHw==}
/reselect/4.1.4:
resolution: {integrity: sha512-i1LgXw8DKSU5qz1EV0ZIKz4yIUHJ7L3bODh+Da6HmVSm9vdL/hG7IpbgzQ3k2XSirzf8/eI7OMEs81gb1VV2fQ==}
/resolve-from/4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
@ -4403,8 +4415,8 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/source-map-support/0.5.20:
resolution: {integrity: sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==}
/source-map-support/0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
@ -4543,7 +4555,7 @@ packages:
resolution: {integrity: sha512-5DkIxeA7XERBqMwJq0aHZOdMadBx4e6eDoFRuyT5VR82J0Ycg2DwM6GfA/EQAhJ+toRTaS1lIdSQCqgrmhPnlw==}
engines: {node: '>=10.0.0'}
dependencies:
ajv: 8.8.0
ajv: 8.8.1
lodash.truncate: 4.4.2
slice-ansi: 4.0.0
string-width: 4.2.3
@ -4624,7 +4636,7 @@ packages:
dependencies:
commander: 2.20.3
source-map: 0.7.3
source-map-support: 0.5.20
source-map-support: 0.5.21
dev: true
/text-table/0.2.0:
@ -4673,8 +4685,8 @@ packages:
punycode: 2.1.1
dev: true
/tsconfig-paths/3.11.0:
resolution: {integrity: sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==}
/tsconfig-paths/3.12.0:
resolution: {integrity: sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==}
dependencies:
'@types/json5': 0.0.29
json5: 1.0.1
@ -4686,14 +4698,14 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true
/tsutils/3.21.0_typescript@4.4.4:
/tsutils/3.21.0_typescript@4.5.2:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies:
tslib: 1.14.1
typescript: 4.4.4
typescript: 4.5.2
dev: true
/type-check/0.4.0:
@ -4719,8 +4731,8 @@ packages:
history: 5.1.0
dev: false
/typescript/4.4.4:
resolution: {integrity: sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==}
/typescript/4.5.2:
resolution: {integrity: sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
@ -4803,8 +4815,8 @@ packages:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
dev: true
/vite-plugin-pwa/0.11.5[email protected]:
resolution: {integrity: sha512-qn79L7008ZMn9GS0ClxypOBRA3Ft8/a8saIQ03SC2R1QndbZVW+YQVHTlFno33Wp6fu5UJacoHWuZYCuKZKaOA==}
/vite-plugin-pwa/0.11.6[email protected]:
resolution: {integrity: sha512-C95xVO8csEedN29aHszKjRSb/P7Odd6+tCP3LfjqQQkNRkZPieT6y8mS5MlEbs9V+8D+z4THD6ksYB5mzLTzPg==}
peerDependencies:
vite: ^2.0.0
dependencies:
@ -4924,16 +4936,16 @@ packages:
resolution: {integrity: sha512-cvH74tEO8SrziFrCntZ/35B0uaMZFKG+gnk3vZmKLSUTab/6MlhL+UwYXf1sMV5SD/W/v7pnFKZbdAOAg5Ne2w==}
engines: {node: '>=10.0.0'}
dependencies:
'@apideck/better-ajv-errors': 0.2[email protected].0
'@apideck/better-ajv-errors': 0.2[email protected].1
'@babel/core': 7.16.0
'@babel/preset-env': 7.16.0_@[email protected]
'@babel/preset-env': 7.16.4_@[email protected]
'@babel/runtime': 7.16.3
'@rollup/plugin-babel': 5.3.0_@[email protected][email protected]
'@rollup/plugin-node-resolve': 11.2[email protected]
'@rollup/plugin-replace': 2.4[email protected]
'@surma/rollup-plugin-off-main-thread': 2.2.3
ajv: 8.8.0
common-tags: 1.8.1
ajv: 8.8.1
common-tags: 1.8.2
fast-json-stable-stringify: 2.1.0
fs-extra: 9.1.0
glob: 7.2.0

151
src/App.tsx

@ -1,34 +1,14 @@
import React from 'react';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { DeviceStatusDropdown } from '@components/menu/buttons/DeviceStatusDropdown';
import { DeviceStatus } from '@app/components/menu/buttons/DeviceStatus';
import { useAppSelector } from '@app/hooks/redux';
import { Connection } from '@components/Connection';
import { MobileNavToggle } from '@components/menu/buttons/MobileNavToggle';
import { ThemeToggle } from '@components/menu/buttons/ThemeToggle';
import { Logo } from '@components/menu/Logo';
import { MobileNav } from '@components/menu/MobileNav';
import { Navigation } from '@components/menu/Navigation';
import { connection, setConnection } from '@core/connection';
import { useRoute } from '@core/router';
import {
addChannel,
addMessage,
addNode,
addPosition,
addUser,
setDeviceStatus,
setLastMeshInterraction,
setMyNodeInfo,
setPreferences,
setReady,
} from '@core/slices/meshtasticSlice';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
SettingsManager,
Types,
} from '@meshtastic/meshtasticjs';
import { Messages } from '@pages/Messages';
import { Nodes } from '@pages/Nodes/Index';
import { Settings } from '@pages/settings/Index';
@ -36,128 +16,13 @@ import { Settings } from '@pages/settings/Index';
import { NotFound } from './pages/NotFound';
import { Plugins } from './pages/Plugins/Index';
const App = (): JSX.Element => {
const dispatch = useAppDispatch();
export const App = (): JSX.Element => {
const route = useRoute();
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo);
const darkMode = useAppSelector((state) => state.app.darkMode);
const hostOverrideEnabled = useAppSelector(
(state) => state.meshtastic.hostOverrideEnabled,
);
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const connectionURL = hostOverrideEnabled
? hostOverride
: import.meta.env.PROD
? window.location.hostname
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ??
'http://meshtastic.local';
React.useEffect(() => {
const connectionMethod = localStorage.getItem('connectionMethod');
switch (connectionMethod) {
case 'serial':
setConnection(new ISerialConnection());
//show connection dialogue
break;
case 'bluetooth':
setConnection(new IBLEConnection());
//show connection dialogue
break;
default:
setConnection(new IHTTPConnection());
void connection.connect({
address: connectionURL,
tls: false,
receiveBatchRequests: false,
fetchInterval: 2000,
});
break;
}
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
}, [hostOverrideEnabled, hostOverride, connectionURL]);
React.useEffect(() => {
connection.onDeviceStatus.subscribe((status) => {
dispatch(setDeviceStatus(status));
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
dispatch(setReady(true));
}
if (status === Types.DeviceStatusEnum.DEVICE_DISCONNECTED) {
dispatch(setReady(false));
}
});
connection.onMyNodeInfo.subscribe((nodeInfo) => {
dispatch(setMyNodeInfo(nodeInfo));
});
connection.onUserPacket.subscribe((user) => {
dispatch(addUser(user));
});
connection.onPositionPacket.subscribe((position) => {
dispatch(addPosition(position));
});
connection.onNodeInfoPacket.subscribe(
(nodeInfoPacket): void | { payload: Protobuf.NodeInfo; type: string } => {
dispatch(addNode(nodeInfoPacket.data));
},
);
connection.onAdminPacket.subscribe((adminPacket) => {
switch (adminPacket.data.variant.oneofKind) {
case 'getChannelResponse':
dispatch(addChannel(adminPacket.data.variant.getChannelResponse));
break;
case 'getRadioResponse':
if (adminPacket.data.variant.getRadioResponse.preferences) {
dispatch(
setPreferences(
adminPacket.data.variant.getRadioResponse.preferences,
),
);
}
break;
}
});
connection.onMeshHeartbeat.subscribe(
(date): void | { payload: number; type: string } =>
dispatch(setLastMeshInterraction(date.getTime())),
);
connection.onTextPacket.subscribe((message) => {
dispatch(
addMessage({
message: message,
ack: message.packet.from !== myNodeInfo.myNodeNum,
isSender: message.packet.from === myNodeInfo.myNodeNum,
received: new Date(message.packet.rxTime),
}),
);
});
return (): void => {
connection.onDeviceStatus.cancelAll();
connection.onMyNodeInfo.cancelAll();
connection.onNodeInfoPacket.cancelAll();
connection.onAdminPacket.cancelAll();
connection.onMeshHeartbeat.cancelAll();
connection.onTextPacket.cancelAll();
connection.onRoutingPacket.cancelAll();
};
}, [dispatch, myNodeInfo.myNodeNum]);
return (
<div
className={`h-screen w-screen ${darkMode ? 'dark rs-theme-dark' : ''}`}
>
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
<Connection />
<div className="flex flex-col h-full bg-gray-200 dark:bg-primaryDark">
<div className="flex flex-shrink-0 overflow-hidden bg-primary dark:bg-primary">
<div className="w-full overflow-hidden bg-white border-b border-gray-300 md:mt-6 md:mx-6 md:pt-4 md:pb-3 md:rounded-t-3xl dark:border-gray-600 md:shadow-md dark:bg-primaryDark">
@ -168,7 +33,7 @@ const App = (): JSX.Element => {
<Navigation className="hidden md:flex" />
<MobileNavToggle />
<div className="flex items-center space-x-2">
<DeviceStatusDropdown />
<DeviceStatus />
<ThemeToggle />
</div>
</div>
@ -189,5 +54,3 @@ const App = (): JSX.Element => {
</div>
);
};
export default App;

269
src/components/Connection.tsx

@ -0,0 +1,269 @@
import React from 'react';
import { FiCheck } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { Modal } from '@components/generic/Modal';
import {
ble,
connection,
connectionUrl,
serial,
setConnection,
} from '@core/connection';
import { closeConnectionModal } from '@core/slices/appSlice';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
SettingsManager,
} from '@meshtastic/meshtasticjs';
import type {
BLEConnectionParameters,
HTTPConnectionParameters,
SerialConnectionParameters,
} from '@meshtastic/meshtasticjs/dist/types';
import { DeviceStatus } from './menu/buttons/DeviceStatus';
enum connType {
HTTP,
BLE,
SERIAL,
}
export const Connection = (): JSX.Element => {
const dispatch = useAppDispatch();
const [selectedConnType, setSelectedConnType] = React.useState(connType.HTTP);
const [bleDevices, setBleDevices] = React.useState<BluetoothDevice[]>([]);
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]);
const [httpIpSource, setHttpIpSource] = React.useState<'local' | 'remote'>(
'local',
);
const hostOverrideEnabled = useAppSelector(
(state) => state.meshtastic.hostOverrideEnabled,
);
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const connectionModalOpen = useAppSelector(
(state) => state.app.connectionModalOpen,
);
const ready = useAppSelector((state) => state.meshtastic.ready);
const connect = async (
connectionType: connType,
params:
| HTTPConnectionParameters
| SerialConnectionParameters
| BLEConnectionParameters,
): Promise<void> => {
connection.complete();
await connection.disconnect();
if (connectionType === connType.BLE) {
setConnection(new IBLEConnection());
} else if (connectionType === connType.HTTP) {
setConnection(new IHTTPConnection());
} else {
setConnection(new ISerialConnection());
}
// @ts-ignore tmp
await connection.connect(params);
};
const updateBleDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await ble.getDevices();
setBleDevices(devices);
}, []);
const updateSerialDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await serial.getPorts();
setSerialDevices(devices);
}, []);
React.useEffect(() => {
if (ready) {
dispatch(closeConnectionModal());
}
}, [ready, dispatch]);
React.useEffect(() => {
if (selectedConnType === connType.BLE) {
void updateBleDeviceList();
}
if (selectedConnType === connType.SERIAL) {
void updateSerialDeviceList();
}
}, [selectedConnType, updateBleDeviceList, updateSerialDeviceList]);
React.useEffect(() => {
const connectionMethod = localStorage.getItem('connectionMethod');
switch (connectionMethod) {
case 'serial':
setConnection(new ISerialConnection());
//show connection dialogue
break;
case 'bluetooth':
setConnection(new IBLEConnection());
//show connection dialogue
break;
default:
setConnection(new IHTTPConnection());
void connection.connect({
address: connectionUrl,
tls: false,
receiveBatchRequests: false,
fetchInterval: 2000,
});
break;
}
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
}, [hostOverrideEnabled, hostOverride]);
return (
<Modal
open={connectionModalOpen}
onClose={(): void => {
dispatch(closeConnectionModal());
}}
>
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
{ready ? (
<form className="space-y-2">
<Select
label="Method"
optionsEnum={connType}
value={selectedConnType}
onChange={(e): void => {
setSelectedConnType(parseInt(e.target.value));
}}
/>
{selectedConnType === connType.HTTP && (
<>
<Select
label="Host Source"
options={[
{
name: 'Local',
value: 'local',
},
{
name: 'Remote',
value: 'remote',
},
]}
value={httpIpSource}
onChange={(e): void => {
setHttpIpSource(e.target.value as 'local' | 'remote');
}}
/>
{httpIpSource === 'local' ? (
<Input label="Host" value={connectionUrl} disabled />
) : (
<Input label="Host" />
)}
<Checkbox label="Use TLS?" />
</>
)}
{selectedConnType === connType.BLE && (
<div>
<div className="flex space-x-2">
<Button border onClick={updateBleDeviceList}>
Refresh List
</Button>
<Button
border
onClick={async (): Promise<void> => {
await ble.getDevice();
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{bleDevices.map((device, index) => (
<div
onClick={async (): Promise<void> => {
await connect(connType.BLE, {
device: device,
});
}}
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">{device.name}</div>
<IconButton
onClick={async (): Promise<void> => {
await connect(connType.BLE, {
device: device,
});
}}
icon={<FiCheck />}
/>
</div>
))}
</div>
</div>
)}
{selectedConnType === connType.SERIAL && (
<div>
<div className="flex space-x-2">
<Button border onClick={updateSerialDeviceList}>
Refresh List
</Button>
<Button
border
onClick={async (): Promise<void> => {
console.log(await serial.getPort());
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{serialDevices.map((device, index) => (
<div
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">
{device.getInfo().usbProductId}
{device.getInfo().usbVendorId}
</div>
<IconButton
onClick={async (): Promise<void> => {
await connect(connType.SERIAL, {
// @ts-ignore tmp
device: device,
});
}}
icon={<FiCheck />}
/>
<JSONPretty data={device.getInfo()} />
</div>
))}
</div>
</div>
)}
</form>
) : (
<div>
<DeviceStatus />
</div>
)}
</div>
</Card>
</Modal>
);
};

9
src/components/LoraConfig.tsx

@ -2,14 +2,13 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Loading } from '@components/generic/Loading';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { connection } from '../core/connection';
import { Card } from './generic/Card';
import { Checkbox } from './generic/form/Checkbox';
import { Input } from './generic/form/Input';
export interface LoraConfigProps {
channel: Protobuf.Channel;
}

37
src/components/chat/MessageBar.tsx

@ -3,30 +3,42 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { FiSend } from 'react-icons/fi';
import { ackMessage } from '@app/core/slices/meshtasticSlice';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { ackMessage } from '@core/slices/meshtasticSlice';
import { Select } from '../generic/form/Select';
import { IconButton } from '../generic/IconButton';
export const MessageBar = (): JSX.Element => {
export interface MessageBarProps {
channelIndex: number;
}
export const MessageBar = ({ channelIndex }: MessageBarProps): JSX.Element => {
const dispatch = useAppDispatch();
const ready = useAppSelector((state) => state.meshtastic.ready);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo);
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware,
).myNodeNum;
const [currentMessage, setCurrentMessage] = React.useState('');
const [destinationNode, setDestinationNode] =
React.useState<number>(0xffffffff);
const sendMessage = (): void => {
if (ready) {
void connection.sendText(currentMessage, destinationNode, true, (id) => {
dispatch(ackMessage(id));
void connection.sendText(
currentMessage,
destinationNode,
true,
channelIndex--,
(id) => {
dispatch(ackMessage({ channel: 0, messageId: id }));
return Promise.resolve();
});
return Promise.resolve();
},
);
setCurrentMessage('');
}
};
@ -51,14 +63,11 @@ export const MessageBar = (): JSX.Element => {
value: 0xffffffff,
},
...nodes
.filter((node) => node.num !== myNodeInfo.myNodeNum)
.filter((node) => node.number !== myNodeNum)
.map((node) => {
const user = users.filter(
(user) => user.packet.from === node.num,
)[0]?.data;
return {
name: user ? user.shortName : node.num,
value: node.num,
name: node.user ? node.user.shortName : node.number,
value: node.number,
};
}),
]}

4
src/components/generic/Blur.tsx

@ -15,14 +15,14 @@ export const Blur = ({
return (
<div
className={`absolute inset-0 z-20 w-full h-full transition-opacity ${
disableOnMd ? 'md:hidden' : 'test'
disableOnMd ? 'md:hidden' : ''
} ${className}`}
{...props}
>
<div
onClick={onClick}
className={`absolute inset-0 w-full h-full backdrop-filter backdrop-blur-sm ${
disableOnMd ? 'md:hidden' : 'test'
disableOnMd ? 'md:hidden' : ''
}`}
tabIndex={0}
></div>

6
src/components/generic/Button.tsx

@ -9,6 +9,7 @@ interface ButtonProps extends DefaultButtonProps {
circle?: boolean;
active?: boolean;
border?: boolean;
padding?: number;
confirmAction?: () => void;
}
@ -21,6 +22,7 @@ export const Button = ({
confirmAction,
disabled,
children,
padding = 3,
...props
}: ButtonProps): JSX.Element => {
const [hasConfirmed, setHasConfirmed] = React.useState(false);
@ -43,7 +45,9 @@ export const Button = ({
className={`items-center select-none flex dark:text-white active:scale-95 transition duration-200 ease-in-out ${
active && !disabled ? 'bg-gray-100 dark:bg-gray-700' : ''
} ${
circle ? 'rounded-full h-10 w-10' : 'rounded-md p-3 space-x-3 text-sm'
circle
? 'rounded-full h-10 w-10'
: `rounded-md p-${padding} space-x-3 text-sm`
} ${
disabled
? 'cursor-not-allowed dark:bg-primaryDark bg-white'

2
src/components/generic/Card.tsx

@ -24,7 +24,7 @@ export const Card = ({
}: CardProps): JSX.Element => {
return (
<div
className={`relative flex flex-col flex-auto dark:text-white border-y md:border shadow-md select-none dark:bg-primaryDark border-gray-300 dark:border-transparent md:rounded-md ${className}`}
className={`relative flex flex-col flex-auto dark:text-white border-y md:border shadow-md select-none bg-white dark:bg-primaryDark border-gray-300 dark:border-gray-600 md:rounded-md ${className}`}
{...props}
>
{loading && <Loading />}

37
src/components/generic/Modal.tsx

@ -0,0 +1,37 @@
import type React from 'react';
import { useAppSelector } from '@app/hooks/redux';
import { Dialog } from '@headlessui/react';
export interface ModalProps {
children: React.ReactNode;
open: boolean;
onClose: () => void;
}
export const Modal = ({ children, open, onClose }: ModalProps): JSX.Element => {
const darkMode = useAppSelector((state) => state.app.darkMode);
return (
<>
<Dialog
as="div"
className={`fixed inset-0 z-30 ${darkMode ? 'dark' : ''}`}
open={open}
onClose={onClose}
>
<Dialog.Overlay className="fixed w-full h-full backdrop-filter backdrop-blur-sm" />
<div className="text-center ">
<span
className="inline-block h-screen align-middle "
aria-hidden="true"
>
&#8203;
</span>
<div className="inline-block w-full max-w-3xl align-middle">
{children}
</div>
</div>
</Dialog>
</>
);
};

2
src/components/generic/StatCard.tsx

@ -7,7 +7,7 @@ export interface StatCardProps {
export const StatCard = ({ title, value }: StatCardProps): JSX.Element => {
return (
<div className="w-full border-gray-300 shadow-md border-y md:border h-28 md:rounded-3xl dark:bg-primaryDark dark:border-transparent ">
<div className="w-full bg-white border-gray-300 shadow-md select-none dark:text-white border-y md:border dark:bg-primaryDark dark:border-gray-600 md:rounded-md ">
<div className="m-4">
<div className="text-lg font-light">{title}</div>
<div className="text-3xl font-bold">{value}</div>

38
src/components/menu/buttons/DeviceStatusDropdown.tsx → src/components/menu/buttons/DeviceStatus.tsx

@ -2,20 +2,28 @@ import type React from 'react';
import { FiWifi, FiWifiOff } from 'react-icons/fi';
import { useAppSelector } from '@app/hooks/redux';
import { IconButton } from '@components/generic/IconButton';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { openConnectionModal } from '@core/slices/appSlice';
import { Types } from '@meshtastic/meshtasticjs';
export const DeviceStatusDropdown = (): JSX.Element => {
export const DeviceStatus = (): JSX.Element => {
const dispatch = useAppDispatch();
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus);
const ready = useAppSelector((state) => state.meshtastic.ready);
return (
<div className="flex bg-gray-100 rounded-md dark:bg-gray-700">
<div className="flex pl-2 my-auto space-x-2 dark:text-white">
<Button
padding={0}
active
onClick={(): void => {
dispatch(dispatch(openConnectionModal()));
}}
>
<div className="flex gap-2 px-2">
<div
className={`
my-auto mx-2 w-2 h-2 rounded-full min-w-[2] ${
my-auto w-2 h-2 rounded-full min-w-[2] ${
[
Types.DeviceStatusEnum.DEVICE_CONNECTED,
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
@ -31,16 +39,14 @@ export const DeviceStatusDropdown = (): JSX.Element => {
}`}
></div>
<div className="my-auto">{Types.DeviceStatusEnum[deviceStatus]}</div>
<IconButton
icon={
ready ? (
<FiWifi className="w-5 h-5" />
) : (
<FiWifiOff className="w-5 h-5 animate-pulse" />
)
}
/>
<div className="py-2">
{ready ? (
<FiWifi className="w-5 h-5" />
) : (
<FiWifiOff className="w-5 h-5 animate-pulse" />
)}
</div>
</div>
</div>
</Button>
);
};

4
src/components/pwa/ReloadPrompt.tsx

@ -6,7 +6,7 @@ import type React from 'react';
// eslint-disable-next-line import/no-unresolved
import { useRegisterSW } from 'virtual:pwa-register/react';
const ReloadPrompt = (): JSX.Element => {
export const ReloadPrompt = (): JSX.Element => {
const {
offlineReady: [offlineReady, setOfflineReady],
needRefresh: [needRefresh, setNeedRefresh],
@ -58,5 +58,3 @@ const ReloadPrompt = (): JSX.Element => {
</div>
);
};
export default ReloadPrompt;

103
src/core/connection.ts

@ -1,16 +1,119 @@
import {
addChannel,
addMessage,
addNode,
addPosition,
addUser,
setDeviceStatus,
setLastMeshInterraction,
setMyNodeInfo,
setPreferences,
setReady,
} from '@core/slices/meshtasticSlice';
import { store } from '@core/store';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
Types,
} from '@meshtastic/meshtasticjs';
type connectionType = IBLEConnection | IHTTPConnection | ISerialConnection;
export let connection: connectionType = new IHTTPConnection();
const state = store.getState().meshtastic;
export const connectionUrl = state.hostOverrideEnabled
? state.hostOverride
: import.meta.env.PROD
? window.location.hostname
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ??
'http://meshtastic.local';
export const ble = new IBLEConnection();
export const serial = new ISerialConnection();
export const setConnection = (conn: connectionType): void => {
cleanupListeners();
connection = conn;
registerListeners();
};
const cleanupListeners = (): void => {
connection.onDeviceStatus.cancelAll();
connection.onMyNodeInfo.cancelAll();
connection.onUserPacket.cancelAll();
connection.onPositionPacket.cancelAll();
connection.onNodeInfoPacket.cancelAll();
connection.onAdminPacket.cancelAll();
connection.onMeshHeartbeat.cancelAll();
connection.onTextPacket.cancelAll();
};
const registerListeners = (): void => {
connection.onDeviceStatus.subscribe((status) => {
store.dispatch(setDeviceStatus(status));
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
store.dispatch(setReady(true));
}
if (status === Types.DeviceStatusEnum.DEVICE_DISCONNECTED) {
store.dispatch(setReady(false));
}
});
connection.onMyNodeInfo.subscribe((nodeInfo) => {
store.dispatch(setMyNodeInfo(nodeInfo));
});
connection.onUserPacket.subscribe((user) => {
store.dispatch(addUser(user));
});
connection.onPositionPacket.subscribe((position) => {
store.dispatch(addPosition(position));
});
connection.onNodeInfoPacket.subscribe(
(nodeInfoPacket): void | { payload: Protobuf.NodeInfo; type: string } => {
store.dispatch(addNode(nodeInfoPacket.data));
},
);
connection.onAdminPacket.subscribe((adminPacket) => {
switch (adminPacket.data.variant.oneofKind) {
case 'getChannelResponse':
store.dispatch(addChannel(adminPacket.data.variant.getChannelResponse));
break;
case 'getRadioResponse':
if (adminPacket.data.variant.getRadioResponse.preferences) {
store.dispatch(
setPreferences(
adminPacket.data.variant.getRadioResponse.preferences,
),
);
}
break;
}
});
connection.onMeshHeartbeat.subscribe(
(date): void | { payload: number; type: string } =>
store.dispatch(setLastMeshInterraction(date.getTime())),
);
connection.onTextPacket.subscribe((message) => {
const myNodeNum = store.getState().meshtastic.radio.hardware.myNodeNum;
store.dispatch(
addMessage({
message: message,
ack: message.packet.from !== myNodeNum,
isSender: message.packet.from === myNodeNum,
received: new Date(message.packet.rxTime),
}),
);
});
};

10
src/core/slices/appSlice.ts

@ -5,12 +5,14 @@ export type currentPageName = 'messages' | 'settings';
interface AppState {
mobileNavOpen: boolean;
connectionModalOpen: boolean;
darkMode: boolean;
currentPage: currentPageName;
}
const initialState: AppState = {
mobileNavOpen: false,
connectionModalOpen: true,
darkMode: localStorage.getItem('darkMode') === 'true' ?? false,
currentPage: 'messages',
};
@ -25,6 +27,12 @@ export const appSlice = createSlice({
closeMobileNav(state) {
state.mobileNavOpen = false;
},
openConnectionModal(state) {
state.connectionModalOpen = true;
},
closeConnectionModal(state) {
state.connectionModalOpen = false;
},
setDarkModeEnabled(state, action: PayloadAction<boolean>) {
localStorage.setItem('darkMode', String(action.payload));
state.darkMode = action.payload;
@ -38,6 +46,8 @@ export const appSlice = createSlice({
export const {
openMobileNav,
closeMobileNav,
openConnectionModal,
closeConnectionModal,
setDarkModeEnabled,
setCurrentPage,
} = appSlice.actions;

151
src/core/slices/meshtasticSlice.ts

@ -2,7 +2,7 @@ import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { connection } from '../connection';
// import { connection } from '../connection';
export interface MessageWithAck {
message: Types.TextPacket;
@ -16,17 +16,31 @@ enum connType {
SERIAL,
}
export interface ChannelData {
channel: Protobuf.Channel;
messages: MessageWithAck[];
}
export interface Node {
number: number;
lastHeard: Date;
snr: number[];
positions: Protobuf.Position[];
user?: Protobuf.User;
}
export interface Radio {
channels: ChannelData[];
preferences: Protobuf.RadioConfig_UserPreferences;
hardware: Protobuf.MyNodeInfo;
}
interface MeshtasticState {
deviceStatus: Types.DeviceStatusEnum;
lastMeshInterraction: number;
ready: boolean;
myNodeInfo: Protobuf.MyNodeInfo;
users: Types.UserPacket[];
positionPackets: Types.PositionPacket[];
nodes: Protobuf.NodeInfo[];
channels: Protobuf.Channel[];
preferences: Protobuf.RadioConfig_UserPreferences;
messages: MessageWithAck[];
nodes: Node[];
radio: Radio;
hostOverrideEnabled: boolean;
hostOverride: string;
connectionType: connType;
@ -36,13 +50,12 @@ const initialState: MeshtasticState = {
deviceStatus: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
lastMeshInterraction: 0,
ready: false,
myNodeInfo: Protobuf.MyNodeInfo.create(),
users: [],
positionPackets: [],
nodes: [],
channels: [],
preferences: Protobuf.RadioConfig_UserPreferences.create(),
messages: [],
radio: {
channels: [],
preferences: Protobuf.RadioConfig_UserPreferences.create(),
hardware: Protobuf.MyNodeInfo.create(),
},
//todo implement
// connectionMethod: localStorage.getItem('connectionMethod'),
hostOverrideEnabled:
@ -65,76 +78,94 @@ export const meshtasticSlice = createSlice({
state.ready = action.payload;
},
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
state.myNodeInfo = action.payload;
state.radio.hardware = action.payload;
},
addUser: (state, action: PayloadAction<Types.UserPacket>) => {
if (
state.users.findIndex(
(user) => user.data.id === action.payload.data.id,
) !== -1
) {
state.users = state.users.map((user) => {
return user.data.id === action.payload.data.id
? action.payload
: user;
});
} else {
state.users.push(action.payload);
const node = state.nodes.find(
(node) => node.number === action.payload.packet.from,
);
if (node) {
node.user = action.payload.data;
// todo: use rx time
node.lastHeard = new Date();
}
},
addPosition: (state, action: PayloadAction<Types.PositionPacket>) => {
if (
state.positionPackets.findIndex(
(position) => position.packet.from === action.payload.packet.from,
) !== -1
) {
state.positionPackets = state.positionPackets.map((position) => {
return position.packet.from === action.payload.packet.from
? action.payload
: position;
});
} else {
state.positionPackets.push(action.payload);
const node = state.nodes.find(
(node) => node.number === action.payload.packet.from,
);
node?.positions.push(action.payload.data);
if (node) {
// todo: use rx time
node.lastHeard = new Date();
}
},
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
if (
state.nodes.findIndex((node) => node.num === action.payload.num) !== -1
) {
state.nodes = state.nodes.map((node) => {
return node.num === action.payload.num ? action.payload : node;
});
const node = state.nodes.find(
(node) => node.number === action.payload.num,
);
if (node) {
console.log('node exists');
node.lastHeard = new Date(action.payload.lastHeard * 1000);
node.snr.push(action.payload.snr);
} else {
state.nodes.push(action.payload);
console.log('node does not exist');
state.nodes.push({
number: action.payload.num,
lastHeard: new Date(action.payload.lastHeard * 1000),
snr: [action.payload.snr],
positions: [],
});
}
},
addChannel: (state, action: PayloadAction<Protobuf.Channel>) => {
if (
state.channels.findIndex(
(channel) => channel.index === action.payload.index,
state.radio.channels.findIndex(
(channel) => channel.channel.index === action.payload.index,
) !== -1
) {
state.channels = state.channels.map((channel) => {
return channel.index === action.payload.index
? action.payload
state.radio.channels = state.radio.channels.map((channel) => {
return channel.channel.index === action.payload.index
? {
channel: action.payload,
messages: channel.messages,
}
: channel;
});
} else {
state.channels.push(action.payload);
state.radio.channels.push({
channel: action.payload,
messages: [],
});
}
},
setPreferences: (
state,
action: PayloadAction<Protobuf.RadioConfig_UserPreferences>,
) => {
state.preferences = action.payload;
state.radio.preferences = action.payload;
},
addMessage: (state, action: PayloadAction<MessageWithAck>) => {
state.messages.push(action.payload);
const channelIndex = state.radio.channels.findIndex(
(channel) =>
channel.channel.index === action.payload.message.packet.channel,
);
state.radio.channels[channelIndex].messages.push(action.payload);
},
ackMessage: (state, messageId: PayloadAction<number>) => {
state.messages.map((message) => {
if (message.message.packet.id === messageId.payload) {
ackMessage: (
state,
action: PayloadAction<{ channel: number; messageId: number }>,
) => {
const channelIndex = state.radio.channels.findIndex(
(channel) => channel.channel.index === action.payload.channel,
);
// todo: update last mesh/user interraction here
state.radio.channels[channelIndex].messages.map((message) => {
if (message.message.packet.id === action.payload.messageId) {
message.ack = true;
}
});
@ -143,21 +174,21 @@ export const meshtasticSlice = createSlice({
state.hostOverrideEnabled = action.payload;
localStorage.setItem('hostOverrideEnabled', String(action.payload));
if (state.hostOverrideEnabled !== action.payload) {
connection.disconnect();
// connection.disconnect();
}
},
setHostOverride: (state, action: PayloadAction<string>) => {
state.hostOverride = action.payload;
localStorage.setItem('hostOverride', action.payload);
if (state.hostOverride !== action.payload) {
connection.disconnect();
// connection.disconnect();
}
},
setConnectionType: (state, action: PayloadAction<connType>) => {
state.connectionType = action.payload;
localStorage.setItem('connectionType', String(action.payload));
if (state.connectionType !== action.payload) {
connection.disconnect();
// connection.disconnect();
}
},
},

5
src/index.tsx

@ -6,12 +6,11 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { App } from '@app/App';
import { ReloadPrompt } from '@components/pwa/ReloadPrompt';
import { RouteProvider } from '@core/router';
import { store } from '@core/store';
import App from './App';
import ReloadPrompt from './components/pwa/ReloadPrompt';
ReactDOM.render(
<React.StrictMode>
<RouteProvider>

34
src/pages/Messages.tsx

@ -1,4 +1,4 @@
import type React from 'react';
import React from 'react';
import { FiHash } from 'react-icons/fi';
@ -10,9 +10,9 @@ import { Protobuf } from '@meshtastic/meshtasticjs';
import { useAppSelector } from '../hooks/redux';
export const Messages = (): JSX.Element => {
const messages = useAppSelector((state) => state.meshtastic.messages);
const users = useAppSelector((state) => state.meshtastic.users);
const channels = useAppSelector((state) => state.meshtastic.channels);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
const [channelIndex, setChannelIndex] = React.useState(0);
return (
<div className="flex flex-col w-full">
@ -23,25 +23,28 @@ export const Messages = (): JSX.Element => {
options={channels
.filter(
(channel) =>
channel.role !== Protobuf.Channel_Role.DISABLED &&
channel.settings?.name !== 'admin',
channel.channel.role !== Protobuf.Channel_Role.DISABLED &&
channel.channel.settings?.name !== 'admin',
)
.map((channel) => {
return {
name: channel.settings?.name.length
? channel.settings.name
: channel.role === Protobuf.Channel_Role.PRIMARY
name: channel.channel.settings?.name.length
? channel.channel.settings.name
: channel.channel.role === Protobuf.Channel_Role.PRIMARY
? 'Primary'
: `CH: ${channel.index}`,
value: channel.index,
: `CH: ${channel.channel.index}`,
value: channel.channel.index,
};
})}
onChange={(e): void => {
setChannelIndex(parseInt(e.target.value));
}}
small
/>
</div>
</div>
<div className="flex flex-col flex-grow p-6 space-y-2 overflow-y-auto bg-white border-b border-gray-300 md:py-8 md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
{messages.map((message, index) => (
{channels[channelIndex]?.messages.map((message, index) => (
<Message
key={index}
isSender={message.isSender}
@ -49,14 +52,13 @@ export const Messages = (): JSX.Element => {
ack={message.ack}
rxTime={new Date()}
senderName={
users.find(
(user) => user.packet.from === message.message.packet.from,
)?.data.longName ?? 'UNK'
nodes.find((node) => node.number === message.message.packet.from)
?.user?.longName ?? 'UNK'
}
/>
))}
</div>
<MessageBar />
<MessageBar channelIndex={channelIndex} />
</div>
);
};

17
src/pages/Nodes/Index.tsx

@ -9,23 +9,26 @@ import { Protobuf } from '@meshtastic/meshtasticjs';
import { Node } from './Node';
export const Nodes = (): JSX.Element => {
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware,
).myNodeNum;
const nodes = useAppSelector((state) => state.meshtastic.nodes).filter(
(node) => node.number !== myNodeNum,
);
return (
<PageLayout
title="Nodes"
emptyMessage="No nodes discovered yet..."
sidebarItems={nodes.map((node) => {
const user = users.find((user) => user.packet.from === node.num)?.data;
return {
title: user ? user.longName : node.num.toString(),
description: user
? Protobuf.HardwareModel[user.hwModel]
title: node.user?.longName ?? node.number.toString(),
description: node.user
? Protobuf.HardwareModel[node.user.hwModel]
: 'Unknown Hardware',
icon: (
<Avatar
size={30}
name={user ? user.longName : node.num.toString()}
name={node.user?.longName ?? node.number.toString()}
variant="beam"
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
/>

40
src/pages/Nodes/Node.tsx

@ -6,33 +6,24 @@ import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import TimeAgo from 'timeago-react';
import { Cover } from '@app/components/generic/Cover';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Cover } from '@components/generic/Cover';
import { IconButton } from '@components/generic/IconButton';
import { StatCard } from '@components/generic/StatCard';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import type { Protobuf } from '@meshtastic/meshtasticjs';
import type { Node as NodeType } from '@core/slices/meshtasticSlice';
export interface NodeProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
node: Protobuf.NodeInfo;
node: NodeType;
}
export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
const user = useAppSelector((state) => state.meshtastic.users).find(
(user) => user.packet.from === node.num,
)?.data;
const position = useAppSelector(
(state) => state.meshtastic.positionPackets,
).find((position) => position.packet.from === node.num)?.data;
const [debug, setDebug] = React.useState(false);
return (
<PrimaryTemplate
title={user ? user.longName : node.num.toString()}
title={node.user?.longName ?? node.number.toString()}
tagline="Node"
leftButton={
<IconButton
@ -51,38 +42,21 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
}}
/>
}
footer={<></>}
>
<div className="w-full space-y-4">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<StatCard
title="Last heard"
value={
node.lastHeard ? (
<TimeAgo datetime={new Date(node.lastHeard * 1000)} />
) : (
'Never'
)
node.lastHeard ? <TimeAgo datetime={node.lastHeard} /> : 'Never'
}
/>
<StatCard title="SNR" value={node.snr.toString()} />
</div>
<Card
title="Position"
description={new Date(node.lastHeard * 1000).toLocaleString()}
>
<Card title="Position" description={node.lastHeard.toLocaleString()}>
<Cover enabled={debug} content={<JSONPretty data={node} />} />
<div className="p-10">
<JSONPretty data={position} />
</div>
</Card>
<Card title="Settings" description="Remote node settings">
<div className="p-10">
<form className="space-y-4">
<Input label="Device Name" />
<Input label="Short Name" maxLength={3} />
<Checkbox label="Licenced Operator?" />
</form>
<JSONPretty data={node.positions[node.positions.length - 1]} />
</div>
</Card>
</div>

21
src/pages/Plugins/ExternalNotification.tsx

@ -1,16 +1,16 @@
import type React from 'react';
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface ExternalNotificationProps {
@ -22,7 +22,9 @@ export const ExternalNotification = ({
navOpen,
setNavOpen,
}: ExternalNotificationProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
@ -39,6 +41,19 @@ export const ExternalNotification = ({
},
});
React.useEffect(() => {
reset({
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell:
preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});

22
src/pages/Plugins/Files.tsx

@ -4,11 +4,11 @@ import type React from 'react';
import { FiMenu, FiTrash, FiUploadCloud } from 'react-icons/fi';
import useSWR from 'swr';
import fetcher from '@app/core/utils/fetcher';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connectionUrl } from '@core/connection';
import fetcher from '@core/utils/fetcher';
export interface RangeTestProps {
navOpen?: boolean;
@ -33,20 +33,8 @@ interface IFiles {
}
export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
const hostOverrideEnabled = useAppSelector(
(state) => state.meshtastic.hostOverrideEnabled,
);
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const connectionURL = hostOverrideEnabled
? hostOverride
: import.meta.env.PROD
? window.location.hostname
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ??
'http://meshtastic.local';
const { data } = useSWR<IFiles>(
`http://${connectionURL}/json/spiffs/browse/static`,
`http://${connectionUrl}/json/spiffs/browse/static`,
fetcher,
);
@ -107,7 +95,7 @@ export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
/> */}
</div>
<a
href={`http://${connectionURL}/${file.name.replace(
href={`http://${connectionUrl}/${file.name.replace(
'static/',
'',
)}`}
@ -120,7 +108,7 @@ export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => {
className="mx-2 my-auto"
// confirmAction={async (): Promise<void> => {
// await fetch(
// `http://${connectionURL}/json/spiffs/delete/static?remove=${file.name}`,
// `http://${connectionUrl}/json/spiffs/delete/static?remove=${file.name}`,
// {
// method: 'DELETE',
// },

18
src/pages/Plugins/RangeTest.tsx

@ -1,16 +1,16 @@
import type React from 'react';
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { FormFooter } from '@components/FormFooter';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface RangeTestProps {
@ -22,7 +22,9 @@ export const RangeTest = ({
navOpen,
setNavOpen,
}: RangeTestProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
@ -33,6 +35,14 @@ export const RangeTest = ({
},
});
React.useEffect(() => {
reset({
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});

21
src/pages/Plugins/Serial.tsx

@ -1,16 +1,16 @@
import type React from 'react';
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { FormFooter } from '@components/FormFooter';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface SerialProps {
@ -19,7 +19,9 @@ export interface SerialProps {
}
export const Serial = ({ navOpen, setNavOpen }: SerialProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
@ -33,6 +35,17 @@ export const Serial = ({ navOpen, setNavOpen }: SerialProps): JSX.Element => {
},
});
React.useEffect(() => {
reset({
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTimeout: preferences.serialpluginTimeout,
serialpluginTxd: preferences.serialpluginTxd,
});
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});

14
src/pages/settings/Channels.tsx

@ -3,15 +3,15 @@ import React from 'react';
import { FiCode, FiMenu, FiSave } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { LoraConfig } from '@app/components/LoraConfig';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Channel } from '@components/Channel';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { IconButton } from '@components/generic/IconButton';
import { LoraConfig } from '@components/LoraConfig';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
export interface ChannelsProps {
navOpen?: boolean;
@ -22,7 +22,7 @@ export const Channels = ({
navOpen,
setNavOpen,
}: ChannelsProps): JSX.Element => {
const channels = useAppSelector((state) => state.meshtastic.channels);
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
const [debug, setDebug] = React.useState(false);
return (
@ -58,15 +58,15 @@ export const Channels = ({
}
>
<div className="space-y-4">
{channels[0] && <LoraConfig channel={channels[0]} />}
{channels[0] && <LoraConfig channel={channels[0].channel} />}
<Card>
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
<div className="w-full p-4 space-y-2 md:p-10">
{channels.map((channel) => (
<Channel
key={channel.index}
channel={channel}
hideEnabled={channel.index === 0}
key={channel.channel.index}
channel={channel.channel}
hideEnabled={channel.channel.index === 0}
/>
))}

271
src/pages/settings/Connection.tsx

@ -1,271 +0,0 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { FiCheck, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { ble, connection, serial, setConnection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import type {
BLEConnectionParameters,
HTTPConnectionParameters,
SerialConnectionParameters,
} from '@meshtastic/meshtasticjs/dist/types';
export interface ConnectionProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
enum connType {
HTTP,
BLE,
SERIAL,
}
export const Connection = ({
navOpen,
setNavOpen,
}: ConnectionProps): JSX.Element => {
const [selectedConnType, setSelectedConnType] = React.useState(connType.HTTP);
const [bleDevices, setBleDevices] = React.useState<BluetoothDevice[]>([]);
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]);
const [httpIpSource, setHttpIpSource] = React.useState<'local' | 'remote'>(
'local',
);
const hostOverrideEnabled = useAppSelector(
(state) => state.meshtastic.hostOverrideEnabled,
);
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const { formState, reset } = useForm<{
method: connType;
}>({
defaultValues: {
method: connType.HTTP,
},
});
const connect = async (
connectionType: connType,
params:
| HTTPConnectionParameters
| SerialConnectionParameters
| BLEConnectionParameters,
): Promise<void> => {
connection.complete();
connection.disconnect();
if (connectionType === connType.BLE) {
setConnection(new IBLEConnection());
} else if (connectionType === connType.HTTP) {
setConnection(new IHTTPConnection());
} else {
setConnection(new ISerialConnection());
}
// @ts-ignore tmp
await connection.connect(params);
};
const updateBleDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await ble.getDevices();
setBleDevices(devices);
}, []);
const updateSerialDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await serial.getPorts();
console.log(devices);
setSerialDevices(devices);
}, []);
React.useEffect(() => {
if (selectedConnType === connType.BLE) {
void updateBleDeviceList();
}
if (selectedConnType === connType.SERIAL) {
void updateSerialDeviceList();
}
}, [selectedConnType, updateBleDeviceList, updateSerialDeviceList]);
const connectionURL: string = hostOverrideEnabled
? hostOverride
: import.meta.env.PROD
? window.location.hostname
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ??
'http://meshtastic.local';
return (
<PrimaryTemplate
title="Connection"
tagline="Settings"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={(): void => {
return;
}}
clearAction={reset}
/>
}
>
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2">
<Select
label="Method"
optionsEnum={connType}
value={selectedConnType}
onChange={(e): void => {
setSelectedConnType(parseInt(e.target.value));
}}
/>
{selectedConnType === connType.HTTP && (
<>
<Select
label="Host Source"
options={[
{
name: 'Local',
value: 'local',
},
{
name: 'Remote',
value: 'remote',
},
]}
value={httpIpSource}
onChange={(e): void => {
setHttpIpSource(e.target.value as 'local' | 'remote');
}}
/>
{httpIpSource === 'local' ? (
<Input label="Host" value={connectionURL} disabled />
) : (
<Input label="Host" />
)}
<Checkbox label="Use TLS?" />
</>
)}
{selectedConnType === connType.BLE && (
<div>
<div className="flex space-x-2">
<Button border onClick={updateBleDeviceList}>
Refresh List
</Button>
<Button
border
onClick={async (): Promise<void> => {
await ble.getDevice();
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{bleDevices.map((device, index) => (
<div
onClick={async (): Promise<void> => {
console.log('clicked');
await connect(connType.BLE, {
device: device,
});
}}
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">{device.name}</div>
<IconButton
onClick={async (): Promise<void> => {
console.log('clicked');
await connect(connType.BLE, {
device: device,
});
}}
icon={<FiCheck />}
/>
</div>
))}
</div>
</div>
)}
{selectedConnType === connType.SERIAL && (
<div>
<div className="flex space-x-2">
<Button border onClick={updateSerialDeviceList}>
Refresh List
</Button>
<Button
border
onClick={async (): Promise<void> => {
console.log(await serial.getPort());
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{serialDevices.map((device, index) => (
<div
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">
{device.getInfo().usbProductId}
{device.getInfo().usbVendorId}
</div>
<IconButton
onClick={async (): Promise<void> => {
await connect(connType.SERIAL, {
// @ts-ignore tmp
device: device,
});
}}
icon={<FiCheck />}
/>
<JSONPretty data={device.getInfo()} />
</div>
))}
</div>
</div>
)}
</form>
<Button
border
onClick={(): void => {
connection.disconnect();
}}
>
Disconnect
</Button>
</div>
</Card>
</PrimaryTemplate>
);
};

8
src/pages/settings/Index.tsx

@ -3,7 +3,6 @@ import type React from 'react';
import {
FiLayers,
FiLayout,
FiLink2,
FiMapPin,
FiRadio,
FiUser,
@ -14,7 +13,6 @@ import {
import { PageLayout } from '@components/templates/PageLayout';
import { Channels } from './Channels';
import { Connection } from './Connection';
import { Interface } from './Interface';
import { Position } from './Position';
import { Power } from './Power';
@ -24,11 +22,6 @@ import { WiFi } from './WiFi';
export const Settings = (): JSX.Element => {
const sidebarItems = [
{
title: 'Connection',
description: 'Connection method and parameters',
icon: <FiLink2 className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'WiFi',
description: 'WiFi credentials and mode',
@ -70,7 +63,6 @@ export const Settings = (): JSX.Element => {
title="Settings"
sidebarItems={sidebarItems}
panels={[
<Connection key={1} />,
<WiFi key={2} />,
<Position key={3} />,
<User key={4} />,

2
src/pages/settings/Interface.tsx

@ -3,11 +3,11 @@ import type React from 'react';
import { useTranslation } from 'react-i18next';
import { FiMenu, FiSave } from 'react-icons/fi';
import i18n from '@app/core/translation';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Select } from '@components/generic/form/Select';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import i18n from '@core/translation';
export interface InterfaceProps {
navOpen?: boolean;

12
src/pages/settings/Position.tsx

@ -4,9 +4,8 @@ import { useForm } from 'react-hook-form';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { FormFooter } from '@components/FormFooter';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
@ -14,6 +13,7 @@ import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface PositionProps {
@ -25,7 +25,9 @@ export const Position = ({
navOpen,
setNavOpen,
}: PositionProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [debug, setDebug] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset } =
@ -41,6 +43,10 @@ export const Position = ({
},
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {

14
src/pages/settings/Power.tsx

@ -4,15 +4,15 @@ import { useForm } from 'react-hook-form';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { Select } from '@app/components/generic/form/Select';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { FormFooter } from '@components/FormFooter';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface PowerProps {
@ -21,7 +21,9 @@ export interface PowerProps {
}
export const Power = ({ navOpen, setNavOpen }: PowerProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [debug, setDebug] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset } =
@ -32,6 +34,10 @@ export const Power = ({ navOpen, setNavOpen }: PowerProps): JSX.Element => {
},
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {

12
src/pages/settings/Radio.tsx

@ -4,15 +4,15 @@ import { useForm } from 'react-hook-form';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { FormFooter } from '@components/FormFooter';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface RadioProps {
@ -21,7 +21,9 @@ export interface RadioProps {
}
export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [debug, setDebug] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset } =
@ -29,6 +31,10 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {

50
src/pages/settings/User.tsx

@ -5,8 +5,8 @@ import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { base16 } from 'rfc4648';
import { FormFooter } from '@app/components/FormFooter';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { useAppSelector } from '@app/hooks/redux';
import { FormFooter } from '@components/FormFooter';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
@ -15,7 +15,6 @@ import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import { addUser } from '@core/slices/meshtasticSlice';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface UserProps {
@ -26,10 +25,11 @@ export interface UserProps {
export const User = ({ navOpen, setNavOpen }: UserProps): JSX.Element => {
const [debug, setDebug] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const dispatch = useAppDispatch();
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo);
const user = useAppSelector((state) => state.meshtastic.users).find(
(user) => user.packet.from === myNodeInfo.myNodeNum,
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware,
).myNodeNum;
const node = useAppSelector((state) => state.meshtastic.nodes).find(
(node) => node.number === myNodeNum,
);
const { register, handleSubmit, formState, reset } = useForm<{
longName: string;
@ -38,22 +38,34 @@ export const User = ({ navOpen, setNavOpen }: UserProps): JSX.Element => {
team: Protobuf.Team;
}>({
defaultValues: {
longName: user?.data.longName,
shortName: user?.data.shortName,
isLicensed: user?.data.isLicensed,
team: user?.data.team,
longName: node?.user?.longName,
shortName: node?.user?.shortName,
isLicensed: node?.user?.isLicensed,
team: node?.user?.team,
},
});
React.useEffect(() => {
reset({
longName: node?.user?.longName,
shortName: node?.user?.shortName,
isLicensed: node?.user?.isLicensed,
team: node?.user?.team,
});
}, [reset, node]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
// TODO: can be removed once getUser is implemented
if (user) {
void connection.setOwner({ ...user.data, ...data }, async () => {
if (node?.user) {
void connection.setOwner({ ...node.user, ...data }, async () => {
await Promise.resolve();
setLoading(false);
});
dispatch(addUser({ ...user, ...{ data: { ...user.data, ...data } } }));
// TODO: can be removed once getUser is implemented
// dispatch(
// addUser({ ...node.user, ...{ data: { ...node.user.data, ...data } } }),
// );
}
});
@ -87,15 +99,15 @@ export const User = ({ navOpen, setNavOpen }: UserProps): JSX.Element => {
}
>
<Card loading={loading}>
<Cover enabled={debug} content={<JSONPretty data={user?.data} />} />
<Cover enabled={debug} content={<JSONPretty data={node?.user} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Input label="Device ID" value={user?.data.id} disabled />
<Input label="Device ID" value={node?.user?.id} disabled />
<Input
label="Hardware"
value={
Protobuf.HardwareModel[
user?.data.hwModel ?? Protobuf.HardwareModel.UNSET
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
]
}
disabled
@ -104,7 +116,7 @@ export const User = ({ navOpen, setNavOpen }: UserProps): JSX.Element => {
label="Mac Address"
defaultValue={
base16
.stringify(user?.data.macaddr ?? [])
.stringify(node?.user?.macaddr ?? [])
.match(/.{1,2}/g)
?.join(':') ?? ''
}

14
src/pages/settings/WiFi.tsx

@ -5,15 +5,15 @@ import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { Checkbox } from '@app/components/generic/form/Checkbox';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { FormFooter } from '@components/FormFooter';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface WiFiProps {
@ -23,7 +23,9 @@ export interface WiFiProps {
export const WiFi = ({ navOpen, setNavOpen }: WiFiProps): JSX.Element => {
const { t } = useTranslation();
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [debug, setDebug] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset, control } =
@ -37,6 +39,10 @@ export const WiFi = ({ navOpen, setNavOpen }: WiFiProps): JSX.Element => {
defaultValue: false,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {

5
todo.txt

@ -1,17 +1,14 @@
Add desctiptions to form elements (below on mobile, to the right on desktop)
full width form elements on channel manager, don't use deprecated `modemConfig`
add default value to undefined protobufs, (omit if default to keep them small (only for ota packets))
add input validation min,max etc
change ch type select to disable, make primary and delete buttons
maybe make channel editor acordion?
add url routing for settings tabs
form not updated if rendered before store populated, populated on remount, also disabled fields will remain disabled even if there disable props provided by redux are true, required toggling
add loading blur to card (prop)
form still considered dirty after save
form prefix should be located in the input (absolute?)
form suffix should focus input
reset store on new connection
redux actions seem to be dispatched twice
meshtastic.js
- either extrapolate user and position out of nodeInfo, or fire off events for all position and user packets even when contained in a nodeInfo packet, decide how to fire events for nodeInfo, wether we merge it or do something else
- fix entering device-reconnecting state and not re-connecting despite packets being received
Loading…
Cancel
Save