From d85f63e6cf5786289f38e6c7b65652164522f982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Tue, 15 Apr 2025 11:39:03 +0200 Subject: [PATCH] pl-fe: support configuring redirect services, i think it works but idk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- packages/pl-fe/package.json | 3 +- .../pl-fe/src/components/parsed-content.tsx | 13 +- .../pl-fe/src/components/status-content.tsx | 1 + .../compose/editor/plugins/state-plugin.tsx | 2 +- .../pl-fe/src/features/url-privacy/index.tsx | 113 ++++++++++++++++-- packages/pl-fe/src/locales/en.json | 11 ++ packages/pl-fe/src/schemas/pl-fe/settings.ts | 3 + packages/pl-fe/src/stores/settings.ts | 81 +++++++++++-- packages/pl-fe/src/utils/url-purify.ts | 98 ++++++++++++--- packages/pl-fe/yarn.lock | 13 +- 10 files changed, 296 insertions(+), 42 deletions(-) diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index c044b285d..8535f7e35 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -55,7 +55,7 @@ "@lexical/selection": "^0.29.0", "@lexical/utils": "^0.29.0", "@mkljczk/react-hotkeys": "^1.3.0", - "@mkljczk/url-purify": "^0.0.2", + "@mkljczk/url-purify": "^0.0.3", "@reach/combobox": "^0.18.0", "@reach/rect": "^0.18.0", "@reach/tabs": "^0.18.0", @@ -136,6 +136,7 @@ "sass": "^1.86.3", "stringz": "^2.1.0", "tiny-queue": "^0.2.1", + "use-mutative": "^1.2.1", "util": "^0.12.5", "valibot": "^1.0.0-beta.12", "zustand": "^5.0.3", diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx index 9ea15dede..457c16a81 100644 --- a/packages/pl-fe/src/components/parsed-content.tsx +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -50,6 +50,8 @@ interface IParsedContent { emojis?: Array; /** Whether to call a function to remove tracking parameters from URLs. */ cleanUrls?: boolean; + /** Whether to call a function to redirect URLs to popular websites to privacy-respecting proxy services. */ + redirectUrls?: boolean; /** Whether to display link target domain when it's not part of the text. */ displayTargetHost?: boolean; greentext?: boolean; @@ -100,6 +102,7 @@ function parseContent({ hasQuote, emojis, cleanUrls = false, + redirectUrls = false, displayTargetHost = true, greentext = false, speakAsCat = false, @@ -172,7 +175,7 @@ function parseContent({ if (cleanUrls) { try { - href = Purify.clearUrl(href); + href = Purify.clearUrl(href, cleanUrls, redirectUrls); } catch (_) { // } @@ -275,9 +278,11 @@ function parseContent({ const ParsedContent: React.FC = React.memo((props) => { const { urlPrivacy } = useSettings(); - if (props.cleanUrls === undefined) { - props = { ...props, cleanUrls: urlPrivacy.clearLinksInContent, displayTargetHost: urlPrivacy.displayTargetHost }; - } + props = { ...props }; + + if (props.cleanUrls === undefined) props.cleanUrls = urlPrivacy.clearLinksInContent; + if (props.redirectUrls === undefined) props.redirectUrls = urlPrivacy.redirectLinksMode !== 'off'; + if (props.displayTargetHost === undefined) props.displayTargetHost = urlPrivacy.displayTargetHost; return parseContent(props, false); }, (prevProps, nextProps) => prevProps.html === nextProps.html); diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index 05257e7fc..e7814abbb 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -147,6 +147,7 @@ const StatusContent: React.FC = React.memo(({ hasQuote: !!status.quote_id, emojis: status.emojis, cleanUrls: urlPrivacy.clearLinksInContent, + redirectUrls: urlPrivacy.redirectLinksMode !== 'off', displayTargetHost: urlPrivacy.displayTargetHost, greentext, speakAsCat: status.account.speak_as_cat, diff --git a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx index 3293c1a3e..1dd0826f9 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx @@ -41,7 +41,7 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { editorState.read(() => { const compareUrl = (url: string) => { - const cleanUrl = Purify.clearUrl(url); + const cleanUrl = Purify.clearUrl(url, true, false); return { originalUrl: url, cleanUrl, diff --git a/packages/pl-fe/src/features/url-privacy/index.tsx b/packages/pl-fe/src/features/url-privacy/index.tsx index 282fa858d..bf59fcf71 100644 --- a/packages/pl-fe/src/features/url-privacy/index.tsx +++ b/packages/pl-fe/src/features/url-privacy/index.tsx @@ -1,5 +1,7 @@ +import { mappings } from '@mkljczk/url-purify'; import React, { useEffect, useState } from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { defineMessages, FormattedList, FormattedMessage, useIntl } from 'react-intl'; +import { useMutative } from 'use-mutative'; import { changeSetting } from 'pl-fe/actions/settings'; import List, { ListItem } from 'pl-fe/components/list'; @@ -12,16 +14,27 @@ import FormGroup from 'pl-fe/components/ui/form-group'; import Input from 'pl-fe/components/ui/input'; import Toggle from 'pl-fe/components/ui/toggle'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useSettings } from 'pl-fe/hooks/use-settings'; +import KVStore from 'pl-fe/storage/kv-store'; +import { KVStoreRedirectServicesItem } from 'pl-fe/utils/url-purify'; + +import { SelectDropdown } from '../forms'; const messages = defineMessages({ urlPrivacy: { id: 'settings.url_privacy', defaultMessage: 'URL privacy' }, rulesUrlPlaceholder: { id: 'url_privacy.rules_url.placeholder', defaultMessage: 'Rules URL' }, hashUrlPlaceholder: { id: 'url_privacy.hash_url.placeholder', defaultMessage: 'Hash URL' }, + redirectLinksModeOff: { id: 'url_privacy.redirect_links_mode.off', defaultMessage: 'Disabled' }, + redirectLinksModeAuto: { id: 'url_privacy.redirect_links_mode.auto', defaultMessage: 'From URL' }, + redirectLinksModeManual: { id: 'url_privacy.redirect_links_mode.manual', defaultMessage: 'Specify manually' }, + redirectServicesUrlPlaceholder: { id: 'url_privacy.redirect_services_url.placeholder', defaultMessage: 'Rules URL' }, + redirectServicePlaceholder: { id: 'url_privacy.redirect_services_url.placeholder', defaultMessage: 'eg. https://proxy.example.org' }, }); const UrlPrivacy = () => { const dispatch = useAppDispatch(); + const me = useAppSelector((state) => state.me); const intl = useIntl(); const { urlPrivacy } = useSettings(); @@ -29,25 +42,62 @@ const UrlPrivacy = () => { const [displayTargetHost, setDisplayTargetHost] = useState(urlPrivacy.displayTargetHost); const [clearLinksInCompose, setClearLinksInCompose] = useState(urlPrivacy.clearLinksInCompose); const [clearLinksInContent, setClearLinksInContent] = useState(urlPrivacy.clearLinksInContent); - // const [allowReferralMarketing, setAllowReferralMarketing] = useState(urlPrivacy.allowReferralMarketing); const [hashUrl, setHashUrl] = useState(urlPrivacy.hashUrl); const [rulesUrl, setRulesUrl] = useState(urlPrivacy.rulesUrl); + const [redirectLinksMode, setRedirectLinksMode] = useState(urlPrivacy.redirectLinksMode); + const [redirectServicesUrl, setRedirectServicesUrl] = useState(urlPrivacy.redirectServicesUrl); + const [redirectServices, setRedirectServices] = useMutative(urlPrivacy.redirectServices); const onSubmit = () => { - dispatch(changeSetting(['urlPrivacy'], { + const value = { ...urlPrivacy, displayTargetHost, clearLinksInCompose, clearLinksInContent, - // allowReferralMarketing, hashUrl, rulesUrl, - }, { + redirectLinksMode, + redirectServicesUrl, + redirectServices, + }; + + switch (redirectLinksMode) { + case 'off': + value.redirectServicesUrl = ''; + value.redirectServices = {}; + break; + case 'manual': + value.redirectServicesUrl = ''; + break; + case 'auto': + value.redirectServices = {}; + break; + } + + dispatch(changeSetting(['urlPrivacy'], value, { save: true, showAlert: true, })); }; + const handleChangeRedirectLinksMode = (event: React.ChangeEvent) => { + if (redirectLinksMode === 'auto' && event.target.value === 'manual') { + KVStore.getItem(`url-purify-redirect-services:${me}`).then((services) => { + if (!services?.redirectServices) return; + + setRedirectServices( + Object.fromEntries( + mappings.map(({ name, targets }) => ([ + name, + services.redirectServices.find((service) => targets.includes(service.type) && service.instances.length)?.instances[0].split('|')[0] || '', + ])), + ), + ); + }).catch(() => { }); + } + return setRedirectLinksMode(event.target.value as 'off'); + }; + useEffect(() => { }, [dispatch]); @@ -74,10 +124,6 @@ const UrlPrivacy = () => { }> setClearLinksInContent(target.checked)} /> - - {/* }> - setAllowReferralMarketing(target.checked)} disabled={!clearLinksInCompose && !clearLinksInContent} /> - */} { /> + + }> + + + + + {redirectLinksMode === 'auto' && ( + } + hintText={} + > + setRedirectServicesUrl(target.value)} + /> + + )} + + {redirectLinksMode === 'manual' && ( + mappings.map((service) => ( + } + hintText={ }} />} + > + setRedirectServices((services) => { + services[service.name] = e.target.value; + })} + placeholder={intl.formatMessage(messages.redirectServicePlaceholder)} + /> + + )) + )} +