pl-fe: WIP: Allow configuring url-purify
Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
@ -82,7 +82,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||
preview,
|
||||
withMedia,
|
||||
}) => {
|
||||
const { cleanUrls, displaySpoilers } = useSettings();
|
||||
const { urlPrivacy, displaySpoilers } = useSettings();
|
||||
const { greentext } = usePlFeConfig();
|
||||
|
||||
const [collapsed, setCollapsed] = useState<boolean | null>(null);
|
||||
@ -146,7 +146,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||
mentions: status.mentions,
|
||||
hasQuote: !!status.quote_id,
|
||||
emojis: status.emojis,
|
||||
}, true, cleanUrls, greentext), [content]);
|
||||
}, true, urlPrivacy.clearLinksInContent, greentext), [content]);
|
||||
|
||||
useEffect(() => {
|
||||
setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96);
|
||||
|
||||
@ -13,10 +13,12 @@ interface ISettingToggle {
|
||||
settingPath: string[];
|
||||
/** Callback when the setting is toggled. */
|
||||
onChange: (settingPath: string[], checked: boolean) => void;
|
||||
/** Whether the toggle is disabled. */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/** Stateful toggle to change user settings. */
|
||||
const SettingToggle: React.FC<ISettingToggle> = ({ id, settings, settingPath, onChange }) => {
|
||||
const SettingToggle: React.FC<ISettingToggle> = ({ id, settings, settingPath, onChange, disabled }) => {
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
||||
onChange(settingPath, target.checked);
|
||||
@ -27,6 +29,7 @@ const SettingToggle: React.FC<ISettingToggle> = ({ id, settings, settingPath, on
|
||||
id={id}
|
||||
checked={!!get(settings, settingPath)}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -42,6 +42,7 @@ const messages = defineMessages({
|
||||
security: { id: 'settings.security', defaultMessage: 'Security' },
|
||||
sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' },
|
||||
settings: { id: 'settings.settings', defaultMessage: 'Settings' },
|
||||
urlPrivacy: { id: 'settings.url_privacy', defaultMessage: 'URL privacy' },
|
||||
});
|
||||
|
||||
/** User settings page. */
|
||||
@ -92,39 +93,31 @@ const Settings = () => {
|
||||
</List>
|
||||
</CardBody>
|
||||
|
||||
{any([
|
||||
features.changeEmail,
|
||||
features.changePassword,
|
||||
features.manageMfa,
|
||||
features.sessions,
|
||||
]) && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle title={intl.formatMessage(messages.security)} />
|
||||
</CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle title={intl.formatMessage(messages.security)} />
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
<List>
|
||||
{features.changeEmail && <ListItem label={intl.formatMessage(messages.changeEmail)} to='/settings/email' />}
|
||||
{features.changePassword && <ListItem label={intl.formatMessage(messages.changePassword)} to='/settings/password' />}
|
||||
{features.manageMfa && (
|
||||
<>
|
||||
<ListItem label={intl.formatMessage(messages.configureMfa)} to='/settings/mfa'>
|
||||
<span>
|
||||
{isMfaEnabled ?
|
||||
intl.formatMessage(messages.mfaEnabled) :
|
||||
intl.formatMessage(messages.mfaDisabled)}
|
||||
</span>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
{features.sessions && (
|
||||
<ListItem label={intl.formatMessage(messages.sessions)} to='/settings/tokens' />
|
||||
)}
|
||||
</List>
|
||||
</CardBody>
|
||||
</>
|
||||
)}
|
||||
<CardBody>
|
||||
<List>
|
||||
{features.changeEmail && <ListItem label={intl.formatMessage(messages.changeEmail)} to='/settings/email' />}
|
||||
{features.changePassword && <ListItem label={intl.formatMessage(messages.changePassword)} to='/settings/password' />}
|
||||
{features.manageMfa && (
|
||||
<>
|
||||
<ListItem label={intl.formatMessage(messages.configureMfa)} to='/settings/mfa'>
|
||||
<span>
|
||||
{isMfaEnabled ?
|
||||
intl.formatMessage(messages.mfaEnabled) :
|
||||
intl.formatMessage(messages.mfaDisabled)}
|
||||
</span>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
{features.sessions && (
|
||||
<ListItem label={intl.formatMessage(messages.sessions)} to='/settings/tokens' />
|
||||
)}
|
||||
<ListItem label={intl.formatMessage(messages.urlPrivacy)} to='/settings/url_privacy' />
|
||||
</List>
|
||||
</CardBody>
|
||||
|
||||
{features.chats ? (
|
||||
<>
|
||||
|
||||
@ -139,6 +139,7 @@ import {
|
||||
StatusHoverCard,
|
||||
TestTimeline,
|
||||
ThemeEditor,
|
||||
UrlPrivacy,
|
||||
UserIndex,
|
||||
} from './util/async-components';
|
||||
import GlobalHotkeys from './util/global-hotkeys';
|
||||
@ -304,6 +305,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = React.memo(({ chil
|
||||
<WrappedRoute path='/settings/mfa' layout={DefaultLayout} component={MfaForm} exact />
|
||||
<WrappedRoute path='/settings/tokens' layout={DefaultLayout} component={AuthTokenList} content={children} />
|
||||
{features.interactionRequests && <WrappedRoute path='/settings/interaction_policies' layout={DefaultLayout} component={InteractionPolicies} content={children} />}
|
||||
<WrappedRoute path='/settings/url_privacy' layout={DefaultLayout} component={UrlPrivacy} content={children} />
|
||||
<WrappedRoute path='/settings' layout={DefaultLayout} component={Settings} content={children} />
|
||||
<WrappedRoute path='/pl-fe/config' adminOnly layout={DefaultLayout} component={PlFeConfig} content={children} />
|
||||
|
||||
|
||||
@ -90,6 +90,7 @@ export const Share = lazy(() => import('pl-fe/features/share'));
|
||||
export const Status = lazy(() => import('pl-fe/features/status'));
|
||||
export const TestTimeline = lazy(() => import('pl-fe/features/test-timeline'));
|
||||
export const ThemeEditor = lazy(() => import('pl-fe/features/theme-editor'));
|
||||
export const UrlPrivacy = lazy(() => import('pl-fe/features/url-privacy'));
|
||||
export const UserIndex = lazy(() => import('pl-fe/features/admin/user-index'));
|
||||
|
||||
// Panels
|
||||
|
||||
101
packages/pl-fe/src/features/url-privacy/index.tsx
Normal file
101
packages/pl-fe/src/features/url-privacy/index.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { changeSetting } from 'pl-fe/actions/settings';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Card, { CardBody, CardHeader, CardTitle } from 'pl-fe/components/ui/card';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import Form from 'pl-fe/components/ui/form';
|
||||
import FormActions from 'pl-fe/components/ui/form-actions';
|
||||
import FormGroup from 'pl-fe/components/ui/form-group';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
|
||||
import SettingToggle from '../notifications/components/setting-toggle';
|
||||
|
||||
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' },
|
||||
});
|
||||
|
||||
const UrlPrivacy = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const settings = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
}, [dispatch]);
|
||||
|
||||
const onToggleChange = (key: string[], checked: boolean) => {
|
||||
dispatch(changeSetting(key, checked));
|
||||
};
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.urlPrivacy)} transparent withHeader={false}>
|
||||
<Card className='space-y-4' variant='rounded'>
|
||||
<CardHeader backHref='/settings'>
|
||||
<CardTitle title={intl.formatMessage(messages.urlPrivacy)} />
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
<Form>
|
||||
<List>
|
||||
<ListItem label={<FormattedMessage id='url_privacy.clear_links_in_compose' defaultMessage='Suggest removing tracking parameters when composing a post' />}>
|
||||
<SettingToggle settings={settings} settingPath={['urlPrivacy', 'clearLinksInCompose']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem label={<FormattedMessage id='url_privacy.clear_links_in_content' defaultMessage='Remove tracking parameters from displayed posts' />}>
|
||||
<SettingToggle settings={settings} settingPath={['urlPrivacy', 'clearLinksInContent']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem label={<FormattedMessage id='url_privacy.allow_referral_marketing' defaultMessage='Make exception for referral marketing parameters' />}>
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['urlPrivacy', 'allowReferralMarketing']}
|
||||
onChange={onToggleChange}
|
||||
disabled={!(settings.urlPrivacy.clearLinksInCompose || settings.urlPrivacy.clearLinksInContent)}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='url_privacy.rules_url.label' defaultMessage='URL cleaning rules database address' />}
|
||||
hintText={<FormattedMessage id='url_privacy.rules_url.placeholder' defaultMessage='Rules database in ClearURLs-compatible format, eg. {url}' values={{ url: 'https://rules2.clearurls.xyz/data.minify.json' }} />}
|
||||
>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.rulesUrlPlaceholder)}
|
||||
value={settings.urlPrivacy.rulesUrl}
|
||||
// onChange={handleChange('tileServer', (e) => e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='url_privacy.hash_url.label' defaultMessage='URL cleaning rules hash address (optional)' />}
|
||||
hintText={<FormattedMessage id='url_privacy.hash_url.placeholder' defaultMessage='SHA256 hash of rules database, used to avoid unnecessary fetches, eg. {url}' values={{ url: 'https://rules2.clearurls.xyz/rules.minify.hash' }} />}
|
||||
>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.hashUrlPlaceholder)}
|
||||
value={settings.urlPrivacy.rulesUrl}
|
||||
// onChange={handleChange('tileServer', (e) => e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit'>
|
||||
<FormattedMessage id='url_privacy.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { UrlPrivacy as default };
|
||||
@ -1447,6 +1447,7 @@
|
||||
"settings.security": "Security",
|
||||
"settings.sessions": "Active sessions",
|
||||
"settings.settings": "Settings",
|
||||
"settings.url_privacy": "URL privacy",
|
||||
"shared.tos": "Terms of Service",
|
||||
"signup_panel.sign_in.title": "Sign in",
|
||||
"signup_panel.sign_in.title.external": "Sign in to external instance",
|
||||
@ -1632,6 +1633,14 @@
|
||||
"upload_form.preview": "Preview",
|
||||
"upload_form.undo": "Delete",
|
||||
"upload_progress.label": "Uploading…",
|
||||
"url_privacy.allow_referral_marketing": "Make exception for referral marketing parameters",
|
||||
"url_privacy.clear_links_in_compose": "Suggest removing tracking parameters when composing a post",
|
||||
"url_privacy.clear_links_in_content": "Remove tracking parameters from displayed posts",
|
||||
"url_privacy.hash_url.label": "URL cleaning rules hash address (optional)",
|
||||
"url_privacy.hash_url.placeholder": "SHA256 hash of rules database, used to avoid unnecessary fetches, eg. {url}",
|
||||
"url_privacy.rules_url.label": "URL cleaning rules database address",
|
||||
"url_privacy.rules_url.placeholder": "Rules database in ClearURLs-compatible format, eg. {url}",
|
||||
"url_privacy.save": "Save",
|
||||
"video.download": "Download file",
|
||||
"video.exit_fullscreen": "Exit full screen",
|
||||
"video.fullscreen": "Full screen",
|
||||
|
||||
@ -37,7 +37,13 @@ const settingsSchema = v.object({
|
||||
autoTranslate: v.fallback(v.boolean(), false),
|
||||
knownLanguages: v.fallback(v.array(v.string()), []),
|
||||
showWrenchButton: v.fallback(v.boolean(), false),
|
||||
cleanUrls: v.fallback(v.boolean(), false),
|
||||
urlPrivacy: coerceObject({
|
||||
clearLinksInCompose: v.fallback(v.boolean(), true),
|
||||
clearLinksInContent: v.fallback(v.boolean(), true),
|
||||
allowReferralMarketing: v.fallback(v.boolean(), false),
|
||||
rulesUrl: v.fallback(v.string(), ''),
|
||||
hashUrl: v.fallback(v.string(), ''),
|
||||
}),
|
||||
|
||||
theme: v.fallback(v.optional(v.object({
|
||||
brandColor: v.fallback(v.string(), ''),
|
||||
|
||||
@ -14,6 +14,9 @@
|
||||
// I hope I got this relicensing stuff right xD
|
||||
import { URLPurify, type SerializedRules } from '@mkljczk/url-purify';
|
||||
|
||||
import KVStore from 'pl-fe/storage/kv-store';
|
||||
import { store } from 'pl-fe/store';
|
||||
|
||||
// Adapted from ClearURLs Rules
|
||||
// https://github.com/ClearURLs/Rules/blob/master/data.min.json
|
||||
// Licensed under the LGPL-3.0 license.
|
||||
@ -88,6 +91,24 @@ const DEFAULT_RULESET: SerializedRules = {
|
||||
|
||||
const Purify = new URLPurify({
|
||||
rulesFromMemory: DEFAULT_RULESET,
|
||||
onFetchedRules: (hash, rules) => {
|
||||
const me = store.getState().auth.me;
|
||||
|
||||
KVStore.setItem('url-purify-rules:last', me || '');
|
||||
KVStore.setItem(`url-purify-rules:${me}`, {
|
||||
hash,
|
||||
rules,
|
||||
fetchedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
KVStore.getItem('url-purify-rules:last', (url: string) => {
|
||||
if (!url) return;
|
||||
KVStore.getItem(`url-purify-rules:${url}`, (rules: any) => {
|
||||
if (!rules) return;
|
||||
Purify.setRules(rules.rules, rules.hash);
|
||||
});
|
||||
});
|
||||
|
||||
export default Purify;
|
||||
|
||||
Reference in New Issue
Block a user