192 Commits

Author SHA1 Message Date
rcarteraz
72fc3ea337 Merge pull request #336 from rcarteraz/master
Fix Title
2024-11-09 10:43:00 -07:00
rcarteraz
82f4784107 Merge pull request #2 from rcarteraz/fix-title
Fix Title
2024-11-09 10:38:35 -07:00
rcarteraz
4f9fb9976d fix title 2024-11-09 10:37:06 -07:00
Sacha Weatherstone
42068ad3d8 move to RsBuild 2024-10-06 21:24:12 +10:00
Hunter Thornsberry
62f8c4509e Merge pull request #311 from medentem/master
Added Password Visibility Toggle
2024-10-02 23:07:11 -04:00
Hunter Thornsberry
d699764546 biome 2024-10-02 22:07:45 -04:00
medentem
8549d56c21 biome 2024-10-02 19:33:09 -05:00
medentem
4b532fc7f8 added password visibility toggle 2024-10-02 15:54:53 -05:00
Hunter Thornsberry
06d2c393ce Merge pull request #310 from Hunter275/remove-client-router
Remove router-client
2024-09-22 19:54:57 -04:00
Hunter Thornsberry
cecdf9758b remove router-client 2024-09-22 18:25:35 -04:00
Hunter Thornsberry
02cb4f2584 Merge pull request #303 from Hunter275/gps-precision 2024-09-18 13:45:17 -04:00
Hunter Thornsberry
8cfcd7b1af Merge pull request #306 from KomelT/feature/better-unknown
Unknown nodes
2024-09-17 16:26:56 -04:00
Tilen Komel
c0cb059f52 Merge branch 'meshtastic:master' into feature/better-unknown 2024-09-17 21:10:29 +02:00
Tilen Komel
a2a9b37238 Unknown to node hex on other missing places 2024-09-17 21:08:35 +02:00
Hunter Thornsberry
57d0d27bbb Merge pull request #304 from KomelT/feature/better-unknown
Unknown nodes
2024-09-17 14:54:05 -04:00
Hunter Thornsberry
0e92dd9bea There is no longer a setting here 2024-09-17 13:53:36 -04:00
Tilen Komel
c16ebf3917 Show hex on map instead of empty 2024-09-17 19:50:27 +02:00
Hunter Thornsberry
3d3a08a23f replace with select 2024-09-17 13:47:07 -04:00
Hunter Thornsberry
4d1227a942 Merge pull request #269 from KomelT/fix/static-ip-display
Fix/static ip display
2024-09-17 13:27:12 -04:00
Hunter Thornsberry
a8ee273b24 biome 2024-09-17 12:36:49 -04:00
Hunter Thornsberry
3ee7a57480 rewrite convertIpAddressToInt 2024-09-17 12:35:51 -04:00
Tilen Komel
2f2c777c56 Optimize 2024-09-17 07:32:12 +02:00
Tilen Komel
2f36118e9d Merge branch 'meshtastic:master' into fix/static-ip-display 2024-09-17 07:16:54 +02:00
Hunter Thornsberry
a6d161581f Merge pull request #300 from Hunter275/primary-channel
Only allow channel index 0 to be PRIMARY
2024-09-15 19:55:24 -04:00
Hunter Thornsberry
d05ea5a2cc Merge remote-tracking branch 'meshtastic-remote/master' into fix/static-ip-display 2024-09-15 19:39:13 -04:00
Hunter Thornsberry
471db94242 Merge branch 'master' into fix/static-ip-display 2024-09-15 19:38:03 -04:00
Hunter Thornsberry
2654e4fbc9 biome manual fixes 2024-09-15 19:24:16 -04:00
Hunter Thornsberry
f2aa5bfbee biome 2024-09-15 19:23:44 -04:00
Hunter Thornsberry
3b018b0c70 Only allow channel index 0 to be PRIMARY 2024-09-15 19:23:07 -04:00
Hunter Thornsberry
921db10d91 Merge pull request #297 from Hunter275/js-version-bump
js version bump
2024-09-12 17:20:09 -04:00
Hunter Thornsberry
bf4f593e3a use new js and remove polyfills 2024-09-12 14:20:32 -04:00
Hunter Thornsberry
1e061a1e19 polyfill 2024-09-11 23:07:52 -04:00
Hunter Thornsberry
9b9f537e2c js version bump 2024-09-11 22:13:46 -04:00
Hunter Thornsberry
985cce0b0d Merge pull request #294 from meshtastic/pki
PKI
2024-09-11 17:51:31 -04:00
Hunter Thornsberry
3fe38eb506 Merge pull request #295 from meshtastic/master
Update release.yml
2024-09-10 20:53:31 -04:00
Hunter Thornsberry
51081d3052 Update release.yml
Create a build artifact on release
2024-09-10 20:51:50 -04:00
Hunter Thornsberry
c08f6d16bb Merge branch 'master' into pki 2024-09-09 18:55:02 -04:00
Hunter Thornsberry
62ad4c49f8 Merge pull request #293 from Hunter275/pki-nodelist
Node List & DMs
2024-09-09 18:50:29 -04:00
Hunter Thornsberry
3b0a1e6108 biome 2024-09-09 18:37:22 -04:00
Hunter Thornsberry
c2f2205626 cleanup 2024-09-09 18:25:58 -04:00
Hunter Thornsberry
87c729d694 Merge branch 'pki' into pki-nodelist 2024-09-09 18:01:17 -04:00
Hunter Thornsberry
8e4f60edf3 biome 2024-09-09 16:36:56 -04:00
Hunter Thornsberry
8811eee9f5 Remove bluetooth debugging and reword generic debug 2024-09-09 16:36:17 -04:00
Hunter Thornsberry
2af93f1acd Fix protobufs, add configOkToMqtt, add PKI icons 2024-09-09 16:22:48 -04:00
Hunter Thornsberry
78a35544c7 Node list and DMs now show icons 2024-09-08 19:48:42 -04:00
Hunter Thornsberry
3ad2d650b0 update protobufs 2024-09-08 18:59:57 -04:00
Hunter Thornsberry
2e12b27566 Merge pull request #277 from KomelT/feature/security-tab
Update key generation
2024-09-08 18:13:42 -04:00
Hunter Thornsberry
989fad7e17 biome 2024-09-07 20:24:45 -04:00
Hunter Thornsberry
f7a2e5f76b Invert adminKey disabledBy 2024-09-07 20:24:18 -04:00
Hunter Thornsberry
2b81fc47e2 disable the public key input 2024-09-07 20:19:51 -04:00
Hunter Thornsberry
b522113cd7 bad merge on Security.tsx 2024-09-07 20:16:29 -04:00
Hunter Thornsberry
fce642c24e Merge branch 'pki' into feature/security-tab 2024-09-07 19:54:57 -04:00
Hunter Thornsberry
bf425a8ec7 Merge pull request #291 from Kongduino/patch-1 2024-09-07 12:21:47 -04:00
Kongduino
a7d0d36086 Update index.tsx
"at least", two words. Thanks...
2024-09-07 13:32:16 +08:00
Tilen Komel
5e72510bef Removed submit button 2024-09-06 09:33:13 +02:00
Hunter Thornsberry
88efdc4758 Merge branch 'master' into pki 2024-09-05 19:26:32 -04:00
Ben Meadors
530d33d1e4 Merge pull request #290 from Hunter275/disabled-dynamicform
Rework disabled in DynamicForms
2024-09-05 06:19:17 -05:00
Hunter Thornsberry
6375187d50 Merge branch 'pki' into disabled-dynamicform 2024-09-04 21:56:24 -04:00
Hunter Thornsberry
300fb5c3e5 fix disabled check location 2024-09-04 21:54:48 -04:00
Hunter Thornsberry
0dcc2b1975 rewrite disabled dynamicforms stuff and fix factory reset in command palette 2024-09-04 21:50:23 -04:00
Hunter Thornsberry
41acc4d25d Merge pull request #289 from Hunter275/gh-actions
GitHub Actions: Releases
2024-09-04 16:15:41 -04:00
Tilen Komel
d10c010b9a Dont disable PKI on Allow Legacy Admin 2024-09-04 18:50:49 +02:00
Hunter Thornsberry
80ab44c8db don't release on release 2024-09-04 00:39:12 -04:00
Hunter Thornsberry
7895df2d9f Stop release on ci 2024-09-04 00:35:22 -04:00
Hunter Thornsberry
c780437355 Merge pull request #286 from TheCyberRonin/feature/provide-compressed-build-during-pr
feat: Provide compressed build during PR GitHub Action
2024-09-04 00:12:26 -04:00
Tilen Komel
9d4aa05316 Updated pki regenerate dialog 2024-09-02 15:45:58 +02:00
Tilen Komel
354d04592b Fix build 2024-09-02 15:05:04 +02:00
Tilen Komel
9bea6870bb Add submit button to work 2024-09-02 13:11:42 +02:00
Tilen Komel
d0bd02980d Update private key generation 2024-09-02 13:10:59 +02:00
Hunter Thornsberry
7d1135b9dc Merge pull request #288 from mrfyda/master
Change 'powerScreenEnabled' config type to toggle
2024-09-01 19:00:25 -04:00
Tilen Komel
f66332b3e3 Handle undefined booleans 2024-08-31 15:58:12 +02:00
Rafael Cortês
05a6b6293e Change 'powerScreenEnabled' config type to toggle 2024-08-31 10:51:53 +01:00
Hunter Thornsberry
b9a8a2ba6c Merge pull request #287 from TheCyberRonin/feature/scope-ci-to-push-on-master
feat: Only do `CI` GHA on push to `master`
2024-08-30 12:31:09 -04:00
Ronin
c16070f02b feat: Only do CI GHA on push to master 2024-08-29 21:54:58 -04:00
Ronin
3e6653a98f chore: Fix typo in package name for uploading 2024-08-29 21:31:04 -04:00
Ronin
59126ca939 feat: Add a build step for build.tar 2024-08-29 21:27:37 -04:00
Tilen Komel
4bde402a53 Add key generation confirmation 2024-08-29 23:19:12 +02:00
Tilen Komel
4cf91272de Removed admin key generation from security 2024-08-29 23:19:12 +02:00
Tilen Komel
c711c39aa9 Add inputChange to FormInpit 2024-08-29 23:19:12 +02:00
Tilen Komel
c4342f1a2b Update passwordGenerator disabled 2024-08-29 23:19:12 +02:00
Tilen Komel
afc45588fa Fixed security key generation 2024-08-29 23:19:12 +02:00
Tilen Komel
22cd5aa88d Add key generation 2024-08-29 23:19:12 +02:00
Tilen Komel
8c4c8a760e Add @noble/curves 2024-08-29 23:19:11 +02:00
Tilen Komel
0911df6b0d Add dynamic bit to password generator 2024-08-29 23:16:02 +02:00
Tilen Komel
06c20fa950 Add dynamic value to input 2024-08-29 23:16:02 +02:00
Hunter Thornsberry
cd0056783c Merge pull request #284 from KomelT/pki
Upgrade @meshtastic/js 2.3.7-0 > 2.3.7-1
2024-08-29 17:13:10 -04:00
Tilen Komel
b1baf2d8e6 .factoryReset() split 2024-08-29 23:04:13 +02:00
Tilen Komel
2d106eb3a9 Upgrade @meshtastic/js 2.3.7-0 > 2.3.7-1 2024-08-29 22:52:36 +02:00
Hunter Thornsberry
22d900a831 Merge pull request #278 from Hunter275/dashboard-footer
flex-grow to make footer stick to bottom
2024-08-26 15:33:40 -04:00
Hunter Thornsberry
c6cc5e5e6f biome 2024-08-26 14:32:18 -04:00
Hunter Thornsberry
f0d8db1c87 flex-grow to make footer stick to bottom 2024-08-26 14:10:43 -04:00
Hunter Thornsberry
8c37be4af3 Merge pull request #274 from rcarteraz/add-footer 2024-08-24 11:50:51 -04:00
Hunter Thornsberry
076dae80b7 first working version 2024-08-23 18:58:22 -04:00
Hunter Thornsberry
7d4001ea9d Update footer and initial groundword 2024-08-23 18:40:17 -04:00
Hunter Thornsberry
4215eb1a55 Merge pull request #276 from KomelT/feature/security-tab
Key verification & generation
2024-08-22 13:28:56 -04:00
Tilen Komel
e3fad3015f Add hide toggle to password generator 2024-08-22 15:45:11 +02:00
Tilen Komel
02a63c213e Add key verification & generation 2024-08-22 14:08:17 +02:00
Tilen Komel
8ed3ce8203 Error & Format fixing 2024-08-21 23:13:35 +02:00
Tilen Komel
ebd5a3d3a6 Implemented IP utils 2024-08-21 21:53:07 +02:00
Tilen Komel
1cdf18747d Added ip utils 2024-08-21 21:53:07 +02:00
Hunter Thornsberry
737fbb4320 Merge pull request #275 from KomelT/feature/security-tab
Feature: Security tab
2024-08-21 13:45:31 -04:00
Tilen Komel
66fb300575 Moved prop. from Device to Security & Hide PKI Eye 2024-08-21 18:52:03 +02:00
Tilen Komel
cf423620c4 Error & formating fixes 2024-08-21 17:41:24 +02:00
Tilen Komel
8bb0a96744 Downgraded @buf/meshtastic_protobufs.bufbuild_es from 2.0 to 1.10 2024-08-21 13:13:42 +02:00
Tilen Komel
65247c4f35 Update Security tab 2024-08-20 18:51:02 +02:00
Tilen Komel
af51659e71 Add always to disabled by in dynamics form 2024-08-20 18:50:19 +02:00
Tilen Komel
f0eae444c7 Update descriptions in Security 2024-08-20 18:10:39 +02:00
Tilen Komel
6d3bf39b76 Updated @buf/meshtastic_protobufs.bufbuild_es to lastest 2024-08-20 18:06:27 +02:00
Tilen Komel
7d5950d6cc Add SecurityValidation to security channel 2024-08-20 17:23:42 +02:00
Tilen Komel
d3836a7250 Add class SecurityValidation 2024-08-20 17:19:36 +02:00
Tilen Komel
f64b96527e Add security to device config store 2024-08-20 17:18:55 +02:00
Tilen Komel
be9169f56f Use protobuf pki branch, tmp for dev 2024-08-20 15:08:13 +02:00
Tilen Komel
f6be57224e Updated inputs 2024-08-20 15:04:10 +02:00
Tilen Komel
a8dcab0844 Added security input fields 2024-08-20 00:33:27 +02:00
Tilen Komel
049f3de919 Added security tab 2024-08-20 00:33:27 +02:00
rcarteraz
6c1f140ad1 oops -- helps if you use the full link 2024-08-19 13:31:22 -07:00
rcarteraz
4c4be2e18f temp solution 2024-08-19 13:20:56 -07:00
Hunter Thornsberry
2b34d78a86 Merge pull request #266 from Hunter275/issue-261-password-generator
PSK Generator
2024-08-15 19:59:02 -04:00
Hunter Thornsberry
54a7b88146 rewrite PSK validation 2024-08-10 19:50:21 -04:00
Hunter Thornsberry
e7892fd6a0 biome 2024-08-10 19:07:02 -04:00
Hunter Thornsberry
1eedb6d97b validation 2024-08-10 18:50:47 -04:00
Hunter Thornsberry
faf094084c remove used imports and rename clickEventCb 2024-08-06 01:04:44 -04:00
Hunter Thornsberry
6bbe995ee5 Merge branch 'meshtastic:master' into issue-261-password-generator 2024-08-06 00:59:04 -04:00
Hunter Thornsberry
32acd23362 biome 2024-08-06 00:58:26 -04:00
Hunter Thornsberry
cd0fcbbf90 initial working version 2024-08-06 00:50:17 -04:00
Hunter Thornsberry
cfc2ea0fe5 Merge pull request #268 from KomelT/fix/hops-singular
Fixed hops singular form
2024-08-06 00:45:10 -04:00
Tilen Komel
5771e1b733 Fixed hops singular form 2024-08-05 16:33:57 +02:00
Hunter Thornsberry
1c7c44a472 biome linting 2024-07-28 17:46:51 -04:00
Hunter Thornsberry
9c6aff534a a bit of cleanup 2024-07-28 16:43:41 -04:00
Hunter Thornsberry
38b7e600b1 key generation 2024-07-28 15:57:17 -04:00
Hunter Thornsberry
1ae879342a added password generator 2024-07-28 15:09:11 -04:00
Hunter Thornsberry
2528391814 Merge pull request #260 from AddisonTustin/qr-code-query-string
fix: move qrcode URL query-string before fragment
2024-07-24 19:13:27 -04:00
Hunter Thornsberry
d6147f5b7f Merge pull request #265 from fifieldt/fix-message-limit
Allow single character messages in MessageInput
2024-07-24 13:16:07 -04:00
Tom Fifield
8c17a8be38 Allow single character messages in MessageInput
In character-based languages (eg Chinese, Japanese, Korean), there
are many common interjections that are single characters. Allow
single character messages to support these users to communicate
naturally.

fixes meshtastic/web#264
2024-07-24 12:47:39 +08:00
Addison Tustin
a4e2e7eec1 fix: move qrcode URL query-string before fragment 2024-07-20 12:23:30 -07:00
Hunter Thornsberry
4653656420 Merge pull request #211 from fifieldt/traceroute
Add Traceroute Feature
2024-07-04 17:43:41 -04:00
Hunter Thornsberry
d42e8c10a0 minor changes and biome fixes 2024-07-04 17:29:40 -04:00
Hunter Thornsberry
0955bbe24b Merge remote-tracking branch 'hunter275/traceroute' into traceroute 2024-07-04 17:09:31 -04:00
Hunter Thornsberry
f3fbe75c66 Merge branch 'meshtastic:master' into traceroute 2024-07-04 17:06:28 -04:00
Hunter Thornsberry
bcebfd211b Merge remote-tracking branch 'meshtastic/master' into traceroute 2024-07-04 17:05:33 -04:00
Hunter Thornsberry
5bd385b535 Merge pull request #258 from Hunter275/issue-257-node-sorting
fix sorting + new protobufs
2024-06-28 22:29:18 -04:00
Hunter Thornsberry
9f5604971b node stuff 2024-06-28 12:41:02 -04:00
Hunter Thornsberry
93d9f721b4 protobuf changes 2024-06-28 12:36:32 -04:00
Hunter Thornsberry
569bb09f94 fix sorting 2024-06-28 12:30:06 -04:00
Hunter Thornsberry
2566395168 Merge pull request #252 from Hunter275/issue-251-mqtt-saving
Fix MQTT settings
2024-06-26 09:24:29 -04:00
Hunter Thornsberry
8893a196c2 ignore vercel.json in biome 2024-06-25 14:31:37 -04:00
Hunter Thornsberry
fdbcdd955b vercel fix v2 2024-06-25 14:18:03 -04:00
Hunter Thornsberry
723c9ee5d8 vercel fix 2024-06-25 14:16:19 -04:00
Hunter Thornsberry
22c862fd08 revert package.json 2024-06-25 12:43:14 -04:00
Hunter Thornsberry
c47aeb652b Merge branch 'issue-251-mqtt-saving' of https://github.com/Hunter275/web into issue-251-mqtt-saving 2024-06-25 12:39:32 -04:00
Hunter Thornsberry
2f37390985 Linting 2024-06-25 12:39:06 -04:00
Hunter Thornsberry
205c9a21e3 Merge branch 'master' into issue-251-mqtt-saving 2024-06-25 12:38:48 -04:00
Hunter Thornsberry
29fab4b6f3 Merge branch 'master' into issue-251-mqtt-saving 2024-06-23 20:19:29 -04:00
Hunter Thornsberry
0dddf6ad2f Merge pull request #213 from bmv437/feature/enforce-biome
Update biome, fix and enforce recommended rules
2024-06-23 20:17:44 -04:00
Hunter Thornsberry
b24c9312c4 linting fixes 2024-06-23 20:15:33 -04:00
Hunter Thornsberry
268ad4908a remove duplicate icon 2024-06-23 20:10:18 -04:00
Hunter Thornsberry
fa127eda33 Merge branch 'master' into feature/enforce-biome 2024-06-23 20:08:56 -04:00
Hunter Thornsberry
8942468c2c Merge pull request #253 from Server2003User/master
Add device firmware version to WebUI
2024-06-23 19:10:59 -04:00
Hunter Thornsberry
3aedc7b7c2 fixes 2024-06-23 19:03:59 -04:00
server2003user
401a594ec7 Add Firmware 2024-06-23 18:24:46 -04:00
Hunter Thornsberry
a5a37cd4ab update setWorkingModuleConfig to include mapReportSettings 2024-06-22 21:14:50 -04:00
Hunter Thornsberry
283f548136 add optional to publishIntervalSecs and positionPrecision 2024-06-22 20:54:45 -04:00
Brandon Vandegrift
93b139c268 Update biome, fix and enforce recommended rules 2024-06-22 12:54:44 -04:00
Tom Fifield
9e04c1f486 Merge branch 'master' into traceroute 2024-06-21 15:36:03 +08:00
Hunter Thornsberry
6b268b79ed Merge pull request #165 from geftactics/master
Fix sorting on peers table
2024-06-20 22:02:11 -04:00
Hunter Thornsberry
11fe2fdb35 fix sorting 2024-06-20 21:55:30 -04:00
Hunter Thornsberry
69f67e3657 Merge pull request #244 from Hunter275/issues-242-243-dropdown-and-warning
update MQTT precision dropdown and encryption warning
2024-06-18 15:36:32 -04:00
Hunter Thornsberry
4143249c55 remove mention of channel 2024-06-18 14:19:29 -04:00
Hunter Thornsberry
4c1737bc44 remove precise location mention as there is a setting for that 2024-06-18 14:18:55 -04:00
Hunter Thornsberry
17c44054be update MQTT precision dropdown and encryption warning 2024-06-17 17:01:29 -04:00
Tom Fifield
026256ac5c Merge branch 'meshtastic:master' into master 2024-06-17 12:14:11 +08:00
Hunter Thornsberry
e9a681ab21 Add tr column and Toast when starting tr 2024-06-16 11:01:39 -04:00
Hunter Thornsberry
bcac95e7ed Merge remote-tracking branch 'origin/master' into traceroute 2024-06-16 11:01:06 -04:00
Tom Fifield
daff97a5e0 Fix double double arrow on unknown nodes in multi-hop traceroutes 2024-05-27 21:04:12 +08:00
Tom Fifield
452f2581e2 Fix incorrect use of operator build error. 2024-05-27 21:02:42 +08:00
Tom Fifield
569c2daa09 Fix traceroute ordering for multi-hop traceroutes 2024-05-27 20:57:09 +08:00
Tom Fifield
e825a737b0 Fix build errors 2024-05-27 12:08:32 +08:00
Tom Fifield
54c983439c Fix type Number - number 2024-05-27 11:52:33 +08:00
Tom Fifield
7b6b8daeba Fix build errors
Several types were wrong.
2024-05-27 11:40:59 +08:00
Tom Fifield
0f89e04bb0 Only show traceroutes on node direct message pageso 2024-05-27 10:56:34 +08:00
Tom Fifield
a728b848e1 Don't show traceroute button on Channels
It doesn't work, it's only for nodes!
2024-05-27 10:39:45 +08:00
Tom Fifield
f16cce90c8 Fix case where intermediary node in traceroute is unknown. 2024-05-27 10:09:59 +08:00
Tom Fifield
056a194ede Add formatting logic for multi-hop traceroutes 2024-05-27 09:53:20 +08:00
Tom Fifield
be3117651c Add basic traceroute display
This patch adds basic traceroute display functionality on the
Messages page for an individual node.
2024-05-26 21:00:30 +08:00
Tom Fifield
86263990e6 Add Toast lib to Messages page 2024-05-26 18:41:48 +08:00
Tom Fifield
390b16812a Subscribe to and store traceroutePackets 2024-05-26 18:41:13 +08:00
Tom Fifield
1458497fc3 Add button to Message page to send a traceroute to a node.
As noted in meshtastic/web#179, it would be great to be able to
send traceroutes from the web application.

This patch adds a button to the messages page that sends a
traceroute to the selected node.
2024-05-26 15:42:50 +08:00
Tom Fifield
97b9570196 Revert "Add button to Message page to send a traceroute to a node."
This reverts commit 6e145421ef.
2024-05-26 15:41:37 +08:00
Chris Drackett
82c1d3e3f1 Support floats in number fields 2024-05-26 15:41:32 +08:00
Tom Fifield
a7237bcb2b Revert "Support floats in number fields"
This reverts commit 5fdd07ee68.
2024-05-26 15:40:55 +08:00
Tom Fifield
6e145421ef Add button to Message page to send a traceroute to a node.
As noted in meshtastic/web#179, it would be great to be able to
send traceroutes from the web application.

This patch adds a button to the messages page that sends a
traceroute to the selected node.
2024-05-26 15:37:17 +08:00
geftactics
98f8965aed Fix sorting on peers table
Enables sorting by clicking column heading.
Defaults to most recent contacts at the top
2024-02-22 12:19:18 +00:00
117 changed files with 6677 additions and 3878 deletions

View File

@@ -1,6 +1,9 @@
name: CI
on: push
on:
push:
branches:
- master
permissions:
contents: write
@@ -21,42 +24,3 @@ jobs:
- name: Build Package
run: pnpm build
- name: Package Output
run: pnpm package
- name: Upload Artifact
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: false
files: |
./dist/build.tar
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Buildah Build
id: build-container
uses: redhat-actions/buildah-build@v2
with:
containerfiles: |
./Containerfile
image: ${{github.event.repository.full_name}}
tags: latest ${{ github.sha }}
oci: true
platforms: linux/amd64, linux/arm64
- name: Push To Registry
id: push-to-registry
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-container.outputs.image }}
tags: ${{ steps.build-container.outputs.tags }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Print image url
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"

View File

@@ -19,3 +19,12 @@ jobs:
- name: Build Package
run: pnpm build
- name: Compress build
run: pnpm package
- name: Archive compressed build
uses: actions/upload-artifact@v4
with:
name: build
path: dist/build.tar

61
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: 'Release'
on:
release:
types: [released]
permissions:
contents: write
packages: write
jobs:
build-and-package:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- name: Install Dependencies
run: pnpm install
- name: Build Package
run: pnpm build
- name: Package Output
run: pnpm package
- name: Archive compressed build
uses: actions/upload-artifact@v4
with:
name: build
path: dist/build.tar
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Buildah Build
id: build-container
uses: redhat-actions/buildah-build@v2
with:
containerfiles: |
./Containerfile
image: ${{github.event.repository.full_name}}
tags: latest ${{ github.sha }}
oci: true
platforms: linux/amd64, linux/arm64
- name: Push To Registry
id: push-to-registry
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-container.outputs.image }}
tags: ${{ steps.build-container.outputs.tags }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Print image url
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"

View File

@@ -1,4 +1,7 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
}
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit"
},
"editor.formatOnSave": true
}

View File

@@ -1,10 +1,11 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.3/schema.json",
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"ignoreUnknown": true
"ignoreUnknown": true,
"ignore": ["vercel.json"]
},
"vcs": {
"enabled": true,
@@ -20,7 +21,7 @@
"linter": {
"enabled": true,
"rules": {
"all": true
"recommended": true
}
}
}

View File

@@ -5,10 +5,12 @@
"description": "Meshtastic web client",
"license": "GPL-3.0-only",
"scripts": {
"dev": "vite --host",
"build": "tsc && vite build",
"build": "rsbuild build",
"check": "biome check .",
"preview": "vite preview",
"check:fix": "pnpm check --write",
"dev": "rsbuild dev --open",
"format": "biome format --write",
"preview": "rsbuild preview",
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)"
},
"repository": {
@@ -20,64 +22,66 @@
},
"homepage": "https://meshtastic.org",
"dependencies": {
"@bufbuild/protobuf": "^1.8.0",
"@bufbuild/protobuf": "^1.10.0",
"@emeraldpay/hashicon-react": "^0.5.2",
"@meshtastic/js": "2.3.4-0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@meshtastic/js": "2.3.7-5",
"@noble/curves": "^1.5.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.1",
"@turf/turf": "^6.5.0",
"base64-js": "^1.5.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"immer": "^10.0.4",
"crypto-random-string": "^5.0.0",
"immer": "^10.1.1",
"lucide-react": "^0.363.0",
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
"mapbox-gl": "^3.6.0",
"maplibre-gl": "4.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.0",
"react-map-gl": "7.1.7",
"react-qrcode-logo": "^2.9.0",
"react-qrcode-logo": "^2.10.0",
"rfc4648": "^1.5.3",
"tailwind-merge": "^2.2.2",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"timeago-react": "^3.0.6",
"vite-plugin-node-polyfills": "^0.22.0",
"zustand": "4.5.2"
},
"devDependencies": {
"@biomejs/biome": "^1.6.3",
"@buf/meshtastic_protobufs.bufbuild_es": "1.8.0-20240325205556-b11811405eea.2",
"@biomejs/biome": "^1.8.2",
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240906232734-3da561588c55.1",
"@rsbuild/core": "^1.0.10",
"@rsbuild/plugin-react": "^1.0.3",
"@types/chrome": "^0.0.263",
"@types/node": "^20.11.30",
"@types/react": "^18.2.73",
"@types/react-dom": "^18.2.23",
"@types/node": "^20.14.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/w3c-web-serial": "^1.0.6",
"@types/web-bluetooth": "^0.0.20",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"gzipper": "^7.2.0",
"postcss": "^8.4.38",
"rollup-plugin-visualizer": "^5.12.0",
"tailwindcss": "^3.4.3",
"tailwindcss": "^3.4.4",
"tar": "^6.2.1",
"tslib": "^2.6.2",
"typescript": "^5.4.3",
"vite": "^5.2.6",
"vite-plugin-environment": "^1.1.3"
"tslib": "^2.6.3",
"typescript": "^5.5.2"
}
}

7735
pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
autoprefixer: {},
},
};

30
rsbuild.config.ts Normal file
View File

@@ -0,0 +1,30 @@
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { execSync } from "node:child_process";
let hash = "";
try {
hash = execSync("git rev-parse --short HEAD").toString().trim();
} catch (error) {
hash = "DEV";
}
export default defineConfig({
plugins: [pluginReact()],
source: {
define: {
"process.env.COMMIT_HASH": JSON.stringify(hash),
},
alias: {
"@app": "./src",
"@pages": "./src/pages",
"@components": "./src/components",
"@core": "./src/core",
"@layouts": "./src/layouts",
},
},
html: {
title: "Meshtastic Web",
},
});

View File

@@ -1,14 +1,15 @@
import { DeviceWrapper } from "@app/DeviceWrapper.js";
import { PageRouter } from "@app/PageRouter.js";
import { CommandPalette } from "@components/CommandPalette.js";
import { DeviceSelector } from "@components/DeviceSelector.js";
import { DialogManager } from "@components/Dialog/DialogManager.js";
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.js";
import { Toaster } from "@components/Toaster.js";
import { ThemeController } from "@components/generic/ThemeController.js";
import { useAppStore } from "@core/stores/appStore.js";
import { useDeviceStore } from "@core/stores/deviceStore.js";
import { Dashboard } from "@pages/Dashboard/index.js";
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
import { PageRouter } from "@app/PageRouter.tsx";
import { CommandPalette } from "@components/CommandPalette.tsx";
import { DeviceSelector } from "@components/DeviceSelector.tsx";
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
import { Toaster } from "@components/Toaster.tsx";
import Footer from "@components/UI/Footer.tsx";
import { ThemeController } from "@components/generic/ThemeController.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { Dashboard } from "@pages/Dashboard/index.tsx";
import { MapProvider } from "react-map-gl";
export const App = (): JSX.Element => {
@@ -40,7 +41,11 @@ export const App = (): JSX.Element => {
<PageRouter />
</div>
) : (
<Dashboard />
<>
<Dashboard />
<div className="flex flex-grow" />
<Footer />
</>
)}
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { DeviceContext } from "@core/stores/deviceStore.js";
import type { Device } from "@core/stores/deviceStore.js";
import { DeviceContext } from "@core/stores/deviceStore.ts";
import type { Device } from "@core/stores/deviceStore.ts";
import type { ReactNode } from "react";
export interface DeviceWrapperProps {

View File

@@ -1,9 +1,9 @@
import { useDevice } from "@core/stores/deviceStore.js";
import { ChannelsPage } from "@pages/Channels.js";
import { ConfigPage } from "@pages/Config/index.js";
import { MapPage } from "@pages/Map.js";
import { MessagesPage } from "@pages/Messages.js";
import { NodesPage } from "@pages/Nodes.js";
import { useDevice } from "@core/stores/deviceStore.ts";
import { ChannelsPage } from "@pages/Channels.tsx";
import { ConfigPage } from "@pages/Config/index.tsx";
import { MapPage } from "@pages/Map.tsx";
import { MessagesPage } from "@pages/Messages.tsx";
import { NodesPage } from "@pages/Nodes.tsx";
export const PageRouter = (): JSX.Element => {
const { activePage } = useDevice();

View File

@@ -5,9 +5,9 @@ import {
CommandInput,
CommandItem,
CommandList,
} from "@components/UI/Command.js";
import { useAppStore } from "@core/stores/appStore.js";
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.js";
} from "@components/UI/Command.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { useCommandState } from "cmdk";
import {
@@ -19,7 +19,7 @@ import {
LayersIcon,
LayoutIcon,
LinkIcon,
LucideIcon,
type LucideIcon,
MapIcon,
MessageSquareIcon,
MoonIcon,
@@ -200,10 +200,17 @@ export const CommandPalette = (): JSX.Element => {
},
},
{
label: "Factory Reset",
label: "Factory Reset Device",
icon: FactoryIcon,
action() {
connection?.factoryReset();
connection?.factoryResetDevice();
},
},
{
label: "Factory Reset Config",
icon: FactoryIcon,
action() {
connection?.factoryResetConfig();
},
},
],
@@ -350,7 +357,7 @@ export const CommandPalette = (): JSX.Element => {
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
}, []);
}, [setCommandPaletteOpen]);
return (
<CommandDialog

View File

@@ -1,16 +1,16 @@
import { DeviceSelectorButton } from "@components/DeviceSelectorButton.js";
import { Separator } from "@components/UI/Seperator.js";
import { Code } from "@components/UI/Typography/Code.js";
import { useAppStore } from "@core/stores/appStore.js";
import { useDeviceStore } from "@core/stores/deviceStore.js";
import { DeviceSelectorButton } from "@components/DeviceSelectorButton.tsx";
import { Separator } from "@components/UI/Seperator.tsx";
import { Code } from "@components/UI/Typography/Code.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import {
HomeIcon,
LanguagesIcon,
MoonIcon,
PlusIcon,
SunIcon,
SearchIcon,
SunIcon,
} from "lucide-react";
export const DeviceSelector = (): JSX.Element => {

View File

@@ -1,5 +1,5 @@
import { useDevice } from "@app/core/stores/deviceStore.js";
import { Button } from "@components/UI/Button.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
@@ -7,9 +7,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Protobuf } from "@meshtastic/js";
import { useForm } from "react-hook-form";

View File

@@ -1,10 +1,10 @@
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
import { ImportDialog } from "@components/Dialog/ImportDialog.js";
import { QRDialog } from "@components/Dialog/QRDialog.js";
import { RebootDialog } from "@components/Dialog/RebootDialog.js";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js"
import { useDevice } from "@core/stores/deviceStore.js";
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
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";
export const DialogManager = (): JSX.Element => {
const { channels, config, dialog, setDialogOpen } = useDevice();

View File

@@ -1,5 +1,5 @@
import { Button } from "@components/UI/Button.js";
import { Checkbox } from "@components/UI/Checkbox.js";
import { Button } from "@components/UI/Button.tsx";
import { Checkbox } from "@components/UI/Checkbox.tsx";
import {
Dialog,
DialogContent,
@@ -7,11 +7,11 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
import { Switch } from "@components/UI/Switch.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Switch } from "@components/UI/Switch.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
import { toByteArray } from "base64-js";
import { useEffect, useState } from "react";
@@ -26,19 +26,34 @@ export const ImportDialog = ({
open,
onOpenChange,
}: ImportDialogProps): JSX.Element => {
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [importDialogInput, setImportDialogInput] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
const [validUrl, setValidUrl] = useState<boolean>(false);
const { connection } = useDevice();
useEffect(() => {
const base64String = qrCodeUrl.split("e/#")[1];
const paddedString = base64String
?.padEnd(base64String.length + ((4 - (base64String.length % 4)) % 4), "=")
.replace(/-/g, "+")
.replace(/_/g, "/");
// the channel information is contained in the URL's fragment, which will be present after a
// non-URL encoded `#`.
try {
const channelsUrl = new URL(importDialogInput);
if (
(channelsUrl.hostname !== "meshtastic.org" &&
channelsUrl.pathname !== "/e/") ||
!channelsUrl.hash
) {
throw "Invalid Meshtastic URL";
}
const encodedChannelConfig = channelsUrl.hash.substring(1);
const paddedString = encodedChannelConfig
.padEnd(
encodedChannelConfig.length +
((4 - (encodedChannelConfig.length % 4)) % 4),
"=",
)
.replace(/-/g, "+")
.replace(/_/g, "/");
setChannelSet(
Protobuf.AppOnly.ChannelSet.fromBinary(toByteArray(paddedString)),
);
@@ -47,7 +62,7 @@ export const ImportDialog = ({
setValidUrl(false);
setChannelSet(undefined);
}
}, [qrCodeUrl]);
}, [importDialogInput]);
const apply = () => {
channelSet?.settings.map((ch, index) => {
@@ -87,10 +102,10 @@ export const ImportDialog = ({
<div className="flex flex-col gap-3">
<Label>Channel Set/QR Code URL</Label>
<Input
value={qrCodeUrl}
value={importDialogInput}
suffix={validUrl ? "✅" : "❌"}
onChange={(e) => {
setQrCodeUrl(e.target.value);
setImportDialogInput(e.target.value);
}}
/>
{validUrl && (

View File

@@ -1,20 +1,20 @@
import { BLE } from "@components/PageComponents/Connect/BLE.js";
import { HTTP } from "@components/PageComponents/Connect/HTTP.js";
import { Serial } from "@components/PageComponents/Connect/Serial.js";
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
} from "@components/UI/Dialog.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.js";
import { Link } from "@components/UI/Typography/Link.js";
import { Subtle } from "@components/UI/Typography/Subtle.js";
} from "@components/UI/Tabs.tsx";
import { Link } from "@components/UI/Typography/Link.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
export interface TabElementProps {
closeDialog: () => void;

View File

@@ -0,0 +1,39 @@
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
export interface PkiRegenerateDialogProps {
open: boolean;
onOpenChange: () => void;
onSubmit: () => void;
}
export const PkiRegenerateDialog = ({
open,
onOpenChange,
onSubmit,
}: PkiRegenerateDialogProps): JSX.Element => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Regenerate Key pair?</DialogTitle>
<DialogDescription>
Are you sure you want to regenerate key pair?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="destructive" onClick={() => onSubmit()}>
Regenerate
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,4 +1,4 @@
import { Checkbox } from "@components/UI/Checkbox.js";
import { Checkbox } from "@components/UI/Checkbox.tsx";
import {
Dialog,
DialogContent,
@@ -6,13 +6,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
import { Protobuf, Types } from "@meshtastic/js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Protobuf, type Types } from "@meshtastic/js";
import { fromByteArray } from "base64-js";
import { ClipboardIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { QRCode } from "react-qrcode-logo";
export interface QRDialogProps {
@@ -32,7 +32,7 @@ export const QRDialog = ({
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
const allChannels = Array.from(channels.values());
const allChannels = useMemo(() => Array.from(channels.values()), [channels]);
useEffect(() => {
const channelsToEncode = allChannels
@@ -50,8 +50,10 @@ export const QRDialog = ({
.replace(/\+/g, "-")
.replace(/\//g, "_");
setQrCodeUrl(`https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`);
}, [channels, selectedChannels, qrCodeAdd, loraConfig]);
setQrCodeUrl(
`https://meshtastic.org/e/${qrCodeAdd ? "?add=true" : ""}#${base64}`,
);
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -97,18 +99,26 @@ export const QRDialog = ({
</div>
<div className="flex justify-center">
<button
type="button"
className={ "border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
type="button"
className={`border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
qrCodeAdd
? "focus:ring-green-800 bg-green-800 text-white"
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
}`}
onClick={() => setQrCodeAdd(true)}
>
Add Channels
>
Add Channels
</button>
<button
type="button"
className={ "border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (!qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
type="button"
className={`border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
!qrCodeAdd
? "focus:ring-green-800 bg-green-800 text-white"
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
}`}
onClick={() => setQrCodeAdd(false)}
>
Replace Channels
>
Replace Channels
</button>
</div>
</div>

View File

@@ -1,13 +1,13 @@
import { Button } from "@components/UI/Button.js";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { ClockIcon, RefreshCwIcon } from "lucide-react";
import { useState } from "react";
@@ -37,7 +37,7 @@ export const RebootDialog = ({
<Input
type="number"
value={time}
onChange={(e) => setTime(parseInt(e.target.value))}
onChange={(e) => setTime(Number.parseInt(e.target.value))}
action={{
icon: ClockIcon,
onClick() {

View File

@@ -1,6 +1,6 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore.js";
import { Button } from "@components/UI/Button.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
@@ -8,8 +8,8 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Label } from "@components/UI/Label.js";
} from "@components/UI/Dialog.tsx";
import { Label } from "@components/UI/Label.tsx";
export interface RemoveNodeDialogProps {
open: boolean;
@@ -44,7 +44,9 @@ export const RemoveNodeDialog = ({
</form>
</div>
<DialogFooter>
<Button variant="destructive" onClick={() => onSubmit()}>Remove</Button>
<Button variant="destructive" onClick={() => onSubmit()}>
Remove
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,13 +1,13 @@
import { Button } from "@components/UI/Button.js";
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@components/UI/Dialog.tsx";
import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { ClockIcon, PowerIcon } from "lucide-react";
import { useState } from "react";
@@ -38,7 +38,7 @@ export const ShutdownDialog = ({
<Input
type="number"
value={time}
onChange={(e) => setTime(parseInt(e.target.value))}
onChange={(e) => setTime(Number.parseInt(e.target.value))}
suffix="Minutes"
/>
<Button

View File

@@ -1,17 +1,17 @@
import {
DynamicFormField,
FieldProps,
} from "@components/Form/DynamicFormField.js";
import { FieldWrapper } from "@components/Form/FormWrapper.js";
import { Button } from "@components/UI/Button.js";
import { H4 } from "@components/UI/Typography/H4.js";
import { Subtle } from "@components/UI/Typography/Subtle.js";
type FieldProps,
} from "@components/Form/DynamicFormField.tsx";
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
import { Button } from "@components/UI/Button.tsx";
import { H4 } from "@components/UI/Typography/H4.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import {
Control,
DefaultValues,
FieldValues,
Path,
SubmitHandler,
type Control,
type DefaultValues,
type FieldValues,
type Path,
type SubmitHandler,
useForm,
} from "react-hook-form";
@@ -23,10 +23,12 @@ interface DisabledBy<T> {
export interface BaseFormBuilderProps<T> {
name: Path<T>;
disabled?: boolean;
disabledBy?: DisabledBy<T>[];
label: string;
description?: string;
properties?: {};
validationText?: string;
properties?: Record<string, unknown>;
}
export interface GenericFormElementProps<T extends FieldValues, Y> {
@@ -39,11 +41,12 @@ export interface DynamicFormProps<T extends FieldValues> {
onSubmit: SubmitHandler<T>;
submitType?: "onChange" | "onSubmit";
hasSubmitButton?: boolean;
// defaultValues?: DeepPartial<T>;
defaultValues?: DefaultValues<T>;
fieldGroups: {
label: string;
description: string;
valid?: boolean;
validationText?: string;
fields: FieldProps<T>[];
}[];
}
@@ -60,11 +63,16 @@ export function DynamicForm<T extends FieldValues>({
defaultValues: defaultValues,
});
const isDisabled = (disabledBy?: DisabledBy<T>[]): boolean => {
const isDisabled = (
disabledBy?: DisabledBy<T>[],
disabled?: boolean,
): boolean => {
if (disabled) return true;
if (!disabledBy) return false;
return disabledBy.some((field) => {
const value = getValues(field.fieldName);
if (value === "always") return true;
if (typeof value === "boolean") return field.invert ? value : !value;
if (typeof value === "number")
return field.invert
@@ -94,12 +102,20 @@ export function DynamicForm<T extends FieldValues>({
</div>
{fieldGroup.fields.map((field) => (
<FieldWrapper label={field.label} description={field.description}>
<FieldWrapper
key={field.label}
label={field.label}
description={field.description}
valid={
field.validationText === undefined ||
field.validationText === ""
}
validationText={field.validationText}
>
<DynamicFormField
key={field.label}
field={field}
control={control}
disabled={isDisabled(field.disabledBy)}
disabled={isDisabled(field.disabledBy, field.disabled)}
/>
</FieldWrapper>
))}

View File

@@ -1,12 +1,26 @@
import { GenericInput, InputFieldProps } from "@components/Form/FormInput.js";
import { SelectFieldProps, SelectInput } from "@components/Form/FormSelect.js";
import { ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.js";
import {
GenericInput,
type InputFieldProps,
} from "@components/Form/FormInput.tsx";
import {
PasswordGenerator,
type PasswordGeneratorProps,
} from "@components/Form/FormPasswordGenerator.tsx";
import {
type SelectFieldProps,
SelectInput,
} from "@components/Form/FormSelect.tsx";
import {
type ToggleFieldProps,
ToggleInput,
} from "@components/Form/FormToggle.tsx";
import type { Control, FieldValues } from "react-hook-form";
export type FieldProps<T> =
| InputFieldProps<T>
| SelectFieldProps<T>
| ToggleFieldProps<T>;
| ToggleFieldProps<T>
| PasswordGeneratorProps<T>;
export interface DynamicFormFieldProps<T extends FieldValues> {
field: FieldProps<T>;
@@ -35,6 +49,14 @@ export function DynamicFormField<T extends FieldValues>({
return (
<SelectInput field={field} control={control} disabled={disabled} />
);
case "passwordGenerator":
return (
<PasswordGenerator
field={field}
control={control}
disabled={disabled}
/>
);
case "multiSelect":
return <div>tmp</div>;
}

View File

@@ -1,14 +1,19 @@
import type {
BaseFormBuilderProps,
GenericFormElementProps,
} from "@components/Form/DynamicForm.js";
import { Input } from "@components/UI/Input.js";
} from "@components/Form/DynamicForm.tsx";
import { Input } from "@components/UI/Input.tsx";
import type { LucideIcon } from "lucide-react";
import { Controller, FieldValues } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { Controller, type FieldValues } from "react-hook-form";
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
type: "text" | "number" | "password";
inputChange?: ChangeEventHandler;
properties?: {
value?: string;
prefix?: string;
suffix?: string;
step?: number;
@@ -24,22 +29,38 @@ export function GenericInput<T extends FieldValues>({
disabled,
field,
}: GenericFormElementProps<T, InputFieldProps<T>>) {
const [passwordShown, setPasswordShown] = useState(false);
const togglePasswordVisiblity = () => {
setPasswordShown(!passwordShown);
};
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, onChange, ...rest } }) => (
<Input
type={field.type}
type={
field.type === "password" && passwordShown ? "text" : field.type
}
action={
field.type === "password"
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined
}
step={field.properties?.step}
value={field.type === "number" ? Number.parseFloat(value) : value}
onChange={(e) =>
onChange={(e) => {
if (field.inputChange) field.inputChange(e);
onChange(
field.type === "number"
? Number.parseFloat(e.target.value)
: e.target.value,
)
}
);
}}
{...field.properties}
{...rest}
disabled={disabled}

View File

@@ -0,0 +1,61 @@
import type {
BaseFormBuilderProps,
GenericFormElementProps,
} from "@components/Form/DynamicForm.tsx";
import { Generator } from "@components/UI/Generator.tsx";
import { Eye, EyeOff } from "lucide-react";
import type { ChangeEventHandler, MouseEventHandler } from "react";
import { useState } from "react";
import { Controller, type FieldValues } from "react-hook-form";
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
type: "passwordGenerator";
hide?: boolean;
bits?: { text: string; value: string; key: string }[];
devicePSKBitCount: number;
inputChange: ChangeEventHandler;
selectChange: (event: string) => void;
buttonClick: MouseEventHandler;
}
export function PasswordGenerator<T extends FieldValues>({
control,
field,
disabled,
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
const [passwordShown, setPasswordShown] = useState(false);
const togglePasswordVisiblity = () => {
setPasswordShown(!passwordShown);
};
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, ...rest } }) => (
<Generator
type={field.hide && !passwordShown ? "password" : "text"}
action={
field.hide
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined
}
devicePSKBitCount={field.devicePSKBitCount}
bits={field.bits}
inputChange={field.inputChange}
selectChange={field.selectChange}
buttonClick={field.buttonClick}
value={value}
variant={field.validationText ? "invalid" : "default"}
buttonText="Generate"
{...field.properties}
{...rest}
disabled={disabled}
/>
)}
/>
);
}

View File

@@ -1,15 +1,15 @@
import type {
BaseFormBuilderProps,
GenericFormElementProps,
} from "@components/Form/DynamicForm.js";
} from "@components/Form/DynamicForm.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@components/UI/Select.js";
import { Controller, FieldValues } from "react-hook-form";
} from "@components/UI/Select.tsx";
import { Controller, type FieldValues } from "react-hook-form";
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "select" | "multiSelect";
@@ -40,7 +40,7 @@ export function SelectInput<T extends FieldValues>({
: [];
return (
<Select
onValueChange={(e) => onChange(parseInt(e))}
onValueChange={(e) => onChange(Number.parseInt(e))}
disabled={disabled}
value={value?.toString()}
{...remainingProperties}

View File

@@ -1,10 +1,10 @@
import type {
BaseFormBuilderProps,
GenericFormElementProps,
} from "@components/Form/DynamicForm.js";
import { Switch } from "@components/UI/Switch.js";
import { ChangeEvent } from "react";
import { Controller, FieldValues } from "react-hook-form";
} from "@components/Form/DynamicForm.tsx";
import { Switch } from "@components/UI/Switch.tsx";
import type { ChangeEvent } from "react";
import { Controller, type FieldValues } from "react-hook-form";
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
type: "toggle";

View File

@@ -1,16 +1,20 @@
import { Label } from "@components/UI/Label.js";
import { Label } from "@components/UI/Label.tsx";
export interface FieldWrapperProps {
label: string;
description?: string;
disabled?: boolean;
children?: React.ReactNode;
valid?: boolean;
validationText?: string;
}
export const FieldWrapper = ({
label,
description,
children,
valid,
validationText,
}: FieldWrapperProps): JSX.Element => (
<div className="pt-6 sm:pt-5">
<div role="group" aria-labelledby="label-notifications">
@@ -19,6 +23,9 @@ export const FieldWrapper = ({
<div className="sm:col-span-2">
<div className="max-w-lg">
<p className="text-sm text-gray-500">{description}</p>
<p hidden={valid ?? true} className="text-sm text-red-500">
{validationText}
</p>
<div className="mt-4 space-y-4">
<div className="flex items-center">{children}</div>
</div>

View File

@@ -1,9 +1,11 @@
import type{ ChannelValidation } from "@app/validation/channel.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useToast } from "@core/hooks/useToast.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { ChannelValidation } from "@app/validation/channel.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string";
import { useState } from "react";
export interface SettingsPanelProps {
channel: Protobuf.Channel.Channel;
@@ -13,15 +15,27 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
const { config, connection, addChannel } = useDevice();
const { toast } = useToast();
const [pass, setPass] = useState<string>(
fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
);
const [bitCount, setBits] = useState<number>(
channel?.settings?.psk.length ?? 16,
);
const [validationText, setValidationText] = useState<string>();
const onSubmit = (data: ChannelValidation) => {
const channel = new Protobuf.Channel.Channel({
...data,
settings: {
...data.settings,
psk: toByteArray(data.settings.psk ?? ""),
psk: toByteArray(pass),
moduleSettings: {
positionPrecision: data.settings.positionEnabled ? data.settings.preciseLocation ? 32 : data.settings.positionPrecision : 0,
}
positionPrecision: data.settings.positionEnabled
? data.settings.preciseLocation
? 32
: data.settings.positionPrecision
: 0,
},
},
});
connection?.setChannel(channel).then(() => {
@@ -32,6 +46,38 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
});
};
const clickEvent = () => {
setPass(
btoa(
cryptoRandomString({
length: bitCount ?? 0,
type: "alphanumeric",
}),
),
);
setValidationText(undefined);
};
const validatePass = (input: string, count: number) => {
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
} else {
setValidationText(undefined);
}
};
const inputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
const psk = e.currentTarget?.value;
setPass(psk);
validatePass(psk, bitCount);
};
const selectChangeEvent = (e: string) => {
const count = Number.parseInt(e);
setBits(count);
validatePass(pass, count);
};
return (
<DynamicForm<ChannelValidation>
onSubmit={onSubmit}
@@ -42,10 +88,17 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
...{
settings: {
...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
positionEnabled: channel?.settings?.moduleSettings?.positionPrecision != undefined && channel?.settings?.moduleSettings?.positionPrecision > 0,
preciseLocation: channel?.settings?.moduleSettings?.positionPrecision == 32,
positionPrecision: channel?.settings?.moduleSettings?.positionPrecision == undefined ? 10 : channel?.settings?.moduleSettings?.positionPrecision
psk: pass,
positionEnabled:
channel?.settings?.moduleSettings?.positionPrecision !==
undefined &&
channel?.settings?.moduleSettings?.positionPrecision > 0,
preciseLocation:
channel?.settings?.moduleSettings?.positionPrecision === 32,
positionPrecision:
channel?.settings?.moduleSettings?.positionPrecision === undefined
? 10
: channel?.settings?.moduleSettings?.positionPrecision,
},
},
}}
@@ -58,19 +111,29 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
type: "select",
name: "role",
label: "Role",
disabled: channel.index === 0,
description:
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
properties: {
enumValue: Protobuf.Channel.Channel_Role,
enumValue:
channel.index === 0
? { PRIMARY: 1 }
: { DISABLED: 0, SECONDARY: 2 },
},
},
{
type: "password",
type: "passwordGenerator",
name: "settings.psk",
label: "pre-Shared Key",
description: "16, or 32 bytes",
description: "256, 128, or 8 bit PSKs allowed",
validationText: validationText,
devicePSKBitCount: bitCount ?? 0,
inputChange: inputChangeEvent,
selectChange: selectChangeEvent,
buttonClick: clickEvent,
hide: true,
properties: {
// act
value: pass,
},
},
{
@@ -111,9 +174,32 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
description:
"If not sharing precise location, position shared on channel will be accurate within this distance",
properties: {
enumValue: config.display?.units == 0 ?
{ "Within 23 km":10, "Within 12 km":11, "Within 5.8 km":12, "Within 2.9 km":13, "Within 1.5 km":14, "Within 700 m":15, "Within 350 m":16, "Within 200 m":17, "Within 90 m":18, "Within 50 m":19 } :
{ "Within 15 miles":10, "Within 7.3 miles":11, "Within 3.6 miles":12, "Within 1.8 miles":13, "Within 0.9 miles":14, "Within 0.5 miles":15, "Within 0.2 miles":16, "Within 600 feet":17, "Within 300 feet":18, "Within 150 feet":19 }
enumValue:
config.display?.units === 0
? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
},
},
],

View File

@@ -1,6 +1,6 @@
import type { BluetoothValidation } from "@app/validation/config/bluetooth.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Bluetooth = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import type { DeviceValidation } from "@app/validation/config/device.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { DeviceValidation } from "@app/validation/config/device.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Device = (): JSX.Element => {
@@ -32,23 +32,25 @@ export const Device = (): JSX.Element => {
label: "Role",
description: "What role the device performs on the mesh",
properties: {
enumValue: Protobuf.Config.Config_DeviceConfig_Role,
enumValue: {
Client: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
"Client Mute":
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_MUTE,
Router: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
Repeater: Protobuf.Config.Config_DeviceConfig_Role.REPEATER,
Tracker: Protobuf.Config.Config_DeviceConfig_Role.TRACKER,
Sensor: Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
TAK: Protobuf.Config.Config_DeviceConfig_Role.TAK,
"Client Hidden":
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_HIDDEN,
"Lost and Found":
Protobuf.Config.Config_DeviceConfig_Role.LOST_AND_FOUND,
"TAK Tracker":
Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
},
formatEnumName: true,
},
},
{
type: "toggle",
name: "serialEnabled",
label: "Serial Output Enabled",
description: "Enable the device's serial console",
},
{
type: "toggle",
name: "debugLogEnabled",
label: "Enabled Debug Log",
description:
"Output debugging information to the device's serial port (auto disables when serial client is connected)",
},
{
type: "number",
name: "buttonGpio",
@@ -86,12 +88,6 @@ export const Device = (): JSX.Element => {
label: "Double Tap as Button Press",
description: "Treat double tap as button press",
},
{
type: "toggle",
name: "isManaged",
label: "Managed",
description: "Is this device managed by a mesh administator",
},
{
type: "toggle",
name: "disableTripleClick",

View File

@@ -1,6 +1,6 @@
import type { DisplayValidation } from "@app/validation/config/display.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { DisplayValidation } from "@app/validation/config/display.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Display = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import type { LoRaValidation } from "@app/validation/config/lora.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { LoRaValidation } from "@app/validation/config/lora.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const LoRa = (): JSX.Element => {
@@ -41,7 +41,7 @@ export const LoRa = (): JSX.Element => {
label: "Hop Limit",
description: "Maximum number of hops",
properties: {
enumValue: {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7}
enumValue: { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7 },
},
},
{
@@ -56,6 +56,13 @@ export const LoRa = (): JSX.Element => {
label: "Ignore MQTT",
description: "Don't forward MQTT messages over the mesh",
},
{
type: "toggle",
name: "configOkToMqtt",
label: "OK to MQTT",
description:
"When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
},
],
},
{

View File

@@ -1,6 +1,10 @@
import type { NetworkValidation } from "@app/validation/config/network.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { NetworkValidation } from "@app/validation/config/network.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import {
convertIntToIpAddress,
convertIpAddressToInt,
} from "@core/utils/ip.ts";
import { Protobuf } from "@meshtastic/js";
export const Network = (): JSX.Element => {
@@ -13,9 +17,12 @@ export const Network = (): JSX.Element => {
case: "network",
value: {
...data,
ipv4Config: new Protobuf.Config.Config_NetworkConfig_IpV4Config(
data.ipv4Config,
),
ipv4Config: new Protobuf.Config.Config_NetworkConfig_IpV4Config({
ip: convertIpAddressToInt(data.ipv4Config.ip) ?? 0,
gateway: convertIpAddressToInt(data.ipv4Config.gateway) ?? 0,
subnet: convertIpAddressToInt(data.ipv4Config.subnet) ?? 0,
dns: convertIpAddressToInt(data.ipv4Config.dns) ?? 0,
}),
},
},
}),
@@ -25,7 +32,19 @@ export const Network = (): JSX.Element => {
return (
<DynamicForm<NetworkValidation>
onSubmit={onSubmit}
defaultValues={config.network}
defaultValues={{
...config.network,
ipv4Config: {
ip: convertIntToIpAddress(config.network?.ipv4Config?.ip ?? 0),
gateway: convertIntToIpAddress(
config.network?.ipv4Config?.gateway ?? 0,
),
subnet: convertIntToIpAddress(
config.network?.ipv4Config?.subnet ?? 0,
),
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
},
}}
fieldGroups={[
{
label: "WiFi Config",

View File

@@ -1,6 +1,6 @@
import type { PositionValidation } from "@app/validation/config/position.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { PositionValidation } from "@app/validation/config/position.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Position = (): JSX.Element => {
@@ -77,12 +77,6 @@ export const Position = (): JSX.Element => {
label: "Enable Pin",
description: "GPS module enable pin override",
},
{
type: "number",
name: "channelPrecision",
label: "Channel Precision",
description: "GPS channel precision",
},
],
},
{

View File

@@ -1,6 +1,6 @@
import type { PowerValidation } from "@app/validation/config/power.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { PowerValidation } from "@app/validation/config/power.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Power = (): JSX.Element => {

View File

@@ -0,0 +1,237 @@
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx";
import {
getX25519PrivateKey,
getX25519PublicKey,
} from "@app/core/utils/x25519";
import type { SecurityValidation } from "@app/validation/config/security.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
import { fromByteArray, toByteArray } from "base64-js";
import { Eye, EyeOff } from "lucide-react";
import { useState } from "react";
export const Security = (): JSX.Element => {
const { config, nodes, hardware, setWorkingConfig } = useDevice();
const [privateKey, setPrivateKey] = useState<string>(
fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
);
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
config.security?.privateKey.length ?? 32,
);
const [privateKeyValidationText, setPrivateKeyValidationText] =
useState<string>();
const [publicKey, setPublicKey] = useState<string>(
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
);
const [adminKey, setAdminKey] = useState<string>(
fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)),
);
const [adminKeyValidationText, setAdminKeyValidationText] =
useState<string>();
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const onSubmit = (data: SecurityValidation) => {
if (privateKeyValidationText || adminKeyValidationText) return;
setWorkingConfig(
new Protobuf.Config.Config({
payloadVariant: {
case: "security",
value: {
...data,
adminKey: [toByteArray(adminKey)],
privateKey: toByteArray(privateKey),
publicKey: toByteArray(publicKey),
},
},
}),
);
};
const validateKey = (
input: string,
count: number,
setValidationText: (
value: React.SetStateAction<string | undefined>,
) => void,
) => {
try {
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
} else {
setValidationText(undefined);
}
} catch (e) {
console.error(e);
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
}
};
const privateKeyClickEvent = () => {
setDialogOpen(true);
};
const pkiRegenerate = () => {
const privateKey = getX25519PrivateKey();
const publicKey = getX25519PublicKey(privateKey);
setPrivateKey(fromByteArray(privateKey));
setPublicKey(fromByteArray(publicKey));
validateKey(
fromByteArray(privateKey),
privateKeyBitCount,
setPrivateKeyValidationText,
);
setDialogOpen(false);
};
const privateKeyInputChangeEvent = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
const privateKeyB64String = e.target.value;
setPrivateKey(privateKeyB64String);
validateKey(
privateKeyB64String,
privateKeyBitCount,
setPrivateKeyValidationText,
);
const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String));
setPublicKey(fromByteArray(publicKey));
};
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
const psk = e.currentTarget?.value;
setAdminKey(psk);
validateKey(psk, privateKeyBitCount, setAdminKeyValidationText);
};
const privateKeySelectChangeEvent = (e: string) => {
const count = Number.parseInt(e);
setPrivateKeyBitCount(count);
validateKey(privateKey, count, setPrivateKeyValidationText);
};
return (
<>
<DynamicForm<SecurityValidation>
onSubmit={onSubmit}
submitType="onChange"
defaultValues={{
...config.security,
...{
adminKey: adminKey,
privateKey: privateKey,
publicKey: publicKey,
adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
isManaged: config.security?.isManaged ?? false,
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
serialEnabled: config.security?.serialEnabled ?? false,
},
}}
fieldGroups={[
{
label: "Security Settings",
description: "Settings for the Security configuration",
fields: [
{
type: "passwordGenerator",
name: "privateKey",
label: "Private Key",
description: "Used to create a shared key with a remote device",
bits: [{ text: "256 bit", value: "32", key: "bit256" }],
validationText: privateKeyValidationText,
devicePSKBitCount: privateKeyBitCount,
inputChange: privateKeyInputChangeEvent,
selectChange: privateKeySelectChangeEvent,
hide: !privateKeyVisible,
buttonClick: privateKeyClickEvent,
properties: {
value: privateKey,
action: {
icon: privateKeyVisible ? EyeOff : Eye,
onClick: () => setPrivateKeyVisible(!privateKeyVisible),
},
},
},
{
type: "text",
name: "publicKey",
label: "Public Key",
disabled: true,
description:
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
properties: {
value: publicKey,
},
},
],
},
{
label: "Admin Settings",
description: "Settings for Admin",
fields: [
{
type: "toggle",
name: "adminChannelEnabled",
label: "Allow Legacy Admin",
description:
"Allow incoming device control over the insecure legacy admin channel",
},
{
type: "toggle",
name: "isManaged",
label: "Managed",
description:
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
},
{
type: "text",
name: "adminKey",
label: "Admin Key",
description:
"The public key authorized to send admin messages to this node",
validationText: adminKeyValidationText,
inputChange: adminKeyInputChangeEvent,
disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
properties: {
value: adminKey,
},
},
],
},
{
label: "Logging Settings",
description: "Settings for Logging",
fields: [
{
type: "toggle",
name: "debugLogApiEnabled",
label: "Enable Debug Log API",
description:
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
},
{
type: "toggle",
name: "serialEnabled",
label: "Serial Output Enabled",
description: "Serial Console over the Stream API",
},
],
},
]}
/>
<PkiRegenerateDialog
open={dialogOpen}
onOpenChange={() => setDialogOpen(false)}
onSubmit={() => pkiRegenerate()}
/>
</>
);
};

View File

@@ -1,10 +1,10 @@
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.js";
import { Mono } from "@components/generic/Mono.js";
import { useAppStore } from "@core/stores/appStore.js";
import { useDeviceStore } from "@core/stores/deviceStore.js";
import { subscribeAll } from "@core/subscriptions.js";
import { randId } from "@core/utils/randId.js";
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { BleConnection, Constants } from "@meshtastic/js";
import { useCallback, useEffect, useState } from "react";

View File

@@ -1,14 +1,14 @@
import React, { useState } from "react";
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.js";
import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js";
import { Switch } from "@components/UI/Switch.js";
import { useAppStore } from "@core/stores/appStore.js";
import { useDeviceStore } from "@core/stores/deviceStore.js";
import { subscribeAll } from "@core/subscriptions.js";
import { randId } from "@core/utils/randId.js";
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Switch } from "@components/UI/Switch.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { HttpConnection } from "@meshtastic/js";
import { useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
@@ -20,7 +20,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
}>({
defaultValues: {
ip: ["client.meshtastic.org", "localhost"].includes(
window.location.hostname
window.location.hostname,
)
? "meshtastic.local"
: window.location.hostname,
@@ -38,7 +38,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
const onSubmit = handleSubmit(async (data) => {
setConnectionInProgress(true);
const id = randId();
const device = addDevice(id);
const connection = new HttpConnection(id);
@@ -75,7 +75,9 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
<Switch
// label="Use TLS"
// description="Description"
disabled={location.protocol === "https:" || connectionInProgress}
disabled={
location.protocol === "https:" || connectionInProgress
}
checked={value}
{...rest}
/>
@@ -84,7 +86,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
/>
</div>
<Button type="submit" disabled={connectionInProgress}>
<span>{connectionInProgress ? 'Connecting...' : 'Connect' }</span>
<span>{connectionInProgress ? "Connecting..." : "Connect"}</span>
</Button>
</form>
);

View File

@@ -1,10 +1,10 @@
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.js";
import { Mono } from "@components/generic/Mono.js";
import { useAppStore } from "@core/stores/appStore.js";
import { useDeviceStore } from "@core/stores/deviceStore.js";
import { subscribeAll } from "@core/subscriptions.js";
import { randId } from "@core/utils/randId.js";
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { SerialConnection } from "@meshtastic/js";
import { useCallback, useEffect, useState } from "react";
@@ -48,19 +48,22 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
return (
<div className="flex w-full flex-col gap-2 p-4">
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
{serialPorts.map((port, index) => (
<Button
key={index}
disabled={port.readable !== null}
onClick={async () => {
await onConnect(port);
}}
>
{`# ${index} - ${port.getInfo().usbVendorId ?? "UNK"} - ${
port.getInfo().usbProductId ?? "UNK"
}`}
</Button>
))}
{serialPorts.map((port, index) => {
const { usbProductId, usbVendorId } = port.getInfo();
return (
<Button
key={`${usbVendorId ?? "UNK"}-${usbProductId ?? "UNK"}-${index}`}
disabled={port.readable !== null}
onClick={async () => {
await onConnect(port);
}}
>
{`# ${index} - ${usbVendorId ?? "UNK"} - ${
usbProductId ?? "UNK"
}`}
</Button>
);
})}
{serialPorts.length === 0 && (
<Mono className="m-auto select-none">No devices paired yet.</Mono>
)}

View File

@@ -1,45 +1,74 @@
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
import { MessageWithState, useDevice } from "@app/core/stores/deviceStore.js";
import { Message } from "@components/PageComponents/Messages/Message.js";
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
import type { Types } from "@meshtastic/js";
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import {
type MessageWithState,
useDevice,
} from "@app/core/stores/deviceStore.ts";
import { Message } from "@components/PageComponents/Messages/Message.tsx";
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
import type { Protobuf, Types } from "@meshtastic/js";
import { InboxIcon } from "lucide-react";
export interface ChannelChatProps {
messages?: MessageWithState[];
channel: Types.ChannelNumber;
to: Types.Destination;
traceroutes?: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[];
}
export const ChannelChat = ({
messages,
channel,
to,
traceroutes,
}: ChannelChatProps): JSX.Element => {
const { nodes } = useDevice();
return (
<div className="flex flex-grow flex-col">
<div className="flex flex-grow flex-col">
{messages ? (
messages.map((message, index) => (
<Message
key={message.id}
message={message}
lastMsgSameUser={
index === 0 ? false : messages[index - 1].from === message.from
}
sender={nodes.get(message.from)}
/>
))
) : (
<div className="m-auto">
<InboxIcon className="m-auto" />
<Subtle>No Messages</Subtle>
</div>
)}
<div className="flex flex-grow">
<div className="flex flex-grow flex-col">
{messages ? (
messages.map((message, index) => (
<Message
key={message.id}
message={message}
lastMsgSameUser={
index === 0
? false
: messages[index - 1].from === message.from
}
sender={nodes.get(message.from)}
/>
))
) : (
<div className="m-auto">
<InboxIcon className="m-auto" />
<Subtle>No Messages</Subtle>
</div>
)}
</div>
<div
className={`flex flex-grow flex-col border-slate-400 border-l ${traceroutes === undefined ? "hidden" : ""}`}
>
{to === "broadcast" ? null : traceroutes ? (
traceroutes.map((traceroute, index) => (
<TraceRoute
key={traceroute.id}
from={nodes.get(traceroute.from)}
to={nodes.get(traceroute.to)}
route={traceroute.data.route}
/>
))
) : (
<div className="m-auto">
<InboxIcon className="m-auto" />
<Subtle>No Traceroutes</Subtle>
</div>
)}
</div>
</div>
<div className="p-3">
<div className="pl-3 pr-3 pt-3 pb-1">
<MessageInput to={to} channel={channel} />
</div>
</div>

View File

@@ -1,4 +1,4 @@
import type { MessageWithState } from "@app/core/stores/deviceStore.js";
import type { MessageWithState } from "@app/core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import type { Protobuf } from "@meshtastic/js";
import {

View File

@@ -1,6 +1,6 @@
import { Button } from "@components/UI/Button.js";
import { Input } from "@components/UI/Input.js";
import { useDevice } from "@core/stores/deviceStore.js";
import { Button } from "@components/UI/Button.tsx";
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";
@@ -62,7 +62,7 @@ export const MessageInput = ({
<span className="w-full">
<Input
autoFocus={true}
minLength={2}
minLength={1}
placeholder="Enter Message"
value={messageDraft}
onChange={(e) => setMessageDraft(e.target.value)}

View File

@@ -0,0 +1,36 @@
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
export interface TraceRouteProps {
from?: Protobuf.Mesh.NodeInfo;
to?: Protobuf.Mesh.NodeInfo;
route: Array<number>;
}
export const TraceRoute = ({
from,
to,
route,
}: TraceRouteProps): JSX.Element => {
const { nodes } = useDevice();
return route.length === 0 ? (
<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")}`;
})}
{from?.user?.longName}
</span>
</div>
);
};

View File

@@ -1,6 +1,6 @@
import { useDevice } from "@app/core/stores/deviceStore.js";
import type { AmbientLightingValidation } from "@app/validation/moduleConfig/ambientLighting.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { AmbientLightingValidation } from "@app/validation/moduleConfig/ambientLighting.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/js";
export const AmbientLighting = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import type { AudioValidation } from "@app/validation/moduleConfig/audio.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { AudioValidation } from "@app/validation/moduleConfig/audio.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Audio = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const CannedMessage = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import { useDevice } from "@app/core/stores/deviceStore.js";
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/js";
export const DetectionSensor = (): JSX.Element => {
@@ -38,9 +38,9 @@ export const DetectionSensor = (): JSX.Element => {
label: "Minimum Broadcast Seconds",
description:
"The interval in seconds of how often we can send a message to the mesh when a state change is detected",
properties: {
suffix: "Seconds",
},
properties: {
suffix: "Seconds",
},
disabledBy: [
{
fieldName: "enabled",

View File

@@ -1,6 +1,6 @@
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const ExternalNotification = (): JSX.Element => {

View File

@@ -1,17 +1,23 @@
import { useDevice } from "@app/core/stores/deviceStore.js";
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/js";
export const MQTT = (): JSX.Element => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { config, moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: MqttValidation) => {
setWorkingModuleConfig(
new Protobuf.ModuleConfig.ModuleConfig({
payloadVariant: {
case: "mqtt",
value: data,
value: {
...data,
mapReportSettings:
new Protobuf.ModuleConfig.ModuleConfig_MapReportSettings(
data.mapReportSettings,
),
},
},
}),
);
@@ -70,7 +76,8 @@ export const MQTT = (): JSX.Element => {
type: "toggle",
name: "encryptionEnabled",
label: "Encryption Enabled",
description: "Enable or disable MQTT encryption",
description:
"Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data.",
disabledBy: [
{
fieldName: "enabled",
@@ -151,10 +158,39 @@ export const MQTT = (): JSX.Element => {
],
},
{
type: "number",
type: "select",
name: "mapReportSettings.positionPrecision",
label: "Position Precision",
description: "Precision of the position",
label: "Approximate Location",
description:
"Position shared will be accurate within this distance",
properties: {
enumValue:
config.display?.units === 0
? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
},
disabledBy: [
{
fieldName: "enabled",

View File

@@ -1,6 +1,6 @@
import { useDevice } from "@app/core/stores/deviceStore.js";
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/js";
export const NeighborInfo = (): JSX.Element => {
@@ -36,7 +36,7 @@ export const NeighborInfo = (): JSX.Element => {
type: "number",
name: "updateInterval",
label: "Update Interval",
description:
description:
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
properties: {
suffix: "Seconds",

View File

@@ -1,6 +1,6 @@
import type { PaxcounterValidation } from "@app/validation/moduleConfig/paxcounter.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { PaxcounterValidation } from "@app/validation/moduleConfig/paxcounter.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Paxcounter = (): JSX.Element => {
@@ -36,7 +36,8 @@ export const Paxcounter = (): JSX.Element => {
type: "number",
name: "paxcounterUpdateInterval",
label: "Update Interval (seconds)",
description: "How long to wait between sending paxcounter packets",
description:
"How long to wait between sending paxcounter packets",
properties: {
suffix: "Seconds",
},

View File

@@ -1,6 +1,6 @@
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const RangeTest = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import type { SerialValidation } from "@app/validation/moduleConfig/serial.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { SerialValidation } from "@app/validation/moduleConfig/serial.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Serial = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import type { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.ts";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const StoreForward = (): JSX.Element => {

View File

@@ -1,6 +1,6 @@
import type { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.tsx";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
export const Telemetry = (): JSX.Element => {
@@ -87,7 +87,7 @@ export const Telemetry = (): JSX.Element => {
description: "How often to send Power data over the mesh",
},
{
type: "text",
type: "toggle",
name: "powerScreenEnabled",
label: "Power Screen Enabled",
description: "Enable the Power Telemetry Screen",

View File

@@ -1,5 +1,6 @@
import { cn } from "@app/core/utils/cn.js";
import { AlignLeftIcon, LucideIcon } from "lucide-react";
import { cn } from "@app/core/utils/cn.ts";
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
import Footer from "./UI/Footer";
export interface PageLayoutProps {
label: string;
@@ -7,6 +8,7 @@ export interface PageLayoutProps {
children: React.ReactNode;
actions?: {
icon: LucideIcon;
iconClasses?: string;
onClick: () => void;
}[];
}
@@ -18,40 +20,43 @@ export const PageLayout = ({
children,
}: PageLayoutProps): JSX.Element => {
return (
<div className="relative flex h-full w-full flex-col">
<div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4">
<button
type="button"
className="pl-4 transition-all hover:text-accent md:hidden"
>
<AlignLeftIcon />
</button>
<div className="flex flex-1 items-center justify-between px-4 md:px-0">
<div className="flex w-full items-center">
<span className="w-full text-lg font-medium">{label}</span>
<div className="flex justify-end space-x-4">
{actions?.map((action, index) => (
<button
key={action.icon.name}
type="button"
className="transition-all hover:text-accent"
onClick={action.onClick}
>
<action.icon />
</button>
))}
<>
<div className="relative flex h-full w-full flex-col">
<div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4">
<button
type="button"
className="pl-4 transition-all hover:text-accent md:hidden"
>
<AlignLeftIcon />
</button>
<div className="flex flex-1 items-center justify-between px-4 md:px-0">
<div className="flex w-full items-center">
<span className="w-full text-lg font-medium">{label}</span>
<div className="flex justify-end space-x-4">
{actions?.map((action, index) => (
<button
key={action.icon.name}
type="button"
className="transition-all hover:text-accent"
onClick={action.onClick}
>
<action.icon className={action.iconClasses} />
</button>
))}
</div>
</div>
</div>
</div>
<div
className={cn(
"flex h-full w-full flex-col overflow-y-auto",
!noPadding && "pl-3 pr-3 ",
)}
>
{children}
<Footer />
</div>
</div>
<div
className={cn(
"flex h-full w-full flex-col overflow-y-auto",
!noPadding && "p-3",
)}
>
{children}
</div>
</div>
</>
);
};

View File

@@ -1,18 +1,19 @@
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
import { Subtle } from "@components/UI/Typography/Subtle.js";
import { useDevice } from "@core/stores/deviceStore.js";
import type { Page } from "@core/stores/deviceStore.js";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Page } from "@core/stores/deviceStore.ts";
import {
BatteryMediumIcon,
CpuIcon,
EditIcon,
LayersIcon,
LucideIcon,
type LucideIcon,
MapIcon,
MessageSquareIcon,
SettingsIcon,
UsersIcon,
ZapIcon,
BatteryMediumIcon
} from "lucide-react";
export interface SidebarProps {
@@ -20,8 +21,9 @@ export interface SidebarProps {
}
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
const { hardware, nodes } = useDevice();
const { hardware, nodes, metadata } = useDevice();
const myNode = nodes.get(hardware.myNodeNum);
const myMetadata = metadata.get(0);
const { activePage, setActivePage, setDialogOpen } = useDevice();
interface NavLink {
@@ -77,12 +79,18 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
</div>
<div className="px-8 pb-6">
<div className="flex items-center">
<BatteryMediumIcon size={24} viewBox={'0 0 28 24'}/>
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
<BatteryMediumIcon size={24} viewBox={"0 0 28 24"} />
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
</div>
<div className="flex items-center">
<ZapIcon size={24} viewBox={'0 0 36 24'}/>
<Subtle>{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts</Subtle>
<ZapIcon size={24} viewBox={"0 0 36 24"} />
<Subtle>
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
</Subtle>
</div>
<div className="flex items-center">
<CpuIcon size={24} viewBox={"0 0 36 24"} />
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useToast } from "@core/hooks/useToast.js";
import { useToast } from "@core/hooks/useToast.ts";
import {
Toast,
@@ -7,7 +7,7 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
} from "@components/UI/Toast.js";
} from "@components/UI/Toast.tsx";
export function Toaster() {
const { toasts } = useToast();
@@ -17,8 +17,14 @@ export function Toaster() {
{toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle className="dark:text-white">{title}</ToastTitle>}
{description && <ToastDescription className="dark:text-white-400">{description}</ToastDescription>}
{title && (
<ToastTitle className="dark:text-white">{title}</ToastTitle>
)}
{description && (
<ToastDescription className="dark:text-white-400">
{description}
</ToastDescription>
)}
</div>
{action}
<ToastClose />

View File

@@ -1,7 +1,7 @@
import { VariantProps, cva } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800",
@@ -12,6 +12,8 @@ const buttonVariants = cva(
"bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900",
destructive:
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
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",
subtle:

View File

@@ -2,7 +2,7 @@ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,

View File

@@ -3,8 +3,8 @@ import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
import { Dialog, DialogContent } from "@components/UI/Dialog.js";
import { cn } from "@core/utils/cn.js";
import { Dialog, DialogContent } from "@components/UI/Dialog.tsx";
import { cn } from "@core/utils/cn.ts";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,

View File

@@ -2,7 +2,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Dialog = DialogPrimitive.Root;

View File

@@ -2,7 +2,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const DropdownMenu = DropdownMenuPrimitive.Root;

View File

@@ -0,0 +1,37 @@
import React from "react";
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {}
const Footer = React.forwardRef<HTMLElement, FooterProps>(
({ className, ...props }, ref) => {
return (
<footer
className={`flex flex- justify-center p-2 ${className}`}
style={{
backgroundColor: "var(--backgroundPrimary)",
color: "var(--textPrimary)",
}}
>
<p>
<a
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"
className="hover:underline"
style={{ color: "var(--link)" }}
>
Powered by Vercel
</a>{" "}
| Meshtastic® is a registered trademark of Meshtastic LLC. |{" "}
<a
href="https://meshtastic.org/docs/legal"
className="hover:underline"
style={{ color: "var(--link)" }}
>
Legal Information
</a>
</p>
</footer>
);
},
);
export default Footer;

View File

@@ -0,0 +1,111 @@
import * as React from "react";
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@components/UI/Select.tsx";
import type { LucideIcon } from "lucide-react";
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
type: "text" | "password";
devicePSKBitCount?: number;
value: string;
variant: "default" | "invalid";
buttonText?: string;
bits?: { text: string; value: string; key: string }[];
selectChange: (event: string) => void;
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
action?: {
icon: LucideIcon;
onClick: () => void;
};
disabled?: boolean;
}
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
(
{
type,
devicePSKBitCount,
variant,
value,
buttonText,
bits = [
{ text: "256 bit", value: "32", key: "bit256" },
{ text: "128 bit", value: "16", key: "bit128" },
{ text: "8 bit", value: "1", key: "bit8" },
],
selectChange,
inputChange,
buttonClick,
action,
disabled,
...props
},
ref,
) => {
const inputRef = React.useRef<HTMLInputElement>(null);
// Invokes onChange event on the input element when the value changes from the parent component
React.useEffect(() => {
if (!inputRef.current) return;
const setValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)?.set;
if (!setValue) return;
inputRef.current.value = "";
setValue.call(inputRef.current, value);
inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
}, [value]);
return (
<>
<Input
type={type}
id="pskInput"
variant={variant}
value={value}
onChange={inputChange}
action={action}
disabled={disabled}
ref={inputRef}
/>
<Select
value={devicePSKBitCount?.toString()}
onValueChange={(e) => selectChange(e)}
disabled={disabled}
>
<SelectTrigger className="!max-w-max">
<SelectValue />
</SelectTrigger>
<SelectContent>
{bits.map(({ text, value, key }) => (
<SelectItem key={key} value={value}>
{text}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="success"
onClick={buttonClick}
disabled={disabled}
{...props}
>
{buttonText}
</Button>
</>
);
},
);
Generator.displayName = "Button";
export { Generator };

View File

@@ -1,10 +1,27 @@
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
import { type VariantProps, cva } from "class-variance-authority";
import type { LucideIcon } from "lucide-react";
const inputVariants = cva(
"flex h-10 w-full rounded-md border bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
{
variants: {
variant: {
default: "border-slate-300 dark:border-slate-700",
invalid: "border-red-500 dark:border-red-500",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
extends React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
prefix?: string;
suffix?: string;
action?: {
@@ -14,7 +31,7 @@ export interface InputProps
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, prefix, suffix, action, ...props }, ref) => {
({ className, value, variant, prefix, suffix, action, ...props }, ref) => {
return (
<div className="relative w-full">
{prefix && (
@@ -24,10 +41,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
)}
<input
className={cn(
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
action && "pr-8",
className,
inputVariants({ variant }),
)}
value={value}
ref={ref}
{...props}
/>
@@ -51,4 +69,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
);
Input.displayName = "Input";
export { Input };
export { Input, inputVariants };

View File

@@ -1,7 +1,7 @@
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,

View File

@@ -2,7 +2,7 @@ import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const MenubarMenu = MenubarPrimitive.Menu;

View File

@@ -1,7 +1,7 @@
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Popover = PopoverPrimitive.Root;

View File

@@ -1,7 +1,7 @@
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,

View File

@@ -2,7 +2,7 @@ import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Select = SelectPrimitive.Root;

View File

@@ -1,7 +1,7 @@
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,

View File

@@ -1,4 +1,4 @@
import { H4 } from "@components/UI/Typography/H4.js";
import { H4 } from "@components/UI/Typography/H4.tsx";
export interface SidebarSectionProps {
label: string;

View File

@@ -1,4 +1,4 @@
import { Button } from "@components/UI/Button.js";
import { Button } from "@components/UI/Button.tsx";
import type { LucideIcon } from "lucide-react";
export interface SidebarButtonProps {

View File

@@ -1,7 +1,7 @@
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,

View File

@@ -1,7 +1,7 @@
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const Tabs = TabsPrimitive.Root;
@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 dark:bg-slate-800",
"inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 mt-2 dark:bg-slate-800",
className,
)}
{...props}

View File

@@ -1,9 +1,9 @@
import * as ToastPrimitives from "@radix-ui/react-toast";
import { VariantProps, cva } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const ToastProvider = ToastPrimitives.Provider;

View File

@@ -1,7 +1,7 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
import { cn } from "@core/utils/cn.js";
import { cn } from "@core/utils/cn.ts";
const TooltipProvider = TooltipPrimitive.Provider;

View File

@@ -1,4 +1,4 @@
import { cn } from "@app/core/utils/cn.js";
import { cn } from "@app/core/utils/cn.ts";
export interface H4Props {
className?: string;

View File

@@ -1,4 +1,4 @@
import { cn } from "@app/core/utils/cn.js";
import { cn } from "@app/core/utils/cn.ts";
export interface SubtleProps {
className?: string;

View File

@@ -1,4 +1,5 @@
import { ChevronUpIcon } from "lucide-react";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import React, { useState } from "react";
export interface TableProps {
headings: Heading[];
@@ -12,6 +13,49 @@ export interface Heading {
}
export const Table = ({ headings, rows }: TableProps): JSX.Element => {
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const headingSort = (title: string) => {
if (sortColumn === title) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortColumn(title);
setSortOrder("asc");
}
};
const sortedRows = rows.slice().sort((a, b) => {
if (!sortColumn) return 0;
const columnIndex = headings.findIndex((h) => h.title === sortColumn);
const aValue = a[columnIndex].props.children;
const bValue = b[columnIndex].props.children;
// Custom comparison for 'Last Heard' column
if (sortColumn === "Last Heard") {
const aTimestamp = aValue.props.timestamp ?? 0;
const bTimestamp = bValue.props.timestamp ?? 0;
if (aTimestamp < bTimestamp) {
return sortOrder === "asc" ? -1 : 1;
}
if (aTimestamp > bTimestamp) {
return sortOrder === "asc" ? 1 : -1;
}
return 0;
}
// Default comparison for other columns
if (aValue < bValue) {
return sortOrder === "asc" ? -1 : 1;
}
if (aValue > bValue) {
return sortOrder === "asc" ? 1 : -1;
}
return 0;
});
return (
<table className="min-w-full">
<thead className="bg-backgroundPrimary text-sm font-semibold text-textPrimary">
@@ -25,11 +69,19 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
? "cursor-pointer hover:brightness-hover active:brightness-press"
: ""
}`}
onClick={() => heading.sortable && headingSort(heading.title)}
onKeyUp={() => heading.sortable && headingSort(heading.title)}
>
<div className="flex gap-2">
{heading.title}
{heading.sortable && (
<ChevronUpIcon size={16} className="my-auto" />
{sortColumn === heading.title && (
<>
{sortOrder === "asc" ? (
<ChevronUpIcon size={16} />
) : (
<ChevronDownIcon size={16} />
)}
</>
)}
</div>
</th>
@@ -37,10 +89,12 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
{sortedRows.map((row, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed.
<tr key={index}>
{row.map((item, index) => (
<td
// biome-ignore lint/suspicious/noArrayIndexKey: OK because column order never changes.
key={index}
className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2"
>

View File

@@ -1,4 +1,4 @@
import { useAppStore } from "@core/stores/appStore.js";
import { useAppStore } from "@core/stores/appStore.ts";
import type { ReactNode } from "react";
export interface ThemeControllerProps {

View File

@@ -1,6 +1,6 @@
import { ReactNode, useEffect, useState } from "react";
import { type ReactNode, useSyncExternalStore } from "react";
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.js";
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.tsx";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
@@ -30,21 +30,21 @@ type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
@@ -80,7 +80,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
};
@@ -92,9 +92,9 @@ export const reducer = (state: State, action: Action): State => {
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
for (const toast of state.toasts) {
addToRemoveQueue(toast.id);
});
}
}
return {
@@ -102,10 +102,10 @@ export const reducer = (state: State, action: Action): State => {
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
...t,
open: false,
}
: t
),
};
}
@@ -130,9 +130,9 @@ let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
for (const listener of listeners) {
listener(memoryState);
});
}
}
type Toast = Omit<ToasterToast, "id">;
@@ -166,18 +166,22 @@ function toast({ ...props }: Toast) {
};
}
function useToast() {
const [state, setState] = useState<State>(memoryState);
const subscribe = (listener: () => void) => {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
};
useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
const getState = () => {
return memoryState;
};
function useToast() {
const state = useSyncExternalStore(subscribe, getState);
return {
...state,
@@ -186,4 +190,4 @@ function useToast() {
};
}
export { useToast, toast };
export { toast, useToast };

View File

@@ -50,7 +50,10 @@ export const useAppStore = create<AppState>()((set) => ({
currentPage: "messages",
rasterSources: [],
commandPaletteOpen: false,
darkMode: (localStorage.getItem('theme-dark') !== null ? (localStorage.getItem('theme-dark') === 'true' ? true : false) : window.matchMedia("(prefers-color-scheme: dark)").matches),
darkMode:
localStorage.getItem("theme-dark") !== null
? localStorage.getItem("theme-dark") === "true"
: window.matchMedia("(prefers-color-scheme: dark)").matches,
accent: "orange",
connectDialogOpen: false,
nodeNumToBeRemoved: 0,
@@ -96,7 +99,7 @@ export const useAppStore = create<AppState>()((set) => ({
);
},
setDarkMode: (enabled: boolean) => {
localStorage.setItem('theme-dark', enabled.toString());
localStorage.setItem("theme-dark", enabled.toString());
set(
produce<AppState>((draft) => {
draft.darkMode = enabled;
@@ -105,7 +108,7 @@ export const useAppStore = create<AppState>()((set) => ({
},
setNodeNumToBeRemoved: (nodeNum) =>
set((state) => ({
nodeNumToBeRemoved: nodeNum
nodeNumToBeRemoved: nodeNum,
})),
setAccent(color) {
set(

View File

@@ -42,6 +42,10 @@ export interface Device {
direct: Map<number, MessageWithState[]>;
broadcast: Map<Types.ChannelNumber, MessageWithState[]>;
};
traceroutes: Map<
number,
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
>;
connection?: Types.ConnectionType;
activePage: Page;
activeNode: number;
@@ -75,6 +79,9 @@ export interface Device {
addPosition: (position: Types.PacketMetadata<Protobuf.Mesh.Position>) => void;
addConnection: (connection: Types.ConnectionType) => void;
addMessage: (message: MessageWithState) => void;
addTraceRoute: (
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>,
) => void;
addMetadata: (from: number, metadata: Protobuf.Mesh.DeviceMetadata) => void;
removeNode: (nodeNum: number) => void;
setMessageState: (
@@ -122,6 +129,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
direct: new Map(),
broadcast: new Map(),
},
traceroutes: new Map(),
connection: undefined,
activePage: "messages",
activeNode: 0,
@@ -183,6 +191,9 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
device.config.bluetooth = config.payloadVariant.value;
break;
}
case "security": {
device.config.security = config.payloadVariant.value;
}
}
}
}),
@@ -487,6 +498,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
}),
);
},
addMetadata: (from, metadata) => {
set(
produce<DeviceState>((draft) => {
@@ -498,6 +510,26 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
}),
);
},
addTraceRoute: (traceroute) => {
set(
produce<DeviceState>((draft) => {
console.log("addTraceRoute called");
console.log(traceroute);
const device = draft.devices.get(id);
if (!device) {
return;
}
const nodetraceroutes = device.traceroutes.get(traceroute.from);
if (nodetraceroutes) {
nodetraceroutes.push(traceroute);
device.traceroutes.set(traceroute.from, nodetraceroutes);
} else {
device.traceroutes.set(traceroute.from, [traceroute]);
}
}),
);
},
removeNode: (nodeNum) => {
set(
produce<DeviceState>((draft) => {
@@ -506,8 +538,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
return;
}
device.nodes.delete(nodeNum);
})
)
}),
);
},
setMessageState: (
type: "direct" | "broadcast",

View File

@@ -1,5 +1,5 @@
import type { Device } from "@core/stores/deviceStore.js";
import { Protobuf, Types } from "@meshtastic/js";
import type { Device } from "@core/stores/deviceStore.ts";
import { Protobuf, type Types } from "@meshtastic/js";
export const subscribeAll = (
device: Device,
@@ -86,6 +86,12 @@ export const subscribeAll = (
});
});
connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => {
device.addTraceRoute({
...traceRoutePacket,
});
});
connection.events.onPendingSettingsChange.subscribe((state) => {
device.setPendingSettingsChanges(state);
});

View File

@@ -1,4 +1,4 @@
import { ClassValue, clsx } from "clsx";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {

14
src/core/utils/ip.ts Normal file
View File

@@ -0,0 +1,14 @@
export function convertIntToIpAddress(int: number): string {
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff}`;
}
export function convertIpAddressToInt(ip: string): number | null {
return (
ip
.split(".")
.reverse()
.reduce((ipnum, octet) => {
return (ipnum << 8) + Number.parseInt(octet);
}, 0) >>> 0
);
}

15
src/core/utils/x25519.ts Normal file
View File

@@ -0,0 +1,15 @@
import { x25519 } from "@noble/curves/ed25519";
export function getX25519PrivateKey(): Uint8Array {
const key = x25519.utils.randomPrivateKey();
key[0] &= 248;
key[31] &= 127;
key[31] |= 64;
return key;
}
export function getX25519PublicKey(privateKey: Uint8Array): Uint8Array {
return x25519.getPublicKey(privateKey);
}

View File

@@ -99,4 +99,4 @@
img {
-drag: none;
-webkit-user-drag: none;
}
}

View File

@@ -4,7 +4,7 @@ import "maplibre-gl/dist/maplibre-gl.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "@app/App.js";
import { App } from "@app/App.tsx";
const container = document.getElementById("root") as HTMLElement;
const root = createRoot(container);

View File

@@ -3,11 +3,11 @@ import {
TabsContent,
TabsList,
TabsTrigger,
} from "@app/components/UI/Tabs.js";
import { Channel } from "@components/PageComponents/Channel.js";
import { PageLayout } from "@components/PageLayout.js";
import { Sidebar } from "@components/Sidebar.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@app/components/UI/Tabs.tsx";
import { Channel } from "@components/PageComponents/Channel.tsx";
import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Types } from "@meshtastic/js";
import type { Protobuf } from "@meshtastic/js";
import { ImportIcon, QrCodeIcon } from "lucide-react";

View File

@@ -1,17 +1,18 @@
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.js";
import { Device } from "@components/PageComponents/Config/Device.js";
import { Display } from "@components/PageComponents/Config/Display.js";
import { LoRa } from "@components/PageComponents/Config/LoRa.js";
import { Network } from "@components/PageComponents/Config/Network.js";
import { Position } from "@components/PageComponents/Config/Position.js";
import { Power } from "@components/PageComponents/Config/Power.js";
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
import { Device } from "@components/PageComponents/Config/Device.tsx";
import { Display } from "@components/PageComponents/Config/Display.tsx";
import { LoRa } from "@components/PageComponents/Config/LoRa.tsx";
import { Network } from "@components/PageComponents/Config/Network.tsx";
import { Position } from "@components/PageComponents/Config/Position.tsx";
import { Power } from "@components/PageComponents/Config/Power.tsx";
import { Security } from "@components/PageComponents/Config/Security.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.js";
import { useDevice } from "@core/stores/deviceStore.js";
} from "@components/UI/Tabs.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
export const DeviceConfig = (): JSX.Element => {
const { metadata } = useDevice();
@@ -47,16 +48,17 @@ export const DeviceConfig = (): JSX.Element => {
label: "Bluetooth",
element: Bluetooth,
},
{
label: "Security",
element: Security,
},
];
return (
<Tabs defaultValue="Device">
<TabsList>
{tabs.map((tab) => (
<TabsTrigger
key={tab.label}
value={tab.label}
>
<TabsTrigger key={tab.label} value={tab.label}>
{tab.label}
</TabsTrigger>
))}

View File

@@ -1,21 +1,21 @@
import { AmbientLighting } from "@app/components/PageComponents/ModuleConfig/AmbientLighting.js";
import { DetectionSensor } from "@app/components/PageComponents/ModuleConfig/DetectionSensor.js";
import { NeighborInfo } from "@app/components/PageComponents/ModuleConfig/NeighborInfo.js";
import { Audio } from "@components/PageComponents/ModuleConfig/Audio.js";
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js";
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js";
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
import { AmbientLighting } from "@app/components/PageComponents/ModuleConfig/AmbientLighting.tsx";
import { DetectionSensor } from "@app/components/PageComponents/ModuleConfig/DetectionSensor.tsx";
import { NeighborInfo } from "@app/components/PageComponents/ModuleConfig/NeighborInfo.tsx";
import { Audio } from "@components/PageComponents/ModuleConfig/Audio.tsx";
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.tsx";
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.tsx";
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.tsx";
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.tsx";
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.tsx";
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.tsx";
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.tsx";
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.js";
} from "@components/UI/Tabs.tsx";
export const ModuleConfig = (): JSX.Element => {
const tabs = [

View File

@@ -1,11 +1,11 @@
import { useDevice } from "@app/core/stores/deviceStore.js";
import { PageLayout } from "@components/PageLayout.js";
import { Sidebar } from "@components/Sidebar.js";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
import { useToast } from "@core/hooks/useToast.js";
import { DeviceConfig } from "@pages/Config/DeviceConfig.js";
import { ModuleConfig } from "@pages/Config/ModuleConfig.js";
import { useDevice } from "@app/core/stores/deviceStore.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 { useToast } from "@core/hooks/useToast.ts";
import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx";
import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx";
import { BoxesIcon, SaveIcon, SettingsIcon } from "lucide-react";
import { useState } from "react";

View File

@@ -1,9 +1,9 @@
import { useAppStore } from "@app/core/stores/appStore.js";
import { useDeviceStore } from "@app/core/stores/deviceStore.js";
import { Button } from "@components/UI/Button.js";
import { Separator } from "@components/UI/Seperator.js";
import { H3 } from "@components/UI/Typography/H3.js";
import { Subtle } from "@components/UI/Typography/Subtle.js";
import { useAppStore } from "@app/core/stores/appStore.ts";
import { useDeviceStore } from "@app/core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import { Separator } from "@components/UI/Seperator.tsx";
import { H3 } from "@components/UI/Typography/H3.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import {
BluetoothIcon,
ListPlusIcon,
@@ -21,79 +21,81 @@ export const Dashboard = () => {
const devices = useMemo(() => getDevices(), [getDevices]);
return (
<div className="flex flex-col gap-3 p-3">
<div className="flex items-center justify-between">
<div className="space-y-1">
<H3>Connected Devices</H3>
<Subtle>Manage, connect and disconnect devices</Subtle>
<>
<div className="flex flex-col gap-3 p-3">
<div className="flex items-center justify-between">
<div className="space-y-1">
<H3>Connected Devices</H3>
<Subtle>Manage, connect and disconnect devices</Subtle>
</div>
</div>
<Separator />
<div className="flex h-[450px] rounded-md border border-dashed border-slate-200 p-3 dark:border-slate-700">
{devices.length ? (
<ul className="grow divide-y divide-gray-200">
{devices.map((device) => {
return (
<li key={device.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<p className="truncate text-sm font-medium text-accent">
{device.nodes.get(device.hardware.myNodeNum)?.user
?.longName ?? "UNK"}
</p>
<div className="inline-flex w-24 justify-center gap-2 rounded-full bg-slate-100 py-1 text-xs font-semibold text-slate-900 transition-colors hover:bg-slate-700 hover:text-slate-50">
{device.connection?.connType === "ble" && (
<>
<BluetoothIcon size={16} />
BLE
</>
)}
{device.connection?.connType === "serial" && (
<>
<UsbIcon size={16} />
Serial
</>
)}
{device.connection?.connType === "http" && (
<>
<NetworkIcon size={16} />
Network
</>
)}
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="flex gap-2 text-sm text-gray-500">
<UsersIcon
size={20}
className="text-gray-400"
aria-hidden="true"
/>
{device.nodes.size === 0 ? 0 : device.nodes.size - 1}
</div>
</div>
</div>
</li>
);
})}
</ul>
) : (
<div className="m-auto flex flex-col gap-3 text-center">
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
<H3>No Devices</H3>
<Subtle>Connect at least one device to get started</Subtle>
<Button
className="gap-2"
onClick={() => setConnectDialogOpen(true)}
>
<PlusIcon size={16} />
New Connection
</Button>
</div>
)}
</div>
</div>
<Separator />
<div className="flex h-[450px] rounded-md border border-dashed border-slate-200 p-3 dark:border-slate-700">
{devices.length ? (
<ul className="grow divide-y divide-gray-200">
{devices.map((device) => {
return (
<li key={device.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<p className="truncate text-sm font-medium text-accent">
{device.nodes.get(device.hardware.myNodeNum)?.user
?.longName ?? "UNK"}
</p>
<div className="inline-flex w-24 justify-center gap-2 rounded-full bg-slate-100 py-1 text-xs font-semibold text-slate-900 transition-colors hover:bg-slate-700 hover:text-slate-50">
{device.connection?.connType === "ble" && (
<>
<BluetoothIcon size={16} />
BLE
</>
)}
{device.connection?.connType === "serial" && (
<>
<UsbIcon size={16} />
Serial
</>
)}
{device.connection?.connType === "http" && (
<>
<NetworkIcon size={16} />
Network
</>
)}
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="flex gap-2 text-sm text-gray-500">
<UsersIcon
size={20}
className="text-gray-400"
aria-hidden="true"
/>
{device.nodes.size === 0 ? 0 : device.nodes.size - 1}
</div>
</div>
</div>
</li>
);
})}
</ul>
) : (
<div className="m-auto flex flex-col gap-3 text-center">
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
<H3>No Devices</H3>
<Subtle>Connect atleast one device to get started</Subtle>
<Button
className="gap-2"
onClick={() => setConnectDialogOpen(true)}
>
<PlusIcon size={16} />
New Connection
</Button>
</div>
)}
</div>
</div>
</>
);
};

View File

@@ -1,12 +1,13 @@
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
import { cn } from "@app/core/utils/cn.js";
import { PageLayout } from "@components/PageLayout.js";
import { Sidebar } from "@components/Sidebar.js";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
import { useAppStore } from "@core/stores/appStore.js";
import { useDevice } from "@core/stores/deviceStore.js";
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 { Hashicon } from "@emeraldpay/hashicon-react";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { bbox, lineString } from "@turf/turf";
import {
BoxSelectIcon,
@@ -14,7 +15,7 @@ import {
ZoomInIcon,
ZoomOutIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Marker, useMap } from "react-map-gl";
import MapGl from "react-map-gl/maplibre";
@@ -27,7 +28,7 @@ export const MapPage = (): JSX.Element => {
const allNodes = Array.from(nodes.values());
const getBBox = () => {
const getBBox = useCallback(() => {
if (!map) {
return;
}
@@ -64,7 +65,7 @@ export const MapPage = (): JSX.Element => {
if (center) {
map.easeTo(center);
}
};
}, [allNodes, map]);
useEffect(() => {
map?.on("zoom", () => {
@@ -128,7 +129,11 @@ export const MapPage = (): JSX.Element => {
attributionControl={false}
renderWorldCopies={false}
maxPitch={0}
style = {{filter: darkMode ? 'brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)' : ''}}
style={{
filter: darkMode
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
: "",
}}
dragRotate={false}
touchZoomRotate={false}
initialViewState={{
@@ -140,8 +145,8 @@ export const MapPage = (): JSX.Element => {
{waypoints.map((wp) => (
<Marker
key={wp.id}
longitude={wp.longitudeI / 1e7}
latitude={wp.latitudeI / 1e7}
longitude={(wp.longitudeI ?? 0) / 1e7}
latitude={(wp.latitudeI ?? 0) / 1e7}
anchor="bottom"
>
<div>
@@ -159,26 +164,25 @@ export const MapPage = (): JSX.Element => {
return (
<Marker
key={node.num}
longitude={node.position.longitudeI / 1e7}
latitude={node.position.latitudeI / 1e7}
style = {{filter: darkMode ? 'invert(1)' : ''}}
longitude={(node.position.longitudeI ?? 0) / 1e7}
latitude={(node.position.latitudeI ?? 0) / 1e7}
style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom"
onClick={() => {
map?.easeTo({
zoom: 12,
center: [
(node.position?.longitudeI ?? 0) / 1e7,
(node.position?.latitudeI ?? 0) / 1e7,
],
});
}}
>
<div
className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5"
onClick={() => {
map?.easeTo({
zoom: 12,
center: [
(node.position?.longitudeI ?? 0) / 1e7,
(node.position?.latitudeI ?? 0) / 1e7,
],
});
}}
>
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
<Hashicon value={node.num.toString()} size={22} />
<Subtle className={cn(zoom < 12 && "hidden")}>
{node.user?.longName}
{node.user?.longName ||
`!${numberToHexUnpadded(node.num)}`}
</Subtle>
</div>
</Marker>

Some files were not shown because too many files have changed in this diff Show More