rebuild settings view tabs

This commit is contained in:
hzrd149
2024-08-30 08:53:01 -05:00
parent f9ba9cb60b
commit 8bb7fc1cee
17 changed files with 969 additions and 994 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Rebuilt settings view tabs

View File

@@ -97,6 +97,11 @@ const VideoDetailsView = lazy(() => import("./views/videos/video"));
import BookmarksView from "./views/bookmarks";
import TaskManagerProvider from "./views/task-manager/provider";
import SearchRelaysView from "./views/relays/search";
import DisplaySettings from "./views/settings/display";
import LightningSettings from "./views/settings/lightning";
import PerformanceSettings from "./views/settings/performance";
import PrivacySettings from "./views/settings/privacy";
import PostSettings from "./views/settings/post";
const TracksView = lazy(() => import("./views/tracks"));
const UserTracksTab = lazy(() => import("./views/user/tracks"));
const UserVideosTab = lazy(() => import("./views/user/videos"));
@@ -254,7 +259,18 @@ const router = createHashRouter([
element: <ThreadView />,
},
{ path: "other-stuff", element: <OtherStuffView /> },
{ path: "settings", element: <SettingsView /> },
{
path: "settings",
element: <SettingsView />,
children: [
{ path: "", element: <DisplaySettings /> },
{ path: "post", element: <PostSettings /> },
{ path: "display", element: <DisplaySettings /> },
{ path: "privacy", element: <PrivacySettings /> },
{ path: "lightning", element: <LightningSettings /> },
{ path: "performance", element: <PerformanceSettings /> },
],
},
{
path: "relays",
element: <RelaysView />,

View File

@@ -0,0 +1,23 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { useMatch, Link as RouterLink } from "react-router-dom";
export default function SimpleNavItem({
children,
to,
...props
}: Omit<ButtonProps, "variant" | "colorScheme"> & { to: string }) {
const match = useMatch(to);
return (
<Button
as={RouterLink}
to={to}
justifyContent="flex-start"
{...props}
variant="outline"
colorScheme={match ? "primary" : undefined}
>
{children}
</Button>
);
}

View File

@@ -1,5 +1,5 @@
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
import { Button, Flex, Spinner } from "@chakra-ui/react";
import { Outlet, useLocation } from "react-router-dom";
import { Flex, Spinner } from "@chakra-ui/react";
import VerticalPageLayout from "../../components/vertical-page-layout";
import useCurrentAccount from "../../hooks/use-current-account";
@@ -14,6 +14,7 @@ import UserSquare from "../../components/icons/user-square";
import Image01 from "../../components/icons/image-01";
import Server05 from "../../components/icons/server-05";
import { Suspense } from "react";
import SimpleNavItem from "../../components/simple-nav-item";
export default function RelaysView() {
const account = useCurrentAccount();
@@ -27,90 +28,38 @@ export default function RelaysView() {
const renderContent = () => {
const nav = (
<Flex gap="2" direction="column" minW="60" overflowY="auto" overflowX="hidden" w={vertical ? "full" : undefined}>
<Button
as={RouterLink}
variant="outline"
colorScheme={
(location.pathname === "/relays" && !vertical) || location.pathname === "/relays/app"
? "primary"
: undefined
}
to="/relays/app"
leftIcon={<RelayIcon boxSize={6} />}
>
<SimpleNavItem to="/relays/app" leftIcon={<RelayIcon boxSize={6} />}>
App Relays
</Button>
<Button
as={RouterLink}
variant="outline"
colorScheme={location.pathname.startsWith("/relays/cache") ? "primary" : undefined}
to="/relays/cache"
leftIcon={<Database01 boxSize={6} />}
>
</SimpleNavItem>
<SimpleNavItem to="/relays/cache" leftIcon={<Database01 boxSize={6} />}>
Cache Relay
</Button>
</SimpleNavItem>
{account && (
<>
<Button
variant="outline"
as={RouterLink}
to="/relays/mailboxes"
leftIcon={<Mail02 boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/mailboxes") ? "primary" : undefined}
>
<SimpleNavItem to="/relays/mailboxes" leftIcon={<Mail02 boxSize={6} />}>
Mailboxes
</Button>
<Button
variant="outline"
as={RouterLink}
to="/relays/media-servers"
leftIcon={<Image01 boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/media-servers") ? "primary" : undefined}
>
</SimpleNavItem>
<SimpleNavItem to="/relays/media-servers" leftIcon={<Image01 boxSize={6} />}>
Media Servers
</Button>
<Button
variant="outline"
as={RouterLink}
to="/relays/search"
leftIcon={<SearchIcon boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/search") ? "primary" : undefined}
>
</SimpleNavItem>
<SimpleNavItem to="/relays/search" leftIcon={<SearchIcon boxSize={6} />}>
Search Relays
</Button>
</SimpleNavItem>
</>
)}
<Button
variant="outline"
as={RouterLink}
to="/relays/webrtc"
leftIcon={<Server05 boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/webrtc") ? "primary" : undefined}
>
<SimpleNavItem to="/relays/webrtc" leftIcon={<Server05 boxSize={6} />}>
WebRTC Relays
</Button>
</SimpleNavItem>
{nip05?.exists && (
<Button
variant="outline"
as={RouterLink}
to="/relays/nip05"
leftIcon={<AtIcon boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/nip05") ? "primary" : undefined}
>
<SimpleNavItem to="/relays/nip05" leftIcon={<AtIcon boxSize={6} />}>
NIP-05 Relays
</Button>
</SimpleNavItem>
)}
{account && (
<>
<Button
variant="outline"
as={RouterLink}
to="/relays/contacts"
leftIcon={<UserSquare boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/contacts") ? "primary" : undefined}
>
<SimpleNavItem to="/relays/contacts" leftIcon={<UserSquare boxSize={6} />}>
Contact List Relays
</Button>
</SimpleNavItem>
</>
)}
{/* {account && (
@@ -132,6 +81,7 @@ export default function RelaysView() {
)} */}
</Flex>
);
if (vertical) {
if (location.pathname !== "/relays") return <Outlet />;
else return nav;

View File

@@ -1,70 +0,0 @@
import { useState } from "react";
import { useAsync } from "react-use";
import {
Button,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
Text,
Flex,
} from "@chakra-ui/react";
import { countEvents, countEventsByKind } from "nostr-idb";
import { Link as RouterLink } from "react-router-dom";
import { clearCacheData, deleteDatabase } from "../../services/db";
import { DatabaseIcon } from "../../components/icons";
import { localDatabase } from "../../services/local-relay";
function DatabaseStats() {
const { value: count } = useAsync(async () => await countEvents(localDatabase), []);
const { value: kinds } = useAsync(async () => await countEventsByKind(localDatabase), []);
return (
<>
<Text>{count} cached events</Text>
<Text>
{Object.entries(kinds || {})
.map(([kind, count]) => `${kind} (${count})`)
.join(", ")}
</Text>
</>
);
}
export default function DatabaseSettings() {
const [clearing, setClearing] = useState(false);
const handleClearData = async () => {
setClearing(true);
await clearCacheData();
setClearing(false);
};
const [deleting, setDeleting] = useState(false);
const handleDeleteDatabase = async () => {
setDeleting(true);
await deleteDatabase();
setDeleting(false);
};
return (
<AccordionItem>
<h2>
<AccordionButton fontSize="xl">
<DatabaseIcon mr="2" boxSize={5} />
<Box as="span" flex="1" textAlign="left">
Database
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Text>Database tools have moved</Text>
<Button as={RouterLink} to="/relays/cache/database" size="sm" colorScheme="primary">
Database Tools
</Button>
</AccordionPanel>
</AccordionItem>
);
}

View File

@@ -1,190 +0,0 @@
import { useFormContext } from "react-hook-form";
import { Link as RouterLink } from "react-router-dom";
import {
Flex,
FormControl,
FormLabel,
Switch,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
Input,
Select,
Textarea,
Link,
} from "@chakra-ui/react";
import { AppSettings } from "../../services/settings/migrations";
import { AppearanceIcon } from "../../components/icons";
import useSubject from "../../hooks/use-subject";
import localSettings from "../../services/local-settings";
export default function DisplaySettings() {
const { register } = useFormContext<AppSettings>();
const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
const enableNoteDrawer = useSubject(localSettings.enableNoteThreadDrawer);
return (
<AccordionItem>
<h2>
<AccordionButton fontSize="xl">
<AppearanceIcon mr="2" boxSize={5} />
<Box as="span" flex="1" textAlign="left">
Display
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel htmlFor="theme" mb="0">
Theme
</FormLabel>
<Select id="theme" {...register("theme")} maxW="sm">
<option value="default">Default</option>
<option value="chakraui">ChakraUI</option>
</Select>
</FormControl>
<FormControl>
<FormLabel htmlFor="colorMode" mb="0">
Color Mode
</FormLabel>
<Select id="colorMode" {...register("colorMode")} maxW="sm">
<option value="system">System Default</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</Select>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="primaryColor" mb="0">
Primary Color
</FormLabel>
<Input id="primaryColor" type="color" maxW="120" size="sm" {...register("primaryColor")} />
</Flex>
</FormControl>
<FormControl>
<FormLabel htmlFor="maxPageWidth" mb="0">
Max Page width
</FormLabel>
<Select id="maxPageWidth" {...register("maxPageWidth")} maxW="sm">
<option value="none">None</option>
<option value="md">Medium (~768px)</option>
<option value="lg">Large (~992px)</option>
<option value="xl">Extra Large (~1280px)</option>
</Select>
<FormHelperText>
<span>Setting this will restrict the width of app on desktop</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="blurImages" mb="0">
Blur media from strangers
</FormLabel>
<Switch id="blurImages" {...register("blurImages")} />
</Flex>
<FormHelperText>
<span>Enabled: blur media from people you aren't following</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="hideUsernames" mb="0">
Hide usernames (anon mode)
</FormLabel>
<Switch id="hideUsernames" {...register("hideUsernames")} />
</Flex>
<FormHelperText>
<span>
Enabled: hides usernames and pictures.{" "}
<Link
as={RouterLink}
color="blue.500"
to="/n/nevent1qqsxvkjgpc6zhydj4rxjpl0frev7hmgynruq027mujdgy2hwjypaqfspzpmhxue69uhkummnw3ezuamfdejszythwden5te0dehhxarjw4jjucm0d5sfntd0"
>
Details
</Link>
</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="removeEmojisInUsernames" mb="0">
Hide Emojis in usernames
</FormLabel>
<Switch id="removeEmojisInUsernames" {...register("removeEmojisInUsernames")} />
</Flex>
<FormHelperText>
<span>Enabled: Removes all emojis in other users usernames and display names</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="hideZapBubbles" mb="0">
Hide individual zaps on notes
</FormLabel>
<Switch
id="hideZapBubbles"
isChecked={hideZapBubbles}
onChange={() => localSettings.hideZapBubbles.next(!localSettings.hideZapBubbles.value)}
/>
</Flex>
<FormHelperText>
<span>Enabled: Hides individual zaps on notes in the timeline</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="show-content-warning" mb="0">
Show content warning
</FormLabel>
<Switch id="show-content-warning" {...register("showContentWarning")} />
</Flex>
<FormHelperText>
<span>Enabled: shows a warning for notes with NIP-36 Content Warning</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="enableNoteDrawer" mb="0">
Open embedded notes in side drawer
</FormLabel>
<Switch
id="enableNoteDrawer"
isChecked={enableNoteDrawer}
onChange={() => localSettings.enableNoteThreadDrawer.next(!localSettings.enableNoteThreadDrawer.value)}
/>
</Flex>
<FormHelperText>
<span>Enabled: Clicking on an embedded note will open it in a side drawer</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="muted-words" mb="0">
Muted words
</FormLabel>
<Textarea
id="muted-words"
{...register("mutedWords")}
placeholder="Broccoli, Spinach, Artichoke..."
maxW="2xl"
/>
<FormHelperText>
<span>
Comma separated list of words, phrases or hashtags you never want to see in notes. (case insensitive)
</span>
<br />
<span>Be careful its easy to hide all notes if you add common words.</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
);
}

View File

@@ -0,0 +1,185 @@
import { Link as RouterLink } from "react-router-dom";
import {
Flex,
FormControl,
FormLabel,
Switch,
FormHelperText,
Input,
Select,
Textarea,
Link,
Heading,
Button,
} from "@chakra-ui/react";
import useSubject from "../../../hooks/use-subject";
import localSettings from "../../../services/local-settings";
import useSettingsForm from "../use-settings-form";
import VerticalPageLayout from "../../../components/vertical-page-layout";
export default function DisplaySettings() {
const { register, submit, formState } = useSettingsForm();
const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
const enableNoteDrawer = useSubject(localSettings.enableNoteThreadDrawer);
return (
<VerticalPageLayout flex={1}>
<Heading size="md">Display Settings</Heading>
<Flex as="form" onSubmit={submit} direction="column" gap="4">
<FormControl>
<FormLabel htmlFor="theme" mb="0">
Theme
</FormLabel>
<Select id="theme" {...register("theme")} maxW="sm">
<option value="default">Default</option>
<option value="chakraui">ChakraUI</option>
</Select>
</FormControl>
<FormControl>
<FormLabel htmlFor="colorMode" mb="0">
Color Mode
</FormLabel>
<Select id="colorMode" {...register("colorMode")} maxW="sm">
<option value="system">System Default</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</Select>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="primaryColor" mb="0">
Primary Color
</FormLabel>
<Input id="primaryColor" type="color" maxW="120" size="sm" {...register("primaryColor")} />
</Flex>
</FormControl>
<FormControl>
<FormLabel htmlFor="maxPageWidth" mb="0">
Max Page width
</FormLabel>
<Select id="maxPageWidth" {...register("maxPageWidth")} maxW="sm">
<option value="none">None</option>
<option value="md">Medium (~768px)</option>
<option value="lg">Large (~992px)</option>
<option value="xl">Extra Large (~1280px)</option>
</Select>
<FormHelperText>
<span>Setting this will restrict the width of app on desktop</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="blurImages" mb="0">
Blur media from strangers
</FormLabel>
<Switch id="blurImages" {...register("blurImages")} />
</Flex>
<FormHelperText>
<span>Enabled: blur media from people you aren't following</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="hideUsernames" mb="0">
Hide usernames (anon mode)
</FormLabel>
<Switch id="hideUsernames" {...register("hideUsernames")} />
</Flex>
<FormHelperText>
<span>
Enabled: hides usernames and pictures.{" "}
<Link
as={RouterLink}
color="blue.500"
to="/n/nevent1qqsxvkjgpc6zhydj4rxjpl0frev7hmgynruq027mujdgy2hwjypaqfspzpmhxue69uhkummnw3ezuamfdejszythwden5te0dehhxarjw4jjucm0d5sfntd0"
>
Details
</Link>
</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="removeEmojisInUsernames" mb="0">
Hide Emojis in usernames
</FormLabel>
<Switch id="removeEmojisInUsernames" {...register("removeEmojisInUsernames")} />
</Flex>
<FormHelperText>
<span>Enabled: Removes all emojis in other users usernames and display names</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="hideZapBubbles" mb="0">
Hide individual zaps on notes
</FormLabel>
<Switch
id="hideZapBubbles"
isChecked={hideZapBubbles}
onChange={() => localSettings.hideZapBubbles.next(!localSettings.hideZapBubbles.value)}
/>
</Flex>
<FormHelperText>
<span>Enabled: Hides individual zaps on notes in the timeline</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="show-content-warning" mb="0">
Show content warning
</FormLabel>
<Switch id="show-content-warning" {...register("showContentWarning")} />
</Flex>
<FormHelperText>
<span>Enabled: shows a warning for notes with NIP-36 Content Warning</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="enableNoteDrawer" mb="0">
Open embedded notes in side drawer
</FormLabel>
<Switch
id="enableNoteDrawer"
isChecked={enableNoteDrawer}
onChange={() => localSettings.enableNoteThreadDrawer.next(!localSettings.enableNoteThreadDrawer.value)}
/>
</Flex>
<FormHelperText>
<span>Enabled: Clicking on an embedded note will open it in a side drawer</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="muted-words" mb="0">
Muted words
</FormLabel>
<Textarea
id="muted-words"
{...register("mutedWords")}
placeholder="Broccoli, Spinach, Artichoke..."
maxW="2xl"
/>
<FormHelperText>
<span>
Comma separated list of words, phrases or hashtags you never want to see in notes. (case insensitive)
</span>
<br />
<span>Be careful its easy to hide all notes if you add common words.</span>
</FormHelperText>
</FormControl>
</Flex>
<Button
ml="auto"
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
isDisabled={!formState.isDirty}
colorScheme="primary"
type="submit"
>
Save Settings
</Button>
</VerticalPageLayout>
);
}

View File

@@ -1,64 +1,113 @@
import { Button, Flex, Accordion, Link, useToast } from "@chakra-ui/react";
import { GithubIcon } from "../../components/icons";
import LightningSettings from "./lightning-settings";
import DatabaseSettings from "./database-settings";
import DisplaySettings from "./display-settings";
import PerformanceSettings from "./performance-settings";
import PrivacySettings from "./privacy-settings";
import useAppSettings from "../../hooks/use-app-settings";
import { FormProvider, useForm } from "react-hook-form";
import VerticalPageLayout from "../../components/vertical-page-layout";
import VersionButton from "../../components/version-button";
import PostSettings from "./post-settings";
import { Flex, Heading } from "@chakra-ui/react";
import { Outlet, useMatch } from "react-router-dom";
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
import SimpleNavItem from "../../components/simple-nav-item";
import { ErrorBoundary } from "../../components/error-boundary";
import {
AppearanceIcon,
DatabaseIcon,
LightningIcon,
NotesIcon,
PerformanceIcon,
SpyIcon,
} from "../../components/icons";
export default function SettingsView() {
const toast = useToast();
const { updateSettings, ...settings } = useAppSettings();
const match = useMatch("/settings");
const isMobile = useBreakpointValue({ base: true, lg: false });
const showMenu = !isMobile || !!match;
const form = useForm({
mode: "all",
values: settings,
resetOptions: {
keepDirty: true,
},
});
const saveSettings = form.handleSubmit(async (values) => {
try {
await updateSettings(values);
toast({ title: "Settings saved", status: "success" });
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
});
if (showMenu) {
return (
<Flex overflow="hidden" flex={1} direction={{ base: "column", lg: "row" }}>
<Flex overflowY="auto" overflowX="hidden" h="full" minW="xs" direction="column">
<Heading title="Settings" />
<Flex direction="column" p="2" gap="2">
<SimpleNavItem to="/settings/display" leftIcon={<AppearanceIcon boxSize={5} />}>
Display
</SimpleNavItem>
<SimpleNavItem to="/settings/post" leftIcon={<NotesIcon boxSize={5} />}>
Posts
</SimpleNavItem>
<SimpleNavItem to="/settings/performance" leftIcon={<PerformanceIcon boxSize={5} />}>
Performance
</SimpleNavItem>
<SimpleNavItem to="/settings/lightning" leftIcon={<LightningIcon boxSize={5} />}>
Lightning
</SimpleNavItem>
<SimpleNavItem to="/settings/privacy" leftIcon={<SpyIcon boxSize={5} />}>
Privacy
</SimpleNavItem>
<SimpleNavItem to="/relays/cache/database" leftIcon={<DatabaseIcon boxSize={5} />}>
Database Tools
</SimpleNavItem>
</Flex>
</Flex>
{!isMobile && (
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
)}
</Flex>
);
}
return (
<VerticalPageLayout as="form" onSubmit={saveSettings}>
<FormProvider {...form}>
<Accordion defaultIndex={[]} allowMultiple>
<DisplaySettings />
<PostSettings />
<PerformanceSettings />
<PrivacySettings />
<LightningSettings />
<DatabaseSettings />
</Accordion>
</FormProvider>
<Flex gap="4" padding="4" alignItems="center" wrap="wrap">
<Link isExternal href="https://github.com/hzrd149/nostrudel">
<GithubIcon /> Github
</Link>
<VersionButton />
<Button
ml="auto"
isLoading={form.formState.isLoading || form.formState.isValidating || form.formState.isSubmitting}
isDisabled={!form.formState.isDirty}
colorScheme="primary"
type="submit"
>
Save Settings
</Button>
</Flex>
</VerticalPageLayout>
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
);
}
// export default function SettingsView() {
// const toast = useToast();
// const { updateSettings, ...settings } = useAppSettings();
// const form = useForm({
// mode: "all",
// values: settings,
// resetOptions: {
// keepDirty: true,
// },
// });
// const saveSettings = form.handleSubmit(async (values) => {
// try {
// await updateSettings(values);
// toast({ title: "Settings saved", status: "success" });
// } catch (e) {
// if (e instanceof Error) toast({ description: e.message, status: "error" });
// }
// });
// return (
// <VerticalPageLayout as="form" onSubmit={saveSettings}>
// <FormProvider {...form}>
// <Accordion defaultIndex={[]} allowMultiple>
// <DisplaySettings />
// <PostSettings />
// <PerformanceSettings />
// <PrivacySettings />
// <LightningSettings />
// <DatabaseSettings />
// </Accordion>
// </FormProvider>
// <Flex gap="4" padding="4" alignItems="center" wrap="wrap">
// <Link isExternal href="https://github.com/hzrd149/nostrudel">
// <GithubIcon /> Github
// </Link>
// <VersionButton />
// <Button
// ml="auto"
// isLoading={form.formState.isLoading || form.formState.isValidating || form.formState.isSubmitting}
// isDisabled={!form.formState.isDirty}
// colorScheme="primary"
// type="submit"
// >
// Save Settings
// </Button>
// </Flex>
// </VerticalPageLayout>
// );
// }

View File

@@ -1,79 +0,0 @@
import { Button as BitcoinConnectButton } from "@getalby/bitcoin-connect-react";
import {
Flex,
FormControl,
FormLabel,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
Input,
Switch,
FormErrorMessage,
} from "@chakra-ui/react";
import { LightningIcon } from "../../components/icons";
import { useFormContext } from "react-hook-form";
import { AppSettings } from "../../services/settings/migrations";
export default function LightningSettings() {
const { register, formState } = useFormContext<AppSettings>();
return (
<AccordionItem>
{({ isExpanded }) => (
<>
<h2>
<AccordionButton fontSize="xl">
<LightningIcon mr="2" boxSize={5} />
<Box as="span" flex="1" textAlign="left">
Lightning
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
{isExpanded && <BitcoinConnectButton />}
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="autoPayWithWebLN" mb="0">
Auto pay with WebLN
</FormLabel>
<Switch id="autoPayWithWebLN" {...register("autoPayWithWebLN")} />
</Flex>
<FormHelperText>
<span>Enabled: Attempt to automatically pay with WebLN if its available</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="customZapAmounts" mb="0">
Zap Amounts
</FormLabel>
<Input
id="customZapAmounts"
autoComplete="off"
{...register("customZapAmounts", {
validate: (v) => {
if (!/^[\d,]*$/.test(v)) return "Must be a list of comma separated numbers";
return true;
},
})}
/>
{formState.errors.customZapAmounts && (
<FormErrorMessage>{formState.errors.customZapAmounts.message}</FormErrorMessage>
)}
<FormHelperText>
<span>Comma separated list of custom zap amounts</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</>
)}
</AccordionItem>
);
}

View File

@@ -0,0 +1,71 @@
import { Button as BitcoinConnectButton } from "@getalby/bitcoin-connect-react";
import {
Flex,
FormControl,
FormLabel,
FormHelperText,
Input,
Switch,
FormErrorMessage,
Heading,
Button,
} from "@chakra-ui/react";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import useSettingsForm from "../use-settings-form";
export default function LightningSettings() {
const { register, submit, formState } = useSettingsForm();
return (
<VerticalPageLayout as="form" onSubmit={submit} flex={1}>
<Heading size="md">Lightning Settings</Heading>
<Flex direction="column" gap="4">
<BitcoinConnectButton />
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="autoPayWithWebLN" mb="0">
Auto pay with WebLN
</FormLabel>
<Switch id="autoPayWithWebLN" {...register("autoPayWithWebLN")} />
</Flex>
<FormHelperText>
<span>Enabled: Attempt to automatically pay with WebLN if its available</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="customZapAmounts" mb="0">
Zap Amounts
</FormLabel>
<Input
id="customZapAmounts"
maxW="sm"
autoComplete="off"
{...register("customZapAmounts", {
validate: (v) => {
if (!/^[\d,]*$/.test(v)) return "Must be a list of comma separated numbers";
return true;
},
})}
/>
{formState.errors.customZapAmounts && (
<FormErrorMessage>{formState.errors.customZapAmounts.message}</FormErrorMessage>
)}
<FormHelperText>
<span>Comma separated list of custom zap amounts</span>
</FormHelperText>
</FormControl>
</Flex>
<Button
ml="auto"
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
isDisabled={!formState.isDirty}
colorScheme="primary"
type="submit"
>
Save Settings
</Button>
</VerticalPageLayout>
);
}

View File

@@ -1,144 +0,0 @@
import { useFormContext } from "react-hook-form";
import {
Flex,
FormControl,
FormLabel,
Switch,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
Input,
Link,
FormErrorMessage,
Select,
Button,
Text,
} from "@chakra-ui/react";
import { useLocalStorage } from "react-use";
import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
import { PerformanceIcon } from "../../components/icons";
import { selectedMethod } from "../../services/verify-event";
function VerifyEventSettings() {
const [verifyEventMethod, setVerifyEventMethod] = useLocalStorage<string>("verify-event-method", "internal", {
raw: true,
});
return (
<>
<FormControl>
<FormLabel htmlFor="verifyEventMethod" mb="0">
Verify event method
</FormLabel>
<Select value={verifyEventMethod} onChange={(e) => setVerifyEventMethod(e.target.value)} maxW="sm">
<option value="wasm">WebAssembly</option>
<option value="internal">Internal</option>
<option value="none">None</option>
</Select>
<FormHelperText>Default: All events signatures are checked</FormHelperText>
<FormHelperText>WebAssembly: Events signatures are checked in a separate thread</FormHelperText>
<FormHelperText>None: Only Profiles, Follows, and replaceable event signatures are checked</FormHelperText>
{selectedMethod !== verifyEventMethod && (
<>
<Text color="blue.500" mt="2">
NOTE: You must reload the app for this setting to take effect
</Text>
<Button colorScheme="primary" size="sm" onClick={() => location.reload()}>
Reload App
</Button>
</>
)}
</FormControl>
</>
);
}
export default function PerformanceSettings() {
const { register, formState } = useFormContext<AppSettings>();
return (
<AccordionItem>
<h2>
<AccordionButton fontSize="xl">
<PerformanceIcon mr="2" boxSize={5} />
<Box as="span" flex="1" textAlign="left">
Performance
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="proxy-user-media" mb="0">
Proxy user media
</FormLabel>
<Switch id="proxy-user-media" {...register("proxyUserMedia")} />
</Flex>
<FormHelperText>
<span>Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)</span>
<br />
<span>Side Effect: Some user pictures may not load or may be outdated</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="imageProxy" mb="0">
Image proxy service
</FormLabel>
<Input
id="imageProxy"
type="url"
{...register("imageProxy", {
setValueAs: (v) => safeUrl(v) || v,
})}
/>
{formState.errors.imageProxy && <FormErrorMessage>{formState.errors.imageProxy.message}</FormErrorMessage>}
<FormHelperText>
<span>
A URL to an instance of{" "}
<Link href="https://github.com/willnorris/imageproxy" isExternal target="_blank">
willnorris/imageproxy
</Link>
</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="autoShowMedia" mb="0">
Show embeds
</FormLabel>
<Switch id="autoShowMedia" {...register("autoShowMedia")} />
</Flex>
<FormHelperText>Disabled: Embeds will show an expandable button</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="showReactions" mb="0">
Show reactions
</FormLabel>
<Switch id="showReactions" {...register("showReactions")} />
</Flex>
<FormHelperText>Enabled: Show reactions on notes</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="autoDecryptDMs" mb="0">
Automatically decrypt DMs
</FormLabel>
<Switch id="autoDecryptDMs" {...register("autoDecryptDMs")} />
</Flex>
<FormHelperText>Enabled: automatically decrypt direct messages</FormHelperText>
</FormControl>
<VerifyEventSettings />
</Flex>
</AccordionPanel>
</AccordionItem>
);
}

View File

@@ -0,0 +1,139 @@
import {
Flex,
FormControl,
FormLabel,
Switch,
FormHelperText,
Input,
Link,
FormErrorMessage,
Select,
Button,
Text,
Heading,
} from "@chakra-ui/react";
import { useLocalStorage } from "react-use";
import { safeUrl } from "../../../helpers/parse";
import { selectedMethod } from "../../../services/verify-event";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import useSettingsForm from "../use-settings-form";
function VerifyEventSettings() {
const [verifyEventMethod, setVerifyEventMethod] = useLocalStorage<string>("verify-event-method", "internal", {
raw: true,
});
return (
<>
<FormControl>
<FormLabel htmlFor="verifyEventMethod" mb="0">
Verify event method
</FormLabel>
<Select value={verifyEventMethod} onChange={(e) => setVerifyEventMethod(e.target.value)} maxW="sm">
<option value="wasm">WebAssembly</option>
<option value="internal">Internal</option>
<option value="none">None</option>
</Select>
<FormHelperText>Default: All events signatures are checked</FormHelperText>
<FormHelperText>WebAssembly: Events signatures are checked in a separate thread</FormHelperText>
<FormHelperText>None: Only Profiles, Follows, and replaceable event signatures are checked</FormHelperText>
{selectedMethod !== verifyEventMethod && (
<>
<Text color="blue.500" mt="2">
NOTE: You must reload the app for this setting to take effect
</Text>
<Button colorScheme="primary" size="sm" onClick={() => location.reload()}>
Reload App
</Button>
</>
)}
</FormControl>
</>
);
}
export default function PerformanceSettings() {
const { register, submit, formState } = useSettingsForm();
return (
<VerticalPageLayout as="form" onSubmit={submit} flex={1}>
<Heading size="md">Performance Settings</Heading>
<Flex direction="column" gap="4">
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="proxy-user-media" mb="0">
Proxy user media
</FormLabel>
<Switch id="proxy-user-media" {...register("proxyUserMedia")} />
</Flex>
<FormHelperText>
<span>Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)</span>
<br />
<span>Side Effect: Some user pictures may not load or may be outdated</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="imageProxy" mb="0">
Image proxy service
</FormLabel>
<Input
id="imageProxy"
maxW="sm"
type="url"
{...register("imageProxy", {
setValueAs: (v) => safeUrl(v) || v,
})}
/>
{formState.errors.imageProxy && <FormErrorMessage>{formState.errors.imageProxy.message}</FormErrorMessage>}
<FormHelperText>
<span>
A URL to an instance of{" "}
<Link href="https://github.com/willnorris/imageproxy" isExternal target="_blank">
willnorris/imageproxy
</Link>
</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="autoShowMedia" mb="0">
Show embeds
</FormLabel>
<Switch id="autoShowMedia" {...register("autoShowMedia")} />
</Flex>
<FormHelperText>Disabled: Embeds will show an expandable button</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="showReactions" mb="0">
Show reactions
</FormLabel>
<Switch id="showReactions" {...register("showReactions")} />
</Flex>
<FormHelperText>Enabled: Show reactions on notes</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="autoDecryptDMs" mb="0">
Automatically decrypt DMs
</FormLabel>
<Switch id="autoDecryptDMs" {...register("autoDecryptDMs")} />
</Flex>
<FormHelperText>Enabled: automatically decrypt direct messages</FormHelperText>
</FormControl>
<VerifyEventSettings />
</Flex>
<Button
ml="auto"
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
isDisabled={!formState.isDirty}
colorScheme="primary"
type="submit"
>
Save Settings
</Button>
</VerticalPageLayout>
);
}

View File

@@ -1,178 +0,0 @@
import { useMemo, useState } from "react";
import { useFormContext } from "react-hook-form";
import { Link as RouterLink } from "react-router-dom";
import {
Flex,
FormControl,
FormLabel,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
Input,
Tag,
TagLabel,
TagCloseButton,
useDisclosure,
IconButton,
Button,
Select,
Link,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
} from "@chakra-ui/react";
import { matchSorter } from "match-sorter";
import { AppSettings } from "../../services/settings/migrations";
import { EditIcon, NotesIcon } from "../../components/icons";
import { useContextEmojis } from "../../providers/global/emoji-provider";
import useUsersMediaServers from "../../hooks/use-user-media-servers";
import useCurrentAccount from "../../hooks/use-current-account";
export default function PostSettings() {
const account = useCurrentAccount();
const { register, setValue, getValues, watch } = useFormContext<AppSettings>();
const emojiPicker = useDisclosure();
const { servers: mediaServers } = useUsersMediaServers(account?.pubkey);
const emojis = useContextEmojis();
const [emojiSearch, setEmojiSearch] = useState("");
watch("quickReactions");
watch("mediaUploadService");
const filteredEmojis = useMemo(() => {
const values = getValues();
if (emojiSearch.trim()) {
const noCustom = emojis.filter((e) => e.char && !e.url && !values.quickReactions.includes(e.char));
return matchSorter(noCustom, emojiSearch.trim(), { keys: ["keywords", "char"] }).slice(0, 10);
}
return [];
}, [emojiSearch, getValues().quickReactions]);
const addEmoji = (char: string) => {
const values = getValues();
if (values.quickReactions.includes(char)) return;
setValue("quickReactions", values.quickReactions.concat(char), { shouldTouch: true, shouldDirty: true });
};
const removeEmoji = (char: string) => {
const values = getValues();
if (!values.quickReactions.includes(char)) return;
setValue(
"quickReactions",
values.quickReactions.filter((e) => e !== char),
{ shouldTouch: true, shouldDirty: true },
);
};
return (
<AccordionItem>
<h2>
<AccordionButton fontSize="xl">
<NotesIcon mr="2" boxSize={5} />
<Box as="span" flex="1" textAlign="left">
Post
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel htmlFor="quickReactions" mb="0">
Quick Reactions
</FormLabel>
<Flex gap="2" wrap="wrap">
{getValues().quickReactions.map((char, i) => (
<Tag key={char + i} size="lg">
<TagLabel>{char}</TagLabel>
{emojiPicker.isOpen && <TagCloseButton onClick={() => removeEmoji(char)} />}
</Tag>
))}
{!emojiPicker.isOpen && (
<Button size="sm" onClick={emojiPicker.onOpen} leftIcon={<EditIcon />}>
Customize
</Button>
)}
</Flex>
{emojiPicker.isOpen && (
<>
<Input
type="search"
w="sm"
h="8"
value={emojiSearch}
onChange={(e) => setEmojiSearch(e.target.value)}
my="2"
/>
<Flex gap="2" wrap="wrap">
{filteredEmojis.map((emoji) => (
<IconButton
key={emoji.char}
icon={<span>{emoji.char}</span>}
aria-label={`Add ${emoji.name}`}
title={`Add ${emoji.name}`}
variant="outline"
size="sm"
fontSize="lg"
onClick={() => addEmoji(emoji.char)}
/>
))}
</Flex>
</>
)}
</FormControl>
<FormControl>
<FormLabel htmlFor="theme" mb="0">
Media upload service
</FormLabel>
<Select id="mediaUploadService" w="sm" {...register("mediaUploadService")}>
<option value="nostr.build">nostr.build</option>
<option value="blossom">Blossom</option>
</Select>
{getValues().mediaUploadService === "nostr.build" && (
<>
<FormHelperText>
Its a good idea to sign up and pay for an account on{" "}
<Link href="https://nostr.build/login/" target="_blank" color="blue.500">
nostr.build
</Link>
</FormHelperText>
</>
)}
{getValues().mediaUploadService === "blossom" && (!mediaServers || mediaServers.length === 0) && (
<Alert status="error" mt="2" flexWrap="wrap">
<AlertIcon />
<AlertTitle>Missing media servers!</AlertTitle>
<AlertDescription>Looks like you don't have any media servers setup</AlertDescription>
<Button as={RouterLink} colorScheme="primary" ml="auto" size="sm" to="/relays/media-servers">
Setup servers
</Button>
</Alert>
)}
</FormControl>
<FormControl>
<FormLabel htmlFor="noteDifficulty" mb="0">
Proof of work
</FormLabel>
<Input
id="noteDifficulty"
{...register("noteDifficulty", { min: 0, max: 64, valueAsNumber: true })}
step={1}
maxW="xs"
/>
<FormHelperText>
<span>How much Proof of work to mine when writing notes. setting this to 0 will disable it</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
);
}

View File

@@ -0,0 +1,173 @@
import { useMemo, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import {
Flex,
FormControl,
FormLabel,
FormHelperText,
Input,
Tag,
TagLabel,
TagCloseButton,
useDisclosure,
IconButton,
Button,
Select,
Link,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
Heading,
} from "@chakra-ui/react";
import { matchSorter } from "match-sorter";
import { EditIcon } from "../../../components/icons";
import { useContextEmojis } from "../../../providers/global/emoji-provider";
import useUsersMediaServers from "../../../hooks/use-user-media-servers";
import useCurrentAccount from "../../../hooks/use-current-account";
import useSettingsForm from "../use-settings-form";
import VerticalPageLayout from "../../../components/vertical-page-layout";
export default function PostSettings() {
const account = useCurrentAccount();
const { register, setValue, getValues, watch, submit, formState } = useSettingsForm();
const emojiPicker = useDisclosure();
const { servers: mediaServers } = useUsersMediaServers(account?.pubkey);
const emojis = useContextEmojis();
const [emojiSearch, setEmojiSearch] = useState("");
watch("quickReactions");
watch("mediaUploadService");
const filteredEmojis = useMemo(() => {
const values = getValues();
if (emojiSearch.trim()) {
const noCustom = emojis.filter((e) => e.char && !e.url && !values.quickReactions.includes(e.char));
return matchSorter(noCustom, emojiSearch.trim(), { keys: ["keywords", "char"] }).slice(0, 10);
}
return [];
}, [emojiSearch, getValues().quickReactions]);
const addEmoji = (char: string) => {
const values = getValues();
if (values.quickReactions.includes(char)) return;
setValue("quickReactions", values.quickReactions.concat(char), { shouldTouch: true, shouldDirty: true });
};
const removeEmoji = (char: string) => {
const values = getValues();
if (!values.quickReactions.includes(char)) return;
setValue(
"quickReactions",
values.quickReactions.filter((e) => e !== char),
{ shouldTouch: true, shouldDirty: true },
);
};
return (
<VerticalPageLayout as="form" onSubmit={submit} flex={1}>
<Heading size="md">Post Settings</Heading>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel htmlFor="quickReactions" mb="0">
Quick Reactions
</FormLabel>
<Flex gap="2" wrap="wrap">
{getValues().quickReactions.map((char, i) => (
<Tag key={char + i} size="lg">
<TagLabel>{char}</TagLabel>
{emojiPicker.isOpen && <TagCloseButton onClick={() => removeEmoji(char)} />}
</Tag>
))}
{!emojiPicker.isOpen && (
<Button size="sm" onClick={emojiPicker.onOpen} leftIcon={<EditIcon />}>
Customize
</Button>
)}
</Flex>
{emojiPicker.isOpen && (
<>
<Input
type="search"
w="sm"
h="8"
value={emojiSearch}
onChange={(e) => setEmojiSearch(e.target.value)}
my="2"
/>
<Flex gap="2" wrap="wrap">
{filteredEmojis.map((emoji) => (
<IconButton
key={emoji.char}
icon={<span>{emoji.char}</span>}
aria-label={`Add ${emoji.name}`}
title={`Add ${emoji.name}`}
variant="outline"
size="sm"
fontSize="lg"
onClick={() => addEmoji(emoji.char)}
/>
))}
</Flex>
</>
)}
</FormControl>
<FormControl>
<FormLabel htmlFor="theme" mb="0">
Media upload service
</FormLabel>
<Select id="mediaUploadService" w="sm" {...register("mediaUploadService")}>
<option value="nostr.build">nostr.build</option>
<option value="blossom">Blossom</option>
</Select>
{getValues().mediaUploadService === "nostr.build" && (
<>
<FormHelperText>
Its a good idea to sign up and pay for an account on{" "}
<Link href="https://nostr.build/login/" target="_blank" color="blue.500">
nostr.build
</Link>
</FormHelperText>
</>
)}
{getValues().mediaUploadService === "blossom" && (!mediaServers || mediaServers.length === 0) && (
<Alert status="error" mt="2" flexWrap="wrap">
<AlertIcon />
<AlertTitle>Missing media servers!</AlertTitle>
<AlertDescription>Looks like you don't have any media servers setup</AlertDescription>
<Button as={RouterLink} colorScheme="primary" ml="auto" size="sm" to="/relays/media-servers">
Setup servers
</Button>
</Alert>
)}
</FormControl>
<FormControl>
<FormLabel htmlFor="noteDifficulty" mb="0">
Proof of work
</FormLabel>
<Input
id="noteDifficulty"
{...register("noteDifficulty", { min: 0, max: 64, valueAsNumber: true })}
step={1}
maxW="sm"
/>
<FormHelperText>
<span>How much Proof of work to mine when writing notes. setting this to 0 will disable it</span>
</FormHelperText>
</FormControl>
</Flex>
<Button
ml="auto"
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
isDisabled={!formState.isDirty}
colorScheme="primary"
type="submit"
>
Save Settings
</Button>
</VerticalPageLayout>
);
}

View File

@@ -1,205 +0,0 @@
import { useLocalStorage } from "react-use";
import {
Flex,
FormControl,
FormLabel,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
Input,
Link,
FormErrorMessage,
Code,
Switch,
Select,
} from "@chakra-ui/react";
import { useFormContext } from "react-hook-form";
import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
import { createRequestProxyUrl } from "../../helpers/request";
import { SpyIcon } from "../../components/icons";
import { RelayAuthMode } from "../../classes/relay-pool";
async function validateInvidiousUrl(url?: string) {
if (!url) return true;
try {
const res = await fetch(new URL("/api/v1/stats", url));
return res.ok || "Cant reach instance";
} catch (e) {
return "Cant reach instance";
}
}
async function validateRequestProxy(url?: string) {
if (!url) return true;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
const res = await fetch(createRequestProxyUrl("https://example.com", url), { signal: controller.signal });
return res.ok || "Cant reach instance";
} catch (e) {
return "Cant reach instance";
}
}
export default function PrivacySettings() {
const { register, formState } = useFormContext<AppSettings>();
const [defaultAuthMode, setDefaultAuthMode] = useLocalStorage<RelayAuthMode>("default-relay-auth-mode", "ask", {
raw: true,
});
return (
<AccordionItem>
<h2>
<AccordionButton fontSize="xl">
<SpyIcon mr="2" boxSize={5} />
<Box as="span" flex="1" textAlign="left">
Privacy
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel>Default authorization behavior</FormLabel>
<Select
size="sm"
w="xs"
rounded="md"
flexShrink={0}
value={defaultAuthMode || "ask"}
onChange={(e) => setDefaultAuthMode(e.target.value as RelayAuthMode)}
>
<option value="always">Always authenticate</option>
<option value="ask">Ask every time</option>
<option value="never">Never authenticate</option>
</Select>
<FormHelperText>How should the app handle relays requesting identification</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.twitterRedirect}>
<FormLabel>Nitter instance</FormLabel>
<Input
type="url"
placeholder="https://nitter.net/"
{...register("twitterRedirect", { setValueAs: safeUrl })}
/>
{formState.errors.twitterRedirect && (
<FormErrorMessage>{formState.errors.twitterRedirect.message}</FormErrorMessage>
)}
<FormHelperText>
Nitter is a privacy focused UI for twitter.{" "}
<Link href="https://github.com/zedeus/nitter/wiki/Instances" isExternal color="blue.500">
Nitter instances
</Link>
</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.youtubeRedirect}>
<FormLabel>Invidious instance</FormLabel>
<Input
type="url"
placeholder="Invidious instance url"
{...register("youtubeRedirect", {
validate: validateInvidiousUrl,
setValueAs: safeUrl,
})}
/>
{formState.errors.youtubeRedirect && (
<FormErrorMessage>{formState.errors.youtubeRedirect.message}</FormErrorMessage>
)}
<FormHelperText>
Invidious is a privacy focused UI for youtube.{" "}
<Link href="https://docs.invidious.io/instances" isExternal color="blue.500">
Invidious instances
</Link>
</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.redditRedirect}>
<FormLabel>Teddit / Libreddit instance</FormLabel>
<Input
type="url"
placeholder="https://nitter.net/"
{...register("redditRedirect", { setValueAs: safeUrl })}
/>
{formState.errors.redditRedirect && (
<FormErrorMessage>{formState.errors.redditRedirect.message}</FormErrorMessage>
)}
<FormHelperText>
Libreddit and Teddit are both privacy focused UIs for reddit.{" "}
<Link
href="https://github.com/libreddit/libreddit-instances/blob/master/instances.md"
isExternal
color="blue.500"
>
Libreddit instances
</Link>
{", "}
<Link href="https://codeberg.org/teddit/teddit#instances" isExternal color="blue.500">
Teddit instances
</Link>
</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.corsProxy}>
<FormLabel>Request Proxy</FormLabel>
{window.REQUEST_PROXY ? (
<>
<Input type="url" value={window.REQUEST_PROXY} onChange={() => {}} readOnly isDisabled />
<FormHelperText color="red.500">
This noStrudel version has the request proxy hard coded to <Code>{window.REQUEST_PROXY}</Code>
</FormHelperText>
</>
) : (
<Input
type="url"
placeholder="https://corsproxy.io/?<encoded_url>"
{...register("corsProxy", { validate: validateRequestProxy })}
/>
)}
{formState.errors.corsProxy && <FormErrorMessage>{formState.errors.corsProxy.message}</FormErrorMessage>}
<FormHelperText>
This is used as a fallback ( to bypass CORS restrictions ) or to make requests to .onion and .i2p domains
<br />
This can either point to an instance of{" "}
<Link href="https://github.com/Rob--W/cors-anywhere" isExternal color="blue.500">
cors-anywhere
</Link>{" "}
or{" "}
<Link href="https://corsproxy.io/" isExternal color="blue.500">
corsproxy.io
</Link>{" "}
<br />
<Code fontSize="0.9em">{`<url>`}</Code> or <Code fontSize="0.9em">{`<encoded_url>`}</Code> can be used to
inject the raw or the encoded url into the proxy url ( example:{" "}
<Code fontSize="0.9em" userSelect="all">{`https://corsproxy.io/?<encoded_url>`}</Code> )
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="loadOpenGraphData" mb="0">
Load Open Graph data
</FormLabel>
<Switch id="loadOpenGraphData" {...register("loadOpenGraphData")} />
</Flex>
<FormHelperText>
<span>
Whether to load{" "}
<Link href="https://ogp.me/" isExternal color="blue.500">
Open Graph
</Link>{" "}
data for links
</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
);
}

View File

@@ -0,0 +1,203 @@
import { useLocalStorage } from "react-use";
import {
Flex,
FormControl,
FormHelperText,
Input,
Link,
FormErrorMessage,
Code,
Switch,
Select,
Button,
Heading,
FormLabel,
} from "@chakra-ui/react";
import { safeUrl } from "../../../helpers/parse";
import { createRequestProxyUrl } from "../../../helpers/request";
import { RelayAuthMode } from "../../../classes/relay-pool";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import useSettingsForm from "../use-settings-form";
async function validateInvidiousUrl(url?: string) {
if (!url) return true;
try {
const res = await fetch(new URL("/api/v1/stats", url));
return res.ok || "Cant reach instance";
} catch (e) {
return "Cant reach instance";
}
}
async function validateRequestProxy(url?: string) {
if (!url) return true;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
const res = await fetch(createRequestProxyUrl("https://example.com", url), { signal: controller.signal });
return res.ok || "Cant reach instance";
} catch (e) {
return "Cant reach instance";
}
}
export default function PrivacySettings() {
const { register, submit, formState } = useSettingsForm();
const [defaultAuthMode, setDefaultAuthMode] = useLocalStorage<RelayAuthMode>("default-relay-auth-mode", "ask", {
raw: true,
});
return (
<VerticalPageLayout as="form" onSubmit={submit} flex={1}>
<Heading size="md">Privacy Settings</Heading>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel>Default authorization behavior</FormLabel>
<Select
w="xs"
rounded="md"
flexShrink={0}
value={defaultAuthMode || "ask"}
onChange={(e) => setDefaultAuthMode(e.target.value as RelayAuthMode)}
>
<option value="always">Always authenticate</option>
<option value="ask">Ask every time</option>
<option value="never">Never authenticate</option>
</Select>
<FormHelperText>How should the app handle relays requesting identification</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.twitterRedirect}>
<FormLabel>Nitter instance</FormLabel>
<Input
type="url"
maxW="sm"
placeholder="https://nitter.net/"
{...register("twitterRedirect", { setValueAs: safeUrl })}
/>
{formState.errors.twitterRedirect && (
<FormErrorMessage>{formState.errors.twitterRedirect.message}</FormErrorMessage>
)}
<FormHelperText>
Nitter is a privacy focused UI for twitter.{" "}
<Link href="https://github.com/zedeus/nitter/wiki/Instances" isExternal color="blue.500">
Nitter instances
</Link>
</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.youtubeRedirect}>
<FormLabel>Invidious instance</FormLabel>
<Input
type="url"
maxW="sm"
placeholder="Invidious instance url"
{...register("youtubeRedirect", {
validate: validateInvidiousUrl,
setValueAs: safeUrl,
})}
/>
{formState.errors.youtubeRedirect && (
<FormErrorMessage>{formState.errors.youtubeRedirect.message}</FormErrorMessage>
)}
<FormHelperText>
Invidious is a privacy focused UI for youtube.{" "}
<Link href="https://docs.invidious.io/instances" isExternal color="blue.500">
Invidious instances
</Link>
</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.redditRedirect}>
<FormLabel>Teddit / Libreddit instance</FormLabel>
<Input
type="url"
placeholder="https://nitter.net/"
maxW="sm"
{...register("redditRedirect", { setValueAs: safeUrl })}
/>
{formState.errors.redditRedirect && (
<FormErrorMessage>{formState.errors.redditRedirect.message}</FormErrorMessage>
)}
<FormHelperText>
Libreddit and Teddit are both privacy focused UIs for reddit.{" "}
<Link
href="https://github.com/libreddit/libreddit-instances/blob/master/instances.md"
isExternal
color="blue.500"
>
Libreddit instances
</Link>
{", "}
<Link href="https://codeberg.org/teddit/teddit#instances" isExternal color="blue.500">
Teddit instances
</Link>
</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.corsProxy}>
<FormLabel>Request Proxy</FormLabel>
{window.REQUEST_PROXY ? (
<>
<Input type="url" value={window.REQUEST_PROXY} onChange={() => {}} readOnly isDisabled />
<FormHelperText color="red.500">
This noStrudel version has the request proxy hard coded to <Code>{window.REQUEST_PROXY}</Code>
</FormHelperText>
</>
) : (
<Input
type="url"
maxW="sm"
placeholder="https://corsproxy.io/?<encoded_url>"
{...register("corsProxy", { validate: validateRequestProxy })}
/>
)}
{formState.errors.corsProxy && <FormErrorMessage>{formState.errors.corsProxy.message}</FormErrorMessage>}
<FormHelperText>
This is used as a fallback ( to bypass CORS restrictions ) or to make requests to .onion and .i2p domains
<br />
This can either point to an instance of{" "}
<Link href="https://github.com/Rob--W/cors-anywhere" isExternal color="blue.500">
cors-anywhere
</Link>{" "}
or{" "}
<Link href="https://corsproxy.io/" isExternal color="blue.500">
corsproxy.io
</Link>{" "}
<br />
<Code fontSize="0.9em">{`<url>`}</Code> or <Code fontSize="0.9em">{`<encoded_url>`}</Code> can be used to
inject the raw or the encoded url into the proxy url ( example:{" "}
<Code fontSize="0.9em" userSelect="all">{`https://corsproxy.io/?<encoded_url>`}</Code> )
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="loadOpenGraphData" mb="0">
Load Open Graph data
</FormLabel>
<Switch id="loadOpenGraphData" {...register("loadOpenGraphData")} />
</Flex>
<FormHelperText>
<span>
Whether to load{" "}
<Link href="https://ogp.me/" isExternal color="blue.500">
Open Graph
</Link>{" "}
data for links
</span>
</FormHelperText>
</FormControl>
</Flex>
<Button
ml="auto"
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
isDisabled={!formState.isDirty}
colorScheme="primary"
type="submit"
>
Save Settings
</Button>
</VerticalPageLayout>
);
}

View File

@@ -0,0 +1,27 @@
import { useToast } from "@chakra-ui/react";
import useAppSettings from "../../hooks/use-app-settings";
import { useForm } from "react-hook-form";
export default function useSettingsForm() {
const toast = useToast();
const { updateSettings, ...settings } = useAppSettings();
const form = useForm({
mode: "all",
values: settings,
resetOptions: {
keepDirty: true,
},
});
const submit = form.handleSubmit(async (values) => {
try {
await updateSettings(values);
toast({ title: "Settings saved", status: "success" });
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
});
return { ...form, submit };
}