diff --git a/.changeset/lovely-needles-invite.md b/.changeset/lovely-needles-invite.md
new file mode 100644
index 000000000..5004b4e5a
--- /dev/null
+++ b/.changeset/lovely-needles-invite.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Rebuilt settings view tabs
diff --git a/src/app.tsx b/src/app.tsx
index c8b3b01b2..6de32873f 100644
--- a/src/app.tsx
+++ b/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: ,
},
{ path: "other-stuff", element: },
- { path: "settings", element: },
+ {
+ path: "settings",
+ element: ,
+ children: [
+ { path: "", element: },
+ { path: "post", element: },
+ { path: "display", element: },
+ { path: "privacy", element: },
+ { path: "lightning", element: },
+ { path: "performance", element: },
+ ],
+ },
{
path: "relays",
element: ,
diff --git a/src/components/simple-nav-item.tsx b/src/components/simple-nav-item.tsx
new file mode 100644
index 000000000..6a2f81a3b
--- /dev/null
+++ b/src/components/simple-nav-item.tsx
@@ -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 & { to: string }) {
+ const match = useMatch(to);
+
+ return (
+
+ );
+}
diff --git a/src/views/relays/index.tsx b/src/views/relays/index.tsx
index 73ea8c735..5df3be093 100644
--- a/src/views/relays/index.tsx
+++ b/src/views/relays/index.tsx
@@ -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 = (
- }
- >
+ }>
App Relays
-
- }
- >
+
+ }>
Cache Relay
-
+
{account && (
<>
- }
- colorScheme={location.pathname.startsWith("/relays/mailboxes") ? "primary" : undefined}
- >
+ }>
Mailboxes
-
- }
- colorScheme={location.pathname.startsWith("/relays/media-servers") ? "primary" : undefined}
- >
+
+ }>
Media Servers
-
- }
- colorScheme={location.pathname.startsWith("/relays/search") ? "primary" : undefined}
- >
+
+ }>
Search Relays
-
+
>
)}
- }
- colorScheme={location.pathname.startsWith("/relays/webrtc") ? "primary" : undefined}
- >
+ }>
WebRTC Relays
-
+
{nip05?.exists && (
- }
- colorScheme={location.pathname.startsWith("/relays/nip05") ? "primary" : undefined}
- >
+ }>
NIP-05 Relays
-
+
)}
{account && (
<>
- }
- colorScheme={location.pathname.startsWith("/relays/contacts") ? "primary" : undefined}
- >
+ }>
Contact List Relays
-
+
>
)}
{/* {account && (
@@ -132,6 +81,7 @@ export default function RelaysView() {
)} */}
);
+
if (vertical) {
if (location.pathname !== "/relays") return ;
else return nav;
diff --git a/src/views/settings/database-settings.tsx b/src/views/settings/database-settings.tsx
deleted file mode 100644
index 413c54e48..000000000
--- a/src/views/settings/database-settings.tsx
+++ /dev/null
@@ -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 (
- <>
- {count} cached events
-
- {Object.entries(kinds || {})
- .map(([kind, count]) => `${kind} (${count})`)
- .join(", ")}
-
- >
- );
-}
-
-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 (
-
-
-
-
-
- Database
-
-
-
-
-
- Database tools have moved
-
-
-
- );
-}
diff --git a/src/views/settings/display-settings.tsx b/src/views/settings/display-settings.tsx
deleted file mode 100644
index aac1b0f3c..000000000
--- a/src/views/settings/display-settings.tsx
+++ /dev/null
@@ -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();
-
- const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
- const enableNoteDrawer = useSubject(localSettings.enableNoteThreadDrawer);
-
- return (
-
-
-
-
-
- Display
-
-
-
-
-
-
-
-
- Theme
-
-
-
-
-
- Color Mode
-
-
-
-
-
-
- Primary Color
-
-
-
-
-
-
- Max Page width
-
-
-
- Setting this will restrict the width of app on desktop
-
-
-
-
-
- Blur media from strangers
-
-
-
-
- Enabled: blur media from people you aren't following
-
-
-
-
-
- Hide usernames (anon mode)
-
-
-
-
-
- Enabled: hides usernames and pictures.{" "}
-
- Details
-
-
-
-
-
-
-
- Hide Emojis in usernames
-
-
-
-
- Enabled: Removes all emojis in other users usernames and display names
-
-
-
-
-
- Hide individual zaps on notes
-
- localSettings.hideZapBubbles.next(!localSettings.hideZapBubbles.value)}
- />
-
-
- Enabled: Hides individual zaps on notes in the timeline
-
-
-
-
-
- Show content warning
-
-
-
-
- Enabled: shows a warning for notes with NIP-36 Content Warning
-
-
-
-
-
- Open embedded notes in side drawer
-
- localSettings.enableNoteThreadDrawer.next(!localSettings.enableNoteThreadDrawer.value)}
- />
-
-
- Enabled: Clicking on an embedded note will open it in a side drawer
-
-
-
-
- Muted words
-
-
-
-
- Comma separated list of words, phrases or hashtags you never want to see in notes. (case insensitive)
-
-
- Be careful its easy to hide all notes if you add common words.
-
-
-
-
-
- );
-}
diff --git a/src/views/settings/display/index.tsx b/src/views/settings/display/index.tsx
new file mode 100644
index 000000000..44510ab23
--- /dev/null
+++ b/src/views/settings/display/index.tsx
@@ -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 (
+
+ Display Settings
+
+
+
+ Theme
+
+
+
+
+
+ Color Mode
+
+
+
+
+
+
+ Primary Color
+
+
+
+
+
+
+ Max Page width
+
+
+
+ Setting this will restrict the width of app on desktop
+
+
+
+
+
+ Blur media from strangers
+
+
+
+
+ Enabled: blur media from people you aren't following
+
+
+
+
+
+ Hide usernames (anon mode)
+
+
+
+
+
+ Enabled: hides usernames and pictures.{" "}
+
+ Details
+
+
+
+
+
+
+
+ Hide Emojis in usernames
+
+
+
+
+ Enabled: Removes all emojis in other users usernames and display names
+
+
+
+
+
+ Hide individual zaps on notes
+
+ localSettings.hideZapBubbles.next(!localSettings.hideZapBubbles.value)}
+ />
+
+
+ Enabled: Hides individual zaps on notes in the timeline
+
+
+
+
+
+ Show content warning
+
+
+
+
+ Enabled: shows a warning for notes with NIP-36 Content Warning
+
+
+
+
+
+ Open embedded notes in side drawer
+
+ localSettings.enableNoteThreadDrawer.next(!localSettings.enableNoteThreadDrawer.value)}
+ />
+
+
+ Enabled: Clicking on an embedded note will open it in a side drawer
+
+
+
+
+ Muted words
+
+
+
+
+ Comma separated list of words, phrases or hashtags you never want to see in notes. (case insensitive)
+
+
+ Be careful its easy to hide all notes if you add common words.
+
+
+
+
+
+ );
+}
diff --git a/src/views/settings/index.tsx b/src/views/settings/index.tsx
index 833ff4c29..ee88ae32b 100644
--- a/src/views/settings/index.tsx
+++ b/src/views/settings/index.tsx
@@ -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 (
+
+
+
+
+ }>
+ Display
+
+ }>
+ Posts
+
+ }>
+ Performance
+
+ }>
+ Lightning
+
+ }>
+ Privacy
+
+ }>
+ Database Tools
+
+
+
+ {!isMobile && (
+
+
+
+ )}
+
+ );
+ }
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- Github
-
-
-
-
-
+
+
+
);
}
+
+// 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 (
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+// Github
+//
+//
+//
+//
+//
+// );
+// }
diff --git a/src/views/settings/lightning-settings.tsx b/src/views/settings/lightning-settings.tsx
deleted file mode 100644
index 6350f8840..000000000
--- a/src/views/settings/lightning-settings.tsx
+++ /dev/null
@@ -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();
-
- return (
-
- {({ isExpanded }) => (
- <>
-
-
-
-
- Lightning
-
-
-
-
-
-
- {isExpanded && }
-
-
-
- Auto pay with WebLN
-
-
-
-
-
- Enabled: Attempt to automatically pay with WebLN if its available
-
-
-
-
-
- Zap Amounts
-
- {
- if (!/^[\d,]*$/.test(v)) return "Must be a list of comma separated numbers";
- return true;
- },
- })}
- />
- {formState.errors.customZapAmounts && (
- {formState.errors.customZapAmounts.message}
- )}
-
- Comma separated list of custom zap amounts
-
-
-
-
- >
- )}
-
- );
-}
diff --git a/src/views/settings/lightning/index.tsx b/src/views/settings/lightning/index.tsx
new file mode 100644
index 000000000..095a9e1d1
--- /dev/null
+++ b/src/views/settings/lightning/index.tsx
@@ -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 (
+
+ Lightning Settings
+
+
+
+
+
+ Auto pay with WebLN
+
+
+
+
+
+ Enabled: Attempt to automatically pay with WebLN if its available
+
+
+
+
+
+ Zap Amounts
+
+ {
+ if (!/^[\d,]*$/.test(v)) return "Must be a list of comma separated numbers";
+ return true;
+ },
+ })}
+ />
+ {formState.errors.customZapAmounts && (
+ {formState.errors.customZapAmounts.message}
+ )}
+
+ Comma separated list of custom zap amounts
+
+
+
+
+
+ );
+}
diff --git a/src/views/settings/performance-settings.tsx b/src/views/settings/performance-settings.tsx
deleted file mode 100644
index 4d85ad94a..000000000
--- a/src/views/settings/performance-settings.tsx
+++ /dev/null
@@ -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("verify-event-method", "internal", {
- raw: true,
- });
-
- return (
- <>
-
-
- Verify event method
-
-
- Default: All events signatures are checked
- WebAssembly: Events signatures are checked in a separate thread
- None: Only Profiles, Follows, and replaceable event signatures are checked
-
- {selectedMethod !== verifyEventMethod && (
- <>
-
- NOTE: You must reload the app for this setting to take effect
-
-
- >
- )}
-
- >
- );
-}
-
-export default function PerformanceSettings() {
- const { register, formState } = useFormContext();
-
- return (
-
-
-
-
-
- Performance
-
-
-
-
-
-
-
-
-
- Proxy user media
-
-
-
-
- Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)
-
- Side Effect: Some user pictures may not load or may be outdated
-
-
-
-
- Image proxy service
-
- safeUrl(v) || v,
- })}
- />
- {formState.errors.imageProxy && {formState.errors.imageProxy.message}}
-
-
- A URL to an instance of{" "}
-
- willnorris/imageproxy
-
-
-
-
-
-
-
- Show embeds
-
-
-
- Disabled: Embeds will show an expandable button
-
-
-
-
- Show reactions
-
-
-
- Enabled: Show reactions on notes
-
-
-
-
- Automatically decrypt DMs
-
-
-
- Enabled: automatically decrypt direct messages
-
-
-
-
-
- );
-}
diff --git a/src/views/settings/performance/index.tsx b/src/views/settings/performance/index.tsx
new file mode 100644
index 000000000..25bb70b35
--- /dev/null
+++ b/src/views/settings/performance/index.tsx
@@ -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("verify-event-method", "internal", {
+ raw: true,
+ });
+
+ return (
+ <>
+
+
+ Verify event method
+
+
+ Default: All events signatures are checked
+ WebAssembly: Events signatures are checked in a separate thread
+ None: Only Profiles, Follows, and replaceable event signatures are checked
+
+ {selectedMethod !== verifyEventMethod && (
+ <>
+
+ NOTE: You must reload the app for this setting to take effect
+
+
+ >
+ )}
+
+ >
+ );
+}
+
+export default function PerformanceSettings() {
+ const { register, submit, formState } = useSettingsForm();
+
+ return (
+
+ Performance Settings
+
+
+
+
+ Proxy user media
+
+
+
+
+ Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)
+
+ Side Effect: Some user pictures may not load or may be outdated
+
+
+
+
+ Image proxy service
+
+ safeUrl(v) || v,
+ })}
+ />
+ {formState.errors.imageProxy && {formState.errors.imageProxy.message}}
+
+
+ A URL to an instance of{" "}
+
+ willnorris/imageproxy
+
+
+
+
+
+
+
+ Show embeds
+
+
+
+ Disabled: Embeds will show an expandable button
+
+
+
+
+ Show reactions
+
+
+
+ Enabled: Show reactions on notes
+
+
+
+
+ Automatically decrypt DMs
+
+
+
+ Enabled: automatically decrypt direct messages
+
+
+
+
+
+ );
+}
diff --git a/src/views/settings/post-settings.tsx b/src/views/settings/post-settings.tsx
deleted file mode 100644
index a05678730..000000000
--- a/src/views/settings/post-settings.tsx
+++ /dev/null
@@ -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();
- 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 (
-
-
-
-
-
- Post
-
-
-
-
-
-
-
-
- Quick Reactions
-
-
- {getValues().quickReactions.map((char, i) => (
-
- {char}
- {emojiPicker.isOpen && removeEmoji(char)} />}
-
- ))}
- {!emojiPicker.isOpen && (
- }>
- Customize
-
- )}
-
- {emojiPicker.isOpen && (
- <>
- setEmojiSearch(e.target.value)}
- my="2"
- />
-
- {filteredEmojis.map((emoji) => (
- {emoji.char}}
- aria-label={`Add ${emoji.name}`}
- title={`Add ${emoji.name}`}
- variant="outline"
- size="sm"
- fontSize="lg"
- onClick={() => addEmoji(emoji.char)}
- />
- ))}
-
- >
- )}
-
-
-
- Media upload service
-
-
-
- {getValues().mediaUploadService === "nostr.build" && (
- <>
-
- Its a good idea to sign up and pay for an account on{" "}
-
- nostr.build
-
-
- >
- )}
-
- {getValues().mediaUploadService === "blossom" && (!mediaServers || mediaServers.length === 0) && (
-
-
- Missing media servers!
- Looks like you don't have any media servers setup
-
-
- )}
-
-
-
-
- Proof of work
-
-
-
- How much Proof of work to mine when writing notes. setting this to 0 will disable it
-
-
-
-
-
- );
-}
diff --git a/src/views/settings/post/index.tsx b/src/views/settings/post/index.tsx
new file mode 100644
index 000000000..9d664bd85
--- /dev/null
+++ b/src/views/settings/post/index.tsx
@@ -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 (
+
+ Post Settings
+
+
+
+ Quick Reactions
+
+
+ {getValues().quickReactions.map((char, i) => (
+
+ {char}
+ {emojiPicker.isOpen && removeEmoji(char)} />}
+
+ ))}
+ {!emojiPicker.isOpen && (
+ }>
+ Customize
+
+ )}
+
+ {emojiPicker.isOpen && (
+ <>
+ setEmojiSearch(e.target.value)}
+ my="2"
+ />
+
+ {filteredEmojis.map((emoji) => (
+ {emoji.char}}
+ aria-label={`Add ${emoji.name}`}
+ title={`Add ${emoji.name}`}
+ variant="outline"
+ size="sm"
+ fontSize="lg"
+ onClick={() => addEmoji(emoji.char)}
+ />
+ ))}
+
+ >
+ )}
+
+
+
+ Media upload service
+
+
+
+ {getValues().mediaUploadService === "nostr.build" && (
+ <>
+
+ Its a good idea to sign up and pay for an account on{" "}
+
+ nostr.build
+
+
+ >
+ )}
+
+ {getValues().mediaUploadService === "blossom" && (!mediaServers || mediaServers.length === 0) && (
+
+
+ Missing media servers!
+ Looks like you don't have any media servers setup
+
+
+ )}
+
+
+
+
+ Proof of work
+
+
+
+ How much Proof of work to mine when writing notes. setting this to 0 will disable it
+
+
+
+
+
+ );
+}
diff --git a/src/views/settings/privacy-settings.tsx b/src/views/settings/privacy-settings.tsx
deleted file mode 100644
index 88276ade3..000000000
--- a/src/views/settings/privacy-settings.tsx
+++ /dev/null
@@ -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();
-
- const [defaultAuthMode, setDefaultAuthMode] = useLocalStorage("default-relay-auth-mode", "ask", {
- raw: true,
- });
-
- return (
-
-
-
-
-
- Privacy
-
-
-
-
-
-
-
- Default authorization behavior
-
- How should the app handle relays requesting identification
-
-
-
- Nitter instance
-
- {formState.errors.twitterRedirect && (
- {formState.errors.twitterRedirect.message}
- )}
-
- Nitter is a privacy focused UI for twitter.{" "}
-
- Nitter instances
-
-
-
-
-
- Invidious instance
-
- {formState.errors.youtubeRedirect && (
- {formState.errors.youtubeRedirect.message}
- )}
-
- Invidious is a privacy focused UI for youtube.{" "}
-
- Invidious instances
-
-
-
-
-
- Teddit / Libreddit instance
-
- {formState.errors.redditRedirect && (
- {formState.errors.redditRedirect.message}
- )}
-
- Libreddit and Teddit are both privacy focused UIs for reddit.{" "}
-
- Libreddit instances
-
- {", "}
-
- Teddit instances
-
-
-
-
-
- Request Proxy
- {window.REQUEST_PROXY ? (
- <>
- {}} readOnly isDisabled />
-
- This noStrudel version has the request proxy hard coded to {window.REQUEST_PROXY}
-
- >
- ) : (
-
- )}
- {formState.errors.corsProxy && {formState.errors.corsProxy.message}}
-
- This is used as a fallback ( to bypass CORS restrictions ) or to make requests to .onion and .i2p domains
-
- This can either point to an instance of{" "}
-
- cors-anywhere
- {" "}
- or{" "}
-
- corsproxy.io
- {" "}
-
- {``}
or {``}
can be used to
- inject the raw or the encoded url into the proxy url ( example:{" "}
- {`https://corsproxy.io/?`}
)
-
-
-
-
-
- Load Open Graph data
-
-
-
-
-
- Whether to load{" "}
-
- Open Graph
- {" "}
- data for links
-
-
-
-
-
-
- );
-}
diff --git a/src/views/settings/privacy/index.tsx b/src/views/settings/privacy/index.tsx
new file mode 100644
index 000000000..54f45067b
--- /dev/null
+++ b/src/views/settings/privacy/index.tsx
@@ -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("default-relay-auth-mode", "ask", {
+ raw: true,
+ });
+
+ return (
+
+ Privacy Settings
+
+
+ Default authorization behavior
+
+ How should the app handle relays requesting identification
+
+
+
+ Nitter instance
+
+ {formState.errors.twitterRedirect && (
+ {formState.errors.twitterRedirect.message}
+ )}
+
+ Nitter is a privacy focused UI for twitter.{" "}
+
+ Nitter instances
+
+
+
+
+
+ Invidious instance
+
+ {formState.errors.youtubeRedirect && (
+ {formState.errors.youtubeRedirect.message}
+ )}
+
+ Invidious is a privacy focused UI for youtube.{" "}
+
+ Invidious instances
+
+
+
+
+
+ Teddit / Libreddit instance
+
+ {formState.errors.redditRedirect && (
+ {formState.errors.redditRedirect.message}
+ )}
+
+ Libreddit and Teddit are both privacy focused UIs for reddit.{" "}
+
+ Libreddit instances
+
+ {", "}
+
+ Teddit instances
+
+
+
+
+
+ Request Proxy
+ {window.REQUEST_PROXY ? (
+ <>
+ {}} readOnly isDisabled />
+
+ This noStrudel version has the request proxy hard coded to {window.REQUEST_PROXY}
+
+ >
+ ) : (
+
+ )}
+ {formState.errors.corsProxy && {formState.errors.corsProxy.message}}
+
+ This is used as a fallback ( to bypass CORS restrictions ) or to make requests to .onion and .i2p domains
+
+ This can either point to an instance of{" "}
+
+ cors-anywhere
+ {" "}
+ or{" "}
+
+ corsproxy.io
+ {" "}
+
+ {``}
or {``}
can be used to
+ inject the raw or the encoded url into the proxy url ( example:{" "}
+ {`https://corsproxy.io/?`}
)
+
+
+
+
+
+ Load Open Graph data
+
+
+
+
+
+ Whether to load{" "}
+
+ Open Graph
+ {" "}
+ data for links
+
+
+
+
+
+
+ );
+}
diff --git a/src/views/settings/use-settings-form.ts b/src/views/settings/use-settings-form.ts
new file mode 100644
index 000000000..f9879444f
--- /dev/null
+++ b/src/views/settings/use-settings-form.ts
@@ -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 };
+}