Misc fixes

This commit is contained in:
Sacha Weatherstone
2021-12-17 23:09:17 +11:00
parent 8b221e28c5
commit 87dcf35547
14 changed files with 239 additions and 89 deletions
+36 -3
View File
@@ -1,9 +1,12 @@
import React from 'react';
import { FiBell } from 'react-icons/fi';
import { DeviceStatus } from '@app/components/menu/buttons/DeviceStatus';
import { useAppSelector } from '@app/hooks/redux';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Connection } from '@components/Connection';
import { MobileNavToggle } from '@components/menu/buttons/MobileNavToggle';
import { Notifications } from '@components/menu/buttons/Notifications';
import { ThemeToggle } from '@components/menu/buttons/ThemeToggle';
import { Logo } from '@components/menu/Logo';
import { MobileNav } from '@components/menu/MobileNav';
@@ -16,22 +19,51 @@ import { NotFound } from '@pages/NotFound';
import { Plugins } from '@pages/Plugins/Index';
import { Settings } from '@pages/settings/Index';
import { addNotification, removeNotification } from './core/slices/appSlice.js';
export const App = (): JSX.Element => {
const route = useRoute();
const dispatch = useAppDispatch();
const notifications = useAppSelector((state) => state.app.notifications);
const darkMode = useAppSelector((state) => state.app.darkMode);
React.useEffect(() => {
if (
Notification.permission !== 'granted' &&
notifications.findIndex((n) => n.id === 'notification-permission') === -1
) {
dispatch(
addNotification({
id: 'notification-permission',
icon: <FiBell className="w-4 h-4" />,
read: Notification.permission === 'denied',
title: 'Enable Push Notifications',
action: {
message: 'Enable',
action: async () => await requestNotificationPermission(),
},
}),
);
}
// Notification.permission === ''
requestNotificationPermission().catch((e) => {
console.log(e);
});
}, []);
}, [dispatch, notifications]);
React.useEffect(() => {
if (Notification.permission === 'granted') {
dispatch(removeNotification('notification-permission'));
}
}, [dispatch]);
return (
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
<Connection />
<div className="flex flex-col h-full bg-gray-200 dark:bg-primaryDark">
<div className="flex flex-shrink-0 overflow-hidden bg-primary dark:bg-primary">
<div className="w-full overflow-hidden bg-white border-b border-gray-300 md:mt-6 md:mx-6 md:pt-4 md:pb-3 md:rounded-t-3xl dark:border-gray-600 md:shadow-md dark:bg-primaryDark">
<div className="w-full overflow-hidden bg-white border-b border-gray-300 md:mt-6 md:mx-6 md:py-2 md:rounded-t-3xl dark:border-gray-600 md:shadow-md dark:bg-primaryDark">
<div className="flex items-center justify-between h-12 px-4 md:px-6">
<div className="hidden md:flex">
<Logo />
@@ -40,6 +72,7 @@ export const App = (): JSX.Element => {
<MobileNavToggle />
<div className="flex items-center space-x-2">
<DeviceStatus />
<Notifications />
<ThemeToggle />
</div>
</div>
-1
View File
@@ -76,7 +76,6 @@ export const Connection = (): JSX.Element => {
{state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED && (
<Button
padding={2}
border
onClick={async (): Promise<void> => {
await connection.disconnect();
+11 -1
View File
@@ -3,13 +3,15 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { FiCheck } from 'react-icons/fi';
import { useAppDispatch } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { IconButton } from '@components/generic/IconButton';
import { serial, setConnection } from '@core/connection';
import { connType } from '@core/slices/appSlice';
import { connType, setConnectionParams } from '@core/slices/appSlice';
export const Serial = (): JSX.Element => {
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]);
const dispatch = useAppDispatch();
const { register, handleSubmit, control } = useForm<{
device?: SerialPort;
@@ -45,6 +47,14 @@ export const Serial = (): JSX.Element => {
</div>
<IconButton
onClick={async (): Promise<void> => {
dispatch(
setConnectionParams({
type: connType.SERIAL,
params: {
port: device,
},
}),
);
await setConnection(connType.SERIAL);
}}
icon={<FiCheck />}
+5 -17
View File
@@ -6,23 +6,21 @@ type DefaultButtonProps = JSX.IntrinsicElements['button'];
interface ButtonProps extends DefaultButtonProps {
icon?: JSX.Element;
circle?: boolean;
active?: boolean;
border?: boolean;
padding?: number;
padding?: string;
confirmAction?: () => void;
}
export const Button = ({
icon,
circle,
className,
active,
border,
confirmAction,
disabled,
children,
padding = 3,
padding = '2',
...props
}: ButtonProps): JSX.Element => {
const [hasConfirmed, setHasConfirmed] = React.useState(false);
@@ -42,27 +40,17 @@ export const Button = ({
return (
<button
onClick={handleConfirm}
className={`items-center select-none flex dark:text-white active:scale-95 transition duration-200 ease-in-out ${
className={`items-center select-none flex border border-transparent dark:text-white active:scale-95 transition duration-200 ease-in-out focus-within:border-primary dark:focus-within:border-primary focus-within:shadow-border rounded-md p-${padding} space-x-3 text-sm ${
active && !disabled ? 'bg-gray-100 dark:bg-gray-700' : ''
} ${
circle
? 'rounded-full h-10 w-10'
: `rounded-md p-${padding} space-x-3 text-sm`
} ${
disabled
? 'cursor-not-allowed dark:bg-primaryDark bg-white'
: 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 hover:shadow-md'
} ${
border ? 'border border-gray-400 dark:border-gray-200' : ''
} ${className}`}
} ${border ? 'border-gray-400 dark:border-gray-200' : ''} ${className}`}
{...props}
>
{icon && (
<div
className={`text-gray-500 dark:text-gray-400 ${
circle ? 'mx-auto' : ''
}`}
>
<div className="text-gray-500 dark:text-gray-400">
{hasConfirmed ? <FiCheck /> : icon}
</div>
)}
+4 -4
View File
@@ -22,7 +22,7 @@ export const Navigation = ({
>
<div onClick={onClick}>
<Button
icon={<FiMessageSquare className="w-6 h-6" />}
icon={<FiMessageSquare className="w-5 h-5" />}
active={route.name === 'messages'}
className="w-full md:w-auto"
{...routes.messages().link}
@@ -32,7 +32,7 @@ export const Navigation = ({
</div>
<div onClick={onClick}>
<Button
icon={<FiGrid className="w-6 h-6" />}
icon={<FiGrid className="w-5 h-5" />}
className="w-full md:w-auto"
active={route.name === 'nodes'}
{...routes.nodes().link}
@@ -42,7 +42,7 @@ export const Navigation = ({
</div>
<div onClick={onClick}>
<Button
icon={<FiPackage className="w-6 h-6" />}
icon={<FiPackage className="w-5 h-5" />}
className="w-full md:w-auto"
active={route.name === 'plugins'}
{...routes.plugins().link}
@@ -52,7 +52,7 @@ export const Navigation = ({
</div>
<div onClick={onClick}>
<Button
icon={<FiSettings className="w-6 h-6" />}
icon={<FiSettings className="w-5 h-5" />}
className="w-full md:w-auto"
active={route.name === 'settings'}
{...routes.settings().link}
+1 -1
View File
@@ -14,7 +14,7 @@ export const DeviceStatus = (): JSX.Element => {
return (
<Button
padding={0}
padding="0"
active
onClick={(): void => {
dispatch(dispatch(openConnectionModal()));
@@ -0,0 +1,84 @@
import React from 'react';
import { FiBell, FiX } from 'react-icons/fi';
import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { IconButton } from '@components/generic/IconButton';
import { shift, useFloating } from '@floating-ui/react-dom';
import { Popover } from '@headlessui/react';
export const Notifications = (): JSX.Element => {
const [unreadCount, setUnreadCount] = React.useState(0);
const notifications = useAppSelector((state) => state.app.notifications);
const { x, y, reference, floating, strategy } = useFloating({
placement: 'bottom',
middleware: [shift()],
});
React.useEffect(() => {
setUnreadCount(
notifications.filter((notification) => !notification.read).length,
);
}, [notifications]);
return (
<Popover>
<Popover.Button as="div" className="relative" ref={reference}>
<IconButton icon={<FiBell className="w-5 h-5" />} />
{unreadCount > 0 && (
<div className="absolute pointer-events-none top-1 right-1">
<div className="w-3 h-3 text-xs font-semibold leading-3 text-center text-white bg-orange-500 rounded-full">
{unreadCount}
</div>
</div>
)}
</Popover.Button>
<Popover.Panel
ref={floating}
style={{
position: strategy,
top: y ?? '',
left: x ?? '',
}}
className="fixed z-50 border border-gray-300 rounded-md shadow-md w-72 bg-primaryDark dark:border-gray-600"
>
<div className="divide-y divide-gray-600">
{notifications.map((notification, index) => (
<div
key={index}
className={`p-1 flex text-sm justify-between ${
notification.read
? 'text-gray-600 dark:text-gray-300'
: 'text-gray-900 dark:text-white'
}`}
>
<div className="my-auto">{notification.icon}</div>
<div className="my-auto font-light">{notification.title}</div>
<div className="flex space-x-1">
{notification.action ? (
<div className="my-auto w-18">
<Button
border
padding="0.5"
onClick={notification.action.action}
>
{notification.action.message}
</Button>
</div>
) : (
<div className="w-16" />
)}
<IconButton icon={<FiX />} />
</div>
</div>
))}
</div>
</Popover.Panel>
</Popover>
);
};
+1 -2
View File
@@ -34,8 +34,7 @@ export const connectionUrl = state.hostOverrideEnabled
? state.hostOverride
: import.meta.env.PROD
? window.location.hostname
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ??
'http://meshtastic.local';
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ?? 'meshtastic.local';
export const ble = new IBLEConnection();
export const serial = new ISerialConnection();
+26
View File
@@ -10,6 +10,17 @@ export enum connType {
SERIAL,
}
interface Notification {
id: string;
icon: React.ReactNode;
title: string;
action?: {
message: string;
action: () => void;
};
read: boolean;
}
interface AppState {
mobileNavOpen: boolean;
connectionModalOpen: boolean;
@@ -21,6 +32,7 @@ interface AppState {
HTTP: Types.HTTPConnectionParameters;
SERIAL: Types.SerialConnectionParameters;
};
notifications: Notification[];
}
const initialState: AppState = {
@@ -39,6 +51,7 @@ const initialState: AppState = {
},
SERIAL: {},
},
notifications: [],
};
export const appSlice = createSlice({
@@ -78,6 +91,17 @@ export const appSlice = createSlice({
state.connectionParams[connType[action.payload.type]] =
action.payload.params;
},
addNotification(state, action: PayloadAction<Notification>) {
state.notifications.push(action.payload);
},
removeNotification(state, action: PayloadAction<string>) {
state.notifications.splice(
state.notifications.findIndex(
(notification) => notification.id === action.payload,
),
1,
);
},
},
});
@@ -90,6 +114,8 @@ export const {
setCurrentPage,
setConnType,
setConnectionParams,
addNotification,
removeNotification,
} = appSlice.actions;
export default appSlice.reducer;
-13
View File
@@ -7,7 +7,6 @@ import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/redux';
import { Channel } from '@components/Channel';
import { FormFooter } from '@components/FormFooter';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
@@ -249,18 +248,6 @@ export const Channels = ({
isPrimary={channel.channel.index === 0}
/>
))}
<div className="flex justify-between">
<div
onClick={(): Promise<void> => {
return connection.confirmSetChannel();
}}
className="text-sm font-thin text-gray-400 dark:text-gray-300"
>
Please ensure any changes are working before confirming
</div>
<Button active>Confirm</Button>
</div>
</div>
</Card>
</div>
-1
View File
@@ -30,7 +30,6 @@ export const Interface = ({
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
circle
/>
}
footer={
+1
View File
@@ -91,6 +91,7 @@ export const WiFi = ({ navOpen, setNavOpen }: WiFiProps): JSX.Element => {
/>
<Input
type="password"
autoComplete="off"
label={t('strings.wifi_psk')}
disabled={watchWifiApMode}
{...register('wifiPassword')}