mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 12:00:32 +02:00
rebuild settings view tabs
This commit is contained in:
5
.changeset/lovely-needles-invite.md
Normal file
5
.changeset/lovely-needles-invite.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Rebuilt settings view tabs
|
18
src/app.tsx
18
src/app.tsx
@@ -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 />,
|
||||
|
23
src/components/simple-nav-item.tsx
Normal file
23
src/components/simple-nav-item.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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;
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
185
src/views/settings/display/index.tsx
Normal file
185
src/views/settings/display/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
// );
|
||||
// }
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
71
src/views/settings/lightning/index.tsx
Normal file
71
src/views/settings/lightning/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
139
src/views/settings/performance/index.tsx
Normal file
139
src/views/settings/performance/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
173
src/views/settings/post/index.tsx
Normal file
173
src/views/settings/post/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
203
src/views/settings/privacy/index.tsx
Normal file
203
src/views/settings/privacy/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
27
src/views/settings/use-settings-form.ts
Normal file
27
src/views/settings/use-settings-form.ts
Normal 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 };
|
||||
}
|
Reference in New Issue
Block a user