mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
add setting to use nitter for twitter links
This commit is contained in:
parent
65bd2e93aa
commit
9464e3a234
5
.changeset/rare-carrots-watch.md
Normal file
5
.changeset/rare-carrots-watch.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add settings for Invidious, Nitter, Libreddit, Teddit redirects
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
25
src/components/embed-types/reddit.tsx
Normal file
25
src/components/embed-types/reddit.tsx
Normal 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);
|
||||
}
|
@ -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} />;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>;
|
||||
}
|
||||
|
||||
|
@ -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 />
|
||||
|
@ -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";
|
||||
|
||||
|
130
src/views/settings/privacy-settings.tsx
Normal file
130
src/views/settings/privacy-settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user