Browse Source

WIP

pull/21/head
Sacha Weatherstone 4 years ago
parent
commit
4cf326661f
  1. 3
      package.json
  2. 10
      pnpm-lock.yaml
  3. 4
      src/App.tsx
  4. 42
      src/components/Connection.tsx
  5. 21
      src/components/MapBox/MapboxProvider.tsx
  6. 9
      src/components/MapBox/mapboxContext.ts
  7. 10
      src/components/connection/BLE.tsx
  8. 10
      src/components/connection/Serial.tsx
  9. 53
      src/components/generic/Card.tsx
  10. 7
      src/components/generic/ContextMenu.tsx
  11. 18
      src/components/generic/Modal.tsx
  12. 9
      src/components/generic/Sidebar/CollapsibleSection.tsx
  13. 15
      src/components/generic/Sidebar/ExternalSection.tsx
  14. 32
      src/components/generic/button/Button.tsx
  15. 22
      src/components/generic/button/IconButton.tsx
  16. 5
      src/components/generic/form/Checkbox.tsx
  17. 48
      src/components/generic/form/Input.tsx
  18. 5
      src/components/generic/form/Select.tsx
  19. 2
      src/components/layout/Sidebar/ButtonNav.tsx
  20. 9
      src/components/layout/Sidebar/Settings/Channels.tsx
  21. 7
      src/components/layout/Sidebar/Settings/Index.tsx
  22. 7
      src/components/layout/Sidebar/Settings/Position.tsx
  23. 7
      src/components/layout/Sidebar/Settings/Power.tsx
  24. 7
      src/components/layout/Sidebar/Settings/Radio.tsx
  25. 7
      src/components/layout/Sidebar/Settings/User.tsx
  26. 7
      src/components/layout/Sidebar/Settings/WiFi.tsx
  27. 11
      src/components/layout/Sidebar/Settings/channels/Channels.tsx
  28. 7
      src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx
  29. 7
      src/components/layout/Sidebar/Settings/plugins/RangeTest.tsx
  30. 7
      src/components/layout/Sidebar/Settings/plugins/Serial.tsx
  31. 7
      src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx
  32. 5
      src/components/layout/Sidebar/index.tsx
  33. 5
      src/components/menu/BottomNav.tsx
  34. 7
      src/components/modals/VersionInfo.tsx
  35. 8
      src/core/slices/mapSlice.ts
  36. 21
      src/hooks/useCreateMapbox.ts
  37. 4
      src/hooks/useMapbox.ts
  38. 7
      src/index.tsx
  39. 2
      src/pages/Extensions/Debug.tsx
  40. 25
      src/pages/Extensions/FileBrowser.tsx
  41. 10
      src/pages/Extensions/Index.tsx
  42. 18
      src/pages/Extensions/Info.tsx
  43. 19
      src/pages/Extensions/Logs.tsx
  44. 5
      src/pages/Map/MapContainer.tsx
  45. 24
      src/pages/Map/Marker.tsx
  46. 5
      src/pages/Map/index.tsx
  47. 4
      src/pages/Messages/ChannelChat.tsx
  48. 4
      src/pages/Messages/DmChat.tsx
  49. 16
      src/pages/Messages/MessageBar.tsx
  50. 11
      src/pages/Messages/index.tsx
  51. 15
      src/pages/Nodes/NodeCard.tsx
  52. 14
      src/pages/Nodes/index.tsx
  53. 2
      tailwind.config.cjs
  54. 12
      vite.config.ts

3
package.json

@ -26,11 +26,12 @@
"@reduxjs/toolkit": "^1.7.2", "@reduxjs/toolkit": "^1.7.2",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"framer-motion": "^6.2.6", "framer-motion": "^6.2.7",
"mapbox-gl": "^2.7.0", "mapbox-gl": "^2.7.0",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-flow-renderer": "^10.0.0-next.39", "react-flow-renderer": "^10.0.0-next.39",
"react-hook-form": "^7.27.1", "react-hook-form": "^7.27.1",

10
pnpm-lock.yaml

@ -15,13 +15,14 @@ specifiers:
'@vitejs/plugin-react': ^1.2.0 '@vitejs/plugin-react': ^1.2.0
autoprefixer: ^10.4.2 autoprefixer: ^10.4.2
base64-js: ^1.5.1 base64-js: ^1.5.1
framer-motion: ^6.2.6 framer-motion: ^6.2.7
gzipper: ^7.0.0 gzipper: ^7.0.0
mapbox-gl: ^2.7.0 mapbox-gl: ^2.7.0
postcss: ^8.4.6 postcss: ^8.4.6
prettier: ^2.5.1 prettier: ^2.5.1
react: ^17.0.2 react: ^17.0.2
react-dom: ^17.0.2 react-dom: ^17.0.2
react-draggable: ^4.4.4
react-error-boundary: ^3.1.4 react-error-boundary: ^3.1.4
react-flow-renderer: ^10.0.0-next.39 react-flow-renderer: ^10.0.0-next.39
react-hook-form: ^7.27.1 react-hook-form: ^7.27.1
@ -53,11 +54,12 @@ dependencies:
'@reduxjs/toolkit': 1.7[email protected][email protected] '@reduxjs/toolkit': 1.7[email protected][email protected]
'@tippyjs/react': 4.2[email protected][email protected] '@tippyjs/react': 4.2[email protected][email protected]
base64-js: 1.5.1 base64-js: 1.5.1
framer-motion: 6.2.6[email protected][email protected] framer-motion: 6.2.7[email protected][email protected]
mapbox-gl: 2.7.0 mapbox-gl: 2.7.0
prettier: 2.5.1 prettier: 2.5.1
react: 17.0.2 react: 17.0.2
react-dom: 17.0[email protected] react-dom: 17.0[email protected]
react-draggable: 4.4[email protected][email protected]
react-error-boundary: 3.1[email protected] react-error-boundary: 3.1[email protected]
react-flow-renderer: 10.0.0-[email protected][email protected] react-flow-renderer: 10.0.0-[email protected][email protected]
react-hook-form: 7.27[email protected] react-hook-form: 7.27[email protected]
@ -3448,8 +3450,8 @@ packages:
resolution: {integrity: sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg==} resolution: {integrity: sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg==}
dev: true dev: true
/framer-motion/6.2.6[email protected][email protected]: /framer-motion/6.2.7[email protected][email protected]:
resolution: {integrity: sha512-7eGav5MxEEzDHozQTDY6+psTIOw2i2kM1QVoJOC3bCp9VOKoo+mKR5n7aT5JPh7ksEKFYJYz0GJDils/9S+oLA==} resolution: {integrity: sha512-RExmZCFpJ3OCakoXmZz8iW8ZI5MoaHmVadydetvTSrNaKmZ7ZC/JDQpNyw1NoDG+fchRGP86lXoiTFSQuin+Cg==}
peerDependencies: peerDependencies:
react: '>=16.8 || ^17.0.0 || ^18.0.0' react: '>=16.8 || ^17.0.0 || ^18.0.0'
react-dom: '>=16.8 || ^17.0.0 || ^18.0.0' react-dom: '>=16.8 || ^17.0.0 || ^18.0.0'

4
src/App.tsx

@ -13,10 +13,10 @@ import { NotFound } from '@pages/NotFound';
export const App = (): JSX.Element => { export const App = (): JSX.Element => {
const route = useRoute(); const route = useRoute();
const darkMode = useAppSelector((state) => state.app.darkMode); const appState = useAppSelector((state) => state.app);
return ( return (
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}> <div className={`h-screen w-screen ${appState.darkMode ? 'dark' : ''}`}>
<ContextMenu> <ContextMenu>
<Connection /> <Connection />
<div className="flex h-full flex-col bg-gray-200 dark:bg-secondaryDark"> <div className="flex h-full flex-col bg-gray-200 dark:bg-secondaryDark">

42
src/components/Connection.tsx

@ -1,6 +1,7 @@
import React from 'react'; import type React from 'react';
import { useEffect } from 'react';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence, m } from 'framer-motion';
import { BLE } from '@components/connection/BLE'; import { BLE } from '@components/connection/BLE';
import { HTTP } from '@components/connection/HTTP'; import { HTTP } from '@components/connection/HTTP';
@ -21,10 +22,10 @@ import { Types } from '@meshtastic/meshtasticjs';
export const Connection = (): JSX.Element => { export const Connection = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const state = useAppSelector((state) => state.meshtastic); const meshtasticState = useAppSelector((state) => state.meshtastic);
const appState = useAppSelector((state) => state.app); const appState = useAppSelector((state) => state.app);
React.useEffect(() => { useEffect(() => {
if (!import.meta.env.VITE_PUBLIC_HOSTED) { if (!import.meta.env.VITE_PUBLIC_HOSTED) {
dispatch( dispatch(
setConnectionParams({ setConnectionParams({
@ -41,11 +42,11 @@ export const Connection = (): JSX.Element => {
} }
}, [dispatch]); }, [dispatch]);
React.useEffect(() => { useEffect(() => {
if (state.ready) { if (meshtasticState.ready) {
dispatch(closeConnectionModal()); dispatch(closeConnectionModal());
} }
}, [state.ready, dispatch]); }, [meshtasticState.ready, dispatch]);
return ( return (
<AnimatePresence> <AnimatePresence>
@ -67,14 +68,14 @@ export const Connection = (): JSX.Element => {
dispatch(setConnType(parseInt(e.target.value))); dispatch(setConnType(parseInt(e.target.value)));
}} }}
disabled={ disabled={
state.deviceStatus === meshtasticState.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED Types.DeviceStatusEnum.DEVICE_CONNECTED
} }
/> />
{appState.connType === connType.HTTP && ( {appState.connType === connType.HTTP && (
<HTTP <HTTP
connecting={ connecting={
state.deviceStatus === meshtasticState.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED Types.DeviceStatusEnum.DEVICE_CONNECTED
} }
/> />
@ -82,7 +83,7 @@ export const Connection = (): JSX.Element => {
{appState.connType === connType.BLE && ( {appState.connType === connType.BLE && (
<BLE <BLE
connecting={ connecting={
state.deviceStatus === meshtasticState.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED Types.DeviceStatusEnum.DEVICE_CONNECTED
} }
/> />
@ -90,7 +91,7 @@ export const Connection = (): JSX.Element => {
{appState.connType === connType.SERIAL && ( {appState.connType === connType.SERIAL && (
<Serial <Serial
connecting={ connecting={
state.deviceStatus === meshtasticState.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED Types.DeviceStatusEnum.DEVICE_CONNECTED
} }
/> />
@ -98,8 +99,23 @@ export const Connection = (): JSX.Element => {
</div> </div>
</div> </div>
<div className="md:w-1/2"> <div className="md:w-1/2">
<div className="h-96 overflow-y-auto rounded-md bg-gray-200 p-2 dark:bg-secondaryDark dark:text-gray-400"> <div className="h-96 overflow-y-auto rounded-md border border-gray-300 bg-gray-200 p-2 dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400">
{state.logs {meshtasticState.logs.length === 0 && (
<div className="flex h-full w-full">
<m.img
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="m-auto h-40 w-40 text-green-500"
src={`/placeholders/${
appState.darkMode
? 'View Code Dark.svg'
: 'View Code.svg'
}`}
/>
</div>
)}
{meshtasticState.logs
.filter((log) => { .filter((log) => {
return ![ return ![
Types.Emitter.handleFromRadio, Types.Emitter.handleFromRadio,

21
src/components/MapBox/MapboxProvider.tsx

@ -1,6 +1,7 @@
import React from 'react'; import type React from 'react';
import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl'; import { ScaleControl } from 'mapbox-gl';
import { MapboxContext } from '@components/MapBox/mapboxContext'; import { MapboxContext } from '@components/MapBox/mapboxContext';
import { import {
@ -26,7 +27,7 @@ export const MapboxProvider = ({
const darkMode = useAppSelector((state) => state.app.darkMode); const darkMode = useAppSelector((state) => state.app.darkMode);
const mapState = useAppSelector((state) => state.map); const mapState = useAppSelector((state) => state.map);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const ref = React.useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const map = useCreateMapbox({ const map = useCreateMapbox({
ref, ref,
@ -41,9 +42,9 @@ export const MapboxProvider = ({
}, },
}); });
React.useEffect(() => { useEffect(() => {
map?.on('load', () => { map?.on('load', () => {
map.addControl(new mapboxgl.ScaleControl()); map.addControl(new ScaleControl());
}); });
map?.on('styledata', () => { map?.on('styledata', () => {
if (!map.getSource('mapbox-dem')) { if (!map.getSource('mapbox-dem')) {
@ -73,14 +74,14 @@ export const MapboxProvider = ({
}); });
}, [dispatch, map, mapState.exaggeration]); }, [dispatch, map, mapState.exaggeration]);
React.useEffect(() => { useEffect(() => {
const center = map?.getCenter(); const center = map?.getCenter();
if (center !== mapState.latLng) { if (center !== mapState.latLng) {
map?.setCenter(mapState.latLng); map?.setCenter(mapState.latLng);
} }
}, [map, mapState.latLng]); }, [map, mapState.latLng]);
React.useEffect(() => { useEffect(() => {
if (['Light', 'Dark'].includes(mapState.style)) { if (['Light', 'Dark'].includes(mapState.style)) {
dispatch(setMapStyle(darkMode ? 'Dark' : 'Light')); dispatch(setMapStyle(darkMode ? 'Dark' : 'Light'));
} }
@ -89,7 +90,7 @@ export const MapboxProvider = ({
/** /**
* Hill Shading * Hill Shading
*/ */
React.useEffect(() => { useEffect(() => {
if (map?.loaded()) { if (map?.loaded()) {
if (mapState.hillShade) { if (mapState.hillShade) {
map.addLayer( map.addLayer(
@ -111,7 +112,7 @@ export const MapboxProvider = ({
/** /**
* Exaggeration * Exaggeration
*/ */
React.useEffect(() => { useEffect(() => {
if (map?.loaded()) { if (map?.loaded()) {
map.setTerrain({ map.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
@ -123,7 +124,7 @@ export const MapboxProvider = ({
/** /**
* Map Style * Map Style
*/ */
React.useEffect(() => { useEffect(() => {
if (map?.loaded()) { if (map?.loaded()) {
map.setStyle(MapStyles[mapState.style].data); map.setStyle(MapStyles[mapState.style].data);
} }

9
src/components/MapBox/mapboxContext.ts

@ -1,12 +1,13 @@
import React from 'react'; import type React from 'react';
import { createContext } from 'react';
import type mapbox from 'mapbox-gl'; import type { Map } from 'mapbox-gl';
export interface MapboxContextValue { export interface MapboxContextValue {
ref: React.Ref<HTMLDivElement>; ref: React.Ref<HTMLDivElement>;
map?: mapbox.Map; map?: Map;
} }
export const MapboxContext = React.createContext<MapboxContextValue>( export const MapboxContext = createContext<MapboxContextValue>(
{} as MapboxContextValue, {} as MapboxContextValue,
); );

10
src/components/connection/BLE.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiArrowRightCircle } from 'react-icons/fi'; import { FiArrowRightCircle } from 'react-icons/fi';
@ -14,19 +15,19 @@ export interface BLEProps {
} }
export const BLE = ({ connecting }: BLEProps): JSX.Element => { export const BLE = ({ connecting }: BLEProps): JSX.Element => {
const [bleDevices, setBleDevices] = React.useState<BluetoothDevice[]>([]); const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
const { handleSubmit } = useForm<{ const { handleSubmit } = useForm<{
device?: BluetoothDevice; device?: BluetoothDevice;
}>(); }>();
const updateBleDeviceList = React.useCallback(async (): Promise<void> => { const updateBleDeviceList = useCallback(async (): Promise<void> => {
const ble = new IBLEConnection(); const ble = new IBLEConnection();
const devices = await ble.getDevices(); const devices = await ble.getDevices();
setBleDevices(devices); setBleDevices(devices);
}, []); }, []);
React.useEffect(() => { useEffect(() => {
void updateBleDeviceList(); void updateBleDeviceList();
}, [updateBleDeviceList]); }, [updateBleDeviceList]);
@ -46,6 +47,7 @@ export const BLE = ({ connecting }: BLEProps): JSX.Element => {
> >
<div className="my-auto">{device.name}</div> <div className="my-auto">{device.name}</div>
<IconButton <IconButton
nested
onClick={async (): Promise<void> => { onClick={async (): Promise<void> => {
await setConnection(connType.BLE); await setConnection(connType.BLE);
}} }}

10
src/components/connection/Serial.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiArrowRightCircle } from 'react-icons/fi'; import { FiArrowRightCircle } from 'react-icons/fi';
@ -15,20 +16,20 @@ export interface SerialProps {
} }
export const Serial = ({ connecting }: SerialProps): JSX.Element => { export const Serial = ({ connecting }: SerialProps): JSX.Element => {
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]); const [serialDevices, setSerialDevices] = useState<SerialPort[]>([]);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { handleSubmit } = useForm<{ const { handleSubmit } = useForm<{
device?: SerialPort; device?: SerialPort;
}>(); }>();
const updateSerialDeviceList = React.useCallback(async (): Promise<void> => { const updateSerialDeviceList = useCallback(async (): Promise<void> => {
const serial = new ISerialConnection(); const serial = new ISerialConnection();
const devices = await serial.getPorts(); const devices = await serial.getPorts();
setSerialDevices(devices); setSerialDevices(devices);
}, []); }, []);
React.useEffect(() => { useEffect(() => {
void updateSerialDeviceList(); void updateSerialDeviceList();
}, [updateSerialDeviceList]); }, [updateSerialDeviceList]);
@ -53,6 +54,7 @@ export const Serial = ({ connecting }: SerialProps): JSX.Element => {
</p> </p>
</div> </div>
<IconButton <IconButton
nested
onClick={async (): Promise<void> => { onClick={async (): Promise<void> => {
dispatch( dispatch(
setConnectionParams({ setConnectionParams({

53
src/components/generic/Card.tsx

@ -1,21 +1,56 @@
import type React from 'react'; import type React from 'react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import Draggable from 'react-draggable';
export interface CardProps { export interface CardProps {
className?: string; className?: string;
title?: string;
actions?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
border?: boolean;
draggable?: boolean;
} }
export const Card = ({ className, children }: CardProps): JSX.Element => { export const Card = ({
className,
title,
actions,
draggable,
border,
children,
}: CardProps): JSX.Element => {
return ( return (
<m.div <Draggable handle=".handle" disabled={!draggable}>
className={`flex select-none rounded-md bg-white p-4 shadow-md dark:bg-primaryDark ${className}`} <div
initial={{ opacity: 0 }} className={`flex h-full w-full flex-col rounded-md shadow-md ${
animate={{ opacity: 1 }} border ? 'border border-gray-300 dark:border-gray-600' : ''
exit={{ opacity: 0 }} } ${className ?? ''}`}
> >
{children} {(title || actions) && (
</m.div> <div
className={`w-full select-none justify-between rounded-t-md border-b border-gray-300 bg-gray-200 p-2 px-2 text-lg font-medium dark:border-gray-600 dark:bg-primaryDark dark:text-white ${
draggable ? 'cursor-move' : ''
}`}
>
<div className="handle flex h-8 justify-between">
<div className="my-auto ml-2 truncate">{title}</div>
{actions}
</div>
</div>
)}
<m.div
className={`flex flex-grow select-none flex-col gap-4 p-4 ${
title || actions ? 'rounded-b-md backdrop-blur-xl' : 'rounded-md'
} ${draggable ? '' : 'bg-white dark:bg-primaryDark'}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{children}
</m.div>
</div>
</Draggable>
); );
}; };

7
src/components/generic/ContextMenu.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { FiActivity, FiAperture, FiTag } from 'react-icons/fi'; import { FiActivity, FiAperture, FiTag } from 'react-icons/fi';
@ -13,8 +14,8 @@ export const ContextMenu = ({
items, items,
children, children,
}: ContextMenuProps): JSX.Element => { }: ContextMenuProps): JSX.Element => {
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = useState(false);
const [position, setPosition] = React.useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: 0, y: 0 });
return ( return (
<div <div

18
src/components/generic/Modal.tsx

@ -37,16 +37,18 @@ export const Modal = ({
&#8203; &#8203;
</span> </span>
<div className="inline-block w-full max-w-3xl align-middle"> <div className="inline-block w-full max-w-3xl align-middle">
<Card className="relative flex-col gap-4 border border-gray-300 dark:border-gray-600"> <Card
<div className="flex justify-between"> border
<div className="text-2xl font-medium dark:text-white"> draggable
{title} title={title}
</div> actions={
<div className="flex gap-2"> <>
{actions} {actions}
<IconButton tooltip="Close" icon={<FiX />} onClick={onClose} /> <IconButton tooltip="Close" icon={<FiX />} onClick={onClose} />
</div> </>
</div> }
className="relative flex-col gap-4 "
>
{children} {children}
</Card> </Card>
</div> </div>

9
src/components/generic/Sidebar/CollapsibleSection.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { AnimatePresence, m } from 'framer-motion'; import { AnimatePresence, m } from 'framer-motion';
import { FiArrowUp } from 'react-icons/fi'; import { FiArrowUp } from 'react-icons/fi';
@ -14,13 +15,15 @@ export const CollapsibleSection = ({
icon, icon,
children, children,
}: CollapsibleSectionProps): JSX.Element => { }: CollapsibleSectionProps): JSX.Element => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = useState(false);
const toggleOpen = (): void => setOpen(!open); const toggleOpen = (): void => setOpen(!open);
return ( return (
<m.div> <m.div>
<m.div <m.div
layout layout
className="w-full cursor-pointer select-none overflow-hidden border-b border-gray-300 p-2 text-sm font-medium dark:border-primaryDark dark:bg-secondaryDark dark:text-gray-400" className={`w-full cursor-pointer select-none overflow-hidden border-l-4 border-b border-gray-300 p-2 text-sm font-medium dark:border-primaryDark dark:bg-secondaryDark dark:text-gray-400 ${
open ? 'dark:border-l-primary' : 'dark:border-secondaryDark'
}`}
> >
<m.div <m.div
layout layout

15
src/components/generic/Sidebar/ExternalSection.tsx

@ -1,20 +1,23 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import { FiExternalLink } from 'react-icons/fi'; import { FiChevronRight } from 'react-icons/fi';
export interface ExternalSectionProps { export interface ExternalSectionProps {
title: string; title: string;
icon?: JSX.Element; icon?: JSX.Element;
active?: boolean;
onClick: () => void; onClick: () => void;
} }
export const ExternalSection = ({ export const ExternalSection = ({
title, title,
icon, icon,
active,
onClick, onClick,
}: ExternalSectionProps): JSX.Element => { }: ExternalSectionProps): JSX.Element => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = useState(false);
const toggleOpen = (): void => setOpen(!open); const toggleOpen = (): void => setOpen(!open);
return ( return (
<m.div <m.div
@ -24,7 +27,9 @@ export const ExternalSection = ({
> >
<m.div <m.div
layout layout
className="w-full cursor-pointer select-none overflow-hidden dark:bg-secondaryDark dark:text-gray-400" className={`w-full cursor-pointer select-none overflow-hidden border-l-4 dark:bg-secondaryDark dark:text-gray-400 ${
active ? 'border-primary' : 'dark:border-secondaryDark'
}`}
> >
<m.div <m.div
layout layout
@ -38,7 +43,7 @@ export const ExternalSection = ({
{title} {title}
</m.div> </m.div>
<m.div className="my-auto"> <m.div className="my-auto">
<FiExternalLink /> <FiChevronRight />
</m.div> </m.div>
</m.div> </m.div>
</m.div> </m.div>

32
src/components/generic/button/Button.tsx

@ -1,35 +1,37 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { m } from 'framer-motion';
import { FiCheck } from 'react-icons/fi'; import { FiCheck } from 'react-icons/fi';
type DefaultButtonProps = JSX.IntrinsicElements['button'];
export enum ButtonSize { export enum ButtonSize {
Small = 'small', Small = 'small',
Medium = 'medium', Medium = 'medium',
Large = 'large', Large = 'large',
} }
export interface ButtonProps extends DefaultButtonProps { export interface ButtonProps {
icon?: JSX.Element; icon?: JSX.Element;
active?: boolean;
border?: boolean; border?: boolean;
className?: string;
disabled?: boolean;
children?: React.ReactNode;
size?: ButtonSize; size?: ButtonSize;
onClick?: () => void;
confirmAction?: () => void; confirmAction?: () => void;
} }
export const Button = ({ export const Button = ({
icon, icon,
className, className,
active,
border, border,
size = ButtonSize.Medium, size = ButtonSize.Medium,
confirmAction, confirmAction,
onClick,
disabled, disabled,
children, children,
...props
}: ButtonProps): JSX.Element => { }: ButtonProps): JSX.Element => {
const [hasConfirmed, setHasConfirmed] = React.useState(false); const [hasConfirmed, setHasConfirmed] = useState(false);
const handleConfirm = (): void => { const handleConfirm = (): void => {
if (typeof confirmAction == 'function') { if (typeof confirmAction == 'function') {
@ -44,9 +46,11 @@ export const Button = ({
}; };
return ( return (
<button <m.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.97 }}
onClick={handleConfirm} onClick={handleConfirm}
className={`flex select-none items-center space-x-3 rounded-md border border-transparent text-sm transition duration-200 ease-in-out focus-within:border-primary focus-within:shadow-border active:scale-95 dark:text-white dark:focus-within:border-primary className={`flex select-none items-center space-x-3 rounded-md border border-transparent text-sm focus-within:border-primary focus-within:shadow-border dark:text-white dark:focus-within:border-primary
${ ${
size === ButtonSize.Small size === ButtonSize.Small
? 'p-0' ? 'p-0'
@ -58,8 +62,10 @@ export const Button = ({
disabled disabled
? 'cursor-not-allowed bg-white dark:bg-primaryDark' ? 'cursor-not-allowed bg-white dark:bg-primaryDark'
: 'cursor-pointer hover:bg-gray-100 hover:shadow-md dark:hover:bg-secondaryDark' : 'cursor-pointer hover:bg-gray-100 hover:shadow-md dark:hover:bg-secondaryDark'
} ${border ? 'border-gray-400 dark:border-gray-200' : ''} ${className}`} } ${border ? 'border-gray-400 dark:border-gray-200' : ''} ${
{...props} className ?? ''
}`}
onClickCapture={onClick}
> >
{icon && ( {icon && (
<div className="text-gray-500 dark:text-gray-400"> <div className="text-gray-500 dark:text-gray-400">
@ -68,6 +74,6 @@ export const Button = ({
)} )}
<span>{children}</span> <span>{children}</span>
</button> </m.button>
); );
}; };

22
src/components/generic/button/IconButton.tsx

@ -1,5 +1,7 @@
import type React from 'react'; import type React from 'react';
import { m } from 'framer-motion';
import { Tooltip } from '@components/generic/Tooltip'; import { Tooltip } from '@components/generic/Tooltip';
type DefaulButtonProps = JSX.IntrinsicElements['button']; type DefaulButtonProps = JSX.IntrinsicElements['button'];
@ -7,26 +9,32 @@ type DefaulButtonProps = JSX.IntrinsicElements['button'];
export interface IconButtonProps extends DefaulButtonProps { export interface IconButtonProps extends DefaulButtonProps {
icon: React.ReactNode; icon: React.ReactNode;
tooltip?: string; tooltip?: string;
nested?: boolean;
active?: boolean; active?: boolean;
} }
export const IconButton = ({ export const IconButton = ({
icon, icon,
active,
tooltip, tooltip,
nested,
active,
disabled, disabled,
...props ...props
}: IconButtonProps): JSX.Element => { }: IconButtonProps): JSX.Element => {
return ( return (
<Tooltip disabled={!tooltip} content={tooltip}> <Tooltip disabled={!tooltip} content={tooltip}>
<div className="my-auto text-gray-500 dark:text-gray-400"> <m.div
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.97 }}
className="my-auto text-gray-500 dark:text-gray-400"
>
<button <button
type="button" type="button"
disabled={disabled} disabled={disabled}
className={`rounded-md p-2 transition duration-200 ease-in-out active:scale-95 ${ className={`rounded-md p-2 hover:bg-gray-200 ${
active active ? 'bg-gray-200 dark:bg-secondaryDark' : ''
? 'bg-gray-200 dark:bg-gray-600' } ${
: 'hover:bg-gray-200 dark:hover:bg-gray-600' nested ? 'dark:hover:bg-primaryDark' : 'dark:hover:bg-secondaryDark'
} ${ } ${
disabled disabled
? 'cursor-not-allowed text-gray-400 dark:text-gray-700' ? 'cursor-not-allowed text-gray-400 dark:text-gray-700'
@ -37,7 +45,7 @@ export const IconButton = ({
{icon} {icon}
<span className="sr-only">Refresh</span> <span className="sr-only">Refresh</span>
</button> </button>
</div> </m.div>
</Tooltip> </Tooltip>
); );
}; };

5
src/components/generic/form/Checkbox.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { forwardRef } from 'react';
import { Label } from '@components/generic/form/Label'; import { Label } from '@components/generic/form/Label';
@ -12,7 +13,7 @@ export interface CheckboxProps extends DefaultInputProps {
error?: boolean; error?: boolean;
} }
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>( export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
function Input( function Input(
{ label, valid, validationMessage, id, error, ...props }: CheckboxProps, { label, valid, validationMessage, id, error, ...props }: CheckboxProps,
ref, ref,

48
src/components/generic/form/Input.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { forwardRef } from 'react';
import { InputWrapper } from '@components/generic/form/InputWrapper'; import { InputWrapper } from '@components/generic/form/InputWrapper';
import { Label } from '@components/generic/form/Label'; import { Label } from '@components/generic/form/Label';
@ -13,25 +14,26 @@ export interface InputProps extends DefaultInputProps {
suffix?: string; suffix?: string;
} }
export const Input = React.forwardRef<HTMLInputElement, InputProps>( export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
function Input({ label, error, action, suffix, ...props }: InputProps, ref) { { label, error, action, suffix, ...props }: InputProps,
return ( ref,
<div className="w-full"> ) {
{label && <Label label={label} error={error} />} return (
<InputWrapper error={error} disabled={props.disabled}> <div className="w-full">
<input {label && <Label label={label} error={error} />}
ref={ref} <InputWrapper error={error} disabled={props.disabled}>
className="h-10 w-full bg-transparent px-3 py-2 focus:outline-none disabled:cursor-not-allowed dark:text-white" <input
{...props} ref={ref}
/> className="h-10 w-full bg-transparent px-3 py-2 focus:outline-none disabled:cursor-not-allowed dark:text-white"
{suffix && ( {...props}
<span className="my-auto mr-3 text-sm font-medium text-gray-500 dark:text-gray-400"> />
{suffix} {suffix && (
</span> <span className="my-auto mr-3 text-sm font-medium text-gray-500 dark:text-gray-400">
)} {suffix}
{action && <div className="mr-1 flex">{action}</div>} </span>
</InputWrapper> )}
</div> {action && <div className="mr-1 flex">{action}</div>}
); </InputWrapper>
}, </div>
); );
});

5
src/components/generic/form/Select.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { forwardRef } from 'react';
import { InputWrapper } from '@components/generic/form/InputWrapper'; import { InputWrapper } from '@components/generic/form/InputWrapper';
import { Label } from '@components/generic/form/Label'; import { Label } from '@components/generic/form/Label';
@ -16,7 +17,7 @@ export interface SelectProps extends DefaultSelectProps {
small?: boolean; small?: boolean;
} }
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>( export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ options, optionsEnum, label, error, small, ...props }, ref) => { ({ options, optionsEnum, label, error, small, ...props }, ref) => {
const optionsEnumValues = optionsEnum const optionsEnumValues = optionsEnum
? Object.entries(optionsEnum).filter( ? Object.entries(optionsEnum).filter(

2
src/components/layout/Sidebar/ButtonNav.tsx

@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { FiMessageCircle, FiSettings } from 'react-icons/fi'; import { FiMessageCircle, FiSettings } from 'react-icons/fi';
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri'; import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';

9
src/components/layout/Sidebar/Settings/Channels.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -17,9 +18,9 @@ export const Channels = (): JSX.Element => {
channels.find( channels.find(
(channel) => channel.role === Protobuf.Channel_Role.PRIMARY, (channel) => channel.role === Protobuf.Channel_Role.PRIMARY,
) ?? channels[0]; ) ?? channels[0];
const [usePreset, setUsePreset] = React.useState(true); const [usePreset, setUsePreset] = useState(true);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const [selectedChannel, setSelectedChannel] = React.useState< const [selectedChannel, setSelectedChannel] = useState<
Protobuf.Channel | undefined Protobuf.Channel | undefined
>(); >();

7
src/components/layout/Sidebar/Settings/Index.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { import {
FiAlignLeft, FiAlignLeft,
@ -38,8 +39,8 @@ export interface SettingsProps {
} }
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => { export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
const [pluginsOpen, setPluginsOpen] = React.useState(false); const [pluginsOpen, setPluginsOpen] = useState(false);
const [channelsOpen, setChannelsOpen] = React.useState(false); const [channelsOpen, setChannelsOpen] = useState(false);
const { const {
rangeTestPluginEnabled, rangeTestPluginEnabled,
extNotificationPluginEnabled, extNotificationPluginEnabled,

7
src/components/layout/Sidebar/Settings/Position.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -18,7 +19,7 @@ export const Position = (): JSX.Element => {
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
); );
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } = const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({ useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: { defaultValues: {
@ -32,7 +33,7 @@ export const Position = (): JSX.Element => {
}, },
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

7
src/components/layout/Sidebar/Settings/Power.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -14,7 +15,7 @@ export const Power = (): JSX.Element => {
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
); );
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset } = const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({ useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: { defaultValues: {
@ -23,7 +24,7 @@ export const Power = (): JSX.Element => {
}, },
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

7
src/components/layout/Sidebar/Settings/Radio.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -14,13 +15,13 @@ export const Radio = (): JSX.Element => {
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
); );
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset } = const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({ useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences, defaultValues: preferences,
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

7
src/components/layout/Sidebar/Settings/User.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -13,7 +14,7 @@ import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const User = (): JSX.Element => { export const User = (): JSX.Element => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const myNodeNum = useAppSelector( const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
).myNodeNum; ).myNodeNum;
@ -40,7 +41,7 @@ export const User = (): JSX.Element => {
}, },
}); });
React.useEffect(() => { useEffect(() => {
reset({ reset({
longName: node?.user?.longName, longName: node?.user?.longName,
shortName: node?.user?.shortName, shortName: node?.user?.shortName,

7
src/components/layout/Sidebar/Settings/WiFi.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -14,7 +15,7 @@ export const WiFi = (): JSX.Element => {
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
); );
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const { register, handleSubmit, formState, reset, control } = const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({ useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences, defaultValues: preferences,
@ -32,7 +33,7 @@ export const WiFi = (): JSX.Element => {
defaultValue: false, defaultValue: false,
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

11
src/components/layout/Sidebar/Settings/channels/Channels.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { fromByteArray, toByteArray } from 'base64-js'; import { fromByteArray, toByteArray } from 'base64-js';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@ -18,9 +19,9 @@ export interface SettingsPanelProps {
} }
export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => { export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const [keySize, setKeySize] = React.useState<128 | 256>(256); const [keySize, setKeySize] = useState<128 | 256>(256);
const [pskHidden, setPskHidden] = React.useState(true); const [pskHidden, setPskHidden] = useState(true);
const { register, handleSubmit, setValue, formState, reset } = useForm< const { register, handleSubmit, setValue, formState, reset } = useForm<
Omit<Protobuf.ChannelSettings, 'psk'> & { psk: string; enabled: boolean } Omit<Protobuf.ChannelSettings, 'psk'> & { psk: string; enabled: boolean }
@ -37,7 +38,7 @@ export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
}, },
}); });
React.useEffect(() => { useEffect(() => {
reset({ reset({
enabled: [ enabled: [
Protobuf.Channel_Role.SECONDARY, Protobuf.Channel_Role.SECONDARY,

7
src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -12,7 +13,7 @@ import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs'; import type { Protobuf } from '@meshtastic/meshtasticjs';
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => { export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
@ -23,7 +24,7 @@ export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
defaultValues: preferences, defaultValues: preferences,
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

7
src/components/layout/Sidebar/Settings/plugins/RangeTest.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -12,7 +13,7 @@ import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs'; import type { Protobuf } from '@meshtastic/meshtasticjs';
export const RangeTestSettingsPanel = (): JSX.Element => { export const RangeTestSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
@ -23,7 +24,7 @@ export const RangeTestSettingsPanel = (): JSX.Element => {
defaultValues: preferences, defaultValues: preferences,
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

7
src/components/layout/Sidebar/Settings/plugins/Serial.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -12,7 +13,7 @@ import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs'; import type { Protobuf } from '@meshtastic/meshtasticjs';
export const SerialSettingsPanel = (): JSX.Element => { export const SerialSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
@ -23,7 +24,7 @@ export const SerialSettingsPanel = (): JSX.Element => {
defaultValues: preferences, defaultValues: preferences,
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

7
src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -12,7 +13,7 @@ import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs'; import type { Protobuf } from '@meshtastic/meshtasticjs';
export const StoreForwardSettingsPanel = (): JSX.Element => { export const StoreForwardSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = useState(false);
const preferences = useAppSelector( const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences, (state) => state.meshtastic.radio.preferences,
@ -23,7 +24,7 @@ export const StoreForwardSettingsPanel = (): JSX.Element => {
defaultValues: preferences, defaultValues: preferences,
}); });
React.useEffect(() => { useEffect(() => {
reset(preferences); reset(preferences);
}, [reset, preferences]); }, [reset, preferences]);

5
src/components/layout/Sidebar/index.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { ButtonNav } from '@components/layout/Sidebar/ButtonNav'; import { ButtonNav } from '@components/layout/Sidebar/ButtonNav';
import { Settings } from '@components/layout/Sidebar/Settings/Index'; import { Settings } from '@components/layout/Sidebar/Settings/Index';
@ -9,7 +10,7 @@ export interface SidebarProps {
} }
export const Sidebar = ({ children }: SidebarProps): JSX.Element => { export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
const [settingsOpen, setSettingsOpen] = React.useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const appState = useAppSelector((state) => state.app); const appState = useAppSelector((state) => state.app);

5
src/components/menu/BottomNav.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { import {
FiBluetooth, FiBluetooth,
@ -31,7 +32,7 @@ import { VersionInfo } from '../modals/VersionInfo';
import { BottomNavItem } from './BottomNavItem'; import { BottomNavItem } from './BottomNavItem';
export const BottomNav = (): JSX.Element => { export const BottomNav = (): JSX.Element => {
const [showVersionInfo, setShowVersionInfo] = React.useState(false); const [showVersionInfo, setShowVersionInfo] = useState(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const meshtasticState = useAppSelector((state) => state.meshtastic); const meshtasticState = useAppSelector((state) => state.meshtastic);
const appState = useAppSelector((state) => state.app); const appState = useAppSelector((state) => state.app);

7
src/components/modals/VersionInfo.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect } from 'react';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { MdUpgrade } from 'react-icons/md'; import { MdUpgrade } from 'react-icons/md';
@ -56,7 +57,7 @@ export const VersionInfo = ({
}, },
); );
React.useEffect(() => { useEffect(() => {
if (data) { if (data) {
const index = data.findIndex( const index = data.findIndex(
(commit) => commit.sha.substring(0, 7) === process.env.COMMIT_HASH, (commit) => commit.sha.substring(0, 7) === process.env.COMMIT_HASH,
@ -90,7 +91,7 @@ export const VersionInfo = ({
data.map((commit) => ( data.map((commit) => (
<div <div
key={commit.sha} key={commit.sha}
className={`flex gap-2 rounded-md py-1 px-2 ${ className={`flex gap-2 rounded-md border border-transparent py-1 px-2 hover:border-primary ${
commit.sha.substring(0, 7) === process.env.COMMIT_HASH commit.sha.substring(0, 7) === process.env.COMMIT_HASH
? 'bg-primary' ? 'bg-primary'
: 'dark:bg-secondaryDark' : 'dark:bg-secondaryDark'

8
src/core/slices/mapSlice.ts

@ -1,4 +1,4 @@
import mapboxgl from 'mapbox-gl'; import { LngLat } from 'mapbox-gl';
import type { MapStyleName } from '@pages/Map/styles'; import type { MapStyleName } from '@pages/Map/styles';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
@ -6,7 +6,7 @@ import { createSlice } from '@reduxjs/toolkit';
interface MapState { interface MapState {
firstLoad: boolean; firstLoad: boolean;
latLng: mapboxgl.LngLat; latLng: LngLat;
zoom: number; zoom: number;
bearing: number; bearing: number;
pitch: number; pitch: number;
@ -18,7 +18,7 @@ interface MapState {
const initialState: MapState = { const initialState: MapState = {
firstLoad: true, firstLoad: true,
latLng: new mapboxgl.LngLat(0, 0), latLng: new LngLat(0, 0),
zoom: 2, zoom: 2,
bearing: 0, bearing: 0,
pitch: 0, pitch: 0,
@ -33,7 +33,7 @@ export const mapSlice = createSlice({
name: 'map', name: 'map',
initialState, initialState,
reducers: { reducers: {
setLatLng: (state, action: PayloadAction<mapboxgl.LngLat>) => { setLatLng: (state, action: PayloadAction<LngLat>) => {
state.latLng = action.payload; state.latLng = action.payload;
}, },
setZoom: (state, action: PayloadAction<number>) => { setZoom: (state, action: PayloadAction<number>) => {

21
src/hooks/useCreateMapbox.ts

@ -1,30 +1,29 @@
import 'mapbox-gl/dist/mapbox-gl.css'; import 'mapbox-gl/dist/mapbox-gl.css';
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl'; import { Map, MapboxOptions } from 'mapbox-gl';
type AccessToken = string;
export interface useMapboxProps { export interface useMapboxProps {
ref: React.RefObject<HTMLDivElement>; ref: React.RefObject<HTMLDivElement>;
accessToken: AccessToken; accessToken: string;
options?: Partial<mapboxgl.MapboxOptions>; options?: Partial<MapboxOptions>;
} }
export function useCreateMapbox({ export function useCreateMapbox({
ref, ref,
accessToken, accessToken,
options, options,
}: useMapboxProps): mapboxgl.Map | undefined { }: useMapboxProps): Map | undefined {
const [mapInstance, setMapInstance] = React.useState<mapboxgl.Map>(); const [mapInstance, setMapInstance] = useState<Map>();
React.useEffect(() => { useEffect(() => {
const container = ref.current as HTMLDivElement; const container = ref.current as HTMLDivElement;
if (mapInstance || !container) { if (mapInstance || !container) {
return; return;
} }
mapboxgl.accessToken = accessToken; const map = new Map({
const map = new mapboxgl.Map({ accessToken,
container, container,
antialias: true, antialias: true,
...options, ...options,

4
src/hooks/useMapbox.ts

@ -1,8 +1,8 @@
import React from 'react'; import { useContext } from 'react';
import type { MapboxContextValue } from '@components/MapBox/mapboxContext'; import type { MapboxContextValue } from '@components/MapBox/mapboxContext';
import { MapboxContext } from '@components/MapBox/mapboxContext'; import { MapboxContext } from '@components/MapBox/mapboxContext';
export const useMapbox = (): MapboxContextValue => { export const useMapbox = (): MapboxContextValue => {
return React.useContext(MapboxContext); return useContext(MapboxContext);
}; };

7
src/index.tsx

@ -1,6 +1,7 @@
import '@app/index.css'; import '@app/index.css';
import React from 'react'; import type React from 'react';
import { StrictMode } from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { domAnimation, LazyMotion } from 'framer-motion'; import { domAnimation, LazyMotion } from 'framer-motion';
@ -14,7 +15,7 @@ import { RouteProvider } from '@core/router';
import { store } from '@core/store'; import { store } from '@core/store';
render( render(
<React.StrictMode> <StrictMode>
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
<RouteProvider> <RouteProvider>
<Provider store={store}> <Provider store={store}>
@ -25,6 +26,6 @@ render(
</Provider> </Provider>
</RouteProvider> </RouteProvider>
</ErrorBoundary> </ErrorBoundary>
</React.StrictMode>, </StrictMode>,
document.getElementById('root'), document.getElementById('root'),
); );

2
src/pages/Extensions/Debug.tsx

@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { Button } from '@app/components/generic/button/Button'; import { Button } from '@app/components/generic/button/Button';
import { Card } from '@app/components/generic/Card'; import { Card } from '@app/components/generic/Card';

25
src/pages/Extensions/FileBrowser.tsx

@ -1,8 +1,10 @@
import React from 'react'; import type React from 'react';
import { AnimatePresence, m } from 'framer-motion'; import { AnimatePresence, m } from 'framer-motion';
import { FiFilePlus } from 'react-icons/fi';
import useSWR from 'swr'; import useSWR from 'swr';
import { Button } from '@app/components/generic/button/Button';
import { Card } from '@app/components/generic/Card'; import { Card } from '@app/components/generic/Card';
import { fetcher } from '@core/utils/fetcher'; import { fetcher } from '@core/utils/fetcher';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
@ -29,22 +31,27 @@ export const FileBrowser = (): JSX.Element => {
const connectionParams = useAppSelector( const connectionParams = useAppSelector(
(state) => state.app.connectionParams, (state) => state.app.connectionParams,
); );
const darkMode = useAppSelector((state) => state.app.darkMode); const appState = useAppSelector((state) => state.app);
const meshtasticState = useAppSelector((state) => state.meshtastic);
const { data } = useSWR<Files>( const { data } = useSWR<Files>(
`${connectionParams.HTTP.tls ? 'https' : 'http'}://${ `${connectionParams.HTTP.tls ? 'https' : 'http'}://${
connectionParams.HTTP.address connectionParams.HTTP.address
}/json/spiffs/browse/static`, }${
meshtasticState.radio.hardware.firmwareVersion.includes('1.2')
? '/json/spiffs/browse/static'
: '/json/fs/browse/static'
}`,
fetcher, fetcher,
); );
return ( return (
<div className="flex h-full p-4"> <div className="flex h-full p-4">
<Card className="flex-grow flex-col"> <Card
<div className="flex h-10 w-full rounded-t-md border-b border-gray-300 px-4 text-lg font-semibold shadow-md dark:border-gray-600 dark:bg-zinc-700 dark:text-white"> title="File Browser"
<div className="my-auto w-1/3">FileName</div> actions={<Button icon={<FiFilePlus />}>Upload File</Button>}
<div className="my-auto w-1/3">Actions</div> className="flex-grow flex-col"
</div> >
<div className="h-full px-4"> <div className="h-full px-4">
<AnimatePresence> <AnimatePresence>
{(!data || data?.data.files.length === 0) && ( {(!data || data?.data.files.length === 0) && (
@ -55,7 +62,7 @@ export const FileBrowser = (): JSX.Element => {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="m-auto h-64 w-64 text-green-500" className="m-auto h-64 w-64 text-green-500"
src={`/placeholders/${ src={`/placeholders/${
darkMode ? 'Files Dark.svg' : 'Files.svg' appState.darkMode ? 'Files Dark.svg' : 'Files.svg'
}`} }`}
/> />
</div> </div>

10
src/pages/Extensions/Index.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { FiFile, FiInfo } from 'react-icons/fi'; import { FiFile, FiInfo } from 'react-icons/fi';
import { MdSubject } from 'react-icons/md'; import { MdSubject } from 'react-icons/md';
@ -14,7 +15,7 @@ import { Logs } from '@pages/Extensions/Logs';
import { Debug } from './Debug'; import { Debug } from './Debug';
export const Extensions = (): JSX.Element => { export const Extensions = (): JSX.Element => {
const [selectedExtension, setSelectedExtension] = React.useState< const [selectedExtension, setSelectedExtension] = useState<
'info' | 'logs' | 'fileBrowser' | 'rangeTest' | 'debug' 'info' | 'logs' | 'fileBrowser' | 'rangeTest' | 'debug'
>('info'); >('info');
@ -29,6 +30,7 @@ export const Extensions = (): JSX.Element => {
setSelectedExtension('info'); setSelectedExtension('info');
}} }}
icon={<FiInfo />} icon={<FiInfo />}
active={selectedExtension === 'info'}
title="Node Info" title="Node Info"
/> />
<ExternalSection <ExternalSection
@ -36,6 +38,7 @@ export const Extensions = (): JSX.Element => {
setSelectedExtension('logs'); setSelectedExtension('logs');
}} }}
icon={<MdSubject />} icon={<MdSubject />}
active={selectedExtension === 'logs'}
title="Logs" title="Logs"
/> />
<ExternalSection <ExternalSection
@ -43,6 +46,7 @@ export const Extensions = (): JSX.Element => {
setSelectedExtension('fileBrowser'); setSelectedExtension('fileBrowser');
}} }}
icon={<FiFile />} icon={<FiFile />}
active={selectedExtension === 'fileBrowser'}
title="File Browser" title="File Browser"
/> />
<ExternalSection <ExternalSection
@ -50,6 +54,7 @@ export const Extensions = (): JSX.Element => {
setSelectedExtension('rangeTest'); setSelectedExtension('rangeTest');
}} }}
icon={<RiPinDistanceFill />} icon={<RiPinDistanceFill />}
active={selectedExtension === 'rangeTest'}
title="Range Test" title="Range Test"
/> />
<ExternalSection <ExternalSection
@ -57,6 +62,7 @@ export const Extensions = (): JSX.Element => {
setSelectedExtension('debug'); setSelectedExtension('debug');
}} }}
icon={<VscDebug />} icon={<VscDebug />}
active={selectedExtension === 'debug'}
title="Debug" title="Debug"
/> />
</div> </div>

18
src/pages/Extensions/Info.tsx

@ -1,7 +1,9 @@
import React from 'react'; import type React from 'react';
import { FiRefreshCw } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import { Button } from '@app/components/generic/button/Button';
import { Card } from '@app/components/generic/Card'; import { Card } from '@app/components/generic/Card';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
@ -17,8 +19,16 @@ export const Info = (): JSX.Element => {
); );
return ( return (
<div className="flex h-full flex-col gap-4 p-4 md:flex-row"> <div className="flex h-full flex-col gap-4 p-4 xl:flex-row">
<Card className="md:w-1/4"> <Card
title="Connected Node Details"
actions={
<Button className="truncate" icon={<FiRefreshCw />}>
Refresh Node info
</Button>
}
className="xl:w-3/5"
>
<div className="m-auto flex flex-col gap-2"> <div className="m-auto flex flex-col gap-2">
<Hashicon value={hardwareInfo.myNodeNum.toString()} size={180} /> <Hashicon value={hardwareInfo.myNodeNum.toString()} size={180} />
<div className="text-center text-lg font-medium dark:text-white"> <div className="text-center text-lg font-medium dark:text-white">
@ -27,7 +37,7 @@ export const Info = (): JSX.Element => {
</div> </div>
</Card> </Card>
<Card className="flex-grow"> <Card title="Debug Information" className="flex-grow">
<JSONPretty className="overflow-y-auto" data={hardwareInfo} /> <JSONPretty className="overflow-y-auto" data={hardwareInfo} />
</Card> </Card>
</div> </div>

19
src/pages/Extensions/Logs.tsx

@ -1,15 +1,16 @@
import type React from 'react'; import type React from 'react';
import { AnimatePresence, m } from 'framer-motion'; import { AnimatePresence, m } from 'framer-motion';
import { FiArrowRight, FiPaperclip } from 'react-icons/fi'; import { FiArrowRight, FiPaperclip, FiX } from 'react-icons/fi';
import { Button } from '@app/components/generic/button/Button';
import { Card } from '@app/components/generic/Card'; import { Card } from '@app/components/generic/Card';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf, Types } from '@meshtastic/meshtasticjs'; import { Protobuf, Types } from '@meshtastic/meshtasticjs';
export const Logs = (): JSX.Element => { export const Logs = (): JSX.Element => {
const logs = useAppSelector((state) => state.meshtastic.logs); const meshtasticState = useAppSelector((state) => state.meshtastic);
const darkMode = useAppSelector((state) => state.app.darkMode); const appState = useAppSelector((state) => state.app);
type lookupType = { [key: number]: string }; type lookupType = { [key: number]: string };
@ -49,14 +50,18 @@ export const Logs = (): JSX.Element => {
return ( return (
<div className="flex h-full flex-col gap-4 p-4"> <div className="flex h-full flex-col gap-4 p-4">
<Card className="flex-grow overflow-y-auto"> <Card
title="Device Logs"
actions={<Button icon={<FiX />}>Clear Logs</Button>}
className="flex-grow overflow-y-auto"
>
<table className="table-cell flex-grow"> <table className="table-cell flex-grow">
<tbody <tbody
className=" className="
block h-full flex-col overflow-y-auto font-mono text-xs dark:text-gray-400" block h-full flex-col overflow-y-auto font-mono text-xs dark:text-gray-400"
> >
<AnimatePresence> <AnimatePresence>
{logs.length === 0 && ( {meshtasticState.logs.length === 0 && (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<m.img <m.img
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@ -64,13 +69,13 @@ export const Logs = (): JSX.Element => {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="m-auto h-64 w-64 text-green-500" className="m-auto h-64 w-64 text-green-500"
src={`/placeholders/${ src={`/placeholders/${
darkMode ? 'View Code Dark.svg' : 'View Code.svg' appState.darkMode ? 'View Code Dark.svg' : 'View Code.svg'
}`} }`}
/> />
</div> </div>
)} )}
</AnimatePresence> </AnimatePresence>
{logs.map((log, index) => ( {meshtasticState.logs.map((log, index) => (
// <ContextMenu // <ContextMenu
// key={index} // key={index}
// items={ // items={

5
src/pages/Map/MapContainer.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useCallback } from 'react';
import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa'; import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa';
import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md'; import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md';
@ -23,7 +24,7 @@ export const MapContainer = (): JSX.Element => {
const { ref } = useMapbox(); const { ref } = useMapbox();
const ChangeMapStyle = React.useCallback( const ChangeMapStyle = useCallback(
(styleName: string, style: MapStyle) => { (styleName: string, style: MapStyle) => {
dispatch( dispatch(
setMapStyle( setMapStyle(

24
src/pages/Map/Marker.tsx

@ -1,13 +1,15 @@
import React from 'react'; import type React from 'react';
import ReactDOM from 'react-dom'; import { useCallback, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import mapbox from 'mapbox-gl'; import type { LngLatLike, MarkerOptions } from 'mapbox-gl';
import { Marker as MapboxMarker } from 'mapbox-gl';
import { useMapbox } from '@hooks/useMapbox'; import { useMapbox } from '@hooks/useMapbox';
export interface MarkerProps extends Omit<mapbox.MarkerOptions, 'element'> { export interface MarkerProps extends Omit<MarkerOptions, 'element'> {
children?: React.ReactNode; children?: React.ReactNode;
center: mapbox.LngLatLike; center: LngLatLike;
} }
export const Marker = ({ export const Marker = ({
@ -16,22 +18,22 @@ export const Marker = ({
...props ...props
}: MarkerProps): JSX.Element => { }: MarkerProps): JSX.Element => {
const { map } = useMapbox(); const { map } = useMapbox();
const ref = React.useRef<HTMLDivElement>(document.createElement('div')); const ref = useRef<HTMLDivElement>(document.createElement('div'));
const addMarker = React.useCallback((): void => { const addMarker = useCallback((): void => {
if (map) { if (map) {
const marker = new mapbox.Marker(ref.current, props).setLngLat(center); const marker = new MapboxMarker(ref.current, props).setLngLat(center);
marker.addTo(map); marker.addTo(map);
} }
}, [map, center, props]); }, [map, center, props]);
React.useEffect(() => { useEffect(() => {
map?.on('load', () => { map?.on('load', () => {
addMarker(); addMarker();
}); });
}, [addMarker, map]); }, [addMarker, map]);
React.useEffect(() => { useEffect(() => {
if (map?.loaded()) { if (map?.loaded()) {
addMarker(); addMarker();
} }
@ -39,5 +41,5 @@ export const Marker = ({
<div ref={ref}>{children}</div>; <div ref={ref}>{children}</div>;
return ReactDOM.createPortal(children, ref.current); return createPortal(children, ref.current);
}; };

5
src/pages/Map/index.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { FiMapPin } from 'react-icons/fi'; import { FiMapPin } from 'react-icons/fi';
@ -13,7 +14,7 @@ import { Marker } from '@pages/Map/Marker';
import { NodeCard } from '@pages/Nodes/NodeCard'; import { NodeCard } from '@pages/Nodes/NodeCard';
export const Map = (): JSX.Element => { export const Map = (): JSX.Element => {
const [selectedNode, setSelectedNode] = React.useState<Node>(); const [selectedNode, setSelectedNode] = useState<Node>();
const nodes = useAppSelector((state) => state.meshtastic.nodes); const nodes = useAppSelector((state) => state.meshtastic.nodes);
const myNodeNum = useAppSelector( const myNodeNum = useAppSelector(

4
src/pages/Messages/ChannelChat.tsx

@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import { FiSettings } from 'react-icons/fi'; import { FiSettings } from 'react-icons/fi';
@ -41,7 +41,7 @@ export const ChannelChat = ({
setSelected={(): void => { setSelected={(): void => {
setSelectedIndex(channel.index); setSelectedIndex(channel.index);
}} }}
actions={<IconButton icon={<FiSettings />} />} actions={<IconButton nested icon={<FiSettings />} />}
> >
<Tooltip <Tooltip
content={ content={

4
src/pages/Messages/DmChat.tsx

@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { FiSettings } from 'react-icons/fi'; import { FiSettings } from 'react-icons/fi';
@ -25,7 +25,7 @@ export const DmChat = ({
setSelected={(): void => { setSelected={(): void => {
setSelectedIndex(node.number); setSelectedIndex(node.number);
}} }}
actions={<IconButton icon={<FiSettings />} />} actions={<IconButton nested icon={<FiSettings />} />}
> >
<div className="flex dark:text-white"> <div className="flex dark:text-white">
<div className="m-auto"> <div className="m-auto">

16
src/pages/Messages/MessageBar.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useState } from 'react';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
@ -14,9 +15,9 @@ export const MessageBar = ({ chatIndex }: MessageBarProps): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const meshtasticState = useAppSelector((state) => state.meshtastic); const meshtasticState = useAppSelector((state) => state.meshtastic);
const [isChannel, setIsChannel] = React.useState(false); const [isChannel, setIsChannel] = useState(false);
React.useState(() => { useState(() => {
setIsChannel( setIsChannel(
meshtasticState.radio.channels.findIndex( meshtasticState.radio.channels.findIndex(
(channel) => channel.index === chatIndex, (channel) => channel.index === chatIndex,
@ -24,7 +25,7 @@ export const MessageBar = ({ chatIndex }: MessageBarProps): JSX.Element => {
); );
}); });
const [currentMessage, setCurrentMessage] = React.useState(''); const [currentMessage, setCurrentMessage] = useState('');
const sendMessage = (): void => { const sendMessage = (): void => {
if (meshtasticState.ready) { if (meshtasticState.ready) {
void connection.sendText( void connection.sendText(
@ -33,6 +34,13 @@ export const MessageBar = ({ chatIndex }: MessageBarProps): JSX.Element => {
true, true,
isChannel ? chatIndex-- : 0, isChannel ? chatIndex-- : 0,
(id) => { (id) => {
console.log(`Chat Index, ${chatIndex}`);
console.log(`Chat Index --, ${chatIndex--}`);
console.log(
`Chat Index computed, ${isChannel ? chatIndex-- : chatIndex}`,
);
dispatch( dispatch(
ackMessage({ ackMessage({
chatIndex: isChannel ? chatIndex-- : chatIndex, chatIndex: isChannel ? chatIndex-- : chatIndex,

11
src/pages/Messages/index.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import { FiHash, FiMessageCircle } from 'react-icons/fi'; import { FiHash, FiMessageCircle } from 'react-icons/fi';
@ -12,9 +13,9 @@ import { Message } from '@pages/Messages/Message';
import { MessageBar } from '@pages/Messages/MessageBar'; import { MessageBar } from '@pages/Messages/MessageBar';
export const Messages = (): JSX.Element => { export const Messages = (): JSX.Element => {
const [selectedChatIndex, setSelectedChatIndex] = React.useState<number>(0); const [selectedChatIndex, setSelectedChatIndex] = useState<number>(0);
const chatRef = React.useRef<HTMLDivElement>(null); const chatRef = useRef<HTMLDivElement>(null);
const myNodeNum = useAppSelector( const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
@ -25,7 +26,7 @@ export const Messages = (): JSX.Element => {
(state) => state.meshtastic.radio.channels, (state) => state.meshtastic.radio.channels,
).filter((ch) => ch.role !== Protobuf.Channel_Role.DISABLED); ).filter((ch) => ch.role !== Protobuf.Channel_Role.DISABLED);
React.useEffect(() => { useEffect(() => {
if (chatRef.current) { if (chatRef.current) {
chatRef.current.scrollTop = chatRef.current.scrollHeight; chatRef.current.scrollTop = chatRef.current.scrollHeight;
} }
@ -66,7 +67,7 @@ export const Messages = (): JSX.Element => {
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<div className="flex w-full justify-between border-b border-gray-300 px-2 dark:border-gray-600 dark:text-gray-300"> <div className="flex w-full justify-between border-b border-gray-300 px-2 dark:border-gray-600 dark:text-gray-300">
<div className="my-auto flex gap-2 py-2 text-sm"> <div className="my-auto flex gap-2 py-2 text-sm">
<IconButton icon={<FiHash className="h-4 w-4" />} /> <IconButton nested icon={<FiHash className="h-4 w-4" />} />
<div className="my-auto"> <div className="my-auto">
{channels.findIndex((ch) => ch.index === selectedChatIndex) !== {channels.findIndex((ch) => ch.index === selectedChatIndex) !==
-1 ? ( -1 ? (

15
src/pages/Nodes/NodeCard.tsx

@ -1,7 +1,8 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import mapbox from 'mapbox-gl'; import { LngLat } from 'mapbox-gl';
import { BiCrown } from 'react-icons/bi'; import { BiCrown } from 'react-icons/bi';
import { import {
FiAlignLeft, FiAlignLeft,
@ -40,11 +41,11 @@ export const NodeCard = ({
setSelected, setSelected,
}: NodeCardProps): JSX.Element => { }: NodeCardProps): JSX.Element => {
const { map } = useMapbox(); const { map } = useMapbox();
const [infoOpen, setInfoOpen] = React.useState(false); const [infoOpen, setInfoOpen] = useState(false);
const [PositionConfidence, setPositionConfidence] = const [PositionConfidence, setPositionConfidence] =
React.useState<PositionConfidence>('none'); useState<PositionConfidence>('none');
React.useEffect(() => { useEffect(() => {
setPositionConfidence( setPositionConfidence(
node.currentPosition node.currentPosition
? new Date(node.currentPosition.posTimestamp * 1000) > ? new Date(node.currentPosition.posTimestamp * 1000) >
@ -62,6 +63,7 @@ export const NodeCard = ({
actions={ actions={
<> <>
<IconButton <IconButton
nested
tooltip={PositionConfidence !== 'none' ? 'Fly to Node' : ''} tooltip={PositionConfidence !== 'none' ? 'Fly to Node' : ''}
disabled={PositionConfidence === 'none'} disabled={PositionConfidence === 'none'}
onClick={(e): void => { onClick={(e): void => {
@ -69,7 +71,7 @@ export const NodeCard = ({
setSelected(); setSelected();
if (PositionConfidence !== 'none' && node.currentPosition) { if (PositionConfidence !== 'none' && node.currentPosition) {
map?.flyTo({ map?.flyTo({
center: new mapbox.LngLat( center: new LngLat(
node.currentPosition.longitudeI / 1e7, node.currentPosition.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7, node.currentPosition.latitudeI / 1e7,
), ),
@ -88,6 +90,7 @@ export const NodeCard = ({
} }
/> />
<IconButton <IconButton
nested
tooltip="Show Node Info" tooltip="Show Node Info"
onClick={(e): void => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();

14
src/pages/Nodes/index.tsx

@ -1,4 +1,5 @@
import React from 'react'; import type React from 'react';
import { useEffect, useState } from 'react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import type { Edge, Node } from 'react-flow-renderer'; import type { Edge, Node } from 'react-flow-renderer';
@ -15,15 +16,15 @@ import { Hashicon } from '@emeraldpay/hashicon-react';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
export const Nodes = (): JSX.Element => { export const Nodes = (): JSX.Element => {
const [graphNodes, setGraphNodes] = React.useState<Node[]>([]); const [graphNodes, setGraphNodes] = useState<Node[]>([]);
const [graphEdges, setGraphEdges] = React.useState<Edge[]>([]); const [graphEdges, setGraphEdges] = useState<Edge[]>([]);
const [selected, setSelected] = React.useState<number>(0); const [selected, setSelected] = useState<number>(0);
const nodes = useAppSelector((state) => state.meshtastic.nodes); const nodes = useAppSelector((state) => state.meshtastic.nodes);
const myNodeNum = useAppSelector( const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware.myNodeNum, (state) => state.meshtastic.radio.hardware.myNodeNum,
); );
React.useEffect(() => { useEffect(() => {
const tmpNodes: Node[] = []; const tmpNodes: Node[] = [];
// User Terminal // User Terminal
tmpNodes.push({ tmpNodes.push({
@ -45,7 +46,7 @@ export const Nodes = (): JSX.Element => {
setGraphNodes(tmpNodes); setGraphNodes(tmpNodes);
}, [nodes, myNodeNum]); }, [nodes, myNodeNum]);
React.useEffect(() => { useEffect(() => {
const tmpEdges: Edge[] = []; const tmpEdges: Edge[] = [];
nodes.map((node, index) => { nodes.map((node, index) => {
@ -91,6 +92,7 @@ export const Nodes = (): JSX.Element => {
}} }}
actions={ actions={
<IconButton <IconButton
nested
onClick={(e): void => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();
setSelected(node.number); setSelected(node.number);

2
tailwind.config.cjs

@ -11,9 +11,7 @@ module.exports = {
extend: { extend: {
colors: { colors: {
primary: '#67ea94', primary: '#67ea94',
// primaryDark: '#1E293B',
primaryDark: '#25262C', primaryDark: '#25262C',
// secondaryDark: '#0F172A',
secondaryDark: '#1C1D23', secondaryDark: '#1C1D23',
accentDark: '25262C', accentDark: '25262C',
}, },

12
vite.config.ts

@ -1,5 +1,5 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import path from 'path'; import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer'; import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import importToCDN from 'vite-plugin-cdn-import'; import importToCDN from 'vite-plugin-cdn-import';
@ -79,11 +79,11 @@ export default defineConfig({
}, },
resolve: { resolve: {
alias: { alias: {
'@app': path.resolve(__dirname, './src'), '@app': resolve(__dirname, './src'),
'@pages': path.resolve(__dirname, './src/pages'), '@pages': resolve(__dirname, './src/pages'),
'@components': path.resolve(__dirname, './src/components'), '@components': resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'), '@hooks': resolve(__dirname, './src/hooks'),
'@core': path.resolve(__dirname, './src/core'), '@core': resolve(__dirname, './src/core'),
'@skypack/': 'https://cdn.skypack.dev/', '@skypack/': 'https://cdn.skypack.dev/',
}, },
}, },

Loading…
Cancel
Save