mirror of
https://github.com/meshtastic/web.git
synced 2026-04-21 22:40:01 -04:00
WIP
This commit is contained in:
30
README.md
30
README.md
@@ -1,25 +1,21 @@
|
||||
# New Project
|
||||
# Meshtastic.js
|
||||
|
||||
> ✨ Bootstrapped with Create Snowpack App (CSA).
|
||||
[](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
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
136
src/App.tsx
136
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>(
|
||||
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
|
||||
);
|
||||
const [connection, setConnection] = React.useState<
|
||||
ISerialConnection | IHTTPConnection | IBLEConnection
|
||||
>(new IHTTPConnection());
|
||||
const [isReady, setIsReady] = React.useState<boolean>(false);
|
||||
const [lastMeshInterraction, setLastMeshInterraction] =
|
||||
React.useState<number>(0);
|
||||
const [darkmode, setDarkmode] = React.useState<boolean>(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 (
|
||||
<div className="flex flex-col h-screen w-screen">
|
||||
<Header
|
||||
status={deviceStatus}
|
||||
IsReady={isReady}
|
||||
LastMeshInterraction={lastMeshInterraction}
|
||||
connection={connection}
|
||||
/>
|
||||
<Main
|
||||
isReady={isReady}
|
||||
connection={connection}
|
||||
darkmode={darkmode}
|
||||
setDarkmode={setDarkmode}
|
||||
/>
|
||||
<Header />
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
72
src/Main.tsx
72
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<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col md:flex-row flex-grow m-3 space-y-2 md:space-y-0 space-x-0 md:space-x-2">
|
||||
@@ -73,9 +55,9 @@ export const Main = (props: MainProps): JSX.Element => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<MessageBox connection={props.connection} isReady={props.isReady} />
|
||||
<MessageBox />
|
||||
</div>
|
||||
<Sidebar connection={props.connection} />
|
||||
<Sidebar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Types.NodeInfoPacket[]>([]);
|
||||
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<Types.NodeInfoPacket>();
|
||||
|
||||
React.useEffect(() => {
|
||||
setNode(
|
||||
nodes.find((node) => node.data.num === props.message.message.packet.from),
|
||||
);
|
||||
}, [nodes, props.message]);
|
||||
return (
|
||||
<div className="flex items-end">
|
||||
<Avatar
|
||||
size={40}
|
||||
name={node?.data.user?.longName ?? 'UNK'}
|
||||
name={node?.user?.longName ?? 'UNK'}
|
||||
variant="beam"
|
||||
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
|
||||
/>
|
||||
@@ -70,9 +46,7 @@ export const ChatMessage = (props: ChatMessageProps): JSX.Element => {
|
||||
}`}
|
||||
>
|
||||
<div className="flex text-xs text-gray-500 space-x-1">
|
||||
<div className="font-medium">
|
||||
{node?.data.user?.longName ?? 'UNK'}
|
||||
</div>
|
||||
<div className="font-medium">{node?.user?.longName ?? 'UNK'}</div>
|
||||
<p>-</p>
|
||||
<div className="underline">
|
||||
{new Date(
|
||||
|
||||
@@ -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 (
|
||||
<nav className="select-none w-full shadow-md">
|
||||
<div className="flex w-full container mx-auto justify-between px-6 py-4">
|
||||
@@ -32,17 +27,15 @@ export const Header = (props: HeaderProps): JSX.Element => {
|
||||
<div className="flex">
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full ${
|
||||
new Date(props.LastMeshInterraction) <
|
||||
new Date(Date.now() - 40000)
|
||||
new Date(lastMeshInterraction) < new Date(Date.now() - 40000)
|
||||
? 'bg-red-400 animate-pulse'
|
||||
: new Date(props.LastMeshInterraction) <
|
||||
: new Date(lastMeshInterraction) <
|
||||
new Date(Date.now() - 20000)
|
||||
? 'bg-yellow-400 animate-pulse'
|
||||
: 'bg-green-400'
|
||||
}`}
|
||||
></div>
|
||||
{new Date(props.LastMeshInterraction) >
|
||||
new Date(Date.now() - 40000) ? (
|
||||
{new Date(lastMeshInterraction) > new Date(Date.now() - 40000) ? (
|
||||
<StatusOnlineIcon className="m-auto ml-1 h-5 w-5" />
|
||||
) : (
|
||||
<StatusOfflineIcon className="m-auto ml-1 h-5 w-5" />
|
||||
@@ -52,12 +45,12 @@ export const Header = (props: HeaderProps): JSX.Element => {
|
||||
<div className="flex">
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full ${
|
||||
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
|
||||
deviceStatus <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
|
||||
? 'bg-red-400 animate-pulse'
|
||||
: props.status <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
|
||||
!props.IsReady
|
||||
: deviceStatus <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
|
||||
!ready
|
||||
? 'bg-yellow-400 animate-pulse'
|
||||
: props.IsReady
|
||||
: ready
|
||||
? 'bg-green-400'
|
||||
: 'bg-gray-400'
|
||||
}`}
|
||||
|
||||
@@ -3,25 +3,17 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
|
||||
import type {
|
||||
IBLEConnection,
|
||||
IHTTPConnection,
|
||||
ISerialConnection,
|
||||
} from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { useAppDispatch } from '../hooks/redux';
|
||||
import { connection } from '../connection';
|
||||
import { useAppDispatch, useAppSelector } from '../hooks/redux';
|
||||
import { toggleSidebar } from '../slices/appSlice';
|
||||
|
||||
export interface MessageBoxProps {
|
||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||
isReady: boolean;
|
||||
}
|
||||
|
||||
export const MessageBox = (props: MessageBoxProps): JSX.Element => {
|
||||
export const MessageBox = (): JSX.Element => {
|
||||
const ready = useAppSelector((state) => state.meshtastic.ready);
|
||||
const [currentMessage, setCurrentMessage] = React.useState('');
|
||||
const sendMessage = () => {
|
||||
if (props.isReady) {
|
||||
props.connection.sendText(currentMessage, undefined, true);
|
||||
if (ready) {
|
||||
connection.sendText(currentMessage, undefined, true);
|
||||
setCurrentMessage('');
|
||||
}
|
||||
};
|
||||
@@ -45,24 +37,24 @@ export const MessageBox = (props: MessageBoxProps): JSX.Element => {
|
||||
sendMessage();
|
||||
}}
|
||||
>
|
||||
{props.isReady}
|
||||
{ready}
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`${t('placeholder.no_messages')}...`}
|
||||
disabled={!props.isReady}
|
||||
disabled={!ready}
|
||||
value={currentMessage}
|
||||
onChange={(e) => {
|
||||
setCurrentMessage(e.target.value);
|
||||
}}
|
||||
className={`p-3 placeholder-gray-400 text-gray-700 relative rounded-md border shadow-md focus:outline-none w-full pr-10 ${
|
||||
props.isReady ? 'cursor-text' : 'cursor-not-allowed'
|
||||
ready ? 'cursor-text' : 'cursor-not-allowed'
|
||||
}`}
|
||||
/>
|
||||
<span className="flex z-10 h-full text-gray-400 absolute w-8 right-0">
|
||||
<PaperAirplaneIcon
|
||||
onClick={sendMessage}
|
||||
className={`text-xl hover:text-gray-500 h-6 w-6 my-auto ${
|
||||
props.isReady ? 'cursor-pointer' : 'cursor-not-allowed'
|
||||
ready ? 'cursor-pointer' : 'cursor-not-allowed'
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import type {
|
||||
IBLEConnection,
|
||||
IHTTPConnection,
|
||||
ISerialConnection,
|
||||
} from '@meshtastic/meshtasticjs';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import { useAppSelector } from '../hooks/redux';
|
||||
import { Channels } from './Sidebar/Channels/Index';
|
||||
@@ -12,23 +8,24 @@ import { Device } from './Sidebar/Device/Index';
|
||||
import { Nodes } from './Sidebar/Nodes/Index';
|
||||
import { UI } from './Sidebar/UI/Index';
|
||||
|
||||
interface SidebarProps {
|
||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||
}
|
||||
|
||||
export const Sidebar = (props: SidebarProps): JSX.Element => {
|
||||
export const Sidebar = (): JSX.Element => {
|
||||
const sidebarOpen = useAppSelector((state) => state.app.sidebarOpen);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
sidebarOpen ? 'flex' : 'hidden md:flex'
|
||||
} flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm`}
|
||||
>
|
||||
<Nodes />
|
||||
<Device connection={props.connection} />
|
||||
<Channels />
|
||||
<div className="flex-grow border-b"></div>
|
||||
<UI />
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{sidebarOpen && (
|
||||
<motion.div
|
||||
className={`${
|
||||
sidebarOpen ? 'flex' : 'hidden md:flex'
|
||||
} flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm`}
|
||||
>
|
||||
<Nodes />
|
||||
<Device />
|
||||
<Channels />
|
||||
<div className="flex-grow border-b"></div>
|
||||
<UI />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useObservableSuspense } from 'observable-hooks';
|
||||
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { channelResource } from '../../../streams';
|
||||
import { useAppSelector } from '../../../hooks/redux';
|
||||
import { Channel } from './Channel';
|
||||
|
||||
export const ChannelList = (): JSX.Element => {
|
||||
const channelSource = useObservableSuspense(channelResource);
|
||||
|
||||
const [channels, setChannels] = React.useState<Protobuf.Channel[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
channels.findIndex(
|
||||
(currentChannel) => currentChannel.index === channelSource.index,
|
||||
) >= 0
|
||||
) {
|
||||
setChannels(
|
||||
channels.map((currentChannel) =>
|
||||
currentChannel.index === channelSource.index
|
||||
? channelSource
|
||||
: currentChannel,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setChannels((channels) => [...channels, channelSource]);
|
||||
}
|
||||
}, [channelSource, channels]);
|
||||
const channels = useAppSelector((state) => state.meshtastic.channels);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -3,26 +3,17 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AdjustmentsIcon } from '@heroicons/react/outline';
|
||||
import type {
|
||||
IBLEConnection,
|
||||
IHTTPConnection,
|
||||
ISerialConnection,
|
||||
} from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { Dropdown } from '../../basic/Dropdown';
|
||||
import { Settings } from './Settings';
|
||||
|
||||
interface DeviceProps {
|
||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||
}
|
||||
|
||||
export const Device = (props: DeviceProps): JSX.Element => {
|
||||
export const Device = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dropdown
|
||||
icon={<AdjustmentsIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
|
||||
title={t('settings.device')}
|
||||
content={<Settings connection={props.connection} />}
|
||||
content={<Settings />}
|
||||
fallbackMessage={'Loading...'}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,35 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useObservableSuspense } from 'observable-hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import type {
|
||||
IBLEConnection,
|
||||
IHTTPConnection,
|
||||
ISerialConnection,
|
||||
} from '@meshtastic/meshtasticjs';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { preferencesResource } from '../../../streams';
|
||||
import { connection } from '../../../connection';
|
||||
import { useAppSelector } from '../../../hooks/redux';
|
||||
|
||||
interface SettingsProps {
|
||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||
}
|
||||
|
||||
export const Settings = (props: SettingsProps): JSX.Element => {
|
||||
export const Settings = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const preferences = useObservableSuspense(preferencesResource);
|
||||
const preferences = useAppSelector((state) => state.meshtastic.preferences);
|
||||
|
||||
const { register, handleSubmit } =
|
||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
defaultValues: preferences,
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((data) =>
|
||||
props.connection.setPreferences(data),
|
||||
);
|
||||
const onSubmit = handleSubmit((data) => connection.setPreferences(data));
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
GlobeIcon,
|
||||
LightningBoltIcon,
|
||||
} from '@heroicons/react/outline';
|
||||
import type { Types } from '@meshtastic/meshtasticjs';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { useAppSelector } from '../../../hooks/redux';
|
||||
|
||||
export interface NodeProps {
|
||||
node: Types.NodeInfoPacket;
|
||||
node: Protobuf.NodeInfo;
|
||||
}
|
||||
|
||||
export const Node = (props: NodeProps): JSX.Element => {
|
||||
@@ -34,12 +34,12 @@ export const Node = (props: NodeProps): JSX.Element => {
|
||||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
||||
)}
|
||||
<div className="relative">
|
||||
{props.node.data.num === myId ? (
|
||||
{props.node.num === myId ? (
|
||||
<FlagIcon className="absolute -right-1 -top-2 text-yellow-500 my-auto w-4 h-4" />
|
||||
) : null}
|
||||
<Avatar
|
||||
size={30}
|
||||
name={props.node.data.user?.longName ?? 'Unknown'}
|
||||
name={props.node.user?.longName ?? 'Unknown'}
|
||||
variant="beam"
|
||||
colors={[
|
||||
'#213435',
|
||||
@@ -50,25 +50,16 @@ export const Node = (props: NodeProps): JSX.Element => {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{props.node.data.user?.longName}
|
||||
{props.node.user?.longName}
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel>
|
||||
<div className="border-b bg-gray-100 px-2">
|
||||
<p>
|
||||
SNR:{' '}
|
||||
{props.node.packet?.rxSnr ? props.node.packet.rxSnr : 'Unknown'}
|
||||
</p>
|
||||
<p>
|
||||
RSSI:{' '}
|
||||
{props.node.packet?.rxRssi
|
||||
? props.node.packet.rxRssi
|
||||
: 'Unknown'}
|
||||
</p>
|
||||
<p>{props.node.snr}</p>
|
||||
<p>
|
||||
{`Last heard: ${
|
||||
props.node.data?.lastHeard
|
||||
? new Date(props.node.data.lastHeard).toLocaleString()
|
||||
props.node?.lastHeard
|
||||
? new Date(props.node.lastHeard).toLocaleString()
|
||||
: 'Unknown'
|
||||
}`}{' '}
|
||||
{}
|
||||
@@ -76,23 +67,23 @@ export const Node = (props: NodeProps): JSX.Element => {
|
||||
<div className="flex">
|
||||
<GlobeIcon className="my-auto mr-2 w-5 h-5" />
|
||||
<p>
|
||||
{props.node.data.position?.latitudeI &&
|
||||
props.node.data.position?.longitudeI
|
||||
? `${props.node.data.position.latitudeI / 1e7},
|
||||
${props.node.data.position.longitudeI / 1e7}`
|
||||
{props.node.position?.latitudeI &&
|
||||
props.node.position?.longitudeI
|
||||
? `${props.node.position.latitudeI / 1e7},
|
||||
${props.node.position.longitudeI / 1e7}`
|
||||
: 'Unknown'}
|
||||
, El:
|
||||
{props.node.data.position?.altitude}
|
||||
{props.node.position?.altitude}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<ClockIcon className="my-auto mr-2 w-5 h-5" />
|
||||
<p>{props.node.data.position?.time}</p>
|
||||
<p>{props.node.position?.time}</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<LightningBoltIcon className="my-auto mr-2 w-5 h-5" />
|
||||
<p>{props.node.data.position?.batteryLevel}</p>
|
||||
<p>{props.node.position?.batteryLevel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
|
||||
@@ -1,34 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useObservableSuspense } from 'observable-hooks';
|
||||
|
||||
import type { Types } from '@meshtastic/meshtasticjs';
|
||||
|
||||
import { nodeResource } from '../../../streams';
|
||||
import { useAppSelector } from '../../../hooks/redux';
|
||||
import { Node } from './Node';
|
||||
|
||||
export const NodeList = (): JSX.Element => {
|
||||
const nodeSource = useObservableSuspense(nodeResource);
|
||||
|
||||
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
|
||||
|
||||
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 nodes = useAppSelector((state) => state.meshtastic.nodes);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
3
src/connection.ts
Normal file
3
src/connection.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { IHTTPConnection } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const connection = new IHTTPConnection();
|
||||
@@ -1,11 +1,15 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import type { RootState } from '../store';
|
||||
|
||||
interface AppState {
|
||||
sidebarOpen: boolean;
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
const initialState: AppState = {
|
||||
sidebarOpen: true,
|
||||
darkMode: false,
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
@@ -25,5 +29,6 @@ export const appSlice = createSlice({
|
||||
});
|
||||
|
||||
export const { openSidebar, closeSidebar, toggleSidebar } = appSlice.actions;
|
||||
|
||||
export const selectOpenState = (state: RootState): boolean =>
|
||||
state.app.sidebarOpen;
|
||||
export default appSlice.reducer;
|
||||
|
||||
@@ -1,24 +1,138 @@
|
||||
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
interface AppState {
|
||||
deviceStatus: Types.DeviceStatusEnum;
|
||||
myId: number;
|
||||
lastMeshInterraction: number;
|
||||
ready: boolean;
|
||||
fromRaioPackets: Protobuf.FromRadio[];
|
||||
meshPackets: Protobuf.MeshPacket[];
|
||||
myNodeInfo: Protobuf.MyNodeInfo;
|
||||
radioConfig: Protobuf.RadioConfig[];
|
||||
routingPackets: Types.RoutingPacket[];
|
||||
positionPackets: Types.PositionPacket[];
|
||||
textPackets: Types.TextPacket[];
|
||||
logRecords: Protobuf.LogRecord[];
|
||||
//
|
||||
nodes: Protobuf.NodeInfo[];
|
||||
channels: Protobuf.Channel[];
|
||||
preferences: Protobuf.RadioConfig_UserPreferences;
|
||||
}
|
||||
|
||||
const initialState: AppState = {
|
||||
deviceStatus: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
|
||||
myId: 0,
|
||||
lastMeshInterraction: 0,
|
||||
ready: false,
|
||||
fromRaioPackets: [],
|
||||
meshPackets: [],
|
||||
myNodeInfo: Protobuf.MyNodeInfo.create(),
|
||||
radioConfig: [],
|
||||
routingPackets: [],
|
||||
positionPackets: [],
|
||||
textPackets: [],
|
||||
logRecords: [],
|
||||
//
|
||||
nodes: [],
|
||||
channels: [],
|
||||
preferences: Protobuf.RadioConfig_UserPreferences.create(),
|
||||
};
|
||||
|
||||
export const meshtasticSlice = createSlice({
|
||||
name: 'meshtastic',
|
||||
initialState,
|
||||
reducers: {
|
||||
setDeviceStatus: (state, action: PayloadAction<Types.DeviceStatusEnum>) => {
|
||||
state.deviceStatus = action.payload;
|
||||
},
|
||||
setMyId: (state, action: PayloadAction<number>) => {
|
||||
state.myId = action.payload;
|
||||
},
|
||||
setLastMeshInterraction: (state, action: PayloadAction<number>) => {
|
||||
state.lastMeshInterraction = action.payload;
|
||||
},
|
||||
setReady: (state, action: PayloadAction<boolean>) => {
|
||||
state.ready = action.payload;
|
||||
},
|
||||
addFromRadioPacket: (state, action: PayloadAction<Protobuf.FromRadio>) => {
|
||||
state.fromRaioPackets.push(action.payload);
|
||||
},
|
||||
addMeshPacket: (state, action: PayloadAction<Protobuf.MeshPacket>) => {
|
||||
state.meshPackets.push(action.payload);
|
||||
},
|
||||
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
|
||||
state.myNodeInfo = action.payload;
|
||||
},
|
||||
addRadioConfig: (state, action: PayloadAction<Protobuf.RadioConfig>) => {
|
||||
state.radioConfig.push(action.payload);
|
||||
},
|
||||
addRoutingPacket: (state, action: PayloadAction<Types.RoutingPacket>) => {
|
||||
state.routingPackets.push(action.payload);
|
||||
},
|
||||
addPositionPacket: (state, action: PayloadAction<Types.PositionPacket>) => {
|
||||
state.positionPackets.push(action.payload);
|
||||
},
|
||||
addTextPacket: (state, action: PayloadAction<Types.TextPacket>) => {
|
||||
state.textPackets.push(action.payload);
|
||||
},
|
||||
addLogRecord: (state, action: PayloadAction<Protobuf.LogRecord>) => {
|
||||
state.logRecords.push(action.payload);
|
||||
},
|
||||
//
|
||||
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
|
||||
if (
|
||||
state.nodes.findIndex((node) => node.num === action.payload.num) !== -1
|
||||
) {
|
||||
state.nodes = state.nodes.map((node) => {
|
||||
return node.num === action.payload.num ? action.payload : node;
|
||||
});
|
||||
} else {
|
||||
state.nodes.push(action.payload);
|
||||
}
|
||||
},
|
||||
|
||||
addChannel: (state, action: PayloadAction<Protobuf.Channel>) => {
|
||||
if (
|
||||
state.channels.findIndex(
|
||||
(channel) => channel.index === action.payload.index,
|
||||
) !== -1
|
||||
) {
|
||||
state.channels = state.channels.map((channel) => {
|
||||
return channel.index === action.payload.index
|
||||
? action.payload
|
||||
: channel;
|
||||
});
|
||||
} else {
|
||||
state.channels.push(action.payload);
|
||||
}
|
||||
},
|
||||
setPreferences: (
|
||||
state,
|
||||
action: PayloadAction<Protobuf.RadioConfig_UserPreferences>,
|
||||
) => {
|
||||
state.preferences = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMyId } = meshtasticSlice.actions;
|
||||
export const {
|
||||
setDeviceStatus,
|
||||
setMyId,
|
||||
setLastMeshInterraction,
|
||||
setReady,
|
||||
addFromRadioPacket,
|
||||
addMeshPacket,
|
||||
setMyNodeInfo,
|
||||
addRadioConfig,
|
||||
addRoutingPacket,
|
||||
addPositionPacket,
|
||||
addTextPacket,
|
||||
addLogRecord,
|
||||
addNode,
|
||||
addChannel,
|
||||
setPreferences,
|
||||
} = meshtasticSlice.actions;
|
||||
|
||||
export default meshtasticSlice.reducer;
|
||||
|
||||
@@ -8,6 +8,10 @@ export const store = configureStore({
|
||||
app: appSlice,
|
||||
meshtastic: meshtasticSlice,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ObservableResource } from 'observable-hooks';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import type { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const preferencesSubject$ =
|
||||
new Subject<Protobuf.RadioConfig_UserPreferences>();
|
||||
|
||||
export const preferencesResource = new ObservableResource(preferencesSubject$);
|
||||
|
||||
export const nodeSubject$ = new Subject<Types.NodeInfoPacket>();
|
||||
|
||||
export const nodeResource = new ObservableResource(nodeSubject$);
|
||||
|
||||
export const channelSubject$ = new Subject<Protobuf.Channel>();
|
||||
|
||||
export const channelResource = new ObservableResource(channelSubject$);
|
||||
146
yarn.lock
146
yarn.lock
@@ -278,13 +278,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
|
||||
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
|
||||
|
||||
"@meshtastic/meshtasticjs@^0.6.13":
|
||||
version "0.6.13"
|
||||
resolved "https://registry.yarnpkg.com/@meshtastic/meshtasticjs/-/meshtasticjs-0.6.13.tgz#4e459a1380a8761dc119e5f43bbc42c18b18e5a1"
|
||||
integrity sha512-NdS5Cx/da7NW2FXgWvk+3lyFh/iVV3KpoEFkTFOS06liPYty1eKJo/sZgfNd/7DfdiWEDeOq9UG4aAVVsIxnlw==
|
||||
"@meshtastic/meshtasticjs@^0.6.15":
|
||||
version "0.6.15"
|
||||
resolved "https://registry.yarnpkg.com/@meshtastic/meshtasticjs/-/meshtasticjs-0.6.15.tgz#4bb45a5c501cade542134e7f63fb5b960e68dc67"
|
||||
integrity sha512-nelWkWb9DhWS7CffDatIUaPjL4vj+AsMAGqdeKoTm+sCRooi2+7V2bt2Pppl2obfSbE1hl+HdDoSntzvxeXo6A==
|
||||
dependencies:
|
||||
"@protobuf-ts/runtime" "^2.0.0-alpha.25"
|
||||
rxjs "^7.1.0"
|
||||
"@protobuf-ts/runtime" "^1.0.13"
|
||||
sub-events "^1.8.9"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -429,10 +429,10 @@
|
||||
node-gyp "^7.1.0"
|
||||
read-package-json-fast "^2.0.1"
|
||||
|
||||
"@protobuf-ts/runtime@^2.0.0-alpha.25":
|
||||
version "2.0.0-alpha.29"
|
||||
resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime/-/runtime-2.0.0-alpha.29.tgz#8e4099d014b21c88aae7f6694fb52e5e63d24cbe"
|
||||
integrity sha512-TuJO1kkA+33lO9V37y2/w5l7CCfG1xsgOIQtZbcEjBp/KhgB48zsZM4AjYWJcspw0RAB2JmJxbIXZgsiYBiMNw==
|
||||
"@protobuf-ts/runtime@^1.0.13":
|
||||
version "1.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime/-/runtime-1.0.13.tgz#42d6d84ea6f0ded68d6642ab64ca49f7c17f6e71"
|
||||
integrity sha512-uvYYBUtG4eCYMxo+mzxN8SHvpL/l7PbHEmOpXEnDCwBj/wJ+Ezj8+TlEFjjRWpnFidka+SMdDOXPWSyJv2iNAw==
|
||||
|
||||
"@reduxjs/toolkit@^1.6.0":
|
||||
version "1.6.1"
|
||||
@@ -673,72 +673,72 @@
|
||||
integrity sha512-zYzMb2aMyzXW5VgOQHy+FgI8N5tLFb+tIsUqk35CIgSr9pT4pji2GR8BCOTMdniusVuRHIp/DaYQNQGYGLVZHQ==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^4.28.1":
|
||||
version "4.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz#8197f1473e7da8218c6a37ff308d695707835684"
|
||||
integrity sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==
|
||||
version "4.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.0.tgz#b866c9cd193bfaba5e89bade0015629ebeb27996"
|
||||
integrity sha512-eiREtqWRZ8aVJcNru7cT/AMVnYd9a2UHsfZT8MR1dW3UUEg6jDv9EQ9Cq4CUPZesyQ58YUpoAADGv71jY8RwgA==
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "4.28.5"
|
||||
"@typescript-eslint/scope-manager" "4.28.5"
|
||||
"@typescript-eslint/experimental-utils" "4.29.0"
|
||||
"@typescript-eslint/scope-manager" "4.29.0"
|
||||
debug "^4.3.1"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
regexpp "^3.1.0"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/experimental-utils@4.28.5":
|
||||
version "4.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz#66c28bef115b417cf9d80812a713e0e46bb42a64"
|
||||
integrity sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==
|
||||
"@typescript-eslint/experimental-utils@4.29.0":
|
||||
version "4.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.0.tgz#19b1417602d0e1ef325b3312ee95f61220542df5"
|
||||
integrity sha512-FpNVKykfeaIxlArLUP/yQfv/5/3rhl1ov6RWgud4OgbqWLkEq7lqgQU9iiavZRzpzCRQV4XddyFz3wFXdkiX9w==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.7"
|
||||
"@typescript-eslint/scope-manager" "4.28.5"
|
||||
"@typescript-eslint/types" "4.28.5"
|
||||
"@typescript-eslint/typescript-estree" "4.28.5"
|
||||
"@typescript-eslint/scope-manager" "4.29.0"
|
||||
"@typescript-eslint/types" "4.29.0"
|
||||
"@typescript-eslint/typescript-estree" "4.29.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/parser@^4.28.1":
|
||||
version "4.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.5.tgz#9c971668f86d1b5c552266c47788a87488a47d1c"
|
||||
integrity sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==
|
||||
version "4.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.0.tgz#e5367ca3c63636bb5d8e0748fcbab7a4f4a04289"
|
||||
integrity sha512-+92YRNHFdXgq+GhWQPT2bmjX09X7EH36JfgN2/4wmhtwV/HPxozpCNst8jrWcngLtEVd/4zAwA6BKojAlf+YqA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "4.28.5"
|
||||
"@typescript-eslint/types" "4.28.5"
|
||||
"@typescript-eslint/typescript-estree" "4.28.5"
|
||||
"@typescript-eslint/scope-manager" "4.29.0"
|
||||
"@typescript-eslint/types" "4.29.0"
|
||||
"@typescript-eslint/typescript-estree" "4.29.0"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@typescript-eslint/scope-manager@4.28.5":
|
||||
version "4.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz#3a1b70c50c1535ac33322786ea99ebe403d3b923"
|
||||
integrity sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==
|
||||
"@typescript-eslint/scope-manager@4.29.0":
|
||||
version "4.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.0.tgz#cf5474f87321bedf416ef65839b693bddd838599"
|
||||
integrity sha512-HPq7XAaDMM3DpmuijxLV9Io8/6pQnliiXMQUcAdjpJJSR+fdmbD/zHCd7hMkjJn04UQtCQBtshgxClzg6NIS2w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.5"
|
||||
"@typescript-eslint/visitor-keys" "4.28.5"
|
||||
"@typescript-eslint/types" "4.29.0"
|
||||
"@typescript-eslint/visitor-keys" "4.29.0"
|
||||
|
||||
"@typescript-eslint/types@4.28.5":
|
||||
version "4.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.5.tgz#d33edf8e429f0c0930a7c3d44e9b010354c422e9"
|
||||
integrity sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==
|
||||
"@typescript-eslint/types@4.29.0":
|
||||
version "4.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.0.tgz#c8f1a1e4441ea4aca9b3109241adbc145f7f8a4e"
|
||||
integrity sha512-2YJM6XfWfi8pgU2HRhTp7WgRw78TCRO3dOmSpAvIQ8MOv4B46JD2chnhpNT7Jq8j0APlIbzO1Bach734xxUl4A==
|
||||
|
||||
"@typescript-eslint/typescript-estree@4.28.5":
|
||||
version "4.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz#4906d343de693cf3d8dcc301383ed638e0441cd1"
|
||||
integrity sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==
|
||||
"@typescript-eslint/typescript-estree@4.29.0":
|
||||
version "4.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.0.tgz#af7ab547757b86c91bfdbc54ff86845410856256"
|
||||
integrity sha512-8ZpNHDIOyqzzgZrQW9+xQ4k5hM62Xy2R4RPO3DQxMc5Rq5QkCdSpk/drka+DL9w6sXNzV5nrdlBmf8+x495QXQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.5"
|
||||
"@typescript-eslint/visitor-keys" "4.28.5"
|
||||
"@typescript-eslint/types" "4.29.0"
|
||||
"@typescript-eslint/visitor-keys" "4.29.0"
|
||||
debug "^4.3.1"
|
||||
globby "^11.0.3"
|
||||
is-glob "^4.0.1"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.28.5":
|
||||
version "4.28.5"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz#ffee2c602762ed6893405ee7c1144d9cc0a29675"
|
||||
integrity sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==
|
||||
"@typescript-eslint/visitor-keys@4.29.0":
|
||||
version "4.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.0.tgz#1ff60f240def4d85ea68d4fd2e4e9759b7850c04"
|
||||
integrity sha512-LoaofO1C/jAJYs0uEpYMXfHboGXzOJeV118X4OsZu9f7rG7Pr9B3+4HTU8+err81rADa4xfQmAxnRnPAI2jp+Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.5"
|
||||
"@typescript-eslint/types" "4.29.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
abbrev@1:
|
||||
@@ -1654,9 +1654,9 @@ ecc-jsbn@~0.1.1:
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
electron-to-chromium@^1.3.723:
|
||||
version "1.3.792"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.792.tgz#791b0d8fcf7411885d086193fb49aaef0c1594ca"
|
||||
integrity sha512-RM2O2xrNarM7Cs+XF/OE2qX/aBROyOZqqgP+8FXMXSuWuUqCfUUzg7NytQrzZU3aSqk1Qq6zqnVkJsbfMkIatg==
|
||||
version "1.3.793"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.793.tgz#c10dff5f3126238004de344db458f1da3641d554"
|
||||
integrity sha512-l9NrGV6Mr4ov5mayYPvIWcwklNw5ROmy6rllzz9dCACw9nKE5y+s5uQk+CBJMetxrWZ6QJFsvEfG6WDcH2IGUg==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
@@ -1712,9 +1712,9 @@ error-ex@^1.3.1:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
|
||||
version "1.18.4"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.4.tgz#c6b7a1acd6bb1c8b5afeb54a53c46ad02fab346d"
|
||||
integrity sha512-xjDAPJRxKc1uoTkdW8MEk7Fq/2bzz3YoCADYniDV7+KITCUdu9c90fj1aKI7nEZFZxRrHlDo3wtma/C6QkhlXQ==
|
||||
version "1.18.5"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.5.tgz#9b10de7d4c206a3581fd5b2124233e04db49ae19"
|
||||
integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
es-to-primitive "^1.2.1"
|
||||
@@ -3604,11 +3604,6 @@ object.values@^1.1.3, object.values@^1.1.4:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.18.2"
|
||||
|
||||
observable-hooks@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/observable-hooks/-/observable-hooks-4.0.5.tgz#00185d17979251ea518e1c7e8fe4c52e8b074b35"
|
||||
integrity sha512-st96cqHEEbRl2wQmAOesptA788HJR46MRhKIpBQDyl6s7sfh2SRgulzzk43DXJUwyYZ6KdaDaUDY4LKJ6Cjw8Q==
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
@@ -4269,14 +4264,6 @@ reduce-css-calc@^2.1.8:
|
||||
css-unit-converter "^1.1.1"
|
||||
postcss-value-parser "^3.3.0"
|
||||
|
||||
redux-observable@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-2.0.0.tgz#4358bef2e924723a8b1ad0e835ccebb1612a6b9a"
|
||||
integrity sha512-FJz4rLXX+VmDDwZS/LpvQsKnSanDOe8UVjiLryx1g3seZiS69iLpMrcvXD5oFO7rtkPyRdo/FmTqldnT3X3m+w==
|
||||
dependencies:
|
||||
rxjs "^7.0.0"
|
||||
tslib "~2.1.0"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
@@ -4431,13 +4418,6 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^7.0.0, rxjs@^7.1.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.3.0.tgz#39fe4f3461dc1e50be1475b2b85a0a88c1e938c6"
|
||||
integrity sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==
|
||||
dependencies:
|
||||
tslib "~2.1.0"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
@@ -4818,6 +4798,11 @@ style-value-types@4.1.4:
|
||||
hey-listen "^1.0.8"
|
||||
tslib "^2.1.0"
|
||||
|
||||
sub-events@^1.8.9:
|
||||
version "1.8.9"
|
||||
resolved "https://registry.yarnpkg.com/sub-events/-/sub-events-1.8.9.tgz#57b332134ae1ded738f7c2ddbcd9c1bc81ca8c2e"
|
||||
integrity sha512-RhhA2amqVzL6nO+aiZOqxBCgcA3ZLfp4W9iHFUELwq8132TS7pUReJV+bcRjtNKdqm/Ep1sD/h01eAcTBtgrBQ==
|
||||
|
||||
supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
@@ -4882,9 +4867,9 @@ tailwindcss@^2.2.4:
|
||||
tmp "^0.2.1"
|
||||
|
||||
tar@^6.0.2, tar@^6.1.0:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.2.tgz#1f045a90a6eb23557a603595f41a16c57d47adc6"
|
||||
integrity sha512-EwKEgqJ7nJoS+s8QfLYVGMDmAsj+StbI2AM/RTHeUSsOw6Z8bwNBRv5z3CY0m7laC5qUAqruLX5AhMuc5deY3Q==
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.3.tgz#e44b97ee7d6cc7a4c574e8b01174614538291825"
|
||||
integrity sha512-3rUqwucgVZXTeyJyL2jqtUau8/8r54SioM1xj3AmTX3HnWQdj2AydfJ2qYYayPyIIznSplcvU9mhBb7dR2XF3w==
|
||||
dependencies:
|
||||
chownr "^2.0.0"
|
||||
fs-minipass "^2.0.0"
|
||||
@@ -4949,11 +4934,6 @@ tslib@^2.1.0, tslib@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tslib@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
|
||||
Reference in New Issue
Block a user