Browse Source

WIP

pull/1/head
Sacha Weatherstone 5 years ago
parent
commit
58de82b8d7
  1. 22
      .eslintrc
  2. 29
      package.json
  3. 71
      src/App.tsx
  4. 14
      src/Main.tsx
  5. 19
      src/components/ChatMessage.tsx
  6. 76
      src/components/Header.tsx
  7. 12
      src/components/MessageBox.tsx
  8. 6
      src/components/Sidebar.tsx
  9. 2
      src/components/Sidebar/Channels/Channel.tsx
  10. 2
      src/components/Sidebar/Channels/Index.tsx
  11. 10
      src/components/Sidebar/Device/Index.tsx
  12. 50
      src/components/Sidebar/Device/Settings.tsx
  13. 2
      src/components/Sidebar/Nodes/Index.tsx
  14. 2
      src/components/Sidebar/Nodes/Node.tsx
  15. 2
      src/components/Sidebar/UI/Index.tsx
  16. 5
      src/components/Sidebar/UI/Translations.tsx
  17. 3
      src/components/basic/ToggleSwitch.tsx
  18. 2
      src/components/logo.tsx
  19. 24
      src/index.tsx
  20. 3263
      yarn-error.log
  21. 1010
      yarn.lock

22
.eslintrc

@ -0,0 +1,22 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"plugin:react/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:prettier/recommended"
],
"rules": {
"@typescript-eslint/consistent-type-imports": "error"
},
"settings": {
"react": {
"version": "detect"
}
}
}

29
package.json

@ -7,36 +7,45 @@
"start": "NODE_ENV=development snowpack dev",
"build": "snowpack build",
"package": "yarn gzipper c -i html,js,css build build/output",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\""
"format": "prettier --write 'src/**/*.{ts,tsx}'",
"lint": "eslint 'src/**/*.{ts,tsx}'"
},
"dependencies": {
"@headlessui/react": "^1.2.0",
"@heroicons/react": "^1.0.1",
"@meshtastic/meshtasticjs": "^0.6.12",
"observable-hooks": "^4.0.3",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react": "^0.0.0-experimental-132b72d7b",
"react-dom": "^0.0.0-experimental-132b72d7b",
"react-flags-select": "^2.1.2",
"react-hook-form": "^7.5.2",
"react-hook-form": "^7.6.5",
"react-json-pretty": "^2.2.0",
"rxjs": "^7.0.1",
"yarn": "^1.22.10"
},
"devDependencies": {
"@snowpack/plugin-dotenv": "^2.0.5",
"@snowpack/plugin-postcss": "^1.2.2",
"@snowpack/plugin-postcss": "^1.4.0",
"@snowpack/plugin-react-refresh": "^2.5.0",
"@snowpack/plugin-typescript": "^1.2.0",
"@types/react": "^17.0.5",
"@types/react-dom": "^17.0.4",
"@types/eslint": "^7.2.11",
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
"@types/snowpack-env": "^2.3.2",
"@typescript-eslint/eslint-plugin": "^4.24.0",
"@typescript-eslint/parser": "^4.24.0",
"autoprefixer": "^10.2.5",
"eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"gzipper": "^4.5.0",
"postcss": "^8.2.15",
"postcss": "^8.3.0",
"postcss-cli": "^8.3.1",
"prettier": "^2.3.0",
"snowpack": "^3.3.7",
"snowpack": "^3.5.1",
"tailwindcss": "^2.1.2",
"typescript": "^4.2.4"
}

71
src/App.tsx

@ -1,8 +1,12 @@
import React from 'react';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import {
Client,
IHTTPConnection,
Protobuf,
SettingsManager,
Types,
@ -36,33 +40,27 @@ export interface languageTemplate {
no_message_placeholder: string;
}
const App = () => {
const [
deviceStatus,
setDeviceStatus,
] = React.useState<Types.DeviceStatusEnum>(
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
);
const App = (): JSX.Element => {
const [deviceStatus, setDeviceStatus] =
React.useState<Types.DeviceStatusEnum>(
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
);
const [myNodeInfo, setMyNodeInfo] = React.useState<Protobuf.MyNodeInfo>(
Protobuf.MyNodeInfo.create(),
);
const [channels, setChannels] = React.useState([] as Protobuf.Channel[]);
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
const [connection, setConnection] = React.useState<IHTTPConnection>(
new IHTTPConnection(),
);
const [connection, setConnection] =
React.useState<ISerialConnection | IHTTPConnection | IBLEConnection>();
const [isReady, setIsReady] = React.useState<boolean>(false);
const [
lastMeshInterraction,
setLastMeshInterraction,
] = React.useState<number>(0);
const [lastMeshInterraction, setLastMeshInterraction] =
React.useState<number>(0);
const [language, setLanguage] = React.useState<LanguageEnum>(
LanguageEnum.ENGLISH,
);
const [translations, setTranslations] = React.useState<languageTemplate>(
Translations_English,
);
const [translations, setTranslations] =
React.useState<languageTemplate>(Translations_English);
const [darkmode, setDarkmode] = React.useState<boolean>(false);
React.useEffect(() => {
@ -97,8 +95,12 @@ const App = () => {
});
setConnection(connection);
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
}, []);
const deviceStatusEvent = connection.onDeviceStatusEvent.subscribe(
React.useEffect(() => {
console.log('UPDATING');
const deviceStatusEvent = connection?.onDeviceStatusEvent.subscribe(
(status) => {
setDeviceStatus(status);
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
@ -106,11 +108,10 @@ const App = () => {
}
},
);
const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(
setMyNodeInfo,
);
const myNodeInfoEvent =
connection?.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
const nodeInfoPacketEvent = connection.onNodeInfoPacketEvent.subscribe(
const nodeInfoPacketEvent = connection?.onNodeInfoPacketEvent.subscribe(
(node) => {
if (
nodes.findIndex(
@ -128,34 +129,34 @@ const App = () => {
},
);
const adminPacketEvent = connection.onAdminPacketEvent.subscribe(
const adminPacketEvent = connection?.onAdminPacketEvent.subscribe(
(adminMessage) => {
switch (adminMessage.data.variant.oneofKind) {
case 'getChannelResponse':
if (adminMessage.data.variant.getChannelResponse) {
let message = adminMessage.data.variant.getChannelResponse;
const message = adminMessage.data.variant.getChannelResponse;
setChannels((channels) => [...channels, message]);
}
break;
default:
break;
}
},
);
const meshHeartbeat = connection.onMeshHeartbeat.subscribe(
const meshHeartbeat = connection?.onMeshHeartbeat.subscribe(
setLastMeshInterraction,
);
return () => {
deviceStatusEvent.unsubscribe();
myNodeInfoEvent.unsubscribe();
nodeInfoPacketEvent.unsubscribe();
adminPacketEvent.unsubscribe();
meshHeartbeat.unsubscribe();
connection.disconnect();
deviceStatusEvent?.unsubscribe();
myNodeInfoEvent?.unsubscribe();
nodeInfoPacketEvent?.unsubscribe();
adminPacketEvent?.unsubscribe();
meshHeartbeat?.unsubscribe();
connection?.disconnect();
};
}, []);
}, [connection, nodes]);
return (
<div className="flex flex-col h-screen w-screen">
@ -163,6 +164,8 @@ const App = () => {
status={deviceStatus}
IsReady={isReady}
LastMeshInterraction={lastMeshInterraction}
connection={connection}
setConnection={setConnection}
/>
<Main
isReady={isReady}

14
src/Main.tsx

@ -1,7 +1,9 @@
import React, { useState } from 'react';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
Types,
} from '@meshtastic/meshtasticjs';
@ -12,7 +14,7 @@ import MessageBox from './components/MessageBox';
import Sidebar from './components/Sidebar';
interface MainProps {
connection: IHTTPConnection;
connection?: ISerialConnection | IHTTPConnection | IBLEConnection;
myNodeInfo: Protobuf.MyNodeInfo;
nodes: Types.NodeInfoPacket[];
channels: Protobuf.Channel[];
@ -24,14 +26,14 @@ interface MainProps {
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
const Main = (props: MainProps) => {
const Main = (props: MainProps): JSX.Element => {
const [messages, setMessages] = React.useState<
{ message: Types.TextPacket; ack: boolean }[]
>([]);
const [sidebarOpen, setSidebarOpen] = useState<boolean>(false);
React.useEffect(() => {
const textPacketEvent = props.connection.onTextPacketEvent.subscribe(
const textPacketEvent = props.connection?.onTextPacketEvent.subscribe(
(message) => {
setMessages((messages) => [
...messages,
@ -39,11 +41,11 @@ const Main = (props: MainProps) => {
]);
},
);
return () => textPacketEvent.unsubscribe();
return () => textPacketEvent?.unsubscribe();
}, [props.connection]);
React.useEffect(() => {
const routingPacketEvent = props.connection.onRoutingPacketEvent.subscribe(
const routingPacketEvent = props.connection?.onRoutingPacketEvent.subscribe(
(routingPacket) => {
setMessages(
messages.map((message) => {
@ -60,7 +62,7 @@ const Main = (props: MainProps) => {
);
},
);
return () => routingPacketEvent.unsubscribe();
return () => routingPacketEvent?.unsubscribe();
}, [props.connection, messages]);
return (

19
src/components/ChatMessage.tsx

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
CheckCircleIcon,
@ -13,7 +13,16 @@ interface ChatMessageProps {
nodes: Types.NodeInfoPacket[];
}
const ChatMessage = (props: ChatMessageProps) => {
const ChatMessage = (props: ChatMessageProps): JSX.Element => {
const [node, setNode] = useState<Types.NodeInfoPacket>();
React.useEffect(() => {
setNode(
props.nodes.find(
(node) => node.data.num === props.message.message.packet.from,
),
);
}, [props.nodes, props.message]);
return (
<div className="flex items-end">
<div
@ -35,11 +44,7 @@ const ChatMessage = (props: ChatMessageProps) => {
>
<div className="flex text-xs text-gray-500 space-x-1">
<div className="font-medium">
{/* {
props.nodes.find(
(node) => node.data.num === props.message.message.packet.from,
).data.user.longName
} */}
{node?.data.user?.longName ?? 'UNK'}
</div>
<p>-</p>
<div className="underline">

76
src/components/Header.tsx

@ -1,11 +1,19 @@
import React from 'react';
import {
ChipIcon,
DeviceMobileIcon,
RssIcon,
StatusOfflineIcon,
StatusOnlineIcon,
WifiIcon,
} from '@heroicons/react/outline';
import { Types } from '@meshtastic/meshtasticjs';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Types,
} from '@meshtastic/meshtasticjs';
import Logo from './logo';
@ -13,17 +21,75 @@ interface HeaderProps {
status: Types.DeviceStatusEnum;
IsReady: boolean;
LastMeshInterraction: number;
connection?: IHTTPConnection | ISerialConnection | IBLEConnection;
setConnection: React.Dispatch<
React.SetStateAction<
IHTTPConnection | ISerialConnection | IBLEConnection | undefined
>
>;
}
const Header = (props: HeaderProps) => {
const Header = (props: HeaderProps): JSX.Element => {
const [activeConnection, setActiveConnection] =
React.useState<'http' | 'serial' | 'ble'>('http');
return (
<nav className="w-full shadow-md">
<div className="flex w-full container mx-auto justify-between px-6 py-4">
<Logo />
<div></div>
<div className="flex items-center">
<div className="flex pl-4">
<div className="flex space-x-2 items-center">
<button
className={`rounded-md px-3 py-2 ${
activeConnection === 'serial' ? 'bg-green-300' : 'bg-gray-300'
}`}
onClick={() => {
props.connection?.disconnect();
const connection = new ISerialConnection();
connection.connect({});
setActiveConnection('serial');
props.setConnection(connection);
}}
>
<ChipIcon className="m-auto h-5 w-5" />
</button>
<button
className={`rounded-md px-3 py-2 ${
activeConnection === 'http' ? 'bg-green-300' : 'bg-gray-300'
}`}
onClick={() => {
props.connection?.disconnect();
const connection = new IHTTPConnection();
connection.connect({
address:
import.meta.env.NODE_ENV === 'production'
? window.location.hostname
: import.meta.env.SNOWPACK_PUBLIC_DEVICE_IP,
receiveBatchRequests: false,
tls: false,
fetchInterval: 2000,
});
setActiveConnection('http');
props.setConnection(connection);
}}
>
<WifiIcon className="m-auto h-5 w-5" />
</button>
<button
className={`rounded-md px-3 py-2 ${
activeConnection === 'ble' ? 'bg-green-300' : 'bg-gray-300'
}`}
onClick={() => {
props.connection?.disconnect();
const connection = new IBLEConnection();
connection.connect({});
setActiveConnection('ble');
props.setConnection(connection);
}}
>
<RssIcon className="m-auto h-5 w-5" />
</button>
<div className="flex pl-2">
<div
className={`w-5 h-5 rounded-full ${
new Date(props.LastMeshInterraction) <
@ -43,7 +109,7 @@ const Header = (props: HeaderProps) => {
)}
</div>
<div className="flex pl-4">
<div className="flex pl-2">
<div
className={`w-5 h-5 rounded-full ${
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED

12
src/components/MessageBox.tsx

@ -1,7 +1,11 @@
import React from 'react';
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
import type { IHTTPConnection } from '@meshtastic/meshtasticjs';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../App';
@ -9,15 +13,15 @@ export interface MessageBoxProps {
translations: languageTemplate;
sidebarOpen: boolean;
setSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
connection: IHTTPConnection;
connection?: ISerialConnection | IHTTPConnection | IBLEConnection;
isReady: boolean;
}
const MessageBox = (props: MessageBoxProps) => {
const MessageBox = (props: MessageBoxProps): JSX.Element => {
const [currentMessage, setCurrentMessage] = React.useState('');
const sendMessage = () => {
if (props.isReady) {
props.connection.sendText(currentMessage, undefined, true);
props.connection?.sendText(currentMessage, undefined, true);
setCurrentMessage('');
}
};

6
src/components/Sidebar.tsx

@ -1,7 +1,9 @@
import React from 'react';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
Types,
} from '@meshtastic/meshtasticjs';
@ -16,7 +18,7 @@ interface SidebarProps {
isReady: boolean;
nodes: Types.NodeInfoPacket[];
channels: Protobuf.Channel[];
connection: IHTTPConnection;
connection?: ISerialConnection | IHTTPConnection | IBLEConnection;
language: LanguageEnum;
setLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
translations: languageTemplate;
@ -26,7 +28,7 @@ interface SidebarProps {
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
const Sidebar = (props: SidebarProps) => {
const Sidebar = (props: SidebarProps): JSX.Element => {
return (
<div
className={`${

2
src/components/Sidebar/Channels/Channel.tsx

@ -8,7 +8,7 @@ export interface ChannelProps {
channel: Protobuf.Channel;
}
const Channel = (props: ChannelProps) => {
const Channel = (props: ChannelProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (

2
src/components/Sidebar/Channels/Index.tsx

@ -17,7 +17,7 @@ export interface ChannelsProps {
translations: languageTemplate;
}
const Channels = (props: ChannelsProps) => {
const Channels = (props: ChannelsProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (

10
src/components/Sidebar/Device/Index.tsx

@ -6,18 +6,22 @@ import {
ChevronDownIcon,
ChevronRightIcon,
} from '@heroicons/react/outline';
import type { IHTTPConnection } from '@meshtastic/meshtasticjs';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../../App';
import Settings from './Settings';
interface DeviceProps {
isReady: boolean;
connection: IHTTPConnection;
connection?: ISerialConnection | IHTTPConnection | IBLEConnection;
translations: languageTemplate;
}
const Device = (props: DeviceProps) => {
const Device = (props: DeviceProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (

50
src/components/Sidebar/Device/Settings.tsx

@ -1,27 +1,27 @@
import React from 'react';
import { ObservableResource, useObservableSuspense } from 'observable-hooks';
import { useForm } from 'react-hook-form';
import JSONPretty from 'react-json-pretty';
import type { languageTemplate } from 'src/App';
import { SaveIcon } from '@heroicons/react/outline';
import { IHTTPConnection, Protobuf } from '@meshtastic/meshtasticjs';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { Protobuf } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../../../src/App';
export interface SettingsProps {
isReady: boolean;
connection: IHTTPConnection;
connection?: ISerialConnection | IHTTPConnection | IBLEConnection;
translations: languageTemplate;
}
const Settings = (props: SettingsProps) => {
const Settings = (props: SettingsProps): JSX.Element => {
React.useEffect(() => {
const a = useObservableSuspense(
new ObservableResource(
props.connection.onAdminPacketEvent.asObservable(),
),
);
const adminPacketEvent = props.connection.onAdminPacketEvent.subscribe(
const adminPacketEvent = props.connection?.onAdminPacketEvent.subscribe(
(adminMessage) => {
switch (adminMessage.data.variant.oneofKind) {
case 'getRadioResponse':
@ -34,35 +34,23 @@ const Settings = (props: SettingsProps) => {
},
);
return () => adminPacketEvent.unsubscribe();
return () => adminPacketEvent?.unsubscribe();
}, []);
const [
preferences,
setPreferences,
] = React.useState<Protobuf.RadioConfig_UserPreferences>();
const {
register,
setValue,
handleSubmit,
formState: { errors },
} = useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const [preferences, setPreferences] =
React.useState<Protobuf.RadioConfig_UserPreferences>();
const { register, handleSubmit } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const onSubmit = handleSubmit((data) => console.log(data));
return (
<form onSubmit={onSubmit}>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{props.translations.device_region_title}</div>
<div className="flex shadow-md rounded-md ml-2">
<select
value={preferences.region ?? Protobuf.RegionCode.Unset}
// onChange={(e) => {
// preferences.region = parseInt(e.target.value);
// }}
>
<select value={preferences?.region ?? Protobuf.RegionCode.Unset}>
<option value={Protobuf.RegionCode.ANZ}>
{Protobuf.RegionCode[Protobuf.RegionCode.ANZ]}
</option>

2
src/components/Sidebar/Nodes/Index.tsx

@ -18,7 +18,7 @@ interface NodesProps {
myId: number;
}
const Nodes = (props: NodesProps) => {
const Nodes = (props: NodesProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (

2
src/components/Sidebar/Nodes/Node.tsx

@ -17,7 +17,7 @@ export interface NodeProps {
myId: number;
}
const Node = (props: NodeProps) => {
const Node = (props: NodeProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (

2
src/components/Sidebar/UI/Index.tsx

@ -18,7 +18,7 @@ interface UIProps {
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
const UI = (props: UIProps) => {
const UI = (props: UIProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (

5
src/components/Sidebar/UI/Translations.tsx

@ -5,7 +5,8 @@ import { Br, Jp, Us } from 'react-flags-select';
import { Disclosure } from '@headlessui/react';
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/outline';
import { LanguageEnum, languageTemplate } from '../../../App';
import type { languageTemplate } from '../../../App';
import { LanguageEnum } from '../../../App';
export interface TranslationsProps {
language: LanguageEnum;
@ -13,7 +14,7 @@ export interface TranslationsProps {
translations: languageTemplate;
}
const Translations = (props: TranslationsProps) => {
const Translations = (props: TranslationsProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (

3
src/components/basic/ToggleSwitch.tsx

@ -4,10 +4,9 @@ import { Switch } from '@headlessui/react';
interface ToggleSwitchProps {
active: boolean;
toggle?: Function;
}
const ToggleSwitch = (props: ToggleSwitchProps) => {
const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => {
const [active, setActive] = React.useState(false);
React.useEffect(() => {

2
src/components/logo.tsx

@ -1,6 +1,6 @@
import React from 'react';
const Logo = () => {
const Logo = (): JSX.Element => {
return (
<svg
height="30"

24
src/index.tsx

@ -1,17 +1,25 @@
/// <reference types="react/experimental" />
/// <reference types="react-dom/experimental" />
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
const element = document.getElementById('root');
if (element) {
ReactDOM.createRoot(element).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://snowpack.dev/concepts/hot-module-replacement
// Learn more: https://www.snowpack.dev/#hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept();
}

3263
yarn-error.log

File diff suppressed because it is too large

1010
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save