improve editing and saving app settings

This commit is contained in:
hzrd149 2023-06-27 13:21:15 +00:00
parent 0cc405954f
commit 39ef920289
7 changed files with 167 additions and 269 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Improve editing and saving app settings

View File

@ -1,3 +1,4 @@
import { useState } from "react";
import { import {
Button, Button,
AccordionItem, AccordionItem,
@ -7,7 +8,6 @@ import {
AccordionIcon, AccordionIcon,
ButtonGroup, ButtonGroup,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useState } from "react";
import { clearCacheData, deleteDatabase } from "../../services/db"; import { clearCacheData, deleteDatabase } from "../../services/db";
export default function DatabaseSettings() { export default function DatabaseSettings() {

View File

@ -1,3 +1,4 @@
import { useFormContext } from "react-hook-form";
import { import {
Flex, Flex,
FormControl, FormControl,
@ -10,41 +11,11 @@ import {
AccordionIcon, AccordionIcon,
FormHelperText, FormHelperText,
Input, Input,
InputProps,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react"; import { AppSettings } from "../../services/user-app-settings";
import useAppSettings from "../../hooks/use-app-settings";
function ColorPicker({ value, onPickColor, ...props }: { onPickColor?: (color: string) => void } & InputProps) {
const [tmpColor, setTmpColor] = useState(value);
const ref = useRef<HTMLInputElement>();
useEffect(() => setTmpColor(value), [value]);
useEffect(() => {
if (ref.current) {
ref.current.onchange = () => {
if (onPickColor && ref.current?.value) {
onPickColor(ref.current.value);
}
};
}
});
return (
<Input
{...props}
ref={ref}
value={tmpColor}
onChange={(e) => {
setTmpColor(e.target.value);
if (props.onChange) props.onChange(e);
}}
/>
);
}
export default function DisplaySettings() { export default function DisplaySettings() {
const { blurImages, colorMode, primaryColor, updateSettings, showContentWarning } = useAppSettings(); const { register } = useFormContext<AppSettings>();
return ( return (
<AccordionItem> <AccordionItem>
@ -60,14 +31,10 @@ export default function DisplaySettings() {
<Flex direction="column" gap="4"> <Flex direction="column" gap="4">
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">
<FormLabel htmlFor="use-dark-theme" mb="0"> <FormLabel htmlFor="colorMode" mb="0">
Use dark theme Use dark theme
</FormLabel> </FormLabel>
<Switch <Switch id="colorMode" {...register("colorMode")} />
id="use-dark-theme"
isChecked={colorMode === "dark"}
onChange={(v) => updateSettings({ colorMode: v.target.checked ? "dark" : "light" })}
/>
</Flex> </Flex>
<FormHelperText> <FormHelperText>
<span>Enables hacker mode</span> <span>Enables hacker mode</span>
@ -75,17 +42,10 @@ export default function DisplaySettings() {
</FormControl> </FormControl>
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">
<FormLabel htmlFor="primary-color" mb="0"> <FormLabel htmlFor="primaryColor" mb="0">
Primary Color Primary Color
</FormLabel> </FormLabel>
<ColorPicker <Input id="primaryColor" type="color" maxW="120" size="sm" {...register("primaryColor")} />
id="primary-color"
type="color"
value={primaryColor}
onPickColor={(color) => updateSettings({ primaryColor: color })}
maxW="120"
size="sm"
/>
</Flex> </Flex>
<FormHelperText> <FormHelperText>
<span>The primary color of the theme</span> <span>The primary color of the theme</span>
@ -93,14 +53,10 @@ export default function DisplaySettings() {
</FormControl> </FormControl>
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">
<FormLabel htmlFor="blur-images" mb="0"> <FormLabel htmlFor="blurImages" mb="0">
Blur images from strangers Blur images from strangers
</FormLabel> </FormLabel>
<Switch <Switch id="blurImages" {...register("blurImages")} />
id="blur-images"
isChecked={blurImages}
onChange={(v) => updateSettings({ blurImages: v.target.checked })}
/>
</Flex> </Flex>
<FormHelperText> <FormHelperText>
<span>Enabled: blur images for people you aren't following</span> <span>Enabled: blur images for people you aren't following</span>
@ -111,31 +67,12 @@ export default function DisplaySettings() {
<FormLabel htmlFor="show-content-warning" mb="0"> <FormLabel htmlFor="show-content-warning" mb="0">
Show content warning Show content warning
</FormLabel> </FormLabel>
<Switch <Switch id="show-content-warning" {...register("showContentWarning")} />
id="show-content-warning"
isChecked={showContentWarning}
onChange={(v) => updateSettings({ showContentWarning: v.target.checked })}
/>
</Flex> </Flex>
<FormHelperText> <FormHelperText>
<span>Enabled: shows a warning for notes with NIP-36 Content Warning</span> <span>Enabled: shows a warning for notes with NIP-36 Content Warning</span>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="show-ads" mb="0">
Show Ads
</FormLabel>
<Switch
id="show-ads"
isChecked={false}
onChange={(v) => alert("Sorry, that feature will never be finished.")}
/>
</Flex>
<FormHelperText>
<span>Enabled: shows ads so I can steal your data</span>
</FormHelperText>
</FormControl>
</Flex> </Flex>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>

View File

@ -1,38 +1,56 @@
import { Button, Flex, Accordion, Link } from "@chakra-ui/react"; import { Button, Flex, Accordion, Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import accountService from "../../services/account"; import { GithubIcon, ToolsIcon } from "../../components/icons";
import { GithubIcon, LogoutIcon, ToolsIcon } from "../../components/icons";
import LightningSettings from "./lightning-settings"; import LightningSettings from "./lightning-settings";
import DatabaseSettings from "./database-settings"; import DatabaseSettings from "./database-settings";
import DisplaySettings from "./display-settings"; import DisplaySettings from "./display-settings";
import PerformanceSettings from "./performance-settings"; import PerformanceSettings from "./performance-settings";
import PrivacySettings from "./privacy-settings"; import PrivacySettings from "./privacy-settings";
import useAppSettings from "../../hooks/use-app-settings";
import { FormProvider, useForm } from "react-hook-form";
export default function SettingsView() { export default function SettingsView() {
const { updateSettings, ...settings } = useAppSettings();
const form = useForm({
mode: "onBlur",
values: settings,
});
const saveSettings = form.handleSubmit(async (values) => {
await updateSettings(values);
});
return ( return (
<Flex direction="column" pt="2" pb="2" overflow="auto"> <Flex direction="column" pt="2" pb="2" overflow="auto">
<Accordion defaultIndex={[0]} allowMultiple> <form onSubmit={saveSettings}>
<DisplaySettings /> <FormProvider {...form}>
<Accordion defaultIndex={[0]} allowMultiple>
<PerformanceSettings /> <DisplaySettings />
<PerformanceSettings />
<PrivacySettings /> <PrivacySettings />
<LightningSettings />
<LightningSettings /> <DatabaseSettings />
</Accordion>
<DatabaseSettings /> </FormProvider>
</Accordion> <Flex gap="4" padding="4" alignItems="center">
<Flex gap="2" padding="4" alignItems="center"> <Button as={RouterLink} to="/tools" leftIcon={<ToolsIcon />}>
<Button leftIcon={<LogoutIcon />} onClick={() => accountService.logout()}> Tools
Logout </Button>
</Button> <Link isExternal href="https://github.com/hzrd149/nostrudel">
<Button as={RouterLink} to="/tools" leftIcon={<ToolsIcon />}> <GithubIcon /> Github
Tools </Link>
</Button> <Button
<Link isExternal href="https://github.com/hzrd149/nostrudel" ml="auto"> ml="auto"
<GithubIcon /> Github isLoading={form.formState.isLoading}
</Link> isDisabled={!form.formState.isDirty}
</Flex> colorScheme="brand"
type="submit"
>
Save Settings
</Button>
</Flex>
</form>
</Flex> </Flex>
); );
} }

View File

@ -11,18 +11,12 @@ import {
Input, Input,
Select, Select,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import appSettings, { replaceSettings } from "../../services/app-settings";
import useSubject from "../../hooks/use-subject";
import { LightningIcon } from "../../components/icons"; import { LightningIcon } from "../../components/icons";
import { LightningPayMode } from "../../services/user-app-settings"; import { AppSettings } from "../../services/user-app-settings";
import useAppSettings from "../../hooks/use-app-settings"; import { useFormContext } from "react-hook-form";
export default function LightningSettings() { export default function LightningSettings() {
const { lightningPayMode, zapAmounts, updateSettings } = useAppSettings(); const { register } = useFormContext<AppSettings>();
const [zapInput, setZapInput] = useState(zapAmounts.join(","));
useEffect(() => setZapInput(zapAmounts.join(",")), [zapAmounts.join(",")]);
return ( return (
<AccordionItem> <AccordionItem>
@ -37,14 +31,10 @@ export default function LightningSettings() {
<AccordionPanel> <AccordionPanel>
<Flex direction="column" gap="4"> <Flex direction="column" gap="4">
<FormControl> <FormControl>
<FormLabel htmlFor="lightning-payment-mode" mb="0"> <FormLabel htmlFor="lightningPayMode" mb="0">
Payment mode Payment mode
</FormLabel> </FormLabel>
<Select <Select id="lightningPayMode" {...register("lightningPayMode")}>
id="lightning-payment-mode"
value={lightningPayMode}
onChange={(e) => updateSettings({ lightningPayMode: e.target.value as LightningPayMode })}
>
<option value="prompt">Prompt</option> <option value="prompt">Prompt</option>
<option value="webln">WebLN</option> <option value="webln">WebLN</option>
<option value="external">External</option> <option value="external">External</option>
@ -64,18 +54,20 @@ export default function LightningSettings() {
</FormLabel> </FormLabel>
<Input <Input
id="zap-amounts" id="zap-amounts"
value={zapInput} autoComplete="off"
onChange={(e) => setZapInput(e.target.value)} {...register("zapAmounts", {
onBlur={() => { setValueAs: (value: number[] | string) => {
const amounts = zapInput if (Array.isArray(value)) {
.split(",") return Array.from(value).join(",");
.map((v) => parseInt(v)) } else {
.filter(Boolean) return value
.sort((a, b) => a - b); .split(",")
.map((v) => parseInt(v))
updateSettings({ zapAmounts: amounts }); .filter(Boolean)
setZapInput(amounts.join(",")); .sort((a, b) => a - b);
}} }
},
})}
/> />
<FormHelperText> <FormHelperText>
<span>Comma separated list of custom zap amounts</span> <span>Comma separated list of custom zap amounts</span>

View File

@ -1,3 +1,4 @@
import { useFormContext } from "react-hook-form";
import { import {
Flex, Flex,
FormControl, FormControl,
@ -11,16 +12,13 @@ import {
FormHelperText, FormHelperText,
Input, Input,
Link, Link,
FormErrorMessage,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import useAppSettings from "../../hooks/use-app-settings"; import { AppSettings } from "../../services/user-app-settings";
import { useEffect, useState } from "react"; import { safeUrl } from "../../helpers/parse";
export default function PerformanceSettings() { export default function PerformanceSettings() {
const { autoShowMedia, proxyUserMedia, showReactions, showSignatureVerification, updateSettings, imageProxy } = const { register, formState } = useFormContext<AppSettings>();
useAppSettings();
const [proxyInput, setProxyInput] = useState(imageProxy);
useEffect(() => setProxyInput(imageProxy), [imageProxy]);
return ( return (
<AccordionItem> <AccordionItem>
@ -39,11 +37,7 @@ export default function PerformanceSettings() {
<FormLabel htmlFor="proxy-user-media" mb="0"> <FormLabel htmlFor="proxy-user-media" mb="0">
Proxy user media Proxy user media
</FormLabel> </FormLabel>
<Switch <Switch id="proxy-user-media" {...register("proxyUserMedia")} />
id="proxy-user-media"
isChecked={proxyUserMedia}
onChange={(v) => updateSettings({ proxyUserMedia: v.target.checked })}
/>
</Flex> </Flex>
<FormHelperText> <FormHelperText>
<span>Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)</span> <span>Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)</span>
@ -52,24 +46,17 @@ export default function PerformanceSettings() {
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel htmlFor="image-proxy" mb="0"> <FormLabel htmlFor="imageProxy" mb="0">
Image proxy service Image proxy service
</FormLabel> </FormLabel>
<Input <Input
id="image-proxy" id="imageProxy"
type="url" type="url"
value={proxyInput} {...register("imageProxy", {
onChange={(e) => setProxyInput(e.target.value)} setValueAs: (v) => safeUrl(v) || v,
onBlur={() => { })}
try {
const url = proxyInput ? new URL(proxyInput).toString() : "";
if (url !== imageProxy) {
updateSettings({ imageProxy: url });
setProxyInput(url);
}
} catch (e) {}
}}
/> />
{formState.errors.imageProxy && <FormErrorMessage>{formState.errors.imageProxy.message}</FormErrorMessage>}
<FormHelperText> <FormHelperText>
<span> <span>
A URL to an instance of{" "} A URL to an instance of{" "}
@ -81,40 +68,28 @@ export default function PerformanceSettings() {
</FormControl> </FormControl>
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">
<FormLabel htmlFor="auto-show-embeds" mb="0"> <FormLabel htmlFor="autoShowMedia" mb="0">
Automatically show media Show embeds
</FormLabel> </FormLabel>
<Switch <Switch id="autoShowMedia" {...register("autoShowMedia")} />
id="auto-show-embeds"
isChecked={autoShowMedia}
onChange={(v) => updateSettings({ autoShowMedia: v.target.checked })}
/>
</Flex> </Flex>
<FormHelperText>Disabled: Images and videos will show expandable buttons</FormHelperText> <FormHelperText>Disabled: Embeds will show an expandable button</FormHelperText>
</FormControl> </FormControl>
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">
<FormLabel htmlFor="show-reactions" mb="0"> <FormLabel htmlFor="showReactions" mb="0">
Show reactions Show reactions
</FormLabel> </FormLabel>
<Switch <Switch id="showReactions" {...register("showReactions")} />
id="show-reactions"
isChecked={showReactions}
onChange={(v) => updateSettings({ showReactions: v.target.checked })}
/>
</Flex> </Flex>
<FormHelperText>Enabled: Show reactions on notes</FormHelperText> <FormHelperText>Enabled: Show reactions on notes</FormHelperText>
</FormControl> </FormControl>
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">
<FormLabel htmlFor="show-sig-verify" mb="0"> <FormLabel htmlFor="showSignatureVerification" mb="0">
Show signature verification Show signature verification
</FormLabel> </FormLabel>
<Switch <Switch id="showSignatureVerification" {...register("showSignatureVerification")} />
id="show-sig-verify"
isChecked={showSignatureVerification}
onChange={(v) => updateSettings({ showSignatureVerification: v.target.checked })}
/>
</Flex> </Flex>
<FormHelperText>Enabled: show signature verification on notes</FormHelperText> <FormHelperText>Enabled: show signature verification on notes</FormHelperText>
</FormControl> </FormControl>

View File

@ -2,7 +2,6 @@ import {
Flex, Flex,
FormControl, FormControl,
FormLabel, FormLabel,
Switch,
AccordionItem, AccordionItem,
AccordionPanel, AccordionPanel,
AccordionButton, AccordionButton,
@ -11,12 +10,10 @@ import {
FormHelperText, FormHelperText,
Input, Input,
Link, Link,
Button,
FormErrorMessage, FormErrorMessage,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import useAppSettings from "../../hooks/use-app-settings"; import { useFormContext } from "react-hook-form";
import { useForm } from "react-hook-form"; import { AppSettings } from "../../services/user-app-settings";
import { useAsync } from "react-use";
async function validateInvidiousUrl(url?: string) { async function validateInvidiousUrl(url?: string) {
if (!url) return true; if (!url) return true;
@ -29,21 +26,7 @@ async function validateInvidiousUrl(url?: string) {
} }
export default function PrivacySettings() { export default function PrivacySettings() {
const { youtubeRedirect, twitterRedirect, redditRedirect, corsProxy, updateSettings } = useAppSettings(); const { register, formState } = useFormContext<AppSettings>();
const { register, handleSubmit, formState } = useForm({
mode: "onBlur",
defaultValues: {
youtubeRedirect,
twitterRedirect,
redditRedirect,
corsProxy,
},
});
const save = handleSubmit(async (values) => {
await updateSettings(values);
});
return ( return (
<AccordionItem> <AccordionItem>
@ -56,88 +39,76 @@ export default function PrivacySettings() {
</AccordionButton> </AccordionButton>
</h2> </h2>
<AccordionPanel> <AccordionPanel>
<form onSubmit={save}> <Flex direction="column" gap="4">
<Flex direction="column" gap="4"> <FormControl isInvalid={!!formState.errors.twitterRedirect}>
<FormControl isInvalid={!!formState.errors.twitterRedirect}> <FormLabel>Nitter instance</FormLabel>
<FormLabel>Nitter instance</FormLabel> <Input type="url" placeholder="https://nitter.net/" {...register("twitterRedirect")} />
<Input type="url" placeholder="https://nitter.net/" {...register("twitterRedirect")} /> {formState.errors.twitterRedirect && (
{formState.errors.twitterRedirect && ( <FormErrorMessage>{formState.errors.twitterRedirect.message}</FormErrorMessage>
<FormErrorMessage>{formState.errors.twitterRedirect.message}</FormErrorMessage> )}
)} <FormHelperText>
<FormHelperText> Nitter is a privacy focused UI for twitter.{" "}
Nitter is a privacy focused UI for twitter.{" "} <Link href="https://github.com/zedeus/nitter/wiki/Instances" isExternal color="blue.500">
<Link href="https://github.com/zedeus/nitter/wiki/Instances" isExternal color="blue.500"> Nitter instances
Nitter instances </Link>
</Link> </FormHelperText>
</FormHelperText> </FormControl>
</FormControl>
<FormControl isInvalid={!!formState.errors.youtubeRedirect}> <FormControl isInvalid={!!formState.errors.youtubeRedirect}>
<FormLabel>Invidious instance</FormLabel> <FormLabel>Invidious instance</FormLabel>
<Input <Input
type="url" type="url"
placeholder="Invidious instance url" placeholder="Invidious instance url"
{...register("youtubeRedirect", { {...register("youtubeRedirect", {
validate: validateInvidiousUrl, validate: validateInvidiousUrl,
})} })}
/> />
{formState.errors.youtubeRedirect && ( {formState.errors.youtubeRedirect && (
<FormErrorMessage>{formState.errors.youtubeRedirect.message}</FormErrorMessage> <FormErrorMessage>{formState.errors.youtubeRedirect.message}</FormErrorMessage>
)} )}
<FormHelperText> <FormHelperText>
Invidious is a privacy focused UI for youtube.{" "} Invidious is a privacy focused UI for youtube.{" "}
<Link href="https://docs.invidious.io/instances" isExternal color="blue.500"> <Link href="https://docs.invidious.io/instances" isExternal color="blue.500">
Invidious instances Invidious instances
</Link> </Link>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControl isInvalid={!!formState.errors.redditRedirect}> <FormControl isInvalid={!!formState.errors.redditRedirect}>
<FormLabel>Teddit / Libreddit instance</FormLabel> <FormLabel>Teddit / Libreddit instance</FormLabel>
<Input type="url" placeholder="https://nitter.net/" {...register("redditRedirect")} /> <Input type="url" placeholder="https://nitter.net/" {...register("redditRedirect")} />
{formState.errors.redditRedirect && ( {formState.errors.redditRedirect && (
<FormErrorMessage>{formState.errors.redditRedirect.message}</FormErrorMessage> <FormErrorMessage>{formState.errors.redditRedirect.message}</FormErrorMessage>
)} )}
<FormHelperText> <FormHelperText>
Libreddit and Teddit are both privacy focused UIs for reddit.{" "} Libreddit and Teddit are both privacy focused UIs for reddit.{" "}
<Link <Link
href="https://github.com/libreddit/libreddit-instances/blob/master/instances.md" href="https://github.com/libreddit/libreddit-instances/blob/master/instances.md"
isExternal isExternal
color="blue.500" color="blue.500"
> >
Libreddit instances Libreddit instances
</Link> </Link>
{", "} {", "}
<Link href="https://codeberg.org/teddit/teddit#instances" isExternal color="blue.500"> <Link href="https://codeberg.org/teddit/teddit#instances" isExternal color="blue.500">
Teddit instances Teddit instances
</Link> </Link>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControl isInvalid={!!formState.errors.corsProxy}> <FormControl isInvalid={!!formState.errors.corsProxy}>
<FormLabel>CORS Proxy</FormLabel> <FormLabel>CORS Proxy</FormLabel>
<Input type="url" placeholder="https://cors.example.com/" {...register("corsProxy")} /> <Input type="url" placeholder="https://cors.example.com/" {...register("corsProxy")} />
{formState.errors.corsProxy && <FormErrorMessage>{formState.errors.corsProxy.message}</FormErrorMessage>} {formState.errors.corsProxy && <FormErrorMessage>{formState.errors.corsProxy.message}</FormErrorMessage>}
<FormHelperText> <FormHelperText>
This is used as a fallback when verifying NIP-05 ids and fetching open-graph metadata. URL to an This is used as a fallback when verifying NIP-05 ids and fetching open-graph metadata. URL to an instance
instance of{" "} of{" "}
<Link href="https://github.com/Rob--W/cors-anywhere" isExternal color="blue.500"> <Link href="https://github.com/Rob--W/cors-anywhere" isExternal color="blue.500">
cors-anywhere cors-anywhere
</Link> </Link>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
</Flex>
<Button
colorScheme="brand"
ml="auto"
isLoading={formState.isSubmitting}
type="submit"
isDisabled={!formState.isDirty}
>
Save
</Button>
</Flex>
</form>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
); );