Misc fixes
This commit is contained in:
+36
-3
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -30,7 +30,6 @@ export const Interface = ({
|
||||
onClick={(): void => {
|
||||
setNavOpen && setNavOpen(!navOpen);
|
||||
}}
|
||||
circle
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
|
||||
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user