import './settings.css'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useSnapshot } from 'valtio'; import logo from '../assets/logo.svg'; import Icon from '../components/icon'; import Link from '../components/link'; import RelativeTime from '../components/relative-time'; import targetLanguages from '../data/lingva-target-languages'; import { api } from '../utils/api'; import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import localeCode2Text from '../utils/localeCode2Text'; import { initSubscription, isPushSupported, removeSubscription, updateSubscription, } from '../utils/push-notifications'; import showToast from '../utils/show-toast'; import states from '../utils/states'; import store from '../utils/store'; const DEFAULT_TEXT_SIZE = 16; const TEXT_SIZES = [14, 15, 16, 17, 18, 19, 20]; const { PHANPY_WEBSITE: WEBSITE, PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL, PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL, PHANPY_GIPHY_API_KEY: GIPHY_API_KEY, } = import.meta.env; function Settings({ onClose }) { const snapStates = useSnapshot(states); const currentTheme = store.local.get('theme') || 'auto'; const themeFormRef = useRef(); const targetLanguage = snapStates.settings.contentTranslationTargetLanguage || null; const systemTargetLanguage = getTranslateTargetLanguage(); const systemTargetLanguageText = localeCode2Text(systemTargetLanguage); const currentTextSize = store.local.get('textSize') || DEFAULT_TEXT_SIZE; const [prefs, setPrefs] = useState(store.account.get('preferences') || {}); const { masto, authenticated, instance } = api(); // Get preferences every time Settings is opened // NOTE: Disabled for now because I don't expect this to change often. Also for some reason, the /api/v1/preferences endpoint is cached for a while and return old prefs if refresh immediately after changing them. // useEffect(() => { // const { masto } = api(); // (async () => { // try { // const preferences = await masto.v1.preferences.fetch(); // setPrefs(preferences); // store.account.set('preferences', preferences); // } catch (e) { // // Silently fail // console.error(e); // } // })(); // }, []); return (
{!!onClose && ( )}

Settings

  • { console.log(e); e.preventDefault(); const formData = new FormData(themeFormRef.current); const theme = formData.get('theme'); const html = document.documentElement; if (theme === 'auto') { html.classList.remove('is-light', 'is-dark'); // Disable manual theme const $manualMeta = document.querySelector( 'meta[data-theme-setting="manual"]', ); if ($manualMeta) { $manualMeta.name = ''; } // Enable auto theme s const $autoMetas = document.querySelectorAll( 'meta[data-theme-setting="auto"]', ); $autoMetas.forEach((m) => { m.name = 'theme-color'; }); } else { html.classList.toggle('is-light', theme === 'light'); html.classList.toggle('is-dark', theme === 'dark'); // Enable manual theme const $manualMeta = document.querySelector( 'meta[data-theme-setting="manual"]', ); if ($manualMeta) { $manualMeta.name = 'theme-color'; $manualMeta.content = theme === 'light' ? $manualMeta.dataset.themeLightColor : $manualMeta.dataset.themeDarkColor; } // Disable auto theme s const $autoMetas = document.querySelectorAll( 'meta[data-theme-setting="auto"]', ); $autoMetas.forEach((m) => { m.name = ''; }); } document .querySelector('meta[name="color-scheme"]') .setAttribute( 'content', theme === 'auto' ? 'dark light' : theme, ); if (theme === 'auto') { store.local.del('theme'); } else { store.local.set('theme', theme); } }} >
  • A{' '} { const value = parseInt(e.target.value, 10); const html = document.documentElement; // set CSS variable html.style.setProperty('--text-size', `${value}px`); // save to local storage if (value === DEFAULT_TEXT_SIZE) { store.local.del('textSize'); } else { store.local.set('textSize', e.target.value); } }} />{' '} A {TEXT_SIZES.map((size) => (
{authenticated && ( <>

Posting

{' '} Synced to your instance server's settings.{' '} Go to your instance ({instance}) for more settings.

)}

Experiments


  • Hide "Translate" button for {snapStates.settings.contentTranslationHideLanguages.length > 0 && ( <> {' '} ( { snapStates.settings.contentTranslationHideLanguages .length } ) )} :

    {targetLanguages.map((lang) => ( ))}

    Note: This feature uses external translation services, powered by{' '} Lingva API {' '} &{' '} Lingva Translate .


    Automatically show translation for posts in timeline. Only works for short posts without content warning, media and poll.

  • {!!GIPHY_API_KEY && authenticated && (
  • Note: This feature uses external GIF search service, powered by{' '} GIPHY . G-rated (suitable for viewing by all ages), tracking parameters are stripped, referrer information is omitted from requests, but search queries and IP address information will still reach their servers.
  • )} {!!IMG_ALT_API_URL && authenticated && (
  • Only for new images while composing new posts.
    Note: This feature uses external AI service, powered by{' '} img-alt-api . May not work well. Only for images and in English.
  • )} {authenticated && (
  • ⚠️⚠️⚠️ Very experimental.
    Stored in your own profile’s notes. Profile (private) notes are mainly used for other profiles, and hidden for own profile.
    Note: This feature uses currently-logged-in instance server API.
  • )}
  • Replace text as blocks, useful when taking screenshots, for privacy reasons.
  • {authenticated && (
  • )}
{authenticated && }

About

Sponsor {' '} ·{' '} Donate {' '} ·{' '} Privacy Policy

{__BUILD_TIME__ && (

{WEBSITE && ( <> Site:{' '} {WEBSITE.replace(/https?:\/\//g, '').replace(/\/$/, '')}
)} Version:{' '} { e.target.select(); // Copy to clipboard try { navigator.clipboard.writeText(e.target.value); showToast('Version string copied'); } catch (e) { console.warn(e); showToast('Unable to copy version string'); } }} />{' '} {!__FAKE_COMMIT_HASH__ && ( ( ) )}

)}
); } function PushNotificationsSection({ onClose }) { if (!isPushSupported()) return null; const { instance } = api(); const [uiState, setUIState] = useState('default'); const pushFormRef = useRef(); const [allowNotifications, setAllowNotifications] = useState(false); const [needRelogin, setNeedRelogin] = useState(false); const previousPolicyRef = useRef(); useEffect(() => { (async () => { setUIState('loading'); try { const { subscription, backendSubscription } = await initSubscription(); if ( backendSubscription?.policy && backendSubscription.policy !== 'none' ) { setAllowNotifications(true); const { alerts, policy } = backendSubscription; console.log('backendSubscription', backendSubscription); previousPolicyRef.current = policy; const { elements } = pushFormRef.current; const policyEl = elements.namedItem('policy'); if (policyEl) policyEl.value = policy; // alerts is {}, iterate it Object.keys(alerts).forEach((alert) => { const el = elements.namedItem(alert); if (el?.type === 'checkbox') { el.checked = true; } }); } setUIState('default'); } catch (err) { console.warn(err); if (/outside.*authorized/i.test(err.message)) { setNeedRelogin(true); } else { alert(err?.message || err); } setUIState('error'); } })(); }, []); const isLoading = uiState === 'loading'; return (
{ setTimeout(() => { const values = Object.fromEntries(new FormData(pushFormRef.current)); const allowNotifications = !!values['policy-allow']; const params = { data: { policy: values.policy, alerts: { mention: !!values.mention, favourite: !!values.favourite, reblog: !!values.reblog, follow: !!values.follow, follow_request: !!values.followRequest, poll: !!values.poll, update: !!values.update, status: !!values.status, }, }, }; let alertsCount = 0; // Remove false values from data.alerts // API defaults to false anyway Object.keys(params.data.alerts).forEach((key) => { if (!params.data.alerts[key]) { delete params.data.alerts[key]; } else { alertsCount++; } }); const policyChanged = previousPolicyRef.current !== params.data.policy; console.log('PN Form', { values, allowNotifications: allowNotifications, params, }); if (allowNotifications && alertsCount > 0) { if (policyChanged) { console.debug('Policy changed.'); removeSubscription() .then(() => { updateSubscription(params); }) .catch((err) => { console.warn(err); alert('Failed to update subscription. Please try again.'); }); } else { updateSubscription(params).catch((err) => { console.warn(err); alert('Failed to update subscription. Please try again.'); }); } } else { removeSubscription().catch((err) => { console.warn(err); alert('Failed to remove subscription. Please try again.'); }); } }, 100); }} >

Push Notifications (beta)

NOTE: Push notifications only work for one account.

); } export default Settings;