Browse Source

WIP

pull/1/head
Sacha Weatherstone 5 years ago
parent
commit
c543a4ef5c
  1. 26
      package.json
  2. 21
      src/App.tsx
  3. 70
      src/Main.tsx
  4. 65
      src/components/ChatMessage.tsx
  5. 16
      src/components/Header.tsx
  6. 4
      src/components/Logo.tsx
  7. 19
      src/components/MessageBox.tsx
  8. 24
      src/components/Sidebar.tsx
  9. 4
      src/components/Sidebar/Channels/Channel.tsx
  10. 6
      src/components/Sidebar/Channels/ChannelList.tsx
  11. 53
      src/components/Sidebar/Channels/Index.tsx
  12. 55
      src/components/Sidebar/Device/Index.tsx
  13. 16
      src/components/Sidebar/Device/Settings.tsx
  14. 57
      src/components/Sidebar/Nodes/Index.tsx
  15. 11
      src/components/Sidebar/Nodes/Node.tsx
  16. 12
      src/components/Sidebar/Nodes/NodeList.tsx
  17. 49
      src/components/Sidebar/UI/Index.tsx
  18. 24
      src/components/Sidebar/UI/Translations.tsx
  19. 74
      src/components/basic/Dropdown.tsx
  20. 4
      src/components/basic/ToggleSwitch.tsx
  21. 7
      src/hooks/redux.ts
  22. 42
      src/hooks/useTranslationsContextValue.ts
  23. 3
      src/index.tsx
  24. 29
      src/slices/appSlice.ts
  25. 24
      src/slices/meshtasticSlice.ts
  26. 28
      src/store.ts
  27. 19
      src/translation.ts
  28. 41
      src/translations/TranslationsContext.tsx
  29. 22
      src/translations/en.json
  30. 17
      src/translations/en.ts
  31. 22
      src/translations/jp.json
  32. 16
      src/translations/jp.ts
  33. 22
      src/translations/pt.json
  34. 17
      src/translations/pt.ts
  35. 3263
      yarn-error.log
  36. 2562
      yarn.lock

26
package.json

@ -16,27 +16,31 @@
"@meshtastic/meshtasticjs": "^0.6.13",
"@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-568dc3532",
"react-dom": "^18.0.0-alpha-568dc3532",
"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-rc.2",
"redux-observable": "^2.0.0",
"rxjs": "^7.1.0"
},
"devDependencies": {
"@snowpack/plugin-dotenv": "^2.0.5",
"@snowpack/plugin-postcss": "^1.4.1",
"@snowpack/plugin-postcss": "^1.4.3",
"@snowpack/plugin-react-refresh": "^2.5.0",
"@snowpack/plugin-typescript": "^1.2.0",
"@types/eslint": "^7.2.13",
"@types/react": "^17.0.11",
"@types/react": "^17.0.13",
"@types/react-dom": "^17.0.8",
"@types/react-redux": "^7.1.16",
"@types/snowpack-env": "^2.3.3",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"autoprefixer": "^10.2.6",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
@ -47,9 +51,9 @@
"gzipper": "^5.0.0",
"postcss": "^8.3.5",
"postcss-cli": "^8.3.1",
"prettier": "^2.3.1",
"snowpack": "^3.6.0",
"tailwindcss": "^2.2.2",
"typescript": "^4.3.4"
"prettier": "^2.3.2",
"snowpack": "^3.7.1",
"tailwindcss": "^2.2.4",
"typescript": "^4.3.5"
}
}

21
src/App.tsx

@ -12,18 +12,19 @@ import {
Types,
} from '@meshtastic/meshtasticjs';
import Header from './components/Header';
import Main from './Main';
import { Header } from './components/Header';
import { useAppDispatch } from './hooks/redux';
import { Main } from './Main';
import { setMyId } from './slices/meshtasticSlice';
import { channelSubject$, nodeSubject$, preferencesSubject$ } from './streams';
const App = (): JSX.Element => {
const dispatch = useAppDispatch();
const [deviceStatus, setDeviceStatus] =
React.useState<Types.DeviceStatusEnum>(
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
);
const [myNodeInfo, setMyNodeInfo] = React.useState<Protobuf.MyNodeInfo>(
Protobuf.MyNodeInfo.create(),
);
const [connection, setConnection] = React.useState<
ISerialConnection | IHTTPConnection | IBLEConnection
>(new IHTTPConnection());
@ -58,8 +59,13 @@ const App = (): JSX.Element => {
}
},
);
const myNodeInfoEvent =
connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
// const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(
(nodeInfo) => {
dispatch(setMyId(nodeInfo.myNodeNum));
},
);
const nodeInfoPacketEvent = connection.onNodeInfoPacketEvent.subscribe(
(node) => nodeSubject$.next(node),
@ -108,7 +114,6 @@ const App = (): JSX.Element => {
/>
<Main
isReady={isReady}
myNodeInfo={myNodeInfo}
connection={connection}
darkmode={darkmode}
setDarkmode={setDarkmode}

70
src/Main.tsx

@ -1,35 +1,30 @@
import React, { useState } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
Types,
} from '@meshtastic/meshtasticjs';
import ChatMessage from './components/ChatMessage';
import MessageBox from './components/MessageBox';
import Sidebar from './components/Sidebar';
import { useTranslationsContextValue } from './hooks/useTranslationsContextValue';
import { TranslationsContext } from './translations/TranslationsContext';
import { ChatMessage } from './components/ChatMessage';
import { MessageBox } from './components/MessageBox';
import { Sidebar } from './components/Sidebar';
interface MainProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
myNodeInfo: Protobuf.MyNodeInfo;
isReady: boolean;
darkmode: boolean;
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
const Main = (props: MainProps): JSX.Element => {
const translationsContextValue = useTranslationsContextValue();
const { translations } = React.useContext(TranslationsContext);
export const Main = (props: MainProps): JSX.Element => {
const [messages, setMessages] = React.useState<
{ message: Types.TextPacket; ack: boolean }[]
>([]);
const [sidebarOpen, setSidebarOpen] = useState<boolean>(false);
const { t } = useTranslation();
React.useEffect(() => {
const textPacketEvent = props.connection.onTextPacketEvent.subscribe(
@ -65,41 +60,22 @@ const Main = (props: MainProps): JSX.Element => {
}, [props.connection, messages]);
return (
<TranslationsContext.Provider value={translationsContextValue}>
<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 space-y-2">
{messages.length ? (
messages.map((message, Main) => (
<ChatMessage
key={Main}
message={message}
myId={props.myNodeInfo.myNodeNum}
/>
))
) : (
<div className="m-auto text-2xl text-gray-500">
{translations.no_messages_message}
</div>
)}
</div>
<MessageBox
connection={props.connection}
isReady={props.isReady}
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
/>
<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 space-y-2">
{messages.length ? (
messages.map((message, Main) => (
<ChatMessage key={Main} message={message} />
))
) : (
<div className="m-auto text-2xl text-gray-500">
{t('placeholder.no_messages')}
</div>
)}
</div>
<Sidebar
myId={props.myNodeInfo.myNodeNum}
sidebarOpen={sidebarOpen}
darkmode={props.darkmode}
setDarkmode={props.setDarkmode}
connection={props.connection}
/>
<MessageBox connection={props.connection} isReady={props.isReady} />
</div>
</TranslationsContext.Provider>
<Sidebar connection={props.connection} />
</div>
);
};
export default Main;

65
src/components/ChatMessage.tsx

@ -9,15 +9,16 @@ import {
} from '@heroicons/react/outline';
import type { Types } from '@meshtastic/meshtasticjs';
import { useAppSelector } from '../hooks/redux';
import { nodeResource } from '../streams';
interface ChatMessageProps {
message: { message: Types.TextPacket; ack: boolean };
myId: number;
}
const ChatMessage = (props: ChatMessageProps): JSX.Element => {
export const ChatMessage = (props: ChatMessageProps): JSX.Element => {
const nodeSource = useObservableSuspense(nodeResource);
const myId = useAppSelector((state) => state.meshtastic.myId);
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
@ -54,38 +55,44 @@ const ChatMessage = (props: ChatMessageProps): JSX.Element => {
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
/>
<div className="flex flex-col container px-2 items-start">
<div
className={`px-4 py-2 rounded-md shadow-md ${
props.message.message.packet.from !== props.myId
? 'bg-gray-300'
: 'bg-green-200'
}`}
<React.Suspense
fallback={
<div className="flex border-b border-gray-300">
<div className="m-auto p-3 text-gray-500">Loading</div>
</div>
}
>
<div className="flex text-xs text-gray-500 space-x-1">
<div className="font-medium">
{node?.data.user?.longName ?? 'UNK'}
<div
className={`px-4 py-2 rounded-md shadow-md ${
props.message.message.packet.from !== myId
? 'bg-gray-300'
: 'bg-green-200'
}`}
>
<div className="flex text-xs text-gray-500 space-x-1">
<div className="font-medium">
{node?.data.user?.longName ?? 'UNK'}
</div>
<p>-</p>
<div className="underline">
{new Date(
props.message.message.packet.rxTime > 0
? props.message.message.packet.rxTime
: Date.now(),
).toLocaleString()}
</div>
</div>
<p>-</p>
<div className="underline">
{new Date(
props.message.message.packet.rxTime > 0
? props.message.message.packet.rxTime
: Date.now(),
).toLocaleString()}
<div className="flex justify-between text-gray-600">
<span className="inline-block">{props.message.message.data}</span>
{props.message.ack ? (
<CheckCircleIcon className="my-auto w-5 h-5" />
) : (
<DotsCircleHorizontalIcon className="my-auto animate-pulse w-5 h-5" />
)}
</div>
</div>
<div className="flex justify-between text-gray-600">
<span className="inline-block">{props.message.message.data}</span>
{props.message.ack ? (
<CheckCircleIcon className="my-auto w-5 h-5" />
) : (
<DotsCircleHorizontalIcon className="my-auto animate-pulse w-5 h-5" />
)}
</div>
</div>
</React.Suspense>
</div>
</div>
);
};
export default ChatMessage;

16
src/components/Header.tsx

@ -12,7 +12,7 @@ import type {
} from '@meshtastic/meshtasticjs';
import { Types } from '@meshtastic/meshtasticjs';
import Logo from './logo';
import { Logo } from './Logo';
interface HeaderProps {
status: Types.DeviceStatusEnum;
@ -21,9 +21,9 @@ interface HeaderProps {
connection: IHTTPConnection | ISerialConnection | IBLEConnection;
}
const Header = (props: HeaderProps): JSX.Element => {
export const Header = (props: HeaderProps): JSX.Element => {
return (
<nav className="w-full shadow-md">
<nav className="select-none w-full shadow-md">
<div className="flex w-full container mx-auto justify-between px-6 py-4">
<Logo />
<div></div>
@ -34,10 +34,10 @@ const Header = (props: HeaderProps): JSX.Element => {
className={`w-5 h-5 rounded-full ${
new Date(props.LastMeshInterraction) <
new Date(Date.now() - 40000)
? 'bg-red-400'
? 'bg-red-400 animate-pulse'
: new Date(props.LastMeshInterraction) <
new Date(Date.now() - 20000)
? 'bg-yellow-400'
? 'bg-yellow-400 animate-pulse'
: 'bg-green-400'
}`}
></div>
@ -53,10 +53,10 @@ const Header = (props: HeaderProps): JSX.Element => {
<div
className={`w-5 h-5 rounded-full ${
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
? 'bg-red-400'
? 'bg-red-400 animate-pulse'
: props.status <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
!props.IsReady
? 'bg-yellow-400'
? 'bg-yellow-400 animate-pulse'
: props.IsReady
? 'bg-green-400'
: 'bg-gray-400'
@ -69,5 +69,3 @@ const Header = (props: HeaderProps): JSX.Element => {
</nav>
);
};
export default Header;

4
src/components/logo.tsx → src/components/Logo.tsx

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

19
src/components/MessageBox.tsx

@ -1,5 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
import type {
IBLEConnection,
@ -7,17 +9,15 @@ import type {
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { TranslationsContext } from '../translations/TranslationsContext';
import { useAppDispatch } from '../hooks/redux';
import { toggleSidebar } from '../slices/appSlice';
export interface MessageBoxProps {
sidebarOpen: boolean;
setSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
isReady: boolean;
}
const MessageBox = (props: MessageBoxProps): JSX.Element => {
const { translations } = React.useContext(TranslationsContext);
export const MessageBox = (props: MessageBoxProps): JSX.Element => {
const [currentMessage, setCurrentMessage] = React.useState('');
const sendMessage = () => {
if (props.isReady) {
@ -25,12 +25,15 @@ const MessageBox = (props: MessageBoxProps): JSX.Element => {
setCurrentMessage('');
}
};
const { t } = useTranslation();
const dispatch = useAppDispatch();
return (
<div className="flex text-lg font-medium 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 border shadow-md focus:outline-none cursor-pointer md:hidden"
onClick={() => {
props.setSidebarOpen(!props.sidebarOpen);
dispatch(toggleSidebar());
}}
>
<MenuIcon className="m-auto h-6 2-6" />
@ -45,7 +48,7 @@ const MessageBox = (props: MessageBoxProps): JSX.Element => {
{props.isReady}
<input
type="text"
placeholder={`${translations.no_message_placeholder}...`}
placeholder={`${t('placeholder.no_messages')}...`}
disabled={!props.isReady}
value={currentMessage}
onChange={(e) => {
@ -67,5 +70,3 @@ const MessageBox = (props: MessageBoxProps): JSX.Element => {
</div>
);
};
export default MessageBox;

24
src/components/Sidebar.tsx

@ -6,33 +6,29 @@ import type {
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import Channels from './Sidebar/Channels/Index';
import Device from './Sidebar/Device/Index';
import Nodes from './Sidebar/Nodes/Index';
import UI from './Sidebar/UI/Index';
import { useAppSelector } from '../hooks/redux';
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 {
myId: number;
sidebarOpen: boolean;
darkmode: boolean;
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
}
const Sidebar = (props: SidebarProps): JSX.Element => {
export const Sidebar = (props: SidebarProps): JSX.Element => {
const sidebarOpen = useAppSelector((state) => state.app.sidebarOpen);
return (
<div
className={`${
props.sidebarOpen ? 'flex' : 'hidden md:flex'
sidebarOpen ? 'flex' : 'hidden md:flex'
} flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm`}
>
<Nodes myId={props.myId} />
<Nodes />
<Device connection={props.connection} />
<Channels />
<div className="flex-grow border-b"></div>
<UI darkmode={props.darkmode} setDarkmode={props.setDarkmode} />
<UI />
</div>
);
};
export default Sidebar;

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

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

6
src/components/Sidebar/Channels/ChannelList.tsx

@ -5,9 +5,9 @@ import { useObservableSuspense } from 'observable-hooks';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { channelResource } from '../../../streams';
import Channel from './Channel';
import { Channel } from './Channel';
const ChannelList = (): JSX.Element => {
export const ChannelList = (): JSX.Element => {
const channelSource = useObservableSuspense(channelResource);
const [channels, setChannels] = React.useState<Protobuf.Channel[]>([]);
@ -39,5 +39,3 @@ const ChannelList = (): JSX.Element => {
</>
);
};
export default ChannelList;

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

@ -1,47 +1,20 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
ChevronDownIcon,
ChevronRightIcon,
HashtagIcon,
} from '@heroicons/react/outline';
import { useTranslation } from 'react-i18next';
import { TranslationsContext } from '../../../translations/TranslationsContext';
import ChannelList from './ChannelList';
import { HashtagIcon } from '@heroicons/react/outline';
const Channels = (): JSX.Element => {
const { translations } = React.useContext(TranslationsContext);
import { Dropdown } from '../../basic/Dropdown';
import { ChannelList } from './ChannelList';
export const Channels = (): JSX.Element => {
const { t } = useTranslation();
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">
{open ? (
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
) : (
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
)}
<HashtagIcon className="my-auto text-gray-600 mr-2 2-5 h-5" />
{translations.device_channels_title}
</div>
</Disclosure.Button>
<Disclosure.Panel>
<React.Suspense
fallback={
<div className="flex border-b border-gray-300">
<div className="m-auto p-3 text-gray-500">Loading...</div>
</div>
}
>
<ChannelList />
</React.Suspense>
</Disclosure.Panel>
</>
)}
</Disclosure>
<Dropdown
icon={<HashtagIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
title={t('settings.channel')}
content={<ChannelList />}
fallbackMessage={'Loading...'}
/>
);
};
export default Channels;

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

@ -1,58 +1,29 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
AdjustmentsIcon,
ChevronDownIcon,
ChevronRightIcon,
} from '@heroicons/react/outline';
import { useTranslation } from 'react-i18next';
import { AdjustmentsIcon } from '@heroicons/react/outline';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { TranslationsContext } from '../../../translations/TranslationsContext';
import Settings from './Settings';
import { Dropdown } from '../../basic/Dropdown';
import { Settings } from './Settings';
interface DeviceProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
}
const Device = (props: DeviceProps): JSX.Element => {
const { translations } = React.useContext(TranslationsContext);
export const Device = (props: DeviceProps): JSX.Element => {
const { t } = useTranslation();
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">
{open ? (
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
) : (
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
)}
<AdjustmentsIcon className="text-gray-600 my-auto mr-2 w-5 h-5" />
{translations.device_settings_title}
</div>
</Disclosure.Button>
<Disclosure.Panel>
<>
<React.Suspense
fallback={
<div className="flex border-b border-gray-300">
<div className="m-auto p-3 text-gray-500">Loading...</div>
</div>
}
>
<Settings connection={props.connection} />
</React.Suspense>
</>
</Disclosure.Panel>
</>
)}
</Disclosure>
<Dropdown
icon={<AdjustmentsIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
title={t('settings.device')}
content={<Settings connection={props.connection} />}
fallbackMessage={'Loading...'}
/>
);
};
export default Device;

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

@ -2,6 +2,7 @@ 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 {
@ -12,14 +13,13 @@ import type {
import { Protobuf } from '@meshtastic/meshtasticjs';
import { preferencesResource } from '../../../streams';
import { TranslationsContext } from '../../../translations/TranslationsContext';
interface SettingsProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
}
const Settings = (props: SettingsProps): JSX.Element => {
const { translations } = React.useContext(TranslationsContext);
export const Settings = (props: SettingsProps): JSX.Element => {
const { t } = useTranslation();
const preferences = useObservableSuspense(preferencesResource);
const { register, handleSubmit } =
@ -33,7 +33,7 @@ const Settings = (props: SettingsProps): JSX.Element => {
return (
<form onSubmit={onSubmit}>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{translations.device_region_title}</div>
<div className="my-auto">{t('strings.device_region')}</div>
<div className="flex shadow-md rounded-md ml-2">
<select
{...register('region', {
@ -71,13 +71,13 @@ const Settings = (props: SettingsProps): JSX.Element => {
</div>
</div>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{translations.device_wifi_ssid}</div>
<div className="my-auto">{t('strings.wifi_ssid')}</div>
<div className="flex shadow-md rounded-md ml-2">
<input {...register('wifiSsid', {})} type="text" />
</div>
</div>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{translations.device_wifi_psk}</div>
<div className="my-auto">{t('strings.wifi_psk')}</div>
<div className="flex shadow-md rounded-md ml-2">
<input {...register('wifiPassword', {})} type="password" />
</div>
@ -88,11 +88,9 @@ const Settings = (props: SettingsProps): JSX.Element => {
className="flex m-auto font-medium group-hover:text-gray-700"
>
<SaveIcon className="m-auto mr-2 group-hover:text-gray-700 w-5 h-5" />
{translations.save_changes_button}
{t('strings.save_changes')}
</button>
</div>
</form>
);
};
export default Settings;

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

@ -1,53 +1,20 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
ChevronDownIcon,
ChevronRightIcon,
UsersIcon,
} from '@heroicons/react/outline';
import { useTranslation } from 'react-i18next';
import { TranslationsContext } from '../../../translations/TranslationsContext';
import NodeList from './NodeList';
import { UsersIcon } from '@heroicons/react/outline';
interface NodesProps {
myId: number;
}
import { Dropdown } from '../../basic/Dropdown';
import { NodeList } from './NodeList';
const Nodes = (props: NodesProps): JSX.Element => {
const { translations } = React.useContext(TranslationsContext);
export const Nodes = (): JSX.Element => {
const { t } = useTranslation();
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">
{open ? (
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
) : (
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
)}
<UsersIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />
{translations.nodes_title}
</div>
</Disclosure.Button>
<Disclosure.Panel className="shadow-inner">
<React.Suspense
fallback={
<div className="flex border-b border-gray-300">
<div className="m-auto p-3 text-gray-500">
{translations.no_nodes_message}
</div>
</div>
}
>
<NodeList myId={props.myId} />
</React.Suspense>
</Disclosure.Panel>
</>
)}
</Disclosure>
<Dropdown
icon={<UsersIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
title={t('placeholder.no_nodes')}
content={<NodeList />}
fallbackMessage={t('placeholder.no_messages')}
/>
);
};
export default Nodes;

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

@ -13,12 +13,15 @@ import {
} from '@heroicons/react/outline';
import type { Types } from '@meshtastic/meshtasticjs';
import { useAppSelector } from '../../../hooks/redux';
export interface NodeProps {
node: Types.NodeInfoPacket;
myId: number;
}
const Node = (props: NodeProps): JSX.Element => {
export const Node = (props: NodeProps): JSX.Element => {
const myId = useAppSelector((state) => state.meshtastic.myId);
return (
<Disclosure>
{({ open }) => (
@ -31,7 +34,7 @@ const Node = (props: NodeProps): JSX.Element => {
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
)}
<div className="relative">
{props.node.data.num === props.myId ? (
{props.node.data.num === myId ? (
<FlagIcon className="absolute -right-1 -top-2 text-yellow-500 my-auto w-4 h-4" />
) : null}
<Avatar
@ -98,5 +101,3 @@ const Node = (props: NodeProps): JSX.Element => {
</Disclosure>
);
};
export default Node;

12
src/components/Sidebar/Nodes/NodeList.tsx

@ -5,13 +5,9 @@ import { useObservableSuspense } from 'observable-hooks';
import type { Types } from '@meshtastic/meshtasticjs';
import { nodeResource } from '../../../streams';
import Node from './Node';
import { Node } from './Node';
export interface NodeListProps {
myId: number;
}
const NodeList = (props: NodeListProps): JSX.Element => {
export const NodeList = (): JSX.Element => {
const nodeSource = useObservableSuspense(nodeResource);
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
@ -37,10 +33,8 @@ const NodeList = (props: NodeListProps): JSX.Element => {
return (
<>
{nodes.map((node, index) => (
<Node key={index} node={node} myId={props.myId} />
<Node key={index} node={node} />
))}
</>
);
};
export default NodeList;

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

@ -1,44 +1,21 @@
import React from 'react';
import { Disclosure } from '@headlessui/react';
import {
ChevronDownIcon,
ChevronRightIcon,
CogIcon,
} from '@heroicons/react/outline';
import { useTranslation } from 'react-i18next';
import { TranslationsContext } from '../../../translations/TranslationsContext';
import Translations from './Translations';
import { CogIcon } from '@heroicons/react/outline';
interface UIProps {
darkmode: boolean;
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
import { Dropdown } from '../../basic/Dropdown';
import { Translations } from './Translations';
export const UI = (): JSX.Element => {
const { t } = useTranslation();
const UI = (props: UIProps): JSX.Element => {
const { translations } = React.useContext(TranslationsContext);
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">
{open ? (
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
) : (
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
)}
<CogIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />
{translations.ui_settings_title}
</div>
</Disclosure.Button>
<Disclosure.Panel>
<Translations />
</Disclosure.Panel>
</>
)}
</Disclosure>
<Dropdown
icon={<CogIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
title={t('settings.ui')}
content={<Translations />}
fallbackMessage={'Loading...'}
/>
);
};
export default UI;

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

@ -1,18 +1,14 @@
import React from 'react';
import { Br, Jp, Us } from 'react-flags-select';
import { useTranslation } from 'react-i18next';
import { Disclosure } from '@headlessui/react';
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/outline';
import {
LanguageEnum,
TranslationsContext,
} from '../../../translations/TranslationsContext';
export const Translations = (): JSX.Element => {
const { t } = useTranslation();
const Translations = (): JSX.Element => {
const { translations, language, setLanguage } =
React.useContext(TranslationsContext);
return (
<Disclosure>
{({ open }) => (
@ -24,8 +20,8 @@ const Translations = (): JSX.Element => {
) : (
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
)}
{translations.language_title}
<div className="my-auto">
{t('strings.language')}
{/* <div className="my-auto">
{language === LanguageEnum.ENGLISH ? (
<Us className="ml-2 w-8" />
) : language === LanguageEnum.JAPANESE ? (
@ -33,14 +29,14 @@ const Translations = (): JSX.Element => {
) : language === LanguageEnum.PORTUGUESE ? (
<Br className="ml-2 w-8" />
) : null}
</div>
</div> */}
</div>
</Disclosure.Button>
<Disclosure.Panel>
<div
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
onClick={() => {
setLanguage(LanguageEnum.ENGLISH);
// setLanguage(LanguageEnum.ENGLISH);
}}
>
English <Us className="w-8 my-auto" />
@ -48,7 +44,7 @@ const Translations = (): JSX.Element => {
<div
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
onClick={() => {
setLanguage(LanguageEnum.PORTUGUESE);
// setLanguage(LanguageEnum.PORTUGUESE);
}}
>
Português <Br className="w-8 my-auto" />
@ -56,7 +52,7 @@ const Translations = (): JSX.Element => {
<div
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
onClick={() => {
setLanguage(LanguageEnum.JAPANESE);
// setLanguage(LanguageEnum.JAPANESE);
}}
>
<Jp className="w-8 my-auto" />
@ -67,5 +63,3 @@ const Translations = (): JSX.Element => {
</Disclosure>
);
};
export default Translations;

74
src/components/basic/Dropdown.tsx

@ -0,0 +1,74 @@
import React from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { Disclosure } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/outline';
interface DropdownProps {
icon: JSX.Element;
title: string;
content: JSX.Element;
fallbackMessage: string;
}
export const Dropdown = (props: DropdownProps): JSX.Element => {
return (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="bg-white flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
<div className="flex">
<motion.div
className="my-auto mr-2"
variants={{
initial: { rotate: -90 },
animate: {
rotate: 0,
},
}}
initial="initial"
animate={open ? 'animate' : 'initial'}
>
<ChevronDownIcon className="w-5 h-5" />
</motion.div>
{props.icon}
{props.title}
</div>
</Disclosure.Button>
<AnimatePresence>
{open && (
<Disclosure.Panel
as={motion.div}
static
initial={{
height: 0,
}}
animate={{
height: 'auto',
}}
exit={{
height: 0,
}}
className="shadow-inner"
>
<React.Suspense
fallback={
<div className="flex border-b border-gray-300">
<div className="m-auto p-3 text-gray-500">
{props.fallbackMessage}
</div>
</div>
}
>
{props.content}
</React.Suspense>
</Disclosure.Panel>
)}
</AnimatePresence>
</>
)}
</Disclosure>
);
};

4
src/components/basic/ToggleSwitch.tsx

@ -6,7 +6,7 @@ interface ToggleSwitchProps {
active: boolean;
}
const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => {
export const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => {
const [active, setActive] = React.useState(false);
React.useEffect(() => {
@ -29,5 +29,3 @@ const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => {
</Switch>
);
};
export default ToggleSwitch;

7
src/hooks/redux.ts

@ -0,0 +1,7 @@
import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from '../store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

42
src/hooks/useTranslationsContextValue.ts

@ -1,42 +0,0 @@
import React from 'react';
import Translations_EN from '../translations/en';
import Translations_JP from '../translations/jp';
import Translations_PT from '../translations/pt';
import type {
languageTemplate,
TranslationsContextData,
} from '../translations/TranslationsContext';
import { LanguageEnum } from '../translations/TranslationsContext';
export const useTranslationsContextValue = (): TranslationsContextData => {
const [currentLanguage, setcurrentLanguage] = React.useState<LanguageEnum>(
LanguageEnum.ENGLISH,
);
const [translation, setTranslation] =
React.useState<languageTemplate>(Translations_EN);
const setLanguage = React.useCallback(
(language: LanguageEnum) => {
setcurrentLanguage(language);
switch (language) {
case LanguageEnum.ENGLISH:
setTranslation(Translations_EN);
break;
case LanguageEnum.JAPANESE:
setTranslation(Translations_JP);
break;
case LanguageEnum.PORTUGUESE:
setTranslation(Translations_PT);
break;
}
},
[setcurrentLanguage, setTranslation],
);
return {
language: currentLanguage,
setLanguage: setLanguage,
translations: translation,
};
};

3
src/index.tsx

@ -1,4 +1,5 @@
import './index.css';
import './translation';
import React from 'react';
import ReactDOM from 'react-dom';
@ -6,7 +7,7 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';
import { store } from './store';
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Failed to find the root element');

29
src/slices/appSlice.ts

@ -0,0 +1,29 @@
import { createSlice } from '@reduxjs/toolkit';
interface AppState {
sidebarOpen: boolean;
}
const initialState: AppState = {
sidebarOpen: true,
};
export const appSlice = createSlice({
name: 'auth',
initialState,
reducers: {
openSidebar(state) {
state.sidebarOpen = true;
},
closeSidebar(state) {
state.sidebarOpen = false;
},
toggleSidebar(state) {
state.sidebarOpen = !state.sidebarOpen;
},
},
});
export const { openSidebar, closeSidebar, toggleSidebar } = appSlice.actions;
export default appSlice.reducer;

24
src/slices/meshtasticSlice.ts

@ -0,0 +1,24 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
interface AppState {
myId: number;
}
const initialState: AppState = {
myId: 0,
};
export const meshtasticSlice = createSlice({
name: 'meshtastic',
initialState,
reducers: {
setMyId: (state, action: PayloadAction<number>) => {
state.myId = action.payload;
},
},
});
export const { setMyId } = meshtasticSlice.actions;
export default meshtasticSlice.reducer;

28
src/store.ts

@ -1,24 +1,14 @@
import type { Protobuf } from '@meshtastic/meshtasticjs';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { configureStore } from '@reduxjs/toolkit';
const nodesSlice = createSlice({
name: 'nodes',
initialState: {
members: [],
},
reducers: {
addMember: (state: Protobuf.NodeInfo[], node: Protobuf.NodeInfo) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.push(node);
},
},
});
import appSlice from './slices/appSlice';
import meshtasticSlice from './slices/meshtasticSlice';
export default configureStore({
export const store = configureStore({
reducer: {
nodes: nodesSlice.reducer,
app: appSlice,
meshtastic: meshtasticSlice,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

19
src/translation.ts

@ -0,0 +1,19 @@
import i18n from 'i18next';
import detector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import en from './translations/en.json';
i18n
.use(detector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
resources: {
en: {
translation: en,
},
},
});
export default i18n;

41
src/translations/TranslationsContext.tsx

@ -1,41 +0,0 @@
import React from 'react';
import Translations_EN from './en';
export interface languageTemplate {
no_messages_message: string;
ui_settings_title: string;
nodes_title: string;
color_scheme_title: string;
language_title: string;
device_settings_title: string;
device_channels_title: string;
device_region_title: string;
device_wifi_ssid: string;
device_wifi_psk: string;
save_changes_button: string;
no_nodes_message: string;
no_message_placeholder: string;
}
export enum LanguageEnum {
ENGLISH,
JAPANESE,
PORTUGUESE,
}
export interface TranslationsContextData {
language: LanguageEnum;
setLanguage: (postId: number) => void;
translations: languageTemplate;
}
export const translationsContextDefaultValue: TranslationsContextData = {
language: LanguageEnum.ENGLISH,
setLanguage: () => null,
translations: Translations_EN,
};
export const TranslationsContext = React.createContext<TranslationsContextData>(
translationsContextDefaultValue,
);

22
src/translations/en.json

@ -0,0 +1,22 @@
{
"errors": {},
"placeholder": {
"message": "Enter Message",
"no_messages": "No messages yet",
"no_nodes": "No nodes found"
},
"strings": {
"nodes": "Nodes",
"color_scheme": "Color scheme",
"language": "Language",
"device_region": "Device region",
"wifi_ssid": "WiFi SSID",
"wifi_psk": "WiFi PSK",
"save_changes": "Save changes"
},
"settings": {
"ui": "UI Settings",
"device": "Device Settings",
"channel": "Channels"
}
}

17
src/translations/en.ts

@ -1,17 +0,0 @@
import type { languageTemplate } from './TranslationsContext';
export default {
no_messages_message: 'No messages yet',
ui_settings_title: 'UI Settings',
nodes_title: 'Nodes',
device_settings_title: 'Device Settings',
device_channels_title: 'Channels',
color_scheme_title: 'Color scheme',
language_title: 'Language',
device_region_title: 'Device Region',
device_wifi_ssid: 'WiFi SSID',
device_wifi_psk: 'WiFi PSK',
save_changes_button: 'Save changes',
no_nodes_message: 'No nodes found',
no_message_placeholder: 'Enter Message',
} as languageTemplate;

22
src/translations/jp.json

@ -0,0 +1,22 @@
{
"errors": {},
"placeholder": {
"message": "メッセージを入力してください",
"no_messages": "まだメッセージはありません",
"no_nodes": "ノードが見つかりません"
},
"strings": {
"nodes": "ノード",
"color_scheme": "カラースキーム",
"language": "言語",
"device_region": "デバイスリージョン",
"wifi_ssid": "WiFi名",
"wifi_psk": "WiFiパスワード",
"save_changes": "変更内容を保存"
},
"settings": {
"ui": "UI設定",
"device": "デバイスの設定",
"channel": "#################"
}
}

16
src/translations/jp.ts

@ -1,16 +0,0 @@
import type { languageTemplate } from './TranslationsContext';
export default {
no_messages_message: 'まだメッセージはありません',
ui_settings_title: 'UI設定',
nodes_title: 'ノード',
device_settings_title: 'デバイスの設定',
color_scheme_title: 'カラースキーム',
language_title: '言語',
device_region_title: 'デバイスリージョン',
device_wifi_ssid: 'WiFi名',
device_wifi_psk: 'WiFiパスワード',
save_changes_button: '変更内容を保存',
no_nodes_message: 'ノードが見つかりません',
no_message_placeholder: 'メッセージを入力してください',
} as languageTemplate;

22
src/translations/pt.json

@ -0,0 +1,22 @@
{
"errors": {},
"placeholder": {
"message": "Entre mensagem",
"no_messages": "Não a mensagens ainda",
"no_nodes": "Nenhum nó foi encontrado"
},
"strings": {
"nodes": "Nós",
"color_scheme": "Esquema de cores",
"language": "Idioma",
"device_region": "Região do dispositivo",
"wifi_ssid": "Nome do WiFi",
"wifi_psk": "Senha do WiFi",
"save_changes": "Salvar alterações"
},
"settings": {
"ui": "Configurações da Interface",
"device": "Configurações do dispositivo",
"channel": "Canais"
}
}

17
src/translations/pt.ts

@ -1,17 +0,0 @@
import type { languageTemplate } from './TranslationsContext';
export default {
no_messages_message: 'Não a mensagens ainda',
ui_settings_title: 'Configurações da Interface',
nodes_title: 'Nós',
device_settings_title: 'Configurações do dispositivo',
device_channels_title: 'Canais',
color_scheme_title: 'Esquema de cores',
language_title: 'Idioma',
device_region_title: 'Região do dispositivo',
device_wifi_ssid: 'Nome do WiFi',
device_wifi_psk: 'Senha do WiFi',
save_changes_button: 'Salvar alterações',
no_nodes_message: 'Nenhum nó foi encontrado',
no_message_placeholder: 'Entre mensagem',
} as languageTemplate;

3263
yarn-error.log

File diff suppressed because it is too large

2562
yarn.lock

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