Browse Source

Add temp save buttons for settings, reorder chats

pull/21/head
Sacha Weatherstone 4 years ago
parent
commit
b98af7caca
  1. 36
      src/components/FormFooter.tsx
  2. 31
      src/components/generic/Blur.tsx
  3. 67
      src/components/layout/Sidebar/Settings/Channels.tsx
  4. 170
      src/components/layout/Sidebar/Settings/Position.tsx
  5. 48
      src/components/layout/Sidebar/Settings/Power.tsx
  6. 36
      src/components/layout/Sidebar/Settings/Radio.tsx
  7. 112
      src/components/layout/Sidebar/Settings/User.tsx
  8. 82
      src/components/layout/Sidebar/Settings/WiFi.tsx
  9. 24
      src/pages/Messages/index.tsx
  10. 9
      src/pages/Nodes/NodeCard.tsx

36
src/components/FormFooter.tsx

@ -1,36 +0,0 @@
import type React from 'react';
import { FiSave, FiXCircle } from 'react-icons/fi';
import { IconButton } from '@meshtastic/components';
export interface FormFooterProps {
dirty?: boolean;
clearAction?: () => void;
saveAction?: () => void;
}
export const FormFooter = ({
dirty,
clearAction,
saveAction,
}: FormFooterProps): JSX.Element => {
return (
<div className="float-right flex gap-2">
<IconButton
icon={<FiXCircle className="h-5 w-5" />}
disabled={!dirty}
onClick={(): void => {
clearAction && clearAction();
}}
/>
<IconButton
disabled={!dirty}
onClick={(): void => {
saveAction && saveAction();
}}
icon={<FiSave className="h-5 w-5" />}
/>
</div>
);
};

31
src/components/generic/Blur.tsx

@ -1,31 +0,0 @@
import type React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div'];
interface BlurProps extends DefaultDivProps {
disableOnMd?: boolean;
}
export const Blur = ({
disableOnMd,
className,
onClick,
...props
}: BlurProps): JSX.Element => {
return (
<div
className={`absolute inset-0 z-20 h-full w-full transition-opacity ${
disableOnMd ? 'md:hidden' : ''
} ${className}`}
{...props}
>
<div
onClick={onClick}
className={`absolute inset-0 h-full w-full backdrop-blur-sm backdrop-filter ${
disableOnMd ? 'md:hidden' : ''
}`}
tabIndex={0}
></div>
</div>
);
};

67
src/components/layout/Sidebar/Settings/Channels.tsx

@ -1,17 +1,11 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiExternalLink, FiX } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
import {
RiArrowDownLine,
RiArrowUpDownLine,
RiArrowUpLine,
} from 'react-icons/ri';
import { ListItem } from '@app/components/generic/ListItem';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input, Select, Tooltip } from '@meshtastic/components'; import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const Channels = (): JSX.Element => { export const Channels = (): JSX.Element => {
@ -106,54 +100,19 @@ export const Channels = (): JSX.Element => {
{...register('settings.txPower', { valueAsNumber: true })} {...register('settings.txPower', { valueAsNumber: true })}
/> />
</form> </form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</> </>
)} )}
{channels.map((channel) => (
<ListItem
key={channel.index}
onClick={(): void => {
setSelectedChannel(channel);
}}
status={
<div
className={`my-auto h-3 w-3 rounded-full ${
[
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
].find((role) => role === channel.role)
? 'bg-green-500'
: 'bg-gray-400'
}`}
/>
}
selected={selectedChannel?.index === channel.index}
selectedIcon={<FiExternalLink />}
actions={
<Tooltip content={`MQTT Status`}>
<div className="rounded-md p-2">
{channel.settings?.uplinkEnabled &&
channel.settings?.downlinkEnabled ? (
<RiArrowUpDownLine className="p-0.5 group-active:scale-90" />
) : channel.settings?.uplinkEnabled ? (
<RiArrowUpLine className="p-0.5 group-active:scale-90" />
) : channel.settings?.downlinkEnabled ? (
<RiArrowDownLine className="p-0.5 group-active:scale-90" />
) : (
<FiX className="p-0.5" />
)}
</div>
</Tooltip>
}
>
<div>
{channel.settings?.name.length
? channel.settings.name
: channel.role === Protobuf.Channel_Role.PRIMARY
? 'Primary'
: `Channel: ${channel.index}`}
</div>
</ListItem>
))}
</> </>
); );
}; };

170
src/components/layout/Sidebar/Settings/Position.tsx

@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { MultiSelect } from 'react-multi-select-component'; import { MultiSelect } from 'react-multi-select-component';
import { bitwiseEncode } from '@app/core/utils/bitwise'; import { bitwiseEncode } from '@app/core/utils/bitwise';
import { Label } from '@components/generic/form/Label'; import { Label } from '@components/generic/form/Label';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input, Select } from '@meshtastic/components'; import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const Position = (): JSX.Element => { export const Position = (): JSX.Element => {
@ -54,87 +55,100 @@ export const Position = (): JSX.Element => {
}; };
return ( return (
<form className="space-y-2" onSubmit={onSubmit}> <>
<Input <form className="space-y-2" onSubmit={onSubmit}>
label="Broadcast Interval" <Input
type="number" label="Broadcast Interval"
suffix="Seconds" type="number"
{...register('positionBroadcastSecs', { valueAsNumber: true })} suffix="Seconds"
/> {...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Controller <Controller
name="positionFlags" name="positionFlags"
control={control} control={control}
render={({ field, fieldState }): JSX.Element => { render={({ field, fieldState }): JSX.Element => {
const { value, onChange, ...rest } = field; const { value, onChange, ...rest } = field;
const { error } = fieldState; const { error } = fieldState;
const label = 'Position Flags'; const label = 'Position Flags';
return ( return (
<div className="w-full"> <div className="w-full">
{label && <Label label={label} error={error?.message} />} {label && <Label label={label} error={error?.message} />}
<MultiSelect <MultiSelect
options={Object.entries(Protobuf.PositionFlags) options={Object.entries(Protobuf.PositionFlags)
.filter((value) => typeof value[1] !== 'number') .filter((value) => typeof value[1] !== 'number')
.filter( .filter(
(value) => (value) =>
parseInt(value[0]) !== parseInt(value[0]) !==
Protobuf.PositionFlags.POS_UNDEFINED, Protobuf.PositionFlags.POS_UNDEFINED,
) )
.map((value) => { .map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace('POS_', ''),
};
})}
value={decode(value).map((flag) => {
return { return {
value: parseInt(value[0]), value: flag,
label: value[1].toString().replace('POS_', ''), label: Protobuf.PositionFlags[flag].replace('POS_', ''),
}; };
})} })}
value={decode(value).map((flag) => { onChange={(e: { value: number; label: string }[]): void =>
return { onChange(bitwiseEncode(e.map((v) => v.value)))
value: flag, }
label: Protobuf.PositionFlags[flag].replace('POS_', ''), labelledBy="Select"
}; />
})} </div>
onChange={(e: { value: number; label: string }[]): void => );
onChange(bitwiseEncode(e.map((v) => v.value))) }}
} />
labelledBy="Select"
/>
</div>
);
}}
/>
<Input <Input
label="Position Type (DEBUG)" label="Position Type (DEBUG)"
type="number" type="number"
disabled disabled
{...register('positionFlags', { valueAsNumber: true })} {...register('positionFlags', { valueAsNumber: true })}
/> />
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} /> <Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
<Select <Select
label="Location Sharing" label="Location Sharing"
optionsEnum={Protobuf.LocationSharing} optionsEnum={Protobuf.LocationSharing}
{...register('locationShare', { valueAsNumber: true })} {...register('locationShare', { valueAsNumber: true })}
/> />
<Select <Select
label="GPS Mode" label="GPS Mode"
optionsEnum={Protobuf.GpsOperation} optionsEnum={Protobuf.GpsOperation}
{...register('gpsOperation', { valueAsNumber: true })} {...register('gpsOperation', { valueAsNumber: true })}
/> />
<Select <Select
label="Display Format" label="Display Format"
optionsEnum={Protobuf.GpsCoordinateFormat} optionsEnum={Protobuf.GpsCoordinateFormat}
{...register('gpsFormat', { valueAsNumber: true })} {...register('gpsFormat', { valueAsNumber: true })}
/> />
<Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} /> <Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} />
<Input <Input
label="Max DOP" label="Max DOP"
type="number" type="number"
{...register('gpsMaxDop', { valueAsNumber: true })} {...register('gpsMaxDop', { valueAsNumber: true })}
/> />
<Input <Input
label="Last GPS Attempt" label="Last GPS Attempt"
disabled disabled
{...register('gpsAttemptTime', { valueAsNumber: true })} {...register('gpsAttemptTime', { valueAsNumber: true })}
/> />
</form> </form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
); );
}; };

48
src/components/layout/Sidebar/Settings/Power.tsx

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Select } from '@meshtastic/components'; import { Checkbox, IconButton, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const Power = (): JSX.Element => { export const Power = (): JSX.Element => {
@ -33,21 +34,34 @@ export const Power = (): JSX.Element => {
}); });
}); });
return ( return (
<form className="space-y-2" onSubmit={onSubmit}> <>
<Select <form className="space-y-2" onSubmit={onSubmit}>
label="Charge current" <Select
optionsEnum={Protobuf.ChargeCurrent} label="Charge current"
{...register('chargeCurrent', { valueAsNumber: true })} optionsEnum={Protobuf.ChargeCurrent}
/> {...register('chargeCurrent', { valueAsNumber: true })}
<Checkbox label="Always powered" {...register('isAlwaysPowered')} /> />
<Checkbox <Checkbox label="Always powered" {...register('isAlwaysPowered')} />
label="Powered by low power source (solar)" <Checkbox
disabled={preferences.isRouter} label="Powered by low power source (solar)"
validationMessage={ disabled={preferences.isRouter}
preferences.isRouter ? 'Enabled by default in router mode' : '' validationMessage={
} preferences.isRouter ? 'Enabled by default in router mode' : ''
{...register('isLowPower')} }
/> {...register('isLowPower')}
</form> />
</form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
); );
}; };

36
src/components/layout/Sidebar/Settings/Radio.tsx

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Select } from '@meshtastic/components'; import { Checkbox, IconButton, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const Radio = (): JSX.Element => { export const Radio = (): JSX.Element => {
@ -30,15 +31,28 @@ export const Radio = (): JSX.Element => {
}); });
}); });
return ( return (
<form className="space-y-2" onSubmit={onSubmit}> <>
<Checkbox label="Is Router" {...register('isRouter')} /> <form className="space-y-2" onSubmit={onSubmit}>
<Select <Checkbox label="Is Router" {...register('isRouter')} />
label="Region" <Select
optionsEnum={Protobuf.RegionCode} label="Region"
{...register('region', { valueAsNumber: true })} optionsEnum={Protobuf.RegionCode}
/> {...register('region', { valueAsNumber: true })}
<Checkbox label="Debug Log" {...register('debugLogEnabled')} /> />
<Checkbox label="Serial Disabled" {...register('serialDisabled')} /> <Checkbox label="Debug Log" {...register('debugLogEnabled')} />
</form> <Checkbox label="Serial Disabled" {...register('serialDisabled')} />
</form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
); );
}; };

112
src/components/layout/Sidebar/Settings/User.tsx

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { base16 } from 'rfc4648'; import { base16 } from 'rfc4648';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input, Select } from '@meshtastic/components'; import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
export const User = (): JSX.Element => { export const User = (): JSX.Element => {
@ -62,53 +63,66 @@ export const User = (): JSX.Element => {
}); });
return ( return (
<form className="space-y-2" onSubmit={onSubmit}> <>
<Input label="Device ID" value={node?.user?.id} disabled /> <form className="space-y-2" onSubmit={onSubmit}>
<Input <Input label="Device ID" value={node?.user?.id} disabled />
label="Hardware" <Input
value={ label="Hardware"
Protobuf.HardwareModel[ value={
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET Protobuf.HardwareModel[
] node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
} ]
disabled }
/> disabled
<Input />
label="Mac Address" <Input
defaultValue={ label="Mac Address"
base16 defaultValue={
.stringify(node?.user?.macaddr ?? []) base16
.match(/.{1,2}/g) .stringify(node?.user?.macaddr ?? [])
?.join(':') ?? '' .match(/.{1,2}/g)
} ?.join(':') ?? ''
disabled }
/> disabled
<Input label="Device Name" {...register('longName')} /> />
<Input label="Short Name" maxLength={3} {...register('shortName')} /> <Input label="Device Name" {...register('longName')} />
<Checkbox label="Licenced Operator?" {...register('isLicensed')} /> <Input label="Short Name" maxLength={3} {...register('shortName')} />
<Select <Checkbox label="Licenced Operator?" {...register('isLicensed')} />
label="Team" <Select
optionsEnum={Protobuf.Team} label="Team"
{...register('team', { valueAsNumber: true })} optionsEnum={Protobuf.Team}
/> {...register('team', { valueAsNumber: true })}
<Input />
label="Antenna Azimuth" <Input
suffix="°" label="Antenna Azimuth"
type="number" suffix="°"
{...register('antAzimuth', { valueAsNumber: true })} type="number"
/> {...register('antAzimuth', { valueAsNumber: true })}
<Input />
label="Antenna Gain" <Input
suffix="dBi" label="Antenna Gain"
type="number" suffix="dBi"
{...register('antGainDbi', { valueAsNumber: true })} type="number"
/> {...register('antGainDbi', { valueAsNumber: true })}
<Input />
label="Transmit Power" <Input
suffix="dBm" label="Transmit Power"
type="number" suffix="dBm"
{...register('txPowerDbm', { valueAsNumber: true })} type="number"
/> {...register('txPowerDbm', { valueAsNumber: true })}
</form> />
</form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
); );
}; };

82
src/components/layout/Sidebar/Settings/WiFi.tsx

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input } from '@meshtastic/components'; import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs'; import type { Protobuf } from '@meshtastic/meshtasticjs';
export const WiFi = (): JSX.Element => { export const WiFi = (): JSX.Element => {
@ -42,38 +43,51 @@ export const WiFi = (): JSX.Element => {
}); });
}); });
return ( return (
<form className="space-y-2" onSubmit={onSubmit}> <>
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} /> <form className="space-y-2" onSubmit={onSubmit}>
<Input <Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
label="WiFi SSID" <Input
disabled={watchWifiApMode} label="WiFi SSID"
{...register('wifiSsid')} disabled={watchWifiApMode}
/> {...register('wifiSsid')}
<Input />
type="password" <Input
autoComplete="off" type="password"
label="WiFi PSK" autoComplete="off"
disabled={watchWifiApMode} label="WiFi PSK"
{...register('wifiPassword')} disabled={watchWifiApMode}
/> {...register('wifiPassword')}
<Checkbox label="Disable MQTT" {...register('mqttDisabled')} /> />
<Input <Checkbox label="Disable MQTT" {...register('mqttDisabled')} />
label="MQTT Server Address" <Input
disabled={watchMQTTDisabled} label="MQTT Server Address"
{...register('mqttServer')} disabled={watchMQTTDisabled}
/> {...register('mqttServer')}
<Input />
label="MQTT Username" <Input
disabled={watchMQTTDisabled} label="MQTT Username"
{...register('mqttUsername')} disabled={watchMQTTDisabled}
/> {...register('mqttUsername')}
<Input />
label="MQTT Password" <Input
type="password" label="MQTT Password"
autoComplete="off" type="password"
disabled={watchMQTTDisabled} autoComplete="off"
{...register('mqttPassword')} disabled={watchMQTTDisabled}
/> {...register('mqttPassword')}
</form> />
</form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
); );
}; };

24
src/pages/Messages/index.tsx

@ -38,12 +38,12 @@ export const Messages = (): JSX.Element => {
icon={<FiMessageCircle />} icon={<FiMessageCircle />}
sidebarContents={ sidebarContents={
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{nodes {channels
.filter((node) => node.number !== myNodeNum) .filter((channel) => channel.settings?.name !== 'admin')
.map((node) => ( .map((channel) => (
<DmChat <ChannelChat
key={node.number} key={channel.index}
node={node} channel={channel}
selectedIndex={selectedChatIndex} selectedIndex={selectedChatIndex}
setSelectedIndex={setSelectedChatIndex} setSelectedIndex={setSelectedChatIndex}
/> />
@ -51,12 +51,12 @@ export const Messages = (): JSX.Element => {
{nodes.length !== 0 && channels.length !== 0 && ( {nodes.length !== 0 && channels.length !== 0 && (
<div className="mx-2 rounded-md border-2 border-gray-300 dark:border-gray-600" /> <div className="mx-2 rounded-md border-2 border-gray-300 dark:border-gray-600" />
)} )}
{channels {nodes
.filter((channel) => channel.settings?.name !== 'admin') .filter((node) => node.number !== myNodeNum)
.map((channel) => ( .map((node) => (
<ChannelChat <DmChat
key={channel.index} key={node.number}
channel={channel} node={node}
selectedIndex={selectedChatIndex} selectedIndex={selectedChatIndex}
setSelectedIndex={setSelectedChatIndex} setSelectedIndex={setSelectedChatIndex}
/> />

9
src/pages/Nodes/NodeCard.tsx

@ -116,7 +116,14 @@ export const NodeCard = ({
direction="x" direction="x"
> >
<CollapsibleSection title="User" icon={<FiUser />}> <CollapsibleSection title="User" icon={<FiUser />}>
<div>Info</div> <div className="flex p-2">
<div className="m-auto flex flex-col gap-2">
<Hashicon value={node.number.toString()} size={180} />
<div className="text-center text-lg font-medium dark:text-white">
{node?.user?.longName || 'Unknown'}
</div>
</div>
</div>
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection title="Location" icon={<FiMapPin />}> <CollapsibleSection title="Location" icon={<FiMapPin />}>
<div>Info</div> <div>Info</div>

Loading…
Cancel
Save