add setting to use nitter for twitter links

This commit is contained in:
hzrd149 2023-06-08 12:52:48 -04:00
parent 65bd2e93aa
commit 9464e3a234
16 changed files with 386 additions and 197 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add settings for Invidious, Nitter, Libreddit, Teddit redirects

View File

@ -1,9 +1,8 @@
import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
import appSettings from "../../services/app-settings";
import { ImageGalleryLink } from "../image-gallery";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { matchImageUrls } from "../../helpers/regexp";
import { useTrusted } from "../note/trust";
const BlurredImage = (props: ImageProps) => {
const { isOpen, onOpen } = useDisclosure();
@ -14,9 +13,10 @@ const BlurredImage = (props: ImageProps) => {
);
};
const EmbeddedImage = ({ src, blue }: { src: string; blue: boolean }) => {
const EmbeddedImage = ({ src }: { src: string }) => {
const isMobile = useIsMobile();
const ImageComponent = blue || !appSettings.value.blurImages ? Image : BlurredImage;
const trusted = useTrusted();
const ImageComponent = trusted || !appSettings.value.blurImages ? Image : BlurredImage;
const thumbnail = appSettings.value.imageProxy
? new URL(`/256,fit/${src}`, appSettings.value.imageProxy).toString()
: src;
@ -29,31 +29,24 @@ const EmbeddedImage = ({ src, blue }: { src: string; blue: boolean }) => {
};
// note1n06jceulg3gukw836ghd94p0ppwaz6u3mksnnz960d8vlcp2fnqsgx3fu9
export function embedImages(content: EmbedableContent, trusted = false) {
return embedJSX(content, {
regexp: matchImageUrls,
render: (match) => <EmbeddedImage blue={trusted} src={match[0]} />,
name: "Image",
});
const imageExt = [".svg", ".gif", ".png", ".jpg", ".jpeg", ".webp", ".avif"];
export function renderImageUrl(match: URL) {
if (!imageExt.some((ext) => match.pathname.endsWith(ext))) return null;
return <EmbeddedImage src={match.toString()} />;
}
export function embedVideos(content: EmbedableContent) {
return embedJSX(content, {
name: "Video",
regexp:
/https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,12})((?:\/[\+~%\/\.\w\-_]*)?\.(?:mp4|mkv|webm|mov))(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/i,
render: (match) => <video src={match[0]} controls style={{ maxWidth: "30rem", maxHeight: "20rem" }} />,
});
const videoExt = [".mp4", ".mkv", ".webm", ".mov"];
export function renderVideoUrl(match: URL) {
if (!videoExt.some((ext) => match.pathname.endsWith(ext))) return null;
return <video src={match.toString()} controls style={{ maxWidth: "30rem", maxHeight: "20rem" }} />;
}
export function embedLinks(content: EmbedableContent) {
return embedJSX(content, {
name: "Link",
regexp: /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,12})(\/[\+~%\/\.\w\-_]*)?([\?#][^\s]+)?/i,
render: (match) => (
<Link color="blue.500" href={match[0]} target="_blank" isExternal>
{match[0]}
</Link>
),
});
export function renderDefaultUrl(match: URL) {
return (
<Link color="blue.500" href={match.toString()} target="_blank" isExternal>
{match.toString()}
</Link>
);
}

View File

@ -1,75 +1,81 @@
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
import appSettings from "../../services/app-settings";
export function embedWavlakeTrack(content: EmbedableContent) {
return embedJSX(content, {
name: "Wavlake Track",
regexp: /https?:\/\/wavlake\.com\/track\/[\w-]+/i,
render: (match) => (
<iframe
loading="lazy"
frameBorder="0"
src={match[0].replace("wavlake.com", "embed.wavlake.com")}
style={{ width: "100%", aspectRatio: 576 / 356, maxWidth: 573 }}
></iframe>
),
});
// nostr:nevent1qqsve4ud5v8gjds2f2h7exlmjvhqayu4s520pge7frpwe22wezny0pcpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2mxs3z0
export function renderWavlakeUrl(match: URL) {
if (match.hostname !== "wavlake.com") return null;
const embedUrl = new URL(match);
embedUrl.hostname = "embed.wavlake.com";
return (
<iframe
loading="lazy"
frameBorder="0"
src={embedUrl.toString()}
style={{ width: "100%", aspectRatio: 576 / 356, maxWidth: 573 }}
></iframe>
);
}
// note1tvqk2mu829yr6asf7w5dgpp8t0mlp2ax5t26ctfdx8m0ptkssamqsleeux
// note1ygx9tec3af92704d92jwrj3zs7cws2jl29yvrlxzqlcdlykhwssqpupa7t
export function embedAppleMusic(content: EmbedableContent) {
return embedJSX(content, {
regexp: /https?:\/\/music\.apple\.com(?:\/[\+~%\/\.\w\-_]*)?(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/,
render: (match) => (
<iframe
allow="encrypted-media *; fullscreen *; clipboard-write"
frameBorder="0"
height={match[0].includes("?i=") ? 175 : 450}
style={{ width: "100%", maxWidth: "660px", overflow: "hidden", background: "transparent" }}
// sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"
src={match[0].replace("music.apple.com", "embed.music.apple.com")}
></iframe>
),
name: "Apple Music",
});
// nostr:nevent1qqs9kqt9d7r4zjpawcyl82x5qsn4hals4wn294dv95knrahs4mggwasprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2whhzvz
// nostr:nevent1qqszyrz4uug75j4086kj4f8peg3g0v8g9f04zjxplnpq0uxljtthggqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2aeexmq
export function renderAppleMusicUrl(match: URL) {
if (match.hostname !== "music.apple.com") return null;
const isList = match.searchParams.get("l") !== null;
const embedUrl = new URL(match);
embedUrl.hostname = "embed.music.apple.com";
return (
<iframe
allow="encrypted-media *; fullscreen *; clipboard-write"
frameBorder="0"
height={isList ? 450 : 175}
style={{ width: "100%", maxWidth: "660px", overflow: "hidden", background: "transparent" }}
src={embedUrl.toString()}
></iframe>
);
}
// nostr:nevent1qqs9r94qeqhqayvuz6q6u88spvuz0d25nhpyv0c39wympmfu646x4pgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqmhxhvq
export function embedSpotifyMusic(content: EmbedableContent) {
return embedJSX(content, {
regexp:
/https?:\/\/open\.spotify\.com\/(track|episode|album|playlist)\/(\w+)(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/im,
render: (match) => {
const isList = match[1] === "album" || match[1] === "playlist";
return (
<iframe
style={{ borderRadius: "12px" }}
width="100%"
height={isList ? 400 : 152}
title="Spotify Embed: Beethoven - Fur Elise - Komuz Remix"
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
src={`https://open.spotify.com/embed/${match[1]}/${match[2]}`}
></iframe>
);
},
name: "Spotify",
});
// nostr:nevent1qqsx0lz7m72qzq499exwhnfszvgwea8tv38x9wkv32yhkmwwmhgs7jgprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk25m3sln
// nostr:nevent1qqsqxkmz49hydf8ppa9k6x6zrcq7m4evhhlye0j3lcnz8hrl2q6np4spz3mhxue69uhhyetvv9ujuerpd46hxtnfdult02qz
const spotifyPaths = ["/track", "/episode", "/album", "/playlist"];
export function renderSpotifyUrl(match: URL) {
if (match.hostname !== "open.spotify.com") return null;
if (!spotifyPaths.some((p) => match.pathname.startsWith(p))) return null;
const isList = match.pathname.startsWith("/album") || match.pathname.startsWith("/playlist");
const embedUrl = new URL(match);
embedUrl.pathname = "/embed" + embedUrl.pathname;
return (
<iframe
style={{ borderRadius: "12px" }}
width="100%"
height={isList ? 400 : 152}
title="Spotify Embed: Beethoven - Fur Elise - Komuz Remix"
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
src={embedUrl.toString()}
></iframe>
);
}
// note132m5xc3zhj7fap67vzwx5x3s8xqgz49k669htcn8kppr4m654tuq960tuu
export function embedTidalMusic(content: EmbedableContent) {
return embedJSX(content, {
regexp: /https?:\/\/tidal\.com(\/browse)?\/(track|album)\/(\d+)/im,
render: (match) => (
<iframe
src={`https://embed.tidal.com/${match[2]}s/${match[3]}?disableAnalytics=true`}
width="100%"
height={match[2] === "album" ? 400 : 96}
></iframe>
),
name: "Tidal",
});
// nostr:nevent1qqsg4d6rvg3te0y7sa0xp8r2rgcrnqyp2jmddzm4ufnmqs36aa2247qprpmhxue69uhhyetvv9ujuumwdae8gtnnda3kjctvmdc953
export function renderTidalUrl(match: URL) {
if (match.hostname !== "tidal.com") return null;
const isList = match.pathname.includes("/album");
const [_, _browse, type, id] = match.pathname.match(/(\/browse)?\/(track|album)\/(\d+)/i) ?? [];
const embedUrl = new URL(`https://embed.tidal.com/${type}s/${id}`);
embedUrl.searchParams.set("disableAnalytics", "true");
return <iframe src={embedUrl.toString()} width="100%" height={isList ? 400 : 96}></iframe>;
}

View File

@ -0,0 +1,25 @@
import { replaceDomain } from "../../helpers/url";
import appSettings from "../../services/app-settings";
import { renderDefaultUrl } from "./common";
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/reddit.js
const REDDIT_DOMAINS = [
"www.reddit.com",
"np.reddit.com",
"new.reddit.com",
"amp.reddit.com",
"i.redd.it",
"redd.it",
"old.reddit.com",
];
const bypassPaths = /\/(gallery\/poll\/rpan\/settings\/topics)/;
export function renderRedditUrl(match: URL) {
if (!REDDIT_DOMAINS.includes(match.hostname)) return null;
if (match.pathname.match(bypassPaths)) return null;
const { redditRedirect } = appSettings.value;
const fixed = redditRedirect ? replaceDomain(match, redditRedirect) : match;
return renderDefaultUrl(fixed);
}

View File

@ -1,11 +1,21 @@
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
import { replaceDomain } from "../../helpers/url";
import appSettings from "../../services/app-settings";
import { TweetEmbed } from "../tweet-embed";
import { renderDefaultUrl } from "./common";
export function embedTweet(content: EmbedableContent) {
return embedJSX(content, {
name: "Tweet",
regexp:
/https?:\/\/twitter\.com\/(?:\#!\/)?(\w+)\/status(es)?\/(\d+)(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/im,
render: (match) => <TweetEmbed href={match[0]} conversation={false} />,
});
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js
export const TWITTER_DOMAINS = [
"twitter.com",
"www.twitter.com",
"mobile.twitter.com",
"pbs.twimg.com",
"video.twimg.com",
];
export function renderTwitterUrl(match: URL) {
if (!TWITTER_DOMAINS.includes(match.hostname)) return null;
const { twitterRedirect } = appSettings.value;
if (twitterRedirect) return renderDefaultUrl(replaceDomain(match, twitterRedirect));
else return <TweetEmbed href={match.toString()} conversation={false} />;
}

View File

@ -1,65 +1,40 @@
import { AspectRatio } from "@chakra-ui/react";
import { EmbedType, EmbedableContent, embedJSX } from "../../helpers/embeds";
import appSettings from "../../services/app-settings";
// nostr:note1ya94hd44g3m2x4gagcydkg28qcp924dd238vq5k4chly84mqt2wqnwgu6d
// nostr:note1apu56y4h2ms5uwpzz209vychr309kllhq6wz46te84u9rus5x7kqj5f5n9
export function embedYoutubeVideo(content: EmbedableContent) {
return embedJSX(content, {
name: "Youtube Video",
regexp:
/https?:\/\/(?:(?:www|m)\.)?(?:youtube\.com|youtu\.be)(\/(?:[\w\-]+\?v=|embed\/|v\/|live\/|shorts\/)?)([\w\-]+)(\S+)?/,
render: (match) => (
<AspectRatio ratio={16 / 10} maxWidth="30rem">
<iframe
src={`https://www.youtube.com/embed/${match[2]}`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
width="100%"
></iframe>
</AspectRatio>
),
});
}
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/youtube.js
export const YOUTUBE_DOMAINS = [
"m.youtube.com",
"youtube.com",
"img.youtube.com",
"www.youtube.com",
"youtube-nocookie.com",
"www.youtube-nocookie.com",
"youtu.be",
"s.ytimg.com",
"music.youtube.com",
];
// nostr:note12vqqte3gtd729gp65tgdk0a8yym5ynwqjuhk5s6l333yethlvlcsqptvmk
export function embedYoutubePlaylist(content: EmbedableContent) {
return embedJSX(content, {
name: "Youtube Playlist",
regexp: /https?:\/\/(?:(?:www|m)\.)?(?:youtube\.com|youtu\.be)\/(?:playlist\?list=)([\w\-]+)(\S+)?/im,
render: (match) => (
<AspectRatio ratio={560 / 315} maxWidth="30rem">
<iframe
// width="560"
// height="315"
width="100%"
height="100%"
src={`https://www.youtube.com/embed/videoseries?list=${match[1]}`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
></iframe>
</AspectRatio>
),
});
}
// nostr:nevent1qqszwj6mk665ga4r25w5vzxmy9rsvqj42kk4gnkq2t2utljr6as948qpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk245xvyn
export function renderYoutubeUrl(match: URL) {
if (!YOUTUBE_DOMAINS.includes(match.hostname)) return null;
if (match.pathname.startsWith("/live")) return null;
export function embedYoutubeMusic(content: EmbedableContent) {
return embedJSX(content, {
regexp: /https?:\/\/music\.youtube\.com\/watch\?v=(\w+)(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/,
render: (match) => (
<AspectRatio ratio={16 / 10} maxWidth="30rem">
<iframe
width="100%"
src={`https://youtube.com/embed/${match[1]}`}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
></iframe>
</AspectRatio>
),
name: "Youtube Music",
});
const { youtubeRedirect } = appSettings.value;
const videoId = match.searchParams.get("v");
if (!videoId) throw new Error("cant find video id");
const embedUrl = new URL(`/embed/${videoId}`, youtubeRedirect || "https://youtube.com");
return (
<AspectRatio ratio={16 / 10} maxWidth="40rem">
<iframe
src={embedUrl.toString()}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
width="100%"
></iframe>
</AspectRatio>
);
}

View File

@ -3,44 +3,45 @@ import { Box, Text } from "@chakra-ui/react";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import styled from "@emotion/styled";
import { useExpand } from "./expanded";
import { EmbedableContent } from "../../helpers/embeds";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import {
embedTweet,
embedLightningInvoice,
embedImages,
embedVideos,
embedLinks,
embedSpotifyMusic,
embedTidalMusic,
embedYoutubeVideo,
embedYoutubePlaylist,
embedYoutubeMusic,
embedNostrLinks,
embedNostrMentions,
embedAppleMusic,
embedNostrHashtags,
embedWavlakeTrack,
renderWavlakeUrl,
renderYoutubeUrl,
renderDefaultUrl,
renderImageUrl,
renderTwitterUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderVideoUrl,
} from "../embed-types";
import { ImageGalleryProvider } from "../image-gallery";
import { useTrusted } from "./trust";
import { renderRedditUrl } from "../embed-types/reddit";
function buildContents(event: NostrEvent | DraftNostrEvent, trusted = false) {
let content: EmbedableContent = [event.content.trim()];
content = embedLightningInvoice(content);
content = embedTweet(content);
content = embedYoutubeVideo(content);
content = embedYoutubePlaylist(content);
content = embedYoutubeMusic(content);
content = embedWavlakeTrack(content);
content = embedTidalMusic(content);
content = embedAppleMusic(content);
content = embedSpotifyMusic(content);
// common
content = embedImages(content, trusted);
content = embedVideos(content);
content = embedLinks(content);
content = embedUrls(content, [
renderYoutubeUrl,
renderTwitterUrl,
renderRedditUrl,
renderWavlakeUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderImageUrl,
renderVideoUrl,
renderDefaultUrl,
]);
// bitcoin
content = embedLightningInvoice(content);
// nostr
content = embedNostrLinks(content, event);

View File

@ -1,5 +1,5 @@
import { useColorMode } from "@chakra-ui/react";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
export type TweetEmbedProps = {
href: string;

View File

@ -3,7 +3,7 @@ import { cloneElement } from "react";
export type EmbedableContent = (string | JSX.Element)[];
export type EmbedType = {
regexp: RegExp;
render: (match: RegExpMatchArray) => JSX.Element | string;
render: (match: RegExpMatchArray) => JSX.Element | string | null;
name: string;
};
@ -18,6 +18,8 @@ export function embedJSX(content: EmbedableContent, embed: EmbedType): Embedable
const after = subContent.slice(match.index + match[0].length, subContent.length);
let embedRender = embed.render(match);
if (embedRender === null) return subContent;
if (typeof embedRender !== "string" && !embedRender.props.key) {
embedRender = cloneElement(embedRender, { key: embed.name + i });
}
@ -30,3 +32,26 @@ export function embedJSX(content: EmbedableContent, embed: EmbedType): Embedable
})
.flat();
}
export type LinkEmbedHandler = (link: URL) => JSX.Element | string | null;
export function embedUrls(content: EmbedableContent, handlers: LinkEmbedHandler[]) {
return embedJSX(content, {
name: "embedUrls",
regexp: /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,12})(\/[\+~%\/\.\w\-_]*)?([\?#][^\s]+)?/i,
render: (match) => {
try {
const url = new URL(match[0]);
for (const handler of handlers) {
const content = handler(url);
if (content) return content;
}
} catch (e) {
if (e instanceof Error) {
console.error("Failed to embed link", match[0], e.message);
}
}
return null;
},
});
}

View File

@ -1,3 +1,5 @@
const convertToUrl = (url: string | URL) => (url instanceof URL ? url : new URL(url));
export function normalizeRelayUrl(relayUrl: string) {
const url = new URL(relayUrl);
@ -18,3 +20,14 @@ export function safeRelayUrl(relayUrl: string) {
} catch (e) {}
return null;
}
export function replaceDomain(url: string | URL, replacementUrl: string | URL) {
const newUrl = new URL(url);
replacementUrl = convertToUrl(replacementUrl);
newUrl.host = replacementUrl.host;
newUrl.protocol = replacementUrl.protocol;
if (replacementUrl.port) newUrl.port = replacementUrl.port;
if (replacementUrl.username) newUrl.username = replacementUrl.username;
if (replacementUrl.password) newUrl.password = replacementUrl.password;
return newUrl;
}

View File

@ -27,6 +27,9 @@ export type AppSettings = {
primaryColor: string;
imageProxy: string;
showContentWarning: boolean;
twitterRedirect?: string;
redditRedirect?: string;
youtubeRedirect?: string;
};
export const defaultSettings: AppSettings = {
@ -41,6 +44,9 @@ export const defaultSettings: AppSettings = {
primaryColor: "#8DB600",
imageProxy: "",
showContentWarning: true,
twitterRedirect: undefined,
redditRedirect: undefined,
youtubeRedirect: undefined,
};
function parseAppSettings(event: NostrEvent): AppSettings {

View File

@ -17,19 +17,17 @@ import clientRelaysService from "../../services/client-relays";
import directMessagesService, { getMessageRecipient } from "../../services/direct-messages";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import DecryptPlaceholder from "./decrypt-placeholder";
import { EmbedableContent } from "../../helpers/embeds";
import { embedImages, embedLinks, embedNostrLinks, embedVideos } from "../../components/embed-types";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { embedNostrLinks, renderDefaultUrl, renderImageUrl, renderVideoUrl } from "../../components/embed-types";
import RequireCurrentAccount from "../../providers/require-current-account";
function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
let content: EmbedableContent = [text];
content = embedImages(content, true);
content = embedVideos(content);
content = embedLinks(content);
content = embedNostrLinks(content, event);
content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderDefaultUrl]);
return <Box whiteSpace="pre-wrap">{content}</Box>;
}

View File

@ -6,6 +6,7 @@ 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";
export default function SettingsView() {
return (
@ -15,6 +16,8 @@ export default function SettingsView() {
<PerformanceSettings />
<PrivacySettings />
<LightningSettings />
<DatabaseSettings />

View File

@ -12,8 +12,6 @@ import {
Input,
Link,
} from "@chakra-ui/react";
import appSettings, { replaceSettings } from "../../services/app-settings";
import useSubject from "../../hooks/use-subject";
import useAppSettings from "../../hooks/use-app-settings";
import { useEffect, useState } from "react";

View File

@ -0,0 +1,130 @@
import {
Flex,
FormControl,
FormLabel,
Switch,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
Input,
Link,
Button,
FormErrorMessage,
} from "@chakra-ui/react";
import useAppSettings from "../../hooks/use-app-settings";
import { useForm } from "react-hook-form";
import { useAsync } from "react-use";
async function validateInvidiousUrl(url?: string) {
if (!url) return true;
try {
const res = await fetch(new URL("/api/v1/stats", url));
return res.ok || "Catch reach instance";
} catch (e) {
return "Catch reach instance";
}
}
export default function PrivacySettings() {
const { youtubeRedirect, twitterRedirect, redditRedirect, updateSettings } = useAppSettings();
const { register, handleSubmit, formState } = useForm({
mode: "onBlur",
defaultValues: {
youtubeRedirect,
twitterRedirect,
redditRedirect,
},
});
const save = handleSubmit(async (values) => {
await updateSettings(values);
});
return (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Privacy
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<form onSubmit={save}>
<Flex direction="column" gap="4">
<FormControl isInvalid={!!formState.errors.twitterRedirect}>
<FormLabel>Nitter instance</FormLabel>
<Input type="url" placeholder="https://nitter.net/" {...register("twitterRedirect")} />
{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,
})}
/>
{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")} />
{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>
<Button
colorScheme="brand"
ml="auto"
isLoading={formState.isSubmitting}
type="submit"
isDisabled={!formState.isDirty}
>
Save
</Button>
</Flex>
</form>
</AccordionPanel>
</AccordionItem>
);
}

View File

@ -14,7 +14,8 @@ import { useCurrentAccount } from "../../../hooks/use-current-account";
import { useIsMobile } from "../../../hooks/use-is-mobile";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { UserProfileMenu } from "./user-profile-menu";
import { embedLinks } from "../../../components/embed-types";
import { embedUrls } from "../../../helpers/embeds";
import { renderDefaultUrl } from "../../../components/embed-types";
export default function Header({
pubkey,
@ -52,7 +53,7 @@ export default function Header({
/>
</Flex>
</Flex>
{metadata?.about && <Text>{embedLinks([metadata.about])}</Text>}
{metadata?.about && <Text>{embedUrls([metadata.about], [renderDefaultUrl])}</Text>}
</Flex>
</Flex>
<Flex wrap="wrap" gap="2">