WIP
This commit is contained in:
@@ -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(
|
||||
|
||||
+14
-21
@@ -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>
|
||||
|
||||
+18
-21
@@ -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 (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user