Browse Source

Merge branch 'master' into switch-rsbuild-to-vite

pull/417/head
Tom Fifield 1 year ago
committed by GitHub
parent
commit
0b7bdda4bf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 48
      .github/workflows/format.yml
  2. 1
      package.json
  3. 18
      pnpm-lock.yaml
  4. 5
      public/devices/README.md
  5. 1
      public/devices/diy.svg
  6. 1
      public/devices/heltec-ht62-esp32c3-sx1262.svg
  7. 1
      public/devices/heltec-mesh-node-t114-case.svg
  8. 1
      public/devices/heltec-mesh-node-t114.svg
  9. 1
      public/devices/heltec-v3-case.svg
  10. 1
      public/devices/heltec-v3.svg
  11. 1
      public/devices/heltec-vision-master-e213.svg
  12. 1
      public/devices/heltec-vision-master-e290.svg
  13. 1
      public/devices/heltec-vision-master-t190.svg
  14. 1
      public/devices/heltec-wireless-paper-V1_0.svg
  15. 1
      public/devices/heltec-wireless-paper.svg
  16. 1
      public/devices/heltec-wireless-tracker-V1-0.svg
  17. 1
      public/devices/heltec-wireless-tracker.svg
  18. 1
      public/devices/heltec-wsl-v3.svg
  19. 1
      public/devices/nano-g2-ultra.svg
  20. 2956
      public/devices/pico.svg
  21. 1
      public/devices/promicro.svg
  22. 1
      public/devices/rak-wismeshtap.svg
  23. 2339
      public/devices/rak11310.svg
  24. 1
      public/devices/rak2560.svg
  25. 3514
      public/devices/rak4631.svg
  26. 1
      public/devices/rak4631_case.svg
  27. 1
      public/devices/rpipicow.svg
  28. 1
      public/devices/seeed-sensecap-indicator.svg
  29. 1
      public/devices/seeed-xiao-s3.svg
  30. 1
      public/devices/station-g2.svg
  31. 1
      public/devices/t-deck.svg
  32. 1
      public/devices/t-echo.svg
  33. 1
      public/devices/t-watch-s3.svg
  34. 1
      public/devices/tbeam-s3-core.svg
  35. 1
      public/devices/tbeam.svg
  36. 1
      public/devices/tlora-c6.svg
  37. 1
      public/devices/tlora-t3s3-epaper.svg
  38. 1
      public/devices/tlora-t3s3-v1.svg
  39. 1
      public/devices/tlora-v2-1-1_6.svg
  40. 1
      public/devices/tlora-v2-1-1_8.svg
  41. 1
      public/devices/tracker-t1000-e.svg
  42. 160
      public/devices/unknown.svg
  43. 1
      public/devices/wio-tracker-wm1110.svg
  44. 1
      public/devices/wm1110_dev_kit.svg
  45. 8
      src/components/Dialog/DialogManager.tsx
  46. 62
      src/components/Dialog/LocationResponseDialog.tsx
  47. 163
      src/components/Dialog/NodeDetailsDialog.tsx
  48. 117
      src/components/Dialog/NodeOptionsDialog.tsx
  49. 57
      src/components/Dialog/TracerouteResponseDialog.tsx
  50. 20
      src/components/PageComponents/Connect/HTTP.tsx
  51. 18
      src/components/PageComponents/Map/NodeDetail.tsx
  52. 34
      src/components/PageComponents/Messages/ChannelChat.tsx
  53. 122
      src/components/PageComponents/Messages/Message.tsx
  54. 32
      src/components/PageComponents/Messages/MessageInput.tsx
  55. 48
      src/components/PageComponents/Messages/TraceRoute.tsx
  56. 44
      src/components/UI/Accordion.tsx
  57. 2
      src/components/UI/Button.tsx
  58. 2
      src/components/UI/Dialog.tsx
  59. 2
      src/components/UI/Toast.tsx
  60. 9
      src/components/UI/Tooltip.tsx
  61. 53
      src/components/generic/DeviceImage.tsx
  62. 9
      src/components/generic/Table/tmp/TimeAgo.tsx
  63. 66
      src/components/generic/TimeAgo.tsx
  64. 17
      src/components/generic/Uptime.tsx
  65. 22
      src/core/stores/appStore.ts
  66. 5
      src/core/stores/deviceStore.ts
  67. 31
      src/core/utils/string.ts
  68. 229
      src/pages/Map.tsx
  69. 11
      src/pages/Messages.tsx
  70. 114
      src/pages/Nodes.tsx

48
.github/workflows/format.yml

@ -0,0 +1,48 @@
name: Code Formatting
on:
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Biome
run: npm install --global @biomejs/biome
- name: Format with Biome
run: biome format --write .
- name: Check for changes
id: git-check
run: |
git diff --quiet || echo "changes=true" >> $GITHUB_OUTPUT
- name: Commit and push changes
if: steps.git-check.outputs.changes == 'true'
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git add -A
git commit -m "chore: format code"
git push

1
package.json

@ -68,7 +68,6 @@
"react-map-gl": "7.1.9",
"react-qrcode-logo": "^3.0.0",
"rfc4648": "^1.5.4",
"timeago-react": "^3.0.6",
"vite-plugin-node-polyfills": "^0.23.0",
"zustand": "5.0.3"
},

18
pnpm-lock.yaml

@ -113,9 +113,6 @@ importers:
rfc4648:
specifier: ^1.5.4
version: 1.5.4
timeago-react:
specifier: ^3.0.6
version: 3.0.6([email protected])
vite-plugin-node-polyfills:
specifier: ^0.23.0
version: 0.23.0([email protected])([email protected](@types/[email protected])([email protected])([email protected]))
@ -2860,14 +2857,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
[email protected]:
resolution: {integrity: sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ==}
peerDependencies:
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
[email protected]:
resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==}
[email protected]:
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
engines: {node: '>=0.6.0'}
@ -6629,13 +6618,6 @@ snapshots:
dependencies:
readable-stream: 3.6.2
[email protected]([email protected]):
dependencies:
react: 19.0.0
timeago.js: 4.0.2
[email protected]: {}
[email protected]:
dependencies:
setimmediate: 1.0.5

5
public/devices/README.md

@ -0,0 +1,5 @@
# Copyright Notice
Copyright © 2024 Meshtastic LLC. All Rights Reserved.
## In reference to the GNU GPLv3 License terms defined in Section 7e
Images (or assets) in this directory are protected under international copyright laws and treaties. Unauthorized reproduction, distribution, modification, or use of these images in any form, commercial or otherwise, outside of official Meshtastic creative works or its Backers and Partners is strictly prohibited without prior written consent from the copyright holder (Meshtastic LLC).

1
public/devices/diy.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 89 KiB

1
public/devices/heltec-ht62-esp32c3-sx1262.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 62 KiB

1
public/devices/heltec-mesh-node-t114-case.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="795.27 277.13 409.46 1319.35"><defs><style>.cls-1{fill:#353535;}.cls-2{fill:#1e1e1d;}.cls-3{fill:#b1a368;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8,.cls-9{fill:none;}.cls-4,.cls-6{stroke:#050606;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8{stroke-miterlimit:10;}.cls-4{stroke-width:2.41px;}.cls-5{fill:#30c2db;}.cls-6{stroke-width:3.91px;}.cls-7{fill:#dcf0f2;}.cls-10,.cls-11,.cls-8{stroke:#dcf0f2;}.cls-8{stroke-width:1.81px;}.cls-9{stroke:#17afbf;stroke-linecap:round;stroke-linejoin:round;stroke-width:7.23px;}.cls-10{stroke-width:1.78px;}.cls-11{stroke-width:1.81px;}</style></defs><g id="Layer_7" data-name="Layer 7"><path class="cls-1" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-2" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-2" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-3" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-3" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-3" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-3" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-3" x="885.8" y="715.74" width="84.14" height="7.28"></rect><rect class="cls-4" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-4" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-4" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-4" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-4" x="885.8" y="715.74" width="84.14" height="7.28"></rect><path class="cls-4" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-4" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-4" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-5" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><rect class="cls-1" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-6" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-7" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-7" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><circle class="cls-8" cx="841.7" cy="1537.01" r="16.25"></circle><circle class="cls-8" cx="841.7" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="1504.51" r="16.25"></circle><line class="cls-9" x1="942.51" y1="1592.42" x2="942.51" y2="1381.55"></line><line class="cls-9" x1="966.52" y1="1592.42" x2="966.52" y2="1381.55"></line><line class="cls-9" x1="990.57" y1="1592.42" x2="990.57" y2="1381.55"></line><line class="cls-9" x1="1014.59" y1="1592.42" x2="1014.59" y2="1381.55"></line><line class="cls-9" x1="1038.63" y1="1592.42" x2="1038.63" y2="1381.55"></line><line class="cls-9" x1="1062.65" y1="1592.42" x2="1062.65" y2="1381.55"></line><rect class="cls-4" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><path class="cls-10" d="M1040.1,947.74H960.65A13.93,13.93,0,0,1,947,936.64l-10.23-49.2a13.93,13.93,0,0,1,13.64-16.77h97.72a13.93,13.93,0,0,1,13.75,16.18l-8,49.2A13.94,13.94,0,0,1,1040.1,947.74Z"></path><rect class="cls-11" x="816.35" y="870.67" width="365.51" height="703.12" rx="32.37"></rect><rect class="cls-11" x="888.77" y="963.84" width="223.2" height="374.66" rx="25.21"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

1
public/devices/heltec-mesh-node-t114.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

1
public/devices/heltec-v3-case.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="404.68 390.65 1217.15 959.26"><defs><style>.cls-1{fill:#dfeaf7;}.cls-2{fill:#17907f;}.cls-3{fill:#2b2b2b;}.cls-4,.cls-5{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-4{stroke-width:2.25px;}.cls-5{stroke-width:4px;}.cls-6{fill:#050606;}</style></defs><g id="Layer_5" data-name="Layer 5"><path class="cls-1" d="M1517.73,392.65h0a102.1,102.1,0,0,0-102.1,102.1V770.37A40.62,40.62,0,0,1,1375,811H455.16a48.49,48.49,0,0,0-48.48,48.48v126a11.85,11.85,0,0,0,3.46,8.37l15.34,15.34a11.81,11.81,0,0,1,3.47,8.37v137.16a11.81,11.81,0,0,1-3.47,8.37l-15.34,15.34a11.85,11.85,0,0,0-3.46,8.37v112.67a48.49,48.49,0,0,0,48.48,48.49H1571.34a48.51,48.51,0,0,0,48.49-48.5V494.75A102.1,102.1,0,0,0,1517.73,392.65Zm-110.61,815V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><path class="cls-2" d="M1516,439.16c-30.23.91-53.92,26.54-53.92,56.79V770.37A87.11,87.11,0,0,1,1375,857.48H732.31A27.51,27.51,0,0,0,704.8,885v388.93a27.51,27.51,0,0,0,27.51,27.51h828.38a12.7,12.7,0,0,0,12.65-12.65v-794A55.69,55.69,0,0,0,1516,439.16Zm-108.9,768.47V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><rect class="cls-3" x="787.14" y="943.38" width="429.45" height="224.42"></rect><path class="cls-1" d="M1478.6,915.35A54.23,54.23,0,0,0,1386,953.69v254.23a54.23,54.23,0,1,0,108.45,0V953.69A54,54,0,0,0,1478.6,915.35Zm-5.21,292.28a33.14,33.14,0,0,1-66.27,0V954a33.14,33.14,0,0,1,66.27,0Z"></path></g><g id="Layer_2" data-name="Layer 2"><path class="cls-4" d="M1573.34,494.75v794a12.68,12.68,0,0,1-12.65,12.65H732.31a27.51,27.51,0,0,1-27.51-27.51V885a27.51,27.51,0,0,1,27.51-27.51H1375a87.11,87.11,0,0,0,87.11-87.11V496c0-30.25,23.69-55.88,53.92-56.79A55.69,55.69,0,0,1,1573.34,494.75Z"></path><path class="cls-5" d="M410.14,1178.39,425.49,1163a11.78,11.78,0,0,0,3.46-8.35V1017.5a11.8,11.8,0,0,0-3.46-8.35l-15.35-15.36a11.77,11.77,0,0,1-3.46-8.35v-126A48.47,48.47,0,0,1,455.16,811H1375a40.63,40.63,0,0,0,40.63-40.63V494.75a102.1,102.1,0,0,1,102.1-102.1h0a102.1,102.1,0,0,1,102.1,102.1v804.66a48.51,48.51,0,0,1-48.49,48.5H455.16a48.48,48.48,0,0,1-48.48-48.49V1186.74A11.78,11.78,0,0,1,410.14,1178.39Z"></path><rect class="cls-4" x="1407.12" y="920.85" width="66.26" height="319.9" rx="33.13"></rect><rect class="cls-4" x="1386.03" y="899.46" width="108.46" height="362.69" rx="54.23"></rect><path class="cls-6" d="M639.76,1070.55a2.91,2.91,0,0,1-2.91-2.91v-30.53a5.42,5.42,0,0,0-1.6-3.86l-32.44-32.44a11.86,11.86,0,0,1-3.5-8.44V901a12.52,12.52,0,0,0-12.51-12.51H483.92a12.7,12.7,0,0,0-12.68,12.69v76.78a24.13,24.13,0,0,0,7.11,17.18l14.33,14.33a24.13,24.13,0,0,0,17.18,7.11h50.75a11.86,11.86,0,0,1,8.44,3.5l24.26,24.26a12.47,12.47,0,0,1,3.68,8.88v14.46a2.91,2.91,0,1,1-5.81,0v-14.46a6.72,6.72,0,0,0-2-4.77l-24.26-24.26a6.09,6.09,0,0,0-4.33-1.8H509.86a29.91,29.91,0,0,1-21.29-8.81l-14.33-14.33a29.87,29.87,0,0,1-8.81-21.29V901.14a18.51,18.51,0,0,1,18.49-18.5H586.8A18.34,18.34,0,0,1,605.12,901v91.41a6.09,6.09,0,0,0,1.8,4.33l32.44,32.44a11.19,11.19,0,0,1,3.3,8v30.53A2.9,2.9,0,0,1,639.76,1070.55Z"></path><path class="cls-6" d="M586.8,1289.46H483.92a18.51,18.51,0,0,1-18.49-18.5v-76.78a29.87,29.87,0,0,1,8.81-21.29l14.33-14.34a29.91,29.91,0,0,1,21.29-8.81h50.75a6.09,6.09,0,0,0,4.33-1.8l24.26-24.25a6.74,6.74,0,0,0,2-4.78v-14.46a2.91,2.91,0,0,1,5.81,0v14.46a12.51,12.51,0,0,1-3.68,8.89l-24.26,24.25a11.86,11.86,0,0,1-8.44,3.5H509.86a24.17,24.17,0,0,0-17.18,7.11L478.35,1177a24.09,24.09,0,0,0-7.11,17.18V1271a12.71,12.71,0,0,0,12.68,12.69H586.8a12.53,12.53,0,0,0,12.51-12.52v-91.4a11.83,11.83,0,0,1,3.5-8.44l32.44-32.44a5.46,5.46,0,0,0,1.6-3.87v-30.53a2.91,2.91,0,0,1,5.81,0V1135a11.19,11.19,0,0,1-3.3,8l-32.44,32.45a6.06,6.06,0,0,0-1.8,4.33v91.4A18.35,18.35,0,0,1,586.8,1289.46Z"></path><rect class="cls-4" x="787.14" y="943.38" width="429.45" height="224.42"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

1
public/devices/heltec-v3.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

1
public/devices/heltec-vision-master-e213.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="528.89 806.04 942.22 446.84"><defs><style>.cls-1{fill:#e8eae8;}.cls-2{fill:#dbdddb;}.cls-3{fill:#c6842a;}.cls-4,.cls-5,.cls-7,.cls-9{fill:none;stroke-miterlimit:10;}.cls-4,.cls-5{stroke:#050606;}.cls-4,.cls-9{stroke-width:2.44px;}.cls-5,.cls-7{stroke-width:1.22px;}.cls-6{fill:#b7b7b7;}.cls-7,.cls-9{stroke:#b7b7b7;}.cls-8{fill:#cbcccb;}.cls-10{fill:#434543;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43H549.53a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42h900.94A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-2" d="M574.23,807.26v429.79h-24.7a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42Z"></path><path class="cls-2" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43h-37.56V807.26h37.56A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-3" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-4" x="530.11" y="807.26" width="939.78" height="429.79" rx="19.42"></rect><line class="cls-5" x1="574.23" y1="807.26" x2="574.23" y2="1237.05"></line><path class="cls-5" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-6" x="599.01" y="970.1" width="11.52" height="104.11"></rect><rect class="cls-5" x="599.01" y="970.1" width="11.52" height="104.11"></rect><path class="cls-2" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-1" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><line class="cls-7" x1="666.4" y1="1132.79" x2="666.4" y2="1201.53"></line><line class="cls-7" x1="663.66" y1="916.86" x2="663.66" y2="840.45"></line><circle class="cls-8" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-8" cx="644.99" cy="1171.55" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="1171.55" r="10.7"></circle><path class="cls-10" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-10" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-10" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><path class="cls-4" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-4" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-4" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><line class="cls-4" x1="1412.9" y1="807.26" x2="1412.9" y2="1237.05"></line></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

1
public/devices/heltec-vision-master-e290.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

1
public/devices/heltec-vision-master-t190.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="479.57 786.58 1040.84 433.17"><defs><style>.cls-1{fill:#cccccb;}.cls-2{fill:#2b2b2b;}.cls-3,.cls-6,.cls-7,.cls-8{fill:none;stroke-miterlimit:10;}.cls-3,.cls-6,.cls-7{stroke:#050606;}.cls-3,.cls-8{stroke-width:1.65px;}.cls-4{fill:#40403f;}.cls-5{fill:#ddd;}.cls-6{stroke-width:1.62px;}.cls-7{stroke-width:1.64px;}.cls-8{stroke:#fff;}.cls-9{fill:#353535;}.cls-10{fill:#c08c2d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><rect class="cls-2" x="611.37" y="796.48" width="819.83" height="411.47"></rect><line class="cls-3" x1="1441.99" y1="787.41" x2="1441.99" y2="1218.92"></line><path class="cls-4" d="M620.91,851.7v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8a8.7,8.7,0,0,1-.89,0,10.23,10.23,0,0,1-9.35-10.2v-286a10.24,10.24,0,0,1,9.35-10.2,8.7,8.7,0,0,1,.89,0H619A1.87,1.87,0,0,1,620.91,851.7Z"></path><rect class="cls-5" x="480.4" y="942.42" width="114.6" height="127.58"></rect><rect class="cls-3" x="480.4" y="942.42" width="114.6" height="127.58"></rect><path class="cls-6" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><path class="cls-2" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-2" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-2" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-2" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-3" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-3" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><polyline class="cls-7" points="611.37 1156.35 611.37 1207.95 1431.2 1207.95 1431.2 796.48 611.37 796.48 611.37 849.83"></polyline><line class="cls-3" x1="611.37" y1="1207.95" x2="611.37" y2="1218.93"></line><line class="cls-3" x1="611.37" y1="796.48" x2="611.37" y2="787.42"></line><rect class="cls-8" x="560.58" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><circle class="cls-8" cx="1476.63" cy="831.74" r="27.15"></circle><circle class="cls-8" cx="1476.63" cy="1173.49" r="27.15"></circle><rect class="cls-9" x="676.15" y="804.6" width="742.79" height="396.03"></rect><path class="cls-10" d="M604.35,849.87v306.44a10.23,10.23,0,0,1-9.35-10.2v-286A10.24,10.24,0,0,1,604.35,849.87Z"></path><path class="cls-3" d="M605.24,849.83H619a1.87,1.87,0,0,1,1.87,1.87v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8A10.24,10.24,0,0,1,595,1146.11v-286A10.24,10.24,0,0,1,605.24,849.83Z"></path><rect class="cls-6" x="676.15" y="804.6" width="742.79" height="396.03"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

1
public/devices/heltec-wireless-paper-V1_0.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

1
public/devices/heltec-wireless-paper.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

1
public/devices/heltec-wireless-tracker-V1-0.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 83 KiB

1
public/devices/heltec-wireless-tracker.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 83 KiB

1
public/devices/heltec-wsl-v3.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 43 KiB

1
public/devices/nano-g2-ultra.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

2956
public/devices/pico.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 102 KiB

1
public/devices/promicro.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 71 KiB

1
public/devices/rak-wismeshtap.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

2339
public/devices/rak11310.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 164 KiB

1
public/devices/rak2560.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

3514
public/devices/rak4631.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 128 KiB

1
public/devices/rak4631_case.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

1
public/devices/rpipicow.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

1
public/devices/seeed-sensecap-indicator.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

1
public/devices/seeed-xiao-s3.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

1
public/devices/station-g2.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

1
public/devices/t-deck.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

1
public/devices/t-echo.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

1
public/devices/t-watch-s3.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="733.42 451.82 573.87 931.48"><defs><style>.cls-1{fill:#8e8d8e;}.cls-2{fill:#383839;}.cls-3{fill:#cccccb;}.cls-4{fill:#222226;}.cls-5,.cls-6{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-5{stroke-width:1.87px;}.cls-6{stroke-width:3.77px;}.cls-7{fill:#4c4c4d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-1" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><path class="cls-2" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-2" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-3" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-2" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-4" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-5" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-6" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-6" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-6" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-5" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-6" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-6" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><circle class="cls-7" cx="1083.08" cy="1177.35" r="16.6"></circle><rect class="cls-2" x="1280.24" y="739.77" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="750.91" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="762.06" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="773.2" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="784.34" width="16.77" height="4.59" rx="2.29"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

1
public/devices/tbeam-s3-core.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

1
public/devices/tbeam.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 112 KiB

1
public/devices/tlora-c6.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

1
public/devices/tlora-t3s3-epaper.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

1
public/devices/tlora-t3s3-v1.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

1
public/devices/tlora-v2-1-1_6.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

1
public/devices/tlora-v2-1-1_8.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

1
public/devices/tracker-t1000-e.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

160
public/devices/unknown.svg

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
class="svg-icon"
style="overflow:hidden;fill:currentColor"
viewBox="0 0 909.87988 546.85529"
version="1.1"
id="svg3"
xml:space="preserve"
width="909.87988"
height="546.85529"
sodipodi:docname="unknown.svg"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.57169944"
inkscape:cx="291.23695"
inkscape:cy="107.57401"
inkscape:window-width="1472"
inkscape:window-height="890"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_7" /><defs
id="defs3"><style
id="style1">.cls-1{fill:#383838;}.cls-2{fill:#9f9f9e;}.cls-3{fill:#cbcccb;}.cls-4{fill:#b7b7b7;}.cls-5{fill:#353535;}.cls-6{fill:#b1a368;}.cls-7{fill:#2c2d2d;}.cls-10,.cls-11,.cls-8,.cls-9{fill:none;stroke:#050606;}.cls-10,.cls-11,.cls-8{stroke-miterlimit:10;}.cls-8,.cls-9{stroke-width:2px;}.cls-9{stroke-linecap:round;stroke-linejoin:round;}.cls-10{stroke-width:2.04px;}.cls-11{stroke-width:1.99px;}.cls-12{fill:#c08c2d;}.cls-13{fill:#af7a2b;}</style></defs><g
id="Layer_7"
data-name="Layer 7"
transform="translate(-646.6554,-758.05941)"><path
class="cls-2"
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
id="path1-4" /><rect
class="cls-3"
x="647.6554"
y="862.80469"
width="897.52002"
height="441.10999"
rx="11.7"
id="rect2" /><path
class="cls-2"
d="m 681.12532,862.80468 v 113.47998 a 3.67,3.67 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z M 1492.6453,1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 z"
id="path2-7" /><path
class="cls-3"
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17,17 0 0 0 -7.2,-13.92 v -70.28998 z"
id="path3-7"
style="fill:#ffffff" /><path
class="cls-4"
d="m 745.98532,887.80468 v 70.28998 a 17,17 0 0 0 -9.86,-3.14 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 z"
id="path4" /><rect
class="cls-2"
x="672.10535"
y="1011.4448"
width="13.53"
height="148.39999"
id="rect4" /><path
class="cls-6"
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
id="path7" /><path
class="cls-8"
d="m 1082.9205,761.22647 h 60.8838 a 6.1958134,4.8451518 0 0 1 6.1958,4.84516 v 77.32638 h -73.2754 v -77.32638 a 6.1958134,4.8451518 0 0 1 6.1958,-4.84516 z"
id="path39"
style="fill:#b1a368" /><rect
class="cls-8"
x="1066.9833"
y="778.19855"
width="91.504646"
height="55.957298"
rx="5.5511622"
id="rect39"
style="fill:#b1a368" /><path
class="cls-2"
d="m 1158.4522,782.53954 v 47.24724 a 5.5153484,4.3130254 0 0 1 -5.5512,4.34102 h -80.3665 a 5.5511623,4.341032 0 0 1 -5.587,-4.34102 v -47.24724 a 5.5511623,4.341032 0 0 1 5.587,-4.34103 h 80.5098 a 5.5153484,4.3130254 0 0 1 5.4079,4.34103 z"
id="path41-4"
style="fill:none;stroke:#050606;stroke-width:3.16706;stroke-miterlimit:10" /><rect
class="cls-6"
x="1079.9424"
y="843.73468"
width="65.989998"
height="10.03"
id="rect8" /><path
class="cls-8"
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 m 25,-25 H 681.12532 v 113.47998 a 3.68,3.68 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z"
id="path10" /><line
class="cls-8"
x1="745.99536"
y1="958.09467"
x2="745.99536"
y2="887.80469"
id="line10" /><rect
class="cls-8"
x="672.10535"
y="1011.4448"
width="13.53"
height="148.39999"
id="rect11" /><path
class="cls-8"
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
id="path14" /><path
class="cls-10"
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
id="path16" /><rect
class="cls-11"
x="1079.9424"
y="843.73468"
width="65.989998"
height="10.03"
id="rect17" /><path
class="cls-2"
d="m 725.27532,910.38466 a 14,14 0 1 0 14,14 13.95,13.95 0 0 0 -14,-14 z m 0,21.5 a 7.55,7.55 0 1 1 7.54,-7.55 7.55,7.55 0 0 1 -7.54,7.55 z"
id="path19" /><circle
class="cls-8"
cx="725.27539"
cy="924.33466"
r="7.5500002"
id="circle19" /><circle
class="cls-8"
cx="725.27539"
cy="924.33466"
r="13.95"
id="circle20" /><path
d="m 445.36309,440.05365 c 0,11.52004 10.38375,20.85861 23.19309,20.85861 12.80937,0 23.19311,-9.33857 23.19311,-20.85861 0,-11.52005 -10.38374,-20.85861 -23.19311,-20.85861 -12.80934,0 -23.19309,9.33856 -23.19309,20.85861 z"
fill="#ccc"
id="path1"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227"
transform="translate(646.6554,758.05941)" /><path
d="m 469.40305,538.40107 c -119.83415,0 -217.31582,-93.40624 -217.31582,-208.23067 0,-114.82425 97.48167,-208.23058 217.31582,-208.23058 119.83417,0 217.31585,93.40633 217.31585,208.23058 0,114.82443 -97.48168,208.23067 -217.31585,208.23067 z m 0,-386.58065 c -102.63515,0 -186.13149,80.00572 -186.13149,178.34998 0,98.32948 83.49634,178.34997 186.13149,178.34997 102.61966,0 186.13151,-80.01997 186.13151,-178.34997 0,-98.34426 -83.51185,-178.34998 -186.13151,-178.34998 z"
fill="#ccc"
id="path2"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.474832"
transform="translate(646.6554,758.05941)" /><path
d="m 468.55618,391.96713 c -8.53552,0 -15.46205,-6.22977 -15.46205,-13.90533 v -23.51468 c 0,-22.75028 19.32709,-40.13201 36.39722,-55.47009 12.50833,-11.26363 25.45056,-22.88885 25.45056,-32.16398 0,-23.18095 -20.81195,-42.03718 -46.38573,-42.03718 -26.0067,0 -46.38619,18.0497 -46.38619,41.09158 0,7.67594 -6.92654,13.90533 -15.46208,13.90533 -8.53554,0 -15.46207,-6.22977 -15.46207,-13.9058 0,-37.99002 34.68046,-68.90262 77.31034,-68.90262 42.62989,0 77.31034,31.32967 77.31034,69.84869 0,20.81694 -17.54944,36.5856 -34.51132,51.84064 -13.452,12.07016 -27.33645,24.55758 -27.33645,35.77907 v 23.51468 c 0,7.6764 -6.92702,13.91969 -15.46257,13.91969 z"
fill="#ccc"
id="path3"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227;stroke:#000000;stroke-opacity:1"
transform="translate(646.6554,758.05941)" /><rect
class="cls-8"
x="647.6554"
y="862.80469"
width="897.52002"
height="441.10999"
rx="11.7"
id="rect28" /></g><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path11" /><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path12" /><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path13" /></svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

1
public/devices/wio-tracker-wm1110.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 91 KiB

1
public/devices/wm1110_dev_kit.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 120 KiB

8
src/components/Dialog/DialogManager.tsx

@ -6,6 +6,8 @@ import { QRDialog } from "@components/Dialog/QRDialog.tsx";
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { JSX } from "react";
import { NodeDetailsDialog } from "./NodeDetailsDialog";
export const DialogManager = (): JSX.Element => {
const { channels, config, dialog, setDialogOpen } = useDevice();
@ -56,6 +58,12 @@ export const DialogManager = (): JSX.Element => {
setDialogOpen("pkiBackup", open);
}}
/>
<NodeDetailsDialog
open={dialog.nodeDetails}
onOpenChange={(open) => {
setDialogOpen("nodeDetails", open);
}}
/>
</>
);
};

62
src/components/Dialog/LocationResponseDialog.tsx

@ -0,0 +1,62 @@
import { useDevice } from "@app/core/stores/deviceStore";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import type { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
export interface LocationResponseDialogProps {
location: Types.PacketMetadata<Protobuf.Mesh.location> | undefined;
open: boolean;
onOpenChange: () => void;
}
export const LocationResponseDialog = ({
location,
open,
onOpenChange,
}: LocationResponseDialogProps): JSX.Element => {
const { nodes } = useDevice();
const from = nodes.get(location?.from ?? 0);
const longName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
const shortName =
from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{`Location: ${longName} (${shortName})`}</DialogTitle>
</DialogHeader>
<DialogDescription>
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
<p>
Coordinates:{" "}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${location?.data.latitudeI / 1e7}&mlon=${location?.data.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{location?.data.latitudeI / 1e7},{" "}
{location?.data.longitudeI / 1e7}
</a>
</p>
<p>Altitude: {location?.data.altitude}m</p>
</span>
</div>
</DialogDescription>
</DialogContent>
</Dialog>
);
};

163
src/components/Dialog/NodeDetailsDialog.tsx

@ -0,0 +1,163 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@components/UI/Accordion";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { DeviceImage } from "../generic/DeviceImage";
import { TimeAgo } from "../generic/TimeAgo";
import { Uptime } from "../generic/Uptime";
export interface NodeDetailsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const NodeDetailsDialog = ({
open,
onOpenChange,
}: NodeDetailsDialogProps) => {
const { nodes } = useDevice();
const { nodeNumDetails } = useAppStore();
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
return device ? (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Node Details for {device.user?.longName ?? "UNKNOWN"} (
{device.user?.shortName ?? "UNK"})
</DialogTitle>
</DialogHeader>
<DialogFooter>
<div className="w-full">
<DeviceImage
className="w-32 h-32 mx-auto rounded-lg border-4 border-gray-200 dark:border-gray-800"
deviceType={
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
}
/>
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Details:
</p>
<p>
Hardware:{" "}
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
</p>
<p>Node Number: {device.num}</p>
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
<p>
Role:{" "}
{
Protobuf.Config.Config_DeviceConfig_Role[
device.user?.role ?? 0
]
}
</p>
<p>
Last Heard:{" "}
{device.lastHeard === 0 ? (
"Never"
) : (
<TimeAgo timestamp={device.lastHeard * 1000} />
)}
</p>
</div>
{device.position ? (
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Position:
</p>
{device.position.latitudeI && device.position.longitudeI ? (
<p>
Coordinates:{" "}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7}&mlon=${device.position.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{device.position.latitudeI / 1e7},{" "}
{device.position.longitudeI / 1e7}
</a>
</p>
) : null}
{device.position.altitude ? (
<p>Altitude: {device.position.altitude}m</p>
) : null}
</div>
) : null}
{device.deviceMetrics ? (
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Device Metrics:
</p>
{device.deviceMetrics.airUtilTx ? (
<p>
Air TX utilization:{" "}
{device.deviceMetrics.airUtilTx.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.channelUtilization ? (
<p>
Channel utilization:{" "}
{device.deviceMetrics.channelUtilization.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.batteryLevel ? (
<p>
Battery level:{" "}
{device.deviceMetrics.batteryLevel.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.voltage ? (
<p>Voltage: {device.deviceMetrics.voltage.toFixed(2)}V</p>
) : null}
{device.deviceMetrics.uptimeSeconds ? (
<p>
Uptime:{" "}
<Uptime seconds={device.deviceMetrics.uptimeSeconds} />
</p>
) : null}
</div>
) : null}
{device ? (
<div className="mt-5 w-full max-w-[464px] bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<Accordion className="AccordionRoot" type="single" collapsible>
<AccordionItem className="AccordionItem" value="item-1">
<AccordionTrigger>
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
All Raw Metrics:
</p>
</AccordionTrigger>
<AccordionContent className="overflow-x-scroll">
<pre className="text-xs w-full">
{JSON.stringify(device, null, 2)}
</pre>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
) : null}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
) : null;
};

117
src/components/Dialog/NodeOptionsDialog.tsx

@ -0,0 +1,117 @@
import { toast } from "@app/core/hooks/useToast";
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { TrashIcon } from "lucide-react";
import type { JSX } from "react";
import { Button } from "../UI/Button";
export interface NodeOptionsDialogProps {
node: Protobuf.Mesh.NodeInfo | undefined;
open: boolean;
onOpenChange: () => void;
}
export const NodeOptionsDialog = ({
node,
open,
onOpenChange,
}: NodeOptionsDialogProps): JSX.Element => {
const { setDialogOpen, connection, setActivePage } = useDevice();
const {
setNodeNumToBeRemoved,
setNodeNumDetails,
setChatType,
setActiveChat,
} = useAppStore();
const longName =
node?.user?.longName ??
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
const shortName =
node?.user?.shortName ??
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
function handleDirectMessage() {
if (!node) return;
setChatType("direct");
setActiveChat(node.num);
setActivePage("messages");
}
function handleRequestPosition() {
if (!node) return;
toast({
title: "Requesting position, please wait...",
});
connection?.requestPosition(node.num).then(() =>
toast({
title: "Position request sent.",
}),
);
onOpenChange();
}
function handleTraceroute() {
if (!node) return;
toast({
title: "Sending Traceroute, please wait...",
});
connection?.traceRoute(node.num).then(() =>
toast({
title: "Traceroute sent.",
}),
);
onOpenChange();
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{`${longName} (${shortName})`}</DialogTitle>
</DialogHeader>
<div className="flex flex-col space-y-1">
<div>
<Button onClick={handleDirectMessage}>Direct Message</Button>
</div>
<div>
<Button onClick={handleRequestPosition}>Request Position</Button>
</div>
<div>
<Button onClick={handleTraceroute}>Trace Route</Button>
</div>
<div>
<Button
key="remove"
variant="destructive"
onClick={() => {
setNodeNumToBeRemoved(node.num);
setDialogOpen("nodeRemoval", true);
}}
>
<TrashIcon />
Remove
</Button>
</div>
<div>
<Button
onClick={() => {
setNodeNumDetails(node.num);
setDialogOpen("nodeDetails", true);
}}
>
More Details
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};

57
src/components/Dialog/TracerouteResponseDialog.tsx

@ -0,0 +1,57 @@
import { useDevice } from "@app/core/stores/deviceStore";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import type { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute";
export interface TracerouteResponseDialogProps {
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
open: boolean;
onOpenChange: () => void;
}
export const TracerouteResponseDialog = ({
traceroute,
open,
onOpenChange,
}: TracerouteResponseDialogProps): JSX.Element => {
const { nodes } = useDevice();
const route: number[] = traceroute?.data.route ?? [];
const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = traceroute?.data.snrTowards ?? [];
const snrBack = traceroute?.data.snrBack ?? [];
const from = nodes.get(traceroute?.from ?? 0);
const longName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
const shortName =
from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
const to = nodes.get(traceroute?.to ?? 0);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{`Traceroute: ${longName} (${shortName})`}</DialogTitle>
</DialogHeader>
<DialogDescription>
<TraceRoute
route={route}
routeBack={routeBack}
from={from}
to={to}
snrTowards={snrTowards}
snrBack={snrBack}
/>
</DialogDescription>
</DialogContent>
</Dialog>
);
};

20
src/components/PageComponents/Connect/HTTP.tsx

@ -23,18 +23,13 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
window.location.hostname,
)
? "meshtastic.local"
: window.location.hostname,
: window.location.host,
tls: location.protocol === "https:",
},
});
const tlsEnabled = useWatch({
control,
name: "tls",
defaultValue: location.protocol === "https:",
});
const [connectionInProgress, setConnectionInProgress] = useState(false);
const [https, setHTTPS] = useState(false);
const onSubmit = handleSubmit(async (data) => {
setConnectionInProgress(true);
@ -46,7 +41,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
await connection.connect({
address: data.ip,
fetchInterval: 2000,
tls: data.tls,
tls: https,
});
setSelectedDevice(id);
@ -60,8 +55,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
<div className="flex h-48 flex-col gap-2">
<Label>IP Address/Hostname</Label>
<Input
// label="IP Address/Hostname"
prefix={tlsEnabled ? "https://" : "http://"}
prefix={https ? "https://" : "http://"}
placeholder="000.000.000.000 / meshtastic.local"
disabled={connectionInProgress}
{...register("ip")}
@ -71,8 +65,12 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
control={control}
render={({ field: { value, ...rest } }) => (
<>
<Label>Use TLS</Label>
<Label>Use HTTPS</Label>
<Switch
onCheckedChange={(checked) => {
checked ? setHTTPS(true) : setHTTPS(false);
}}
// label="Use TLS"
// description="Description"
disabled={

18
src/components/PageComponents/Map/NodeDetail.tsx

@ -1,9 +1,10 @@
import { Separator } from "@app/components/UI/Seperator";
import { H5 } from "@app/components/UI/Typography/H5.tsx";
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import { formatQuantity } from "@app/core/utils/string";
import { Avatar } from "@components/UI/Avatar";
import { Mono } from "@components/generic/Mono.tsx";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Protobuf } from "@meshtastic/js";
import type { Protobuf as ProtobufType } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
@ -25,12 +26,12 @@ export interface NodeDetailProps {
export const NodeDetail = ({ node }: NodeDetailProps) => {
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
const hardwareType = Protobuf.Mesh.HardwareModel[
node.user?.hwModel ?? 0
].replaceAll("_", " ");
const hwModel = node.user?.hwModel ?? 0;
const hardwareType =
Protobuf.Mesh.HardwareModel[hwModel]?.replaceAll("_", " ") ?? `${hwModel}`;
return (
<div className="dark:text-black">
<div className="dark:text-black p-1">
<div className="flex gap-2">
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
<Avatar text={node.user?.shortName} />
@ -132,7 +133,12 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
className="ml-2 mr-1"
aria-label="Elevation"
/>
<div>{node.position?.altitude} ft</div>
<div>
{formatQuantity(node.position?.altitude, {
one: "meter",
other: "meters",
})}
</div>
</div>
)}
</div>

34
src/components/PageComponents/Messages/ChannelChat.tsx

@ -51,11 +51,11 @@ export const ChannelChat = ({
if (!messages?.length) {
return (
<div className="flex flex-col h-full w-full">
<div className="flex flex-col h-full w-full container mx-auto">
<div className="flex-1 flex items-center justify-center">
<EmptyState />
</div>
<div className="flex-shrink-0 p-4 w-full bg-gray-900">
<div className="flex-shrink-0 p-4 w-full dark:bg-gray-900">
<MessageInput to={to} channel={channel} maxBytes={200} />
</div>
</div>
@ -63,23 +63,25 @@ export const ChannelChat = ({
}
return (
<div className="flex flex-col h-full w-full">
<div className="flex-1 overflow-y-scroll w-full" ref={scrollContainerRef}>
<div className="w-full h-full flex flex-col justify-end">
{messages.map((message, index) => (
<Message
key={message.id}
message={message}
lastMsgSameUser={
index > 0 && messages[index - 1].from === message.from
}
sender={nodes.get(message.from)}
/>
))}
<div className="flex flex-col h-full w-full container mx-auto">
<div className="flex-1 overflow-y-auto" ref={scrollContainerRef}>
<div className="w-full h-full flex flex-col justify-end pl-4 pr-44">
{messages.map((message, index) => {
return (
<Message
key={message.id}
message={message}
sender={nodes.get(message.from)}
lastMsgSameUser={
index > 0 && messages[index - 1].from === message.from
}
/>
);
})}
<div ref={messagesEndRef} className="w-full" />
</div>
</div>
<div className="flex-shrink-0 mt-2 p-4 w-full bg-gray-900">
<div className="flex-shrink-0 mt-2 p-4 w-full dark:bg-gray-900">
<MessageInput to={to} channel={channel} maxBytes={200} />
</div>
</div>

122
src/components/PageComponents/Messages/Message.tsx

@ -1,10 +1,21 @@
import type { MessageWithState } from "@app/core/stores/deviceStore.ts";
import {
Tooltip,
TooltipArrow,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@app/components/UI/Tooltip";
import { useAppStore } from "@app/core/stores/appStore";
import {
type MessageWithState,
useDeviceStore,
} from "@app/core/stores/deviceStore.ts";
import { cn } from "@app/core/utils/cn";
import { Avatar } from "@components/UI/Avatar";
import type { Protobuf } from "@meshtastic/js";
import * as Tooltip from "@radix-ui/react-tooltip";
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { useMemo } from "react";
const MESSAGE_STATES = {
ACK: "ack",
@ -17,7 +28,7 @@ type MessageState = MessageWithState["state"];
interface MessageProps {
lastMsgSameUser: boolean;
message: MessageWithState;
sender?: Protobuf.Mesh.NodeInfo;
sender: Protobuf.Mesh.NodeInfo;
}
interface StatusTooltipProps {
@ -45,22 +56,20 @@ const STATUS_ICON_MAP: Record<MessageState, LucideIcon> = {
const getStatusText = (state: MessageState): string => STATUS_TEXT_MAP[state];
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
side="top"
align="center"
sideOffset={5}
>
{getStatusText(state)}
<Tooltip.Arrow className="fill-slate-800" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
side="top"
align="center"
sideOffset={5}
>
{getStatusText(state)}
<TooltipArrow className="fill-slate-800" />
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
@ -88,7 +97,7 @@ const getMessageTextStyles = (state: MessageState) => {
const isWaiting = state === MESSAGE_STATES.WAITING;
return cn(
"pl-2 break-words overflow-hidden",
"break-words overflow-hidden",
isAcknowledged
? "text-black dark:text-white"
: "text-black dark:text-gray-400",
@ -96,8 +105,11 @@ const getMessageTextStyles = (state: MessageState) => {
);
};
const TimeDisplay = ({ date }: { date: Date }) => (
<div className="flex items-center gap-2 flex-shrink-0">
const TimeDisplay = ({
date,
className,
}: { date: Date; className?: string }) => (
<div className={cn("flex items-center gap-2 flex-shrink-0", className)}>
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
{date.toLocaleDateString()}
</span>
@ -111,44 +123,46 @@ const TimeDisplay = ({ date }: { date: Date }) => (
);
export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
const messageTextClass = getMessageTextStyles(message.state);
const isFailed = message.state === MESSAGE_STATES.ACK;
const baseMessageWrapper = cn(
"flex items-center gap-2 w-full max-w-full pl-11",
!lastMsgSameUser && "flex-wrap flex-grow",
const { getDevices } = useDeviceStore();
const isDeviceUser = useMemo(
() =>
getDevices()
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
.includes(message.from),
[getDevices, message.from],
);
const messageUser = sender?.user;
const containerClass = cn(
"w-full px-4 relative",
lastMsgSameUser ? "mt-1" : "mt-2",
!lastMsgSameUser && "pt-2",
);
const messageTextClass = getMessageTextStyles(message.state);
return (
<div className={containerClass}>
{!lastMsgSameUser && (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<div className="flex items-center gap-2 min-w-0">
<Avatar
text={sender?.user?.shortName ?? "UNK"}
className="flex-shrink-0"
/>
<span className="font-medium text-gray-900 dark:text-white truncate">
{sender?.user?.longName ?? "UNK"}
</span>
</div>
<TimeDisplay date={message.rxTime} />
<div className="flex flex-col w-full px-4 justify-start">
<div
className={cn(
"flex flex-col flex-wrap items-start py-1",
isDeviceUser && "items-end",
)}
>
<div className="flex items-center gap-2 mb-2">
{!lastMsgSameUser ? (
<div className="flex place-items-center gap-2 mb-1">
<Avatar text={messageUser?.shortName} />
<div className="flex flex-col">
<span className="font-medium text-gray-900 dark:text-white truncate">
{messageUser?.longName}
</span>
</div>
</div>
) : null}
</div>
)}
<div className={baseMessageWrapper}>
<div className="flex-1 min-w-0 max-w-full">
<div className={messageTextClass}>{message.data}</div>
<TimeDisplay date={message.rxTime} />
<div className="flex place-items-center gap-2 pb-2">
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}>
{message.data}
</div>
<StatusIcon state={message.state} />
</div>
<StatusIcon
state={message.state}
className="ml-auto mr-6 flex-shrink-0"
/>
</div>
</div>
);

32
src/components/PageComponents/Messages/MessageInput.tsx

@ -4,7 +4,13 @@ import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Types } from "@meshtastic/js";
import { SendIcon } from "lucide-react";
import { type JSX, useCallback, useMemo, useState } from "react";
import {
type JSX,
startTransition,
useCallback,
useMemo,
useState,
} from "react";
export interface MessageInputProps {
to: Types.Destination;
@ -26,7 +32,7 @@ export const MessageInput = ({
} = useDevice();
const myNodeNum = hardware.myNodeNum;
const [localDraft, setLocalDraft] = useState(messageDraft);
const [messageBytes, setMessageBytes] = useState(maxBytes);
const [messageBytes, setMessageBytes] = useState(0);
const debouncedSetMessageDraft = useMemo(
() => debounce(setMessageDraft, 300),
@ -64,10 +70,11 @@ export const MessageInput = ({
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
const byteLength = new Blob([newValue]).size;
if (byteLength <= maxBytes) {
setLocalDraft(newValue);
debouncedSetMessageDraft(newValue);
setMessageBytes(maxBytes - byteLength);
setMessageBytes(byteLength);
}
};
@ -75,11 +82,16 @@ export const MessageInput = ({
<div className="flex gap-2">
<form
className="w-full"
onSubmit={(e) => {
e.preventDefault();
sendText(localDraft);
setLocalDraft("");
setMessageDraft("");
action={async (formData: FormData) => {
// prevent user from sending blank/empty message
if (localDraft === "") return;
const message = formData.get("messageInput") as string;
startTransition(() => {
sendText(message);
setLocalDraft("");
setMessageDraft("");
setMessageBytes(0);
});
}}
>
<div className="flex flex-grow gap-2">
@ -87,14 +99,16 @@ export const MessageInput = ({
<Input
autoFocus={true}
minLength={1}
name="messageInput"
placeholder="Enter Message"
value={localDraft}
onChange={handleInputChange}
/>
</span>
<div className="flex items-center">
<div className="flex items-center w-24 p-2 place-content-end">
{messageBytes}/{maxBytes}
</div>
<Button type="submit">
<SendIcon size={16} />
</Button>

48
src/components/PageComponents/Messages/TraceRoute.tsx

@ -1,36 +1,60 @@
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
export interface TraceRouteProps {
from?: Protobuf.Mesh.NodeInfo;
to?: Protobuf.Mesh.NodeInfo;
route: Array<number>;
routeBack?: Array<number>;
snrTowards?: Array<number>;
snrBack?: Array<number>;
}
export const TraceRoute = ({
from,
to,
route,
routeBack,
snrTowards,
snrBack,
}: TraceRouteProps): JSX.Element => {
const { nodes } = useDevice();
return route.length === 0 ? (
return (
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
{to?.user?.longName}{from?.user?.longName}
</span>
</div>
) : (
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
{to?.user?.longName}
{route.map((hop) => {
const node = nodes.get(hop);
return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}`;
})}
<p className="font-semibold">Route to destination:</p>
<p>{to?.user?.longName}</p>
<p> {snrTowards?.[0] ? snrTowards[0] : "??"}dB</p>
{route.map((hop, i) => (
<span key={nodes.get(hop)?.num}>
<p>
{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
</p>
<p> {snrTowards?.[i + 1] ? snrTowards[i + 1] : "??"}dB</p>
</span>
))}
{from?.user?.longName}
</span>
{routeBack ? (
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
<p className="font-semibold">Route back:</p>
<p>{from?.user?.longName}</p>
<p> {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
{routeBack.map((hop, i) => (
<span key={nodes.get(hop)?.num}>
<p>
{nodes.get(hop)?.user?.longName ??
`!${numberToHexUnpadded(hop)}`}
</p>
<p> {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
</span>
))}
{to?.user?.longName}
</span>
) : null}
</div>
);
};

44
src/components/UI/Accordion.tsx

@ -0,0 +1,44 @@
import { cn } from "@core/utils/cn.ts";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import { type ComponentRef, forwardRef } from "react";
export const Accordion = AccordionPrimitive.Root;
export const AccordionHeader = AccordionPrimitive.Header;
export const AccordionItem = AccordionPrimitive.Item;
export const AccordionTrigger = forwardRef<
ComponentRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex justify-between items-center w-full p-4 border-b border-gray-200 dark:border-gray-800 group",
className,
)}
{...props}
>
{props.children}
<ChevronDownIcon
className="h-5 w-5 transition-transform duration-300 group-data-[state=open]:rotate-180"
aria-hidden
/>
</AccordionPrimitive.Trigger>
));
export const AccordionContent = forwardRef<
ComponentRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"p-4 border-b border-gray-200 dark:border-gray-800",
className,
)}
{...props}
/>
));

2
src/components/UI/Button.tsx

@ -15,7 +15,7 @@ const buttonVariants = cva(
success:
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
outline:
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-400 dark:text-slate-100",
subtle:
"bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100",
ghost:

2
src/components/UI/Dialog.tsx

@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
"fixed z-50 grid w-full max-w-[512px] max-h-[100vh] overflow-y-scroll scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
"dark:bg-slate-900",
className,
)}

2
src/components/UI/Toast.tsx

@ -28,7 +28,7 @@ const toastVariants = cva(
variants: {
variant: {
default:
"border bg-background text-foreground dark:bg-slate-700 dark:border-slate-600 dark:text-slate-50",
"border bg-backgroundPrimary text-foreground dark:bg-slate-700 dark:border-slate-600 dark:text-slate-50",
destructive:
"group destructive bg-red-600 text-white dark:border-red-900 dark:bg-red-900 dark:text-red-50",
},

9
src/components/UI/Tooltip.tsx

@ -9,6 +9,7 @@ const Tooltip = ({ ...props }) => <TooltipPrimitive.Root {...props} />;
Tooltip.displayName = TooltipPrimitive.Tooltip.displayName;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipArrow = TooltipPrimitive.Arrow;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
@ -26,4 +27,10 @@ const TooltipContent = React.forwardRef<
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
TooltipArrow,
};

53
src/components/generic/DeviceImage.tsx

@ -0,0 +1,53 @@
export interface DeviceImageProps {
deviceType: string;
className?: React.HTMLAttributes<HTMLImageElement>["className"];
}
const hardwareModelToFilename: { [key: string]: string } = {
DIY_V1: "diy.svg",
NANO_G2_ULTRA: "nano-g2-ultra.svg",
TBEAM: "tbeam.svg",
HELTEC_HT62: "heltec-ht62-esp32c3-sx1262.svg",
RPI_PICO: "pico.svg",
T_DECK: "t-deck.svg",
HELTEC_MESH_NODE_T114: "heltec-mesh-node-t114.svg",
HELTEC_MESH_NODE_T114_CASE: "heltec-mesh-node-t114-case.svg",
HELTEC_V3: "heltec-v3.svg",
HELTEC_V3_CASE: "heltec-v3-case.svg",
HELTEC_VISION_MASTER_E213: "heltec-vision-master-e213.svg",
HELTEC_VISION_MASTER_E290: "heltec-vision-master-e290.svg",
HELTEC_VISION_MASTER_T190: "heltec-vision-master-t190.svg",
HELTEC_WIRELESS_PAPER: "heltec-wireless-paper.svg",
HELTEC_WIRELESS_PAPER_V1_0: "heltec-wireless-paper-V1_0.svg",
HELTEC_WIRELESS_TRACKER: "heltec-wireless-tracker.svg",
HELTEC_WIRELESS_TRACKER_V1_0: "heltec-wireless-tracker-V1-0.svg",
HELTEC_WSL_V3: "heltec-wsl-v3.svg",
TLORA_C6: "tlora-c6.svg",
TLORA_T3_S3: "tlora-t3s3-v1.svg",
TLORA_T3_S3_EPAPER: "tlora-t3s3-epaper.svg",
TLORA_V2: "tlora-v2-1-1_6.svg",
TLORA_V2_1_1P6: "tlora-v2-1-1_6.svg",
TLORA_V2_1_1P8: "tlora-v2-1-1_8.svg",
RAK11310: "rak11310.svg",
RAK2560: "rak2560.svg",
RAK4631: "rak4631.svg",
RAK4631_CASE: "rak4631_case.svg",
WIO_WM1110: "wio-tracker-wm1110.svg",
WM1110_DEV_KIT: "wm1110_dev_kit.svg",
STATION_G2: "station-g2.svg",
TBEAM_V0P7: "tbeam-s3-core.svg",
T_ECHO: "t-echo.svg",
TRACKER_T1000_E: "tracker-t1000-e.svg",
T_WATCH_S3: "t-watch-s3.svg",
SEEED_XIAO_S3: "seeed-xiao-s3.svg",
SENSECAP_INDICATOR: "seeed-sensecap-indicator.svg",
PROMICRO: "promicro.svg",
RPIPICOW: "rpipicow.svg",
UNKNOWN: "unknown.svg",
};
export const DeviceImage = ({ deviceType, className }: DeviceImageProps) => {
const getPath = (device: string) => `/devices/${device}`;
const device = hardwareModelToFilename[deviceType] || "unknown.svg";
return <img className={className} src={getPath(device)} alt={device} />;
};

9
src/components/generic/Table/tmp/TimeAgo.tsx

@ -1,9 +0,0 @@
import TimeAgoReact from "timeago-react";
export interface TimeAgoProps {
timestamp: number;
}
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
return <TimeAgoReact datetime={timestamp} opts={{ minInterval: 10 }} />;
};

66
src/components/generic/TimeAgo.tsx

@ -0,0 +1,66 @@
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import type { JSX } from "react";
export interface TimeAgoProps {
timestamp: number;
}
const getTimeAgo = (
unixTimestamp: number,
locale: Intl.LocalesArgument = "en",
): string => {
const timestamp = new Date(unixTimestamp);
const diff = (new Date().getTime() - timestamp.getTime()) / 1000;
const minutes = Math.floor(diff / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(months / 12);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
if (years > 0) {
return rtf.format(0 - years, "year");
}
if (months > 0) {
return rtf.format(0 - months, "month");
}
if (days > 0) {
return rtf.format(0 - days, "day");
}
if (hours > 0) {
return rtf.format(0 - hours, "hour");
}
if (minutes > 0) {
return rtf.format(0 - minutes, "minute");
}
return rtf.format(Math.floor(0 - diff), "second");
};
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<span>{getTimeAgo(timestamp)}</span>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
side="top"
align="center"
sideOffset={5}
>
{new Date(timestamp).toLocaleString()}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</TooltipProvider>
);
};

17
src/components/generic/Uptime.tsx

@ -0,0 +1,17 @@
import type { JSX } from "react";
export interface UptimeProps {
seconds: number;
}
const getUptime = (seconds: number): string => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor(((seconds % 86400) % 3600) / 60);
const secondsLeft = Math.floor(((seconds % 86400) % 3600) % 60);
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
};
export const Uptime = ({ seconds }: UptimeProps): JSX.Element => {
return <span>{getUptime(seconds)}</span>;
};

22
src/core/stores/appStore.ts

@ -1,3 +1,4 @@
import { Types } from "@meshtastic/js";
import { produce } from "immer";
import { create } from "zustand";
@ -29,6 +30,9 @@ interface AppState {
nodeNumToBeRemoved: number;
accent: AccentColor;
connectDialogOpen: boolean;
nodeNumDetails: number;
activeChat: number;
chatType: "broadcast" | "direct";
setRasterSources: (sources: RasterSource[]) => void;
addRasterSource: (source: RasterSource) => void;
@ -42,6 +46,9 @@ interface AppState {
setNodeNumToBeRemoved: (nodeNum: number) => void;
setAccent: (color: AccentColor) => void;
setConnectDialogOpen: (open: boolean) => void;
setNodeNumDetails: (nodeNum: number) => void;
setActiveChat: (chat: number) => void;
setChatType: (type: "broadcast" | "direct") => void;
}
export const useAppStore = create<AppState>()((set) => ({
@ -57,6 +64,9 @@ export const useAppStore = create<AppState>()((set) => ({
accent: "orange",
connectDialogOpen: false,
nodeNumToBeRemoved: 0,
nodeNumDetails: 0,
activeChat: Types.ChannelNumber.Primary,
chatType: "broadcast",
setRasterSources: (sources: RasterSource[]) => {
set(
@ -124,4 +134,16 @@ export const useAppStore = create<AppState>()((set) => ({
}),
);
},
setNodeNumDetails: (nodeNum) =>
set((state) => ({
nodeNumDetails: nodeNum,
})),
setActiveChat: (chat) =>
set(() => ({
activeChat: chat,
})),
setChatType: (type) =>
set(() => ({
chatType: type,
})),
}));

5
src/core/stores/deviceStore.ts

@ -26,7 +26,8 @@ export type DialogVariant =
| "reboot"
| "deviceName"
| "nodeRemoval"
| "pkiBackup";
| "pkiBackup"
| "nodeDetails";
export interface Device {
id: number;
@ -62,6 +63,7 @@ export interface Device {
deviceName: boolean;
nodeRemoval: boolean;
pkiBackup: boolean;
nodeDetails: boolean;
};
setStatus: (status: Types.DeviceStatusEnum) => void;
@ -145,6 +147,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
deviceName: false,
nodeRemoval: false,
pkiBackup: false,
nodeDetails: false,
},
pendingSettingsChanges: false,
messageDraft: "",

31
src/core/utils/string.ts

@ -0,0 +1,31 @@
interface PluralForms {
one: string;
other: string;
[key: string]: string;
}
interface FormatOptions {
locale?: string;
pluralRules?: Intl.PluralRulesOptions;
numberFormat?: Intl.NumberFormatOptions;
}
export function formatQuantity(
value: number,
forms: PluralForms,
options: FormatOptions = {},
) {
const {
locale = "en-US",
pluralRules: pluralOptions = { type: "cardinal" },
numberFormat: numberOptions = {},
} = options;
const pluralRules = new Intl.PluralRules(locale, pluralOptions);
const numberFormat = new Intl.NumberFormat(locale, numberOptions);
const pluralCategory = pluralRules.select(value);
const word = forms[pluralCategory];
return `${numberFormat.format(value)} ${word}`;
}

229
src/pages/Map.tsx

@ -1,59 +1,93 @@
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
import { Avatar } from "@app/components/UI/Avatar";
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import { cn } from "@app/core/utils/cn.ts";
import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { bbox, lineString } from "@turf/turf";
import {
BoxSelectIcon,
MapPinIcon,
ZoomInIcon,
ZoomOutIcon,
} from "lucide-react";
import { MapPinIcon } from "lucide-react";
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
import {
AttributionControl,
GeolocateControl,
Marker,
NavigationControl,
Popup,
ScaleControl,
useMap,
} from "react-map-gl";
import MapGl from "react-map-gl/maplibre";
type NodePosition = {
latitude: number;
longitude: number;
};
const convertToLatLng = (position: {
latitudeI?: number;
longitudeI?: number;
}): NodePosition => ({
latitude: (position.latitudeI ?? 0) / 1e7,
longitude: (position.longitudeI ?? 0) / 1e7,
});
const MapPage = (): JSX.Element => {
const { nodes, waypoints } = useDevice();
const { rasterSources, darkMode } = useAppStore();
const { darkMode } = useAppStore();
const { default: map } = useMap();
const [zoom, setZoom] = useState(0);
const [selectedNode, setSelectedNode] =
useState<Protobuf.Mesh.NodeInfo | null>(null);
const allNodes = useMemo(() => Array.from(nodes.values()), [nodes]);
// Filter out nodes without a valid position
const validNodes = useMemo(
() =>
Array.from(nodes.values()).filter(
(node): node is Protobuf.Mesh.NodeInfo =>
Boolean(node.position?.latitudeI),
),
[nodes],
);
const handleMarkerClick = useCallback(
(node: Protobuf.Mesh.NodeInfo, event: { originalEvent: MouseEvent }) => {
event?.originalEvent?.stopPropagation();
setSelectedNode(node);
if (map) {
const position = convertToLatLng(node.position);
map.easeTo({
center: [position.longitude, position.latitude],
zoom: map?.getZoom(),
});
}
},
[map],
);
const getBBox = useCallback(() => {
// Get the bounds of the map based on the nodes furtherest away from center
const getMapBounds = useCallback(() => {
if (!map) {
return;
}
const nodesWithPosition = allNodes.filter(
(node) => node.position?.latitudeI,
);
if (!nodesWithPosition.length) {
if (!validNodes.length) {
return;
}
if (nodesWithPosition.length === 1) {
if (validNodes.length === 1) {
map.easeTo({
zoom: 12,
zoom: map.getZoom(),
center: [
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
(validNodes[0].position?.longitudeI ?? 0) / 1e7,
(validNodes[0].position?.latitudeI ?? 0) / 1e7,
],
});
return;
}
const line = lineString(
nodesWithPosition.map((n) => [
validNodes.map((n) => [
(n.position?.latitudeI ?? 0) / 1e7,
(n.position?.longitudeI ?? 0) / 1e7,
]),
@ -69,78 +103,54 @@ const MapPage = (): JSX.Element => {
if (center) {
map.easeTo(center);
}
}, [allNodes, map]);
}, [validNodes, map]);
useEffect(() => {
map?.on("zoom", () => {
setZoom(map?.getZoom() ?? 0);
});
}, [map]);
// Generate all markers
const markers = useMemo(
() =>
validNodes.map((node) => {
const position = convertToLatLng(node.position);
return (
<Marker
key={`marker-${node.num}`}
longitude={position.longitude}
latitude={position.latitude}
anchor="bottom"
onClick={(e) => handleMarkerClick(node, e)}
>
<Avatar
text={node.user?.shortName?.toString() ?? node.num.toString()}
className="border-[1.5px] border-slate-600 shadow-xl shadow-slate-600"
/>
</Marker>
);
}),
[validNodes, handleMarkerClick],
);
useEffect(() => {
map?.on("load", () => {
getBBox();
getMapBounds();
});
}, [map, getBBox]);
}, [map, getMapBounds]);
return (
<>
<Sidebar>
<SidebarSection label="Sources">
{rasterSources.map((source) => (
<SidebarButton key={source.title} label={source.title} />
))}
</SidebarSection>
</Sidebar>
<PageLayout
label="Map"
noPadding={true}
actions={[
{
icon: ZoomInIcon,
onClick() {
map?.zoomIn();
},
},
{
icon: ZoomOutIcon,
onClick() {
map?.zoomOut();
},
},
{
icon: BoxSelectIcon,
onClick() {
getBBox();
},
},
]}
>
<Sidebar />
<PageLayout label="Map" noPadding={true} actions={[]}>
<MapGl
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
// onClick={(e) => {
// const waypoint = new Protobuf.Waypoint({
// name: "test",
// description: "test description",
// latitudeI: Math.trunc(e.lngLat.lat * 1e7),
// longitudeI: Math.trunc(e.lngLat.lng * 1e7)
// });
// addWaypoint(waypoint);
// connection?.sendWaypoint(waypoint, "broadcast");
// }}
// @ts-ignore
attributionControl={false}
renderWorldCopies={false}
maxPitch={0}
antialias={true}
style={{
filter: darkMode ? "brightness(0.8)" : "",
filter: darkMode ? "brightness(0.9)" : "",
}}
dragRotate={false}
touchZoomRotate={false}
initialViewState={{
zoom: 1.6,
zoom: 1.8,
latitude: 35,
longitude: 0,
}}
@ -151,6 +161,14 @@ const MapPage = (): JSX.Element => {
color: darkMode ? "black" : "",
}}
/>
<GeolocateControl
position="top-right"
positionOptions={{ enableHighAccuracy: true }}
trackUserLocation
/>
<NavigationControl position="top-right" showCompass={false} />
<ScaleControl />
{waypoints.map((wp) => (
<Marker
key={wp.id}
@ -163,58 +181,17 @@ const MapPage = (): JSX.Element => {
</div>
</Marker>
))}
{/* {rasterSources.map((source, index) => (
<Source key={index} type="raster" {...source}>
<Layer type="raster" />
</Source>
))} */}
{allNodes.map((node) => {
if (node.position?.latitudeI && node.num !== selectedNode?.num) {
return (
<Marker
key={node.num}
longitude={(node.position.longitudeI ?? 0) / 1e7}
latitude={(node.position.latitudeI ?? 0) / 1e7}
// style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom"
onClick={() => {
setSelectedNode(node);
map?.easeTo({
zoom: 12,
center: [
(node.position?.longitudeI ?? 0) / 1e7,
(node.position?.latitudeI ?? 0) / 1e7,
],
});
}}
>
<div className="flex cursor-pointer gap-2 rounded-md bg-transparent p-1.5">
<Avatar
text={
node.user?.shortName.toString() ?? node.num.toString()
}
size="sm"
/>
<Subtle className={cn(zoom < 12 && "hidden")}>
{node.user?.longName ||
`!${numberToHexUnpadded(node.num)}`}
</Subtle>
</div>
</Marker>
);
}
})}
{selectedNode?.position && (
{markers}
{selectedNode ? (
<Popup
longitude={(selectedNode.position.longitudeI ?? 0) / 1e7}
latitude={(selectedNode.position.latitudeI ?? 0) / 1e7}
anchor="left"
closeOnClick={false}
anchor="top"
longitude={convertToLatLng(selectedNode.position).longitude}
latitude={convertToLatLng(selectedNode.position).latitude}
onClose={() => setSelectedNode(null)}
>
<NodeDetail node={selectedNode} />
</Popup>
)}
) : null}
</MapGl>
</PageLayout>
</>

11
src/pages/Messages.tsx

@ -1,3 +1,4 @@
import { useAppStore } from "@app/core/stores/appStore";
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx";
@ -5,21 +6,17 @@ import { Avatar } from "@components/UI/Avatar.tsx";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
import { useToast } from "@core/hooks/useToast.ts";
import { Device, useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { getChannelName } from "@pages/Channels.tsx";
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
import { useState } from "react";
const MessagesPage = () => {
export const MessagesPage = () => {
const { channels, nodes, hardware, messages, traceroutes, connection } =
useDevice();
const [chatType, setChatType] =
useState<Types.PacketDestination>("broadcast");
const [activeChat, setActiveChat] = useState<number>(
Types.ChannelNumber.Primary,
);
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
const [searchTerm, setSearchTerm] = useState<string>("");
const filteredNodes = Array.from(nodes.values()).filter((node) => {
if (node.num === hardware.myNodeNum) return false;

114
src/pages/Nodes.tsx

@ -1,16 +1,18 @@
import { LocationResponseDialog } from "@app/components/Dialog/LocationResponseDialog";
import { NodeOptionsDialog } from "@app/components/Dialog/NodeOptionsDialog";
import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteResponseDialog";
import Footer from "@app/components/UI/Footer";
import { useAppStore } from "@app/core/stores/appStore";
import { Sidebar } from "@components/Sidebar.tsx";
import { Avatar } from "@components/UI/Avatar.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { Table } from "@components/generic/Table/index.tsx";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
import { Protobuf, type Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { Fragment, type JSX } from "react";
import { LockIcon, LockOpenIcon } from "lucide-react";
import { Fragment, type JSX, useCallback, useEffect, useState } from "react";
import { base16 } from "rfc4648";
export interface DeleteNoteDialogProps {
@ -19,8 +21,16 @@ export interface DeleteNoteDialogProps {
}
const NodesPage = (): JSX.Element => {
const { nodes, hardware, setDialogOpen } = useDevice();
const { setNodeNumToBeRemoved } = useAppStore();
const { nodes, hardware, connection } = useDevice();
const [selectedNode, setSelectedNode] = useState<
Protobuf.Mesh.NodeInfo | undefined
>(undefined);
const [selectedTraceroute, setSelectedTraceroute] = useState<
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined
>();
const [selectedLocation, setSelectedLocation] = useState<
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined
>();
const [searchTerm, setSearchTerm] = useState<string>("");
const filteredNodes = Array.from(nodes.values()).filter((node) => {
@ -29,6 +39,36 @@ const NodesPage = (): JSX.Element => {
return nodeName.toLowerCase().includes(searchTerm.toLowerCase());
});
useEffect(() => {
if (!connection) return;
connection.events.onTraceRoutePacket.subscribe(handleTraceroute);
return () => {
connection.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
};
}, [connection]);
const handleTraceroute = useCallback(
(traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>) => {
setSelectedTraceroute(traceroute);
},
[],
);
useEffect(() => {
if (!connection) return;
connection.events.onPositionPacket.subscribe(handleLocation);
return () => {
connection.events.onPositionPacket.subscribe(handleLocation);
};
}, [connection]);
const handleLocation = useCallback(
(location: Types.PacketMetadata<Protobuf.Mesh.Position>) => {
setSelectedLocation(location);
},
[],
);
return (
<>
<Sidebar />
@ -46,22 +86,38 @@ const NodesPage = (): JSX.Element => {
<Table
headings={[
{ title: "", type: "blank", sortable: false },
{ title: "Name", type: "normal", sortable: true },
{ title: "Short Name", type: "normal", sortable: true },
{ title: "Long Name", type: "normal", sortable: true },
{ title: "Model", type: "normal", sortable: true },
{ title: "MAC Address", type: "normal", sortable: true },
{ title: "Last Heard", type: "normal", sortable: true },
{ title: "SNR", type: "normal", sortable: true },
{ title: "Encryption", type: "normal", sortable: false },
{ title: "Connection", type: "normal", sortable: true },
{ title: "Remove", type: "normal", sortable: false },
]}
rows={filteredNodes.map((node) => [
<span
key={node.num}
className="h-3 w-3 rounded-full bg-accent"
/>,
<div key={node.num}>
<Avatar text={node.user?.shortName.toString() ?? "UNK"} />
</div>,
<h1 key="header">
<h1
key="shortName"
onMouseDown={() => setSelectedNode(node)}
className="cursor-pointer"
>
{node.user?.shortName ??
(node.user?.macaddr
? `${base16
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
.toLowerCase()}`
: `${numberToHexUnpadded(node.num).slice(-4)}`)}
</h1>,
<h1
key="longName"
onMouseDown={() => setSelectedNode(node)}
className="cursor-pointer"
>
{node.user?.longName ??
(node.user?.macaddr
? `Meshtastic ${base16
@ -95,7 +151,7 @@ const NodesPage = (): JSX.Element => {
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
<LockIcon className="text-green-600" />
) : (
<LockOpenIcon className="text-yellow-300" />
<LockOpenIcon className="text-yellow-300 mx-auto" />
)}
</Mono>,
<Mono key="hops">
@ -108,19 +164,23 @@ const NodesPage = (): JSX.Element => {
: "-"}
{node.viaMqtt === true ? ", via MQTT" : ""}
</Mono>,
<Button
key="remove"
variant="destructive"
onClick={() => {
setNodeNumToBeRemoved(node.num);
setDialogOpen("nodeRemoval", true);
}}
>
<TrashIcon />
Remove
</Button>,
])}
/>
<NodeOptionsDialog
node={selectedNode}
open={!!selectedNode}
onOpenChange={() => setSelectedNode(undefined)}
/>
<TracerouteResponseDialog
traceroute={selectedTraceroute}
open={!!selectedTraceroute}
onOpenChange={() => setSelectedTraceroute(undefined)}
/>
<LocationResponseDialog
location={selectedLocation}
open={!!selectedLocation}
onOpenChange={() => setSelectedLocation(undefined)}
/>
</div>
<Footer />
</div>

Loading…
Cancel
Save