Use Headless UI & get visual message ack's working

This commit is contained in:
Sacha Weatherstone
2021-05-09 19:23:13 +10:00
parent d6be17a789
commit 50c276b9bc
21 changed files with 831 additions and 9433 deletions
+101 -91
View File
@@ -37,23 +37,38 @@ export interface languageTemplate {
}
const App = () => {
const [deviceStatus, setDeviceStatus] = React.useState(
{} as Types.DeviceStatusEnum,
const [
deviceStatus,
setDeviceStatus,
] = React.useState<Types.DeviceStatusEnum>(
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
);
const [myNodeInfo, setMyNodeInfo] = React.useState({} as Protobuf.MyNodeInfo);
const [messages, setMessages] = React.useState(
[] as { message: Types.TextPacket; ack: false }[],
const [myNodeInfo, setMyNodeInfo] = React.useState<Protobuf.MyNodeInfo>(
Protobuf.MyNodeInfo.create(),
);
const [channels, setChannels] = React.useState([] as Protobuf.Channel[]);
const [nodes, setNodes] = React.useState([] as Types.NodeInfoPacket[]);
const [connection, setConnection] = React.useState({} as IHTTPConnection);
const [isReady, setIsReady] = React.useState(false);
const [lastMeshInterraction, setLastMeshInterraction] = React.useState(0);
const [preferences, setPreferences] = React.useState(
{} as Protobuf.RadioConfig_UserPreferences,
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
const [connection, setConnection] = React.useState<IHTTPConnection>(
new IHTTPConnection(),
);
const [language, setLanguage] = React.useState(LanguageEnum.ENGLISH);
const [translations, setTranslations] = React.useState(Translations_English);
const [isReady, setIsReady] = React.useState<boolean>(false);
const [
lastMeshInterraction,
setLastMeshInterraction,
] = React.useState<number>(0);
const [
preferences,
setPreferences,
] = React.useState<Protobuf.RadioConfig_UserPreferences>(
Protobuf.RadioConfig_UserPreferences.create(),
);
const [language, setLanguage] = React.useState<LanguageEnum>(
LanguageEnum.ENGLISH,
);
const [translations, setTranslations] = React.useState<languageTemplate>(
Translations_English,
);
const [darkmode, setDarkmode] = React.useState<boolean>(false);
React.useEffect(() => {
switch (language) {
@@ -75,7 +90,7 @@ const App = () => {
React.useEffect(() => {
const client = new Client();
const connection = client.createHTTPConnection();
setConnection(connection);
connection.connect({
address:
import.meta.env.NODE_ENV === 'production'
@@ -87,80 +102,74 @@ const App = () => {
});
setConnection(connection);
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
connection.onDeviceStatusEvent.subscribe((status) => {
setDeviceStatus(status);
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
setIsReady(true);
}
});
connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
connection.onTextPacketEvent.subscribe((message) => {
setMessages((messages) => [
...messages,
{ message: message, ack: false },
]);
});
connection.onNodeInfoPacketEvent.subscribe((node) => {
if (
nodes.findIndex(
(currentNode) => currentNode.data.num === node.data.num,
) >= 0
) {
setNodes(
nodes.map((currentNode) =>
currentNode.data.num === node.data.num ? node : currentNode,
),
);
} else {
setNodes((nodes) => [...nodes, node]);
}
});
connection.onAdminPacketEvent.subscribe((adminMessage) => {
switch (adminMessage.data.variant.oneofKind) {
case 'getRadioResponse':
if (adminMessage.data.variant.getRadioResponse.preferences) {
setPreferences(
adminMessage.data.variant.getRadioResponse.preferences,
);
}
const deviceStatusEvent = connection.onDeviceStatusEvent.subscribe(
(status) => {
setDeviceStatus(status);
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
setIsReady(true);
}
},
);
const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(
setMyNodeInfo,
);
break;
case 'getChannelResponse':
if (adminMessage.data.variant.getChannelResponse) {
let message = adminMessage.data.variant.getChannelResponse;
setChannels((channels) => [...channels, message]);
}
const nodeInfoPacketEvent = connection.onNodeInfoPacketEvent.subscribe(
(node) => {
if (
nodes.findIndex(
(currentNode) => currentNode.data.num === node.data.num,
) >= 0
) {
setNodes(
nodes.map((currentNode) =>
currentNode.data.num === node.data.num ? node : currentNode,
),
);
} else {
setNodes((nodes) => [...nodes, node]);
}
},
);
default:
break;
}
});
const adminPacketEvent = connection.onAdminPacketEvent.subscribe(
(adminMessage) => {
switch (adminMessage.data.variant.oneofKind) {
case 'getRadioResponse':
if (adminMessage.data.variant.getRadioResponse.preferences) {
setPreferences(
adminMessage.data.variant.getRadioResponse.preferences,
);
}
connection.onMeshHeartbeat.subscribe(setLastMeshInterraction);
break;
case 'getChannelResponse':
if (adminMessage.data.variant.getChannelResponse) {
let message = adminMessage.data.variant.getChannelResponse;
setChannels((channels) => [...channels, message]);
}
connection.onRoutingPacketEvent.subscribe((routingPacket) => {
console.log(routingPacket);
// console.log(messages);
default:
break;
}
},
);
// messages.map((message) => {
// console.log(
// `${
// routingPacket.payloadVariant.oneofKind === 'decoded'
// ? routingPacket.payloadVariant.decoded.requestId
// : null
// } === ${message.message.packet.id}: ${
// routingPacket.payloadVariant.oneofKind === 'decoded'
// ? routingPacket.payloadVariant.decoded.requestId
// : null === message.message.packet.id
// }`,
// );
// });
// messages.find((message) => {
// message.message.packet.id === routingPacket.decoded.requestId;
// });
});
const meshHeartbeat = connection.onMeshHeartbeat.subscribe(
setLastMeshInterraction,
);
return () => {
deviceStatusEvent.unsubscribe();
myNodeInfoEvent.unsubscribe();
nodeInfoPacketEvent.unsubscribe();
adminPacketEvent.unsubscribe();
meshHeartbeat.unsubscribe();
connection.disconnect();
};
}, []);
return (
<div className="flex flex-col h-screen w-screen">
<Header
@@ -169,16 +178,17 @@ const App = () => {
LastMeshInterraction={lastMeshInterraction}
/>
<Main
IsReady={isReady}
Messages={messages}
MyNodeInfo={myNodeInfo}
Connection={connection}
Nodes={nodes}
Channels={channels}
Preferences={preferences}
Language={language}
SetLanguage={setLanguage}
Translations={translations}
isReady={isReady}
myNodeInfo={myNodeInfo}
connection={connection}
nodes={nodes}
channels={channels}
preferences={preferences}
language={language}
setLanguage={setLanguage}
translations={translations}
darkmode={darkmode}
setDarkmode={setDarkmode}
/>
</div>
);
+73 -76
View File
@@ -1,6 +1,5 @@
import React from 'react';
import React, { useState } from 'react';
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
import type {
IHTTPConnection,
Protobuf,
@@ -9,102 +8,100 @@ import type {
import type { LanguageEnum, languageTemplate } from './App';
import ChatMessage from './components/ChatMessage';
import MessageBox from './components/MessageBox';
import Sidebar from './components/Sidebar';
interface MainProps {
Messages: { message: Types.TextPacket; ack: boolean }[];
Connection: IHTTPConnection;
MyNodeInfo: Protobuf.MyNodeInfo;
Nodes: Types.NodeInfoPacket[];
Channels: Protobuf.Channel[];
IsReady: boolean;
Preferences: Protobuf.RadioConfig_UserPreferences;
Language: LanguageEnum;
SetLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
Translations: languageTemplate;
connection: IHTTPConnection;
myNodeInfo: Protobuf.MyNodeInfo;
nodes: Types.NodeInfoPacket[];
channels: Protobuf.Channel[];
isReady: boolean;
preferences: Protobuf.RadioConfig_UserPreferences;
language: LanguageEnum;
setLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
translations: languageTemplate;
darkmode: boolean;
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
const Main = (props: MainProps) => {
const [currentMessage, setCurrentMessage] = React.useState('');
const [mobileNavOpen, setMobileNavOpen] = React.useState(true);
const [messages, setMessages] = React.useState<
{ message: Types.TextPacket; ack: boolean }[]
>([]);
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
const sendMessage = () => {
if (props.IsReady) {
props.Connection.sendText(currentMessage, undefined, true);
setCurrentMessage('');
}
};
React.useEffect(() => {
const textPacketEvent = props.connection.onTextPacketEvent.subscribe(
(message) => {
setMessages((messages) => [
...messages,
{ message: message, ack: false },
]);
},
);
return () => textPacketEvent.unsubscribe();
}, [props.connection]);
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]);
return (
<div className="flex flex-col md:flex-row flex-grow space-2">
<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">
<div className="flex flex-col flex-grow container mx-auto">
<div className="flex flex-col flex-grow py-6 px-3 space-y-2">
{props.Messages.length ? (
props.Messages.map((message, Main) => (
<div className="flex flex-col flex-grow py-6 space-y-2">
{messages.length ? (
messages.map((message, Main) => (
<ChatMessage
nodes={props.Nodes}
nodes={props.nodes}
key={Main}
message={message}
myId={props.MyNodeInfo.myNodeNum}
myId={props.myNodeInfo.myNodeNum}
/>
))
) : (
<div className="m-auto text-2xl text-gray-500">
{props.Translations.no_messages_message}
{props.translations.no_messages_message}
</div>
)}
</div>
<div className="flex space-x-2 w-full p-3">
<form
className="flex flex-wrap relative w-full"
onSubmit={(e) => {
e.preventDefault();
sendMessage();
}}
>
{props.IsReady}
<input
type="text"
placeholder={`${props.Translations.no_message_placeholder}...`}
disabled={!props.IsReady}
value={currentMessage}
onChange={(e) => {
setCurrentMessage(e.target.value);
}}
className={`p-3 placeholder-gray-400 text-gray-700 relative rounded-md shadow-md focus:outline-none w-full pr-10 ${
props.IsReady ? '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'
}`}
/>
</span>
</form>
<div
className="flex p-3 text-xl hover:text-gray-500 text-gray-400 rounded-md shadow-md focus:outline-none cursor-pointer md:hidden"
onClick={() => {
setMobileNavOpen(!mobileNavOpen);
}}
>
<MenuIcon className="m-auto h-6 2-6" />
</div>
</div>
<MessageBox
connection={props.connection}
isReady={props.isReady}
setSettingsModalOpen={setSettingsModalOpen}
translations={props.translations}
/>
</div>
<Sidebar
IsReady={props.IsReady}
Nodes={props.Nodes}
Channels={props.Channels}
Preferences={props.Preferences}
Connection={props.Connection}
MobileNavOpen={mobileNavOpen}
Language={props.Language}
SetLanguage={props.SetLanguage}
Translations={props.Translations}
myId={props.MyNodeInfo.myNodeNum}
isReady={props.isReady}
nodes={props.nodes}
channels={props.channels}
preferences={props.preferences}
connection={props.connection}
language={props.language}
setLanguage={props.setLanguage}
translations={props.translations}
myId={props.myNodeInfo.myNodeNum}
darkmode={props.darkmode}
setDarkmode={props.setDarkmode}
/>
</div>
);
+66
View File
@@ -0,0 +1,66 @@
import React from 'react';
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
import type { IHTTPConnection } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../App';
export interface MessageBoxProps {
translations: languageTemplate;
setSettingsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
connection: IHTTPConnection;
isReady: boolean;
}
const MessageBox = (props: MessageBoxProps) => {
const [currentMessage, setCurrentMessage] = React.useState('');
const sendMessage = () => {
if (props.isReady) {
props.connection.sendText(currentMessage, undefined, true);
setCurrentMessage('');
}
};
return (
<div className="flex text-lg font-medium border rounded-md space-x-2 md:space-x-0 w-full">
<div
className="flex p-3 text-xl hover:text-gray-500 text-gray-400 rounded-md shadow-md focus:outline-none cursor-pointer md:hidden"
onClick={() => {
props.setSettingsModalOpen(true);
}}
>
<MenuIcon className="m-auto h-6 2-6" />
</div>
<form
className="flex flex-wrap relative w-full"
onSubmit={(e) => {
e.preventDefault();
sendMessage();
}}
>
{props.isReady}
<input
type="text"
placeholder={`${props.translations.no_message_placeholder}...`}
disabled={!props.isReady}
value={currentMessage}
onChange={(e) => {
setCurrentMessage(e.target.value);
}}
className={`p-3 placeholder-gray-400 text-gray-700 relative rounded-md shadow-md focus:outline-none w-full pr-10 ${
props.isReady ? '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'
}`}
/>
</span>
</form>
</div>
);
};
export default MessageBox;
-67
View File
@@ -1,67 +0,0 @@
import React from 'react';
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/outline';
interface NavItemProps {
isDropdown: boolean;
open: boolean;
isNested: boolean;
titleContent: React.ReactNode;
dropdownContent?: React.ReactNode;
onClick?: Function;
isLoading?: boolean;
}
const NavItem = (props: NavItemProps) => {
React.useEffect(() => {
if (props.open) {
setNavItemOpen(props.open);
}
}, []);
const [navItemOpen, setNavItemOpen] = React.useState(false);
return (
<>
<div
className={`flex w-full text-lg font-medium justify-between ${
navItemOpen && props.isNested ? 'bg-gray-100' : null
} ${props.isNested ? 'border-b px-3 py-1' : 'p-3'} ${
props.isDropdown && navItemOpen ? 'shadow-md' : 'border-b'
} ${
props.isDropdown || props.isNested
? 'hover:bg-gray-200 cursor-pointer'
: null
}`}
onClick={() => {
if (props.isDropdown) setNavItemOpen(!navItemOpen);
if (props.onClick) {
props.onClick();
}
}}
>
{props.titleContent}
{props.isDropdown && !props.isLoading ? (
navItemOpen ? (
<ChevronDownIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
) : (
<ChevronRightIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
)
) : null}
{props.isLoading ? (
// <FaSpinner className="animate-spin my-auto" />
<div>loading</div>
) : null}
</div>
{props.isDropdown ? (
<div
className={`duration-200 ease-in-out transition-all overflow-hidden max-h-0 border-l-8 ${
props.isNested ? 'border-gray-500' : 'border-gray-300'
} ${navItemOpen ? 'max-h-full' : null}`}
>
{props.dropdownContent}
</div>
) : null}
</>
);
};
export default NavItem;
+34 -37
View File
@@ -7,55 +7,52 @@ import type {
} from '@meshtastic/meshtasticjs';
import type { LanguageEnum, languageTemplate } from '../App';
import SidebarChannels from './Sidebar/SidebarChannels';
import SidebarDeviceSettings from './Sidebar/SidebarDeviceSettings';
import SidebarNodes from './Sidebar/SidebarNodes';
import SidebarUISettings from './Sidebar/SidebarUISettings';
import Channels from './Sidebar/Channels/Index';
import Device from './Sidebar/Device/Index';
import Nodes from './Sidebar/Nodes/Index';
import UI from './Sidebar/UI/Index';
interface SidebarProps {
IsReady: boolean;
Nodes: Types.NodeInfoPacket[];
Channels: Protobuf.Channel[];
Preferences: Protobuf.RadioConfig_UserPreferences;
Connection: IHTTPConnection;
MobileNavOpen: boolean;
Language: LanguageEnum;
SetLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
Translations: languageTemplate;
isReady: boolean;
nodes: Types.NodeInfoPacket[];
channels: Protobuf.Channel[];
preferences: Protobuf.RadioConfig_UserPreferences;
connection: IHTTPConnection;
language: LanguageEnum;
setLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
translations: languageTemplate;
myId: number;
darkmode: boolean;
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
const Sidebar = (props: SidebarProps) => {
const updatePreferences = () => {};
return (
<div
className={`flex flex-col rounded-md m-3 md:ml-0 shadow-md w-full max-w-sm ${
!props.MobileNavOpen ? 'hidden' : 'visible'
}`}
>
<SidebarNodes
IsReady={props.IsReady}
Nodes={props.Nodes}
Translations={props.Translations}
<div className="flex flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm">
<Nodes
isReady={props.isReady}
nodes={props.nodes}
translations={props.translations}
myId={props.myId}
/>
<SidebarDeviceSettings
IsReady={props.IsReady}
Preferences={props.Preferences}
Connection={props.Connection}
Translations={props.Translations}
<Device
isReady={props.isReady}
preferences={props.preferences}
connection={props.connection}
translations={props.translations}
/>
<SidebarChannels
IsReady={props.IsReady}
Channels={props.Channels}
Translations={props.Translations}
<Channels
isReady={props.isReady}
channels={props.channels}
translations={props.translations}
/>
<div className="flex-grow border-b"></div>
<SidebarUISettings
Language={props.Language}
SetLanguage={props.SetLanguage}
Translations={props.Translations}
<UI
language={props.language}
setLanguage={props.setLanguage}
translations={props.translations}
darkmode={props.darkmode}
setDarkmode={props.setDarkmode}
/>
</div>
);
+110
View File
@@ -0,0 +1,110 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface ChannelProps {
channel: Protobuf.Channel;
}
const Channel = (props: ChannelProps) => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex">
{props.channel.index} -{' '}
{Protobuf.Channel_Role[props.channel.role]}
</div>
</Disclosure.Button>
<Disclosure.Panel>
<div className="w-full">
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Bandwidth:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.bandwidth}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Channel Number:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.channelNum}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Coding Rate:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.codingRate}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>ID:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.id}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Modem Config:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.modemConfig
? Protobuf.ChannelSettings_ModemConfig[
props.channel.settings.modemConfig
]
: null}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Name:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.name}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>PSK:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.psk.toLocaleString()}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Spread Factor:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.spreadFactor}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Tx Power:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.txPower}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Uplink:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.uplinkEnabled ? 'true' : 'false'}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Downlink:</p>
<code className="bg-gray-200 rounded-full px-2">
{props.channel.settings?.downlinkEnabled ? 'true' : 'false'}
</code>
</div>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Channel;
+50
View File
@@ -0,0 +1,50 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
ChevronDownIcon,
ChevronRightIcon,
HashtagIcon,
} from '@heroicons/react/outline';
import { Protobuf } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../../App';
import Channel from './Channel';
export interface ChannelsProps {
isReady: boolean;
channels: Protobuf.Channel[];
translations: languageTemplate;
}
const Channels = (props: ChannelsProps) => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex">
<HashtagIcon className="my-auto mr-2 2-5 h-5" />
{props.translations.device_channels_title}
</div>
{open ? (
<ChevronDownIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
) : (
<ChevronRightIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
)}
</Disclosure.Button>
<Disclosure.Panel>
<>
{props.channels.map((channel, index) => {
if (channel.role !== Protobuf.Channel_Role.DISABLED)
return <Channel channel={channel} />;
})}
</>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Channels;
+124
View File
@@ -0,0 +1,124 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
AdjustmentsIcon,
ChevronDownIcon,
ChevronRightIcon,
SaveIcon,
} from '@heroicons/react/outline';
import { IHTTPConnection, Protobuf } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../../App';
interface DeviceProps {
isReady: boolean;
preferences: Protobuf.RadioConfig_UserPreferences;
connection: IHTTPConnection;
translations: languageTemplate;
}
const Device = (props: DeviceProps) => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex">
<AdjustmentsIcon className="my-auto mr-2 w-5 h-5" />
{props.translations.device_settings_title}
</div>
{open ? (
<ChevronDownIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
) : (
<ChevronRightIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
)}
</Disclosure.Button>
<Disclosure.Panel>
<>
<div className="flex 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={
props.preferences?.region ?? Protobuf.RegionCode.Unset
}
onChange={(e) => {
props.preferences.region = parseInt(e.target.value);
}}
>
<option value={Protobuf.RegionCode.ANZ}>
{Protobuf.RegionCode[Protobuf.RegionCode.ANZ]}
</option>
<option value={Protobuf.RegionCode.CN}>
{Protobuf.RegionCode[Protobuf.RegionCode.CN]}
</option>
<option value={Protobuf.RegionCode.EU433}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU433]}
</option>
<option value={Protobuf.RegionCode.EU865}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU865]}
</option>
<option value={Protobuf.RegionCode.JP}>
{Protobuf.RegionCode[Protobuf.RegionCode.JP]}
</option>
<option value={Protobuf.RegionCode.KR}>
{Protobuf.RegionCode[Protobuf.RegionCode.KR]}
</option>
<option value={Protobuf.RegionCode.TW}>
{Protobuf.RegionCode[Protobuf.RegionCode.TW]}
</option>
<option value={Protobuf.RegionCode.US}>
{Protobuf.RegionCode[Protobuf.RegionCode.US]}
</option>
<option value={Protobuf.RegionCode.Unset}>
{Protobuf.RegionCode[Protobuf.RegionCode.Unset]}
</option>
</select>
</div>
</div>
<div className="flex whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">
{props.translations.device_wifi_ssid}
</div>
<div className="flex shadow-md rounded-md ml-2">
<input
onChange={() => {}}
type="text"
value={props.preferences.wifiSsid}
/>
</div>
</div>
<div className="flex whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">
{props.translations.device_wifi_psk}
</div>
<div className="flex shadow-md rounded-md ml-2">
<input
type="password"
value={props.preferences.wifiPassword}
/>
</div>
</div>
<div className="flex group p-1 bg-gray-100 cursor-pointer hover:bg-gray-200 border-b">
<div
className="flex m-auto font-medium group-hover:text-gray-700"
onClick={() => {
props.connection.setPreferences(props.preferences);
}}
>
<SaveIcon className="m-auto mr-2 group-hover:text-gray-700 w-5 h-5" />
{props.translations.save_changes_button}
</div>
</div>
</>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Device;
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
ChevronDownIcon,
ChevronRightIcon,
UsersIcon,
} from '@heroicons/react/outline';
import type { Types } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../../App';
import Node from './Node';
interface NodesProps {
isReady: boolean;
nodes: Types.NodeInfoPacket[];
translations: languageTemplate;
myId: number;
}
const Nodes = (props: NodesProps) => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex rounded-t-md w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex">
<UsersIcon className="my-auto mr-2 w-5 h-5" />
{props.translations.nodes_title}
</div>
{open ? (
<ChevronDownIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
) : (
<ChevronRightIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
)}
</Disclosure.Button>
<Disclosure.Panel className="shadow-inner">
{props.nodes.length ? (
props.nodes.map((node, index) => (
<Node key={index} node={node} myId={props.myId} />
))
) : (
<div className="flex border-b border-gray-300">
<div className="m-auto p-3 text-gray-500">
{props.translations.no_nodes_message}
</div>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Nodes;
+82
View File
@@ -0,0 +1,82 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
ClockIcon,
DesktopComputerIcon,
FlagIcon,
GlobeIcon,
LightningBoltIcon,
} from '@heroicons/react/outline';
import type { Types } from '@meshtastic/meshtasticjs';
export interface NodeProps {
node: Types.NodeInfoPacket;
myId: number;
}
const Node = (props: NodeProps) => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex">
{props.node.data.num === props.myId ? (
<FlagIcon className="text-yellow-500 my-auto mr-2 w-5 h-5" />
) : (
<DesktopComputerIcon className="my-auto mr-2 w-5 h-5" />
)}
<div className="m-auto">{props.node.data.user?.longName}</div>
</div>
</Disclosure.Button>
<Disclosure.Panel>
<div>
<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>
{`Last heard: ${
props.node.data?.lastHeard
? new Date(props.node.data.lastHeard).toLocaleString()
: 'Unknown'
}`}{' '}
{}
</p>
<div className="flex">
<GlobeIcon className="my-auto mr-2 w-5 h-5" />
<p>
{props.node.data.position?.latitudeI},
{props.node.data.position?.longitudeI}
</p>
</div>
<div className="flex">
<GlobeIcon className="my-auto mr-2 w-5 h-5" />
<p>{props.node.data.position?.altitude}</p>
</div>
<div className="flex">
<ClockIcon className="my-auto mr-2 w-5 h-5" />
<p>{props.node.data.position?.time}</p>
</div>
<div className="flex">
<LightningBoltIcon className="my-auto mr-2 w-5 h-5" />
<p>{props.node.data.position?.batteryLevel}</p>
</div>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Node;
-146
View File
@@ -1,146 +0,0 @@
import React from 'react';
import { HashtagIcon } from '@heroicons/react/outline';
import { Protobuf } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../App';
import NavItem from '../NavItem';
interface SidebarChannelsProps {
IsReady: boolean;
Channels: Protobuf.Channel[];
Translations: languageTemplate;
}
const SidebarChannels = (props: SidebarChannelsProps) => {
return (
<NavItem
isDropdown={true}
open={false}
isNested={false}
titleContent={
<div className="flex">
<HashtagIcon className="my-auto mr-2 2-5 h-5" />
{props.Translations.device_channels_title}
</div>
}
isLoading={!props.IsReady}
dropdownContent={
<>
{props.Channels.map((channel, index) => {
if (channel.role !== Protobuf.Channel_Role.DISABLED)
return (
<NavItem
key={index}
isDropdown={true}
isNested={true}
open={false}
titleContent={
<div className="flex">
{channel.index} - {Protobuf.Channel_Role[channel.role]}
</div>
}
dropdownContent={
<NavItem
isDropdown={false}
isNested={false}
open={false}
titleContent={
<div className="w-full">
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Bandwidth:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.bandwidth}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Channel Number:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.channelNum}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Coding Rate:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.codingRate}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>ID:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.id}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Modem Config:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.modemConfig
? Protobuf.ChannelSettings_ModemConfig[
channel.settings.modemConfig
]
: null}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Name:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.name}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>PSK:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.psk.toLocaleString()}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Spread Factor:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.spreadFactor}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Tx Power:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.txPower}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Uplink:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.uplinkEnabled
? 'true'
: 'false'}
</code>
</div>
<div className="flex justify-between border-b hover:bg-gray-200">
<p>Downlink:</p>
<code className="bg-gray-200 rounded-full px-2">
{channel.settings?.downlinkEnabled
? 'true'
: 'false'}
</code>
</div>
</div>
}
/>
}
/>
);
})}
</>
}
/>
);
};
export default SidebarChannels;
@@ -1,105 +0,0 @@
import React from 'react';
import { AdjustmentsIcon, SaveIcon } from '@heroicons/react/outline';
import { IHTTPConnection, Protobuf } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../App';
import NavItem from '../NavItem';
interface SidebarDeviceSettingsProps {
IsReady: boolean;
Preferences: Protobuf.RadioConfig_UserPreferences;
Connection: IHTTPConnection;
Translations: languageTemplate;
}
const SidebarDeviceSettings = (props: SidebarDeviceSettingsProps) => {
return (
<NavItem
isDropdown={true}
open={false}
isNested={false}
titleContent={
<div className="flex">
<AdjustmentsIcon className="my-auto mr-2 w-5 h-5" />
{props.Translations.device_settings_title}
</div>
}
isLoading={!props.IsReady}
dropdownContent={
<>
<div className="flex 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={props.Preferences?.region ?? Protobuf.RegionCode.Unset}
onChange={(e) => {
props.Preferences.region = parseInt(e.target.value);
}}
>
<option value={Protobuf.RegionCode.ANZ}>
{Protobuf.RegionCode[Protobuf.RegionCode.ANZ]}
</option>
<option value={Protobuf.RegionCode.CN}>
{Protobuf.RegionCode[Protobuf.RegionCode.CN]}
</option>
<option value={Protobuf.RegionCode.EU433}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU433]}
</option>
<option value={Protobuf.RegionCode.EU865}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU865]}
</option>
<option value={Protobuf.RegionCode.JP}>
{Protobuf.RegionCode[Protobuf.RegionCode.JP]}
</option>
<option value={Protobuf.RegionCode.KR}>
{Protobuf.RegionCode[Protobuf.RegionCode.KR]}
</option>
<option value={Protobuf.RegionCode.TW}>
{Protobuf.RegionCode[Protobuf.RegionCode.TW]}
</option>
<option value={Protobuf.RegionCode.US}>
{Protobuf.RegionCode[Protobuf.RegionCode.US]}
</option>
<option value={Protobuf.RegionCode.Unset}>
{Protobuf.RegionCode[Protobuf.RegionCode.Unset]}
</option>
</select>
</div>
</div>
<div className="flex whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{props.Translations.device_wifi_ssid}</div>
<div className="flex shadow-md rounded-md ml-2">
<input
onChange={() => {}}
type="text"
value={props.Preferences.wifiSsid}
/>
</div>
</div>
<div className="flex whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{props.Translations.device_wifi_psk}</div>
<div className="flex shadow-md rounded-md ml-2">
<input type="password" value={props.Preferences.wifiPassword} />
</div>
</div>
<div className="flex group p-1 bg-gray-100 cursor-pointer hover:bg-gray-200 border-b">
<div
className="flex m-auto font-medium group-hover:text-gray-700"
onClick={() => {
props.Connection.setPreferences(props.Preferences);
}}
>
<SaveIcon className="m-auto mr-2 group-hover:text-gray-700 w-5 h-5" />
{props.Translations.save_changes_button}
</div>
</div>
</>
}
/>
);
};
export default SidebarDeviceSettings;
-119
View File
@@ -1,119 +0,0 @@
import React from 'react';
import {
ClockIcon,
DesktopComputerIcon,
FlagIcon,
GlobeIcon,
LightningBoltIcon,
UsersIcon,
} from '@heroicons/react/outline';
import type { Types } from '@meshtastic/meshtasticjs';
import type { languageTemplate } from '../../App';
import NavItem from '../NavItem';
interface sidebarNodesProps {
IsReady: boolean;
Nodes: Types.NodeInfoPacket[];
Translations: languageTemplate;
myId: number;
}
const SidebarNodes = (props: sidebarNodesProps) => {
return (
<NavItem
isDropdown={true}
open={false}
isNested={false}
titleContent={
<div className="flex">
<UsersIcon className="my-auto mr-2 w-5 h-5" />
{props.Translations.nodes_title}
<div className="flex m-auto rounded-full bg-gray-300 w-6 h-6 text-sm ml-2">
<div className="m-auto">{props.Nodes.length ?? 0}</div>
</div>
</div>
}
isLoading={!props.IsReady}
dropdownContent={
props.Nodes.length ? (
props.Nodes.map((node, index) => (
<NavItem
key={index}
isDropdown={true}
isNested={true}
open={false}
titleContent={
<div key={index} className="flex">
{node.data.num === props.myId ? (
<FlagIcon className="text-yellow-500 my-auto mr-2 w-5 h-5" />
) : (
<DesktopComputerIcon className="my-auto mr-2 w-5 h-5" />
)}
<div className="m-auto">{node.data.user?.longName}</div>
</div>
}
dropdownContent={
<NavItem
isDropdown={false}
isNested={true}
open={false}
titleContent={
<div>
<p>
SNR:{' '}
{node.packet?.rxSnr ? node.packet.rxSnr : 'Unknown'}
</p>
<p>
RSSI:{' '}
{node.packet?.rxRssi ? node.packet.rxRssi : 'Unknown'}
</p>
<p>
{`Last heard: ${
node.data?.lastHeard
? new Date(node.data.lastHeard).toLocaleString()
: 'Unknown'
}`}{' '}
{}
</p>
<div className="flex">
<GlobeIcon className="my-auto mr-2 w-5 h-5" />
<p>
{node.data.position?.latitudeI},
{node.data.position?.longitudeI}
</p>
</div>
<div className="flex">
<GlobeIcon className="my-auto mr-2 w-5 h-5" />
<p>{node.data.position?.altitude}</p>
</div>
<div className="flex">
<ClockIcon className="my-auto mr-2 w-5 h-5" />
<p>{node.data.position?.time}</p>
</div>
<div className="flex">
<LightningBoltIcon className="my-auto mr-2 w-5 h-5" />
<p>{node.data.position?.batteryLevel}</p>
</div>
</div>
}
/>
}
/>
))
) : (
<div className="flex border-b border-gray-300">
<div className="m-auto p-3 text-gray-500">
{props.Translations.no_nodes_message}
</div>
</div>
)
}
/>
);
};
export default SidebarNodes;
@@ -1,132 +0,0 @@
import React from 'react';
import { Br, Jp, Us } from 'react-flags-select';
import { CogIcon } from '@heroicons/react/outline';
import type { languageTemplate } from '../../App';
import { LanguageEnum } from '../../App';
import ToggleSwitch from '../basic/ToggleSwitch';
import NavItem from '../NavItem';
interface SidebarUISettingsProps {
Language: LanguageEnum;
SetLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
Translations: languageTemplate;
}
const SidebarUISettings = (props: SidebarUISettingsProps) => {
return (
<NavItem
isDropdown={true}
isNested={false}
open={false}
titleContent={
<div className="flex">
<CogIcon className="my-auto mr-2 w-5 h-5" />
{props.Translations.ui_settings_title}
</div>
}
dropdownContent={
<>
<NavItem
isDropdown={false}
isNested={true}
open={false}
titleContent={
<>
<div className="my-auto">
{props.Translations.color_scheme_title}
</div>
<div className="flex shadow-md rounded-md ml-2">
{/* <div className="bg-gray-200 flex group p-2 rounded-l-md border border-gray-300 hover:bg-gray-200 cursor-pointer">
<FaSun className="m-auto group-hover:text-gray-700" />
</div>
<div className="flex group p-2 border border-gray-300 hover:bg-gray-200 cursor-pointer">
<FaMoon className="m-auto group-hover:text-gray-700" />
</div>
<div className="flex group p-2 rounded-r-md border border-gray-300 hover:bg-gray-200 cursor-pointer">
<FaLaptop className="m-auto group-hover:text-gray-700" />
</div> */}
</div>
</>
}
/>
<NavItem
isDropdown={true}
isNested={true}
open={false}
titleContent={
<div className="flex my-auto">
{props.Translations.language_title}
<div className="my-auto">
{props.Language === LanguageEnum.ENGLISH ? (
<Us className="ml-2 w-8 shadow-md" />
) : props.Language === LanguageEnum.JAPANESE ? (
<Jp className="ml-2 w-8 shadow-md" />
) : null}
</div>
</div>
}
dropdownContent={
<>
<NavItem
onClick={() => {
props.SetLanguage(LanguageEnum.ENGLISH);
}}
open={false}
isDropdown={false}
isNested={true}
titleContent={
<>
English <Us className="w-8 shadow-md" />
</>
}
/>
<NavItem
onClick={() => {
props.SetLanguage(LanguageEnum.PORTUGUESE);
}}
open={false}
isDropdown={false}
isNested={true}
titleContent={
<>
Português <Br className="w-8 shadow-md" />
</>
}
/>
<NavItem
onClick={() => {
props.SetLanguage(LanguageEnum.JAPANESE);
}}
open={false}
isDropdown={false}
isNested={true}
titleContent={
<>
日本語 <Jp className="w-8 shadow-md" />
</>
}
/>
</>
}
/>
<NavItem
isDropdown={false}
isNested={true}
open={false}
titleContent={
<>
<div className="">Test</div>
<ToggleSwitch active={true} />
</>
}
/>
</>
}
/>
);
};
export default SidebarUISettings;
+50
View File
@@ -0,0 +1,50 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
ChevronDownIcon,
ChevronRightIcon,
CogIcon,
} from '@heroicons/react/outline';
import type { LanguageEnum, languageTemplate } from '../../../App';
import Translations from './Translations';
interface UIProps {
language: LanguageEnum;
setLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
translations: languageTemplate;
darkmode: boolean;
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
const UI = (props: UIProps) => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex">
<CogIcon className="my-auto mr-2 w-5 h-5" />
{props.translations.ui_settings_title}
</div>
{open ? (
<ChevronDownIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
) : (
<ChevronRightIcon className="my-auto group-hover:text-gray-700 w-5 h-5" />
)}
</Disclosure.Button>
<Disclosure.Panel>
<Translations
language={props.language}
setLanguage={props.setLanguage}
translations={props.translations}
/>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default UI;
@@ -0,0 +1,49 @@
import React from 'react';
import { Br, Jp, Us } from 'react-flags-select';
import { Disclosure } from '@headlessui/react';
import { LanguageEnum, languageTemplate } from '../../../App';
export interface TranslationsProps {
language: LanguageEnum;
setLanguage: React.Dispatch<React.SetStateAction<LanguageEnum>>;
translations: languageTemplate;
}
const Translations = (props: TranslationsProps) => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex my-auto">
{props.translations.language_title}
<div className="my-auto">
{props.language === LanguageEnum.ENGLISH ? (
<Us className="ml-2 w-8 shadow-md" />
) : props.language === LanguageEnum.JAPANESE ? (
<Jp className="ml-2 w-8 shadow-md" />
) : null}
</div>
</div>
</Disclosure.Button>
<Disclosure.Panel>
<div>
English <Us className="w-8 shadow-md" />
</div>
<div>
Português <Br className="w-8 shadow-md" />
</div>
<div>
日本語 <Jp className="w-8 shadow-md" />
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Translations;
+8 -7
View File
@@ -1,5 +1,7 @@
import React from 'react';
import { Switch } from '@headlessui/react';
interface ToggleSwitchProps {
active: boolean;
toggle?: Function;
@@ -13,20 +15,19 @@ const ToggleSwitch = (props: ToggleSwitchProps) => {
}, []);
return (
<div
onClick={() => {
setActive(!active);
}}
<Switch
checked={active}
onChange={setActive}
className={`w-12 h-6 flex items-center bg-gray-300 rounded-full p-1 duration-300 ease-in-out my-auto ${
active ? 'bg-green-400' : null
}`}
>
<div
<span
className={`bg-white w-4 h-4 rounded-full shadow-md transform duration-300 ease-in-out ${
active ? 'translate-x-6' : null
}`}
></div>
</div>
></span>
</Switch>
);
};