diff --git a/README.md b/README.md index aecd4ed2..ac27b250 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,21 @@ -# New Project +# Meshtastic.js -> ✨ Bootstrapped with Create Snowpack App (CSA). +[![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/meshtastic/meshtastic-web) -## Available Scripts +## Overview -### npm start +Official [Meshtastic](https://meshtastic.org) web interface, that can be run independently or on a node -Runs the app in the development mode. -Open http://localhost:8080 to view it in the browser. +## Development & Building -The page will reload if you make edits. -You will also see any lint errors in the console. +Build the project: -### npm run build +```bash +yarn build +``` -Builds a static copy of your site to the `build/` folder. -Your app is ready to be deployed! +GZip the output: -**For the best production performance:** Add a build bundler plugin like "@snowpack/plugin-webpack" to your `snowpack.config.js` config file. - -### npm test - -Launches the application test runner. -Run with the `--watch` flag (`npm test -- --watch`) to run in interactive watch mode. +```bash +yarn package +``` diff --git a/package.json b/package.json index 297cedfb..8bcee26e 100644 --- a/package.json +++ b/package.json @@ -13,21 +13,18 @@ "dependencies": { "@headlessui/react": "^1.3.0", "@heroicons/react": "^1.0.1", - "@meshtastic/meshtasticjs": "^0.6.13", + "@meshtastic/meshtasticjs": "^0.6.15", "@reduxjs/toolkit": "^1.6.0", "boring-avatars": "^1.5.8", "framer-motion": "^4.1.17", "i18next": "^20.3.5", "i18next-browser-languagedetector": "^6.1.2", - "observable-hooks": "^4.0.5", "react": "^18.0.0-alpha-6bf111772-20210701", "react-dom": "^18.0.0-alpha-6bf111772-20210701", "react-flags-select": "^2.1.2", "react-hook-form": "^7.9.0", "react-i18next": "^11.11.4", - "react-redux": "^7.2.4", - "redux-observable": "^2.0.0", - "rxjs": "^7.1.0" + "react-redux": "^7.2.4" }, "devDependencies": { "@snowpack/plugin-dotenv": "^2.0.5", diff --git a/src/App.tsx b/src/App.tsx index 7f5d2821..ba02d847 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,44 +1,29 @@ import React from 'react'; -import type { - IBLEConnection, - ISerialConnection, -} from '@meshtastic/meshtasticjs'; -import { - Client, - IHTTPConnection, - Protobuf, - SettingsManager, - Types, -} from '@meshtastic/meshtasticjs'; +import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs'; import { Header } from './components/Header'; +import { connection } from './connection'; import { useAppDispatch } from './hooks/redux'; import { Main } from './Main'; -import { setMyId } from './slices/meshtasticSlice'; -import { channelSubject$, nodeSubject$, preferencesSubject$ } from './streams'; +import { + addChannel, + addNode, + setDeviceStatus, + setLastMeshInterraction, + setMyId, + setMyNodeInfo, + setPreferences, + setReady, +} from './slices/meshtasticSlice'; const App = (): JSX.Element => { const dispatch = useAppDispatch(); - const [deviceStatus, setDeviceStatus] = - React.useState( - Types.DeviceStatusEnum.DEVICE_DISCONNECTED, - ); - const [connection, setConnection] = React.useState< - ISerialConnection | IHTTPConnection | IBLEConnection - >(new IHTTPConnection()); - const [isReady, setIsReady] = React.useState(false); - const [lastMeshInterraction, setLastMeshInterraction] = - React.useState(0); - const [darkmode, setDarkmode] = React.useState(false); - React.useEffect(() => { - const client = new Client(); - const httpConnection = client.createHTTPConnection(); SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE; - httpConnection.connect({ + connection.connect({ address: import.meta.env.NODE_ENV === 'production' ? window.location.hostname @@ -47,77 +32,52 @@ const App = (): JSX.Element => { tls: false, fetchInterval: 2000, }); - setConnection(httpConnection); }, []); React.useEffect(() => { - const deviceStatusEvent = connection.onDeviceStatusEvent.subscribe( - (status) => { - setDeviceStatus(status); - if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) { - setIsReady(true); - } - }, - ); - // const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo); + connection.onDeviceStatus.subscribe((status) => { + dispatch(setDeviceStatus(status)); - const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe( - (nodeInfo) => { - dispatch(setMyId(nodeInfo.myNodeNum)); - }, + if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) { + dispatch(setReady(true)); + } + }); + + connection.onMyNodeInfo.subscribe((nodeInfo) => { + dispatch(setMyNodeInfo(nodeInfo)); + dispatch(setMyId(nodeInfo.myNodeNum)); + }); + + connection.onNodeInfoPacket.subscribe((nodeInfoPacket) => + dispatch(addNode(nodeInfoPacket.data)), ); - const nodeInfoPacketEvent = connection.onNodeInfoPacketEvent.subscribe( - (node) => nodeSubject$.next(node), - ); + connection.onAdminPacket.subscribe((adminPacket) => { + switch (adminPacket.data.variant.oneofKind) { + case 'getChannelResponse': + dispatch(addChannel(adminPacket.data.variant.getChannelResponse)); + break; + case 'getRadioResponse': + if (adminPacket.data.variant.getRadioResponse.preferences) { + dispatch( + setPreferences( + adminPacket.data.variant.getRadioResponse.preferences, + ), + ); + } + break; + } + }); - const adminPacketEvent = connection.onAdminPacketEvent.subscribe( - (adminMessage) => { - switch (adminMessage.data.variant.oneofKind) { - case 'getChannelResponse': - channelSubject$.next(adminMessage.data.variant.getChannelResponse); - break; - case 'getRadioResponse': - if (adminMessage.data.variant.getRadioResponse.preferences) { - preferencesSubject$.next( - adminMessage.data.variant.getRadioResponse.preferences, - ); - } - break; - default: - break; - } - }, + connection.onMeshHeartbeat.subscribe((date) => + dispatch(setLastMeshInterraction(date.getTime())), ); - - const meshHeartbeat = connection.onMeshHeartbeat.subscribe( - setLastMeshInterraction, - ); - - return () => { - deviceStatusEvent?.unsubscribe(); - myNodeInfoEvent?.unsubscribe(); - nodeInfoPacketEvent?.unsubscribe(); - adminPacketEvent?.unsubscribe(); - meshHeartbeat?.unsubscribe(); - connection.disconnect(); - }; - }, [connection]); + }, [dispatch]); return (
-
-
+
+
); }; diff --git a/src/Main.tsx b/src/Main.tsx index eca57ba5..8b9acb89 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -2,62 +2,44 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import type { - IBLEConnection, - IHTTPConnection, - ISerialConnection, - Types, -} from '@meshtastic/meshtasticjs'; +import type { Types } from '@meshtastic/meshtasticjs'; import { ChatMessage } from './components/ChatMessage'; import { MessageBox } from './components/MessageBox'; import { Sidebar } from './components/Sidebar'; +import { connection } from './connection'; -interface MainProps { - connection: ISerialConnection | IHTTPConnection | IBLEConnection; - isReady: boolean; - darkmode: boolean; - setDarkmode: React.Dispatch>; -} - -export const Main = (props: MainProps): JSX.Element => { +export const Main = (): JSX.Element => { const [messages, setMessages] = React.useState< { message: Types.TextPacket; ack: boolean }[] >([]); const { t } = useTranslation(); React.useEffect(() => { - const textPacketEvent = props.connection.onTextPacketEvent.subscribe( - (message) => { - setMessages((messages) => [ - ...messages, - { message: message, ack: false }, - ]); - }, - ); - return () => textPacketEvent?.unsubscribe(); - }, [props.connection]); + connection.onTextPacket.subscribe((message) => { + setMessages((messages) => [ + ...messages, + { message: message, ack: false }, + ]); + }); + }, []); React.useEffect(() => { - const routingPacketEvent = props.connection.onRoutingPacketEvent.subscribe( - (routingPacket) => { - setMessages( - messages.map((message) => { - return routingPacket.packet.payloadVariant.oneofKind === - 'decoded' && - message.message.packet.id === - routingPacket.packet.payloadVariant.decoded.requestId - ? { - ack: true, - message: message.message, - } - : message; - }), - ); - }, - ); - return () => routingPacketEvent?.unsubscribe(); - }, [props.connection, messages]); + connection.onRoutingPacket.subscribe((routingPacket) => { + setMessages( + messages.map((message) => { + return routingPacket.packet.payloadVariant.oneofKind === 'decoded' && + message.message.packet.id === + routingPacket.packet.payloadVariant.decoded.requestId + ? { + ack: true, + message: message.message, + } + : message; + }), + ); + }); + }, [messages]); return (
@@ -73,9 +55,9 @@ export const Main = (props: MainProps): JSX.Element => {
)} - + - + ); }; diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index 882f47dc..465f690b 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -1,7 +1,6 @@ -import React, { useState } from 'react'; +import React from 'react'; import Avatar from 'boring-avatars'; -import { useObservableSuspense } from 'observable-hooks'; import { CheckCircleIcon, @@ -10,47 +9,24 @@ import { import type { Types } from '@meshtastic/meshtasticjs'; import { useAppSelector } from '../hooks/redux'; -import { nodeResource } from '../streams'; interface ChatMessageProps { message: { message: Types.TextPacket; ack: boolean }; } export const ChatMessage = (props: ChatMessageProps): JSX.Element => { - const nodeSource = useObservableSuspense(nodeResource); const myId = useAppSelector((state) => state.meshtastic.myId); + const nodes = useAppSelector((state) => state.meshtastic.nodes); - const [nodes, setNodes] = React.useState([]); + const node = nodes.find((node) => { + node.num === props.message.message.packet.from; + }); - React.useEffect(() => { - if ( - nodes.findIndex( - (currentNode) => currentNode.data.num === nodeSource.data.num, - ) >= 0 - ) { - setNodes( - nodes.map((currentNode) => - currentNode.data.num === nodeSource.data.num - ? nodeSource - : currentNode, - ), - ); - } else { - setNodes((nodes) => [...nodes, nodeSource]); - } - }, [nodeSource, nodes]); - const [node, setNode] = useState(); - - React.useEffect(() => { - setNode( - nodes.find((node) => node.data.num === props.message.message.packet.from), - ); - }, [nodes, props.message]); return (
@@ -70,9 +46,7 @@ export const ChatMessage = (props: ChatMessageProps): JSX.Element => { }`} >
-
- {node?.data.user?.longName ?? 'UNK'} -
+
{node?.user?.longName ?? 'UNK'}

-

{new Date( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9c70ac62..22498c2f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -5,23 +5,18 @@ import { StatusOfflineIcon, StatusOnlineIcon, } from '@heroicons/react/outline'; -import type { - IBLEConnection, - IHTTPConnection, - ISerialConnection, -} from '@meshtastic/meshtasticjs'; import { Types } from '@meshtastic/meshtasticjs'; +import { useAppSelector } from '../hooks/redux'; import { Logo } from './Logo'; -interface HeaderProps { - status: Types.DeviceStatusEnum; - IsReady: boolean; - LastMeshInterraction: number; - connection: IHTTPConnection | ISerialConnection | IBLEConnection; -} +export const Header = (): JSX.Element => { + const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus); + const ready = useAppSelector((state) => state.meshtastic.ready); + const lastMeshInterraction = useAppSelector( + (state) => state.meshtastic.lastMeshInterraction, + ); -export const Header = (props: HeaderProps): JSX.Element => { return (