phanpy/src/pages/settings.jsx

1071 lines
37 KiB
React
Raw Normal View History

2022-12-10 17:14:48 +08:00
import './settings.css';
2024-08-13 15:26:23 +08:00
import { Plural, t, Trans } from '@lingui/macro';
import { useEffect, useRef, useState } from 'preact/hooks';
2023-01-14 19:42:04 +08:00
import { useSnapshot } from 'valtio';
2022-12-10 17:14:48 +08:00
2023-01-26 00:54:30 +08:00
import logo from '../assets/logo.svg';
2023-10-03 15:07:47 +08:00
2023-04-20 16:10:57 +08:00
import Icon from '../components/icon';
2024-08-13 15:26:23 +08:00
import LangSelector from '../components/lang-selector';
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';
2022-12-10 17:14:48 +08:00
import store from '../utils/store';
import supports from '../utils/supports';
2022-12-10 17:14:48 +08:00
2023-03-08 17:17:23 +08:00
const DEFAULT_TEXT_SIZE = 16;
2024-02-09 20:07:16 +08:00
const TEXT_SIZES = [14, 15, 16, 17, 18, 19, 20];
2023-12-25 19:25:48 +08:00
const {
PHANPY_WEBSITE: WEBSITE,
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
2024-04-02 17:51:48 +08:00
PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
2023-12-25 19:25:48 +08:00
} = import.meta.env;
2023-03-08 17:17:23 +08:00
2022-12-16 13:27:04 +08:00
function Settings({ onClose }) {
2023-01-14 19:42:04 +08:00
const snapStates = useSnapshot(states);
2022-12-10 17:14:48 +08:00
const currentTheme = store.local.get('theme') || 'auto';
const themeFormRef = useRef();
const targetLanguage =
snapStates.settings.contentTranslationTargetLanguage || null;
const systemTargetLanguage = getTranslateTargetLanguage();
const systemTargetLanguageText = localeCode2Text(systemTargetLanguage);
2023-03-08 17:17:23 +08:00
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);
// }
// })();
// }, []);
2022-12-10 17:14:48 +08:00
return (
2022-12-29 16:11:58 +08:00
<div id="settings-container" class="sheet" tabIndex="-1">
2023-04-20 16:10:57 +08:00
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
2024-08-13 15:26:23 +08:00
<Icon icon="x" alt={t`Close`} />
2023-04-20 16:10:57 +08:00
</button>
)}
<header>
2024-08-13 15:26:23 +08:00
<h2>
<Trans>Settings</Trans>
</h2>
</header>
<main>
2023-02-28 17:12:17 +08:00
<section>
<ul>
<li>
<div>
2024-08-13 15:26:23 +08:00
<label>
<Trans>Appearance</Trans>
</label>
2023-02-28 17:12:17 +08:00
</div>
<div>
<form
ref={themeFormRef}
onInput={(e) => {
console.log(e);
e.preventDefault();
const formData = new FormData(themeFormRef.current);
const theme = formData.get('theme');
const html = document.documentElement;
2022-12-10 17:14:48 +08:00
2023-02-28 17:12:17 +08:00
if (theme === 'auto') {
html.classList.remove('is-light', 'is-dark');
2023-12-23 23:04:34 +08:00
// Disable manual theme <meta>
const $manualMeta = document.querySelector(
'meta[data-theme-setting="manual"]',
);
if ($manualMeta) {
$manualMeta.name = '';
}
// Enable auto theme <meta>s
const $autoMetas = document.querySelectorAll(
'meta[data-theme-setting="auto"]',
);
$autoMetas.forEach((m) => {
m.name = 'theme-color';
});
2023-02-28 17:12:17 +08:00
} else {
html.classList.toggle('is-light', theme === 'light');
html.classList.toggle('is-dark', theme === 'dark');
2023-12-23 23:04:34 +08:00
// Enable manual theme <meta>
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 <meta>s
const $autoMetas = document.querySelectorAll(
'meta[data-theme-setting="auto"]',
);
$autoMetas.forEach((m) => {
m.name = '';
});
2023-02-28 17:12:17 +08:00
}
document
.querySelector('meta[name="color-scheme"]')
.setAttribute(
'content',
theme === 'auto' ? 'dark light' : theme,
);
2022-12-10 17:14:48 +08:00
2023-02-28 17:12:17 +08:00
if (theme === 'auto') {
store.local.del('theme');
} else {
store.local.set('theme', theme);
}
}}
>
<div class="radio-group">
<label>
<input
type="radio"
name="theme"
value="light"
defaultChecked={currentTheme === 'light'}
/>
2024-08-13 15:26:23 +08:00
<span>
<Trans>Light</Trans>
</span>
2023-02-28 17:12:17 +08:00
</label>
<label>
<input
type="radio"
name="theme"
value="dark"
defaultChecked={currentTheme === 'dark'}
/>
2024-08-13 15:26:23 +08:00
<span>
<Trans>Dark</Trans>
</span>
2023-02-28 17:12:17 +08:00
</label>
<label>
<input
type="radio"
name="theme"
value="auto"
defaultChecked={
currentTheme !== 'light' && currentTheme !== 'dark'
}
/>
2024-08-13 15:26:23 +08:00
<span>
<Trans>Auto</Trans>
</span>
2023-02-28 17:12:17 +08:00
</label>
</div>
</form>
</div>
</li>
2023-03-08 17:17:23 +08:00
<li>
<div>
2024-08-13 15:26:23 +08:00
<label>
<Trans>Text size</Trans>
</label>
2023-03-08 17:17:23 +08:00
</div>
<div class="range-group">
2024-08-13 15:26:23 +08:00
<span style={{ fontSize: TEXT_SIZES[0] }}>
<Trans comment="Preview of one character, in smallest size">
A
</Trans>
</span>{' '}
2023-03-08 17:17:23 +08:00
<input
type="range"
min={TEXT_SIZES[0]}
max={TEXT_SIZES[TEXT_SIZES.length - 1]}
step="1"
value={currentTextSize}
list="sizes"
onChange={(e) => {
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);
}
}}
/>{' '}
<span style={{ fontSize: TEXT_SIZES[TEXT_SIZES.length - 1] }}>
2024-08-13 15:26:23 +08:00
<Trans comment="Preview of one character, in largest size">
A
</Trans>
2023-03-08 17:17:23 +08:00
</span>
<datalist id="sizes">
{TEXT_SIZES.map((size) => (
<option value={size} />
))}
</datalist>
</div>
</li>
2024-08-13 15:26:23 +08:00
<li>
<label>
<Trans>Display language</Trans>
</label>
<LangSelector />
</li>
</ul>
</section>
{authenticated && (
<>
2024-08-13 15:26:23 +08:00
<h3>
<Trans>Posting</Trans>
</h3>
<section>
<ul>
<li>
<div>
<label for="posting-privacy-field">
2024-08-13 15:26:23 +08:00
<Trans>Default visibility</Trans>{' '}
<Icon icon="cloud" alt={t`Synced`} class="synced-icon" />
</label>
</div>
<div>
<select
id="posting-privacy-field"
value={prefs['posting:default:visibility'] || 'public'}
onChange={(e) => {
const { value } = e.target;
(async () => {
try {
await masto.v1.accounts.updateCredentials({
source: {
privacy: value,
},
});
setPrefs({
...prefs,
'posting:default:visibility': value,
});
store.account.set('preferences', {
...prefs,
'posting:default:visibility': value,
});
} catch (e) {
2024-08-13 15:26:23 +08:00
alert(t`Failed to update posting privacy`);
console.error(e);
}
})();
}}
>
2024-08-13 15:26:23 +08:00
<option value="public">
<Trans>Public</Trans>
</option>
<option value="unlisted">
<Trans>Unlisted</Trans>
</option>
<option value="private">
<Trans>Followers only</Trans>
</option>
</select>
</div>
</li>
</ul>
</section>
<p class="section-postnote">
2024-08-13 15:26:23 +08:00
<Icon icon="cloud" alt={t`Synced`} class="synced-icon" />{' '}
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Synced to your instance server's settings.{' '}
<a
href={`https://${instance}/`}
target="_blank"
rel="noopener noreferrer"
>
Go to your instance ({instance}) for more settings.
</a>
</Trans>
</small>
</p>
</>
)}
2024-08-13 15:26:23 +08:00
<h3>
<Trans>Experiments</Trans>
</h3>
<section>
<ul>
2023-05-05 17:53:16 +08:00
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.autoRefresh}
onChange={(e) => {
states.settings.autoRefresh = e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>Auto refresh timeline posts</Trans>
2023-05-05 17:53:16 +08:00
</label>
</li>
2023-02-28 17:12:17 +08:00
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.boostsCarousel}
onChange={(e) => {
states.settings.boostsCarousel = e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>Boosts carousel</Trans>
2023-02-28 17:12:17 +08:00
</label>
</li>
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.contentTranslation}
onChange={(e) => {
const { checked } = e.target;
states.settings.contentTranslation = checked;
if (!checked) {
states.settings.contentTranslationTargetLanguage = null;
}
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>Post translation</Trans>
</label>
<div
class={`sub-section ${
!snapStates.settings.contentTranslation
? 'more-insignificant'
: ''
}`}
>
<div>
<label>
2024-08-13 15:26:23 +08:00
<Trans>Translate to </Trans>
<select
value={targetLanguage || ''}
disabled={!snapStates.settings.contentTranslation}
onChange={(e) => {
states.settings.contentTranslationTargetLanguage =
e.target.value || null;
}}
>
<option value="">
2024-08-13 15:26:23 +08:00
<Trans>
System language ({systemTargetLanguageText})
</Trans>
</option>
<option disabled></option>
2024-08-13 15:26:23 +08:00
{targetLanguages.map((lang) => {
const common = localeCode2Text({
code: lang.code,
fallback: lang.name,
});
const native = localeCode2Text({
code: lang.code,
locale: lang.code,
});
const same = !native || common === native;
return (
<option value={lang.code}>
{same ? common : `${common} (${native})`}
</option>
);
})}
</select>
</label>
</div>
<hr />
2024-08-13 15:26:23 +08:00
<div class="checkbox-fieldset">
<Plural
value={
snapStates.settings.contentTranslationHideLanguages.length
}
_0={`Hide "Translate" button for:`}
other={`Hide "Translate" button for (#):`}
/>
<div class="checkbox-fields">
2024-08-13 15:26:23 +08:00
{targetLanguages.map((lang) => {
const common = localeCode2Text({
code: lang.code,
fallback: lang.name,
});
const native = localeCode2Text({
code: lang.code,
locale: lang.code,
});
const same = !native || common === native;
return (
<label>
<input
type="checkbox"
checked={snapStates.settings.contentTranslationHideLanguages.includes(
lang.code,
)}
onChange={(e) => {
const { checked } = e.target;
if (checked) {
states.settings.contentTranslationHideLanguages.push(
lang.code,
);
2024-08-13 15:26:23 +08:00
} else {
states.settings.contentTranslationHideLanguages =
snapStates.settings.contentTranslationHideLanguages.filter(
(code) => code !== lang.code,
);
}
}}
/>{' '}
{same ? common : `${common} (${native})`}
</label>
);
})}
</div>
2024-08-13 15:26:23 +08:00
</div>
<p class="insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Note: This feature uses external translation services,
powered by{' '}
<a
href="https://github.com/cheeaun/lingva-api"
target="_blank"
rel="noopener noreferrer"
>
Lingva API
</a>{' '}
&amp;{' '}
<a
href="https://github.com/thedaviddelta/lingva-translate"
target="_blank"
rel="noopener noreferrer"
>
Lingva Translate
</a>
.
</Trans>
</small>
</p>
<hr />
<div>
<label>
<input
type="checkbox"
checked={snapStates.settings.contentTranslationAutoInline}
disabled={!snapStates.settings.contentTranslation}
onChange={(e) => {
states.settings.contentTranslationAutoInline =
e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>Auto inline translation</Trans>
</label>
<p class="insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Automatically show translation for posts in timeline.
Only works for <b>short</b> posts without content
warning, media and poll.
</Trans>
</small>
</p>
</div>
</div>
</li>
2024-04-02 17:51:48 +08:00
{!!GIPHY_API_KEY && authenticated && (
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.composerGIFPicker}
onChange={(e) => {
states.settings.composerGIFPicker = e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>GIF Picker for composer</Trans>
2024-04-02 17:51:48 +08:00
</label>
<div class="sub-section insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Note: This feature uses external GIF search service,
powered by{' '}
<a
href="https://developers.giphy.com/"
target="_blank"
rel="noopener noreferrer"
>
GIPHY
</a>
. 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.
</Trans>
2024-04-02 17:51:48 +08:00
</small>
</div>
</li>
)}
2024-03-04 16:36:34 +08:00
{!!IMG_ALT_API_URL && authenticated && (
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.mediaAltGenerator}
onChange={(e) => {
states.settings.mediaAltGenerator = e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>Image description generator</Trans>{' '}
<Icon icon="sparkles2" class="more-insignificant" />
</label>
2023-12-28 08:29:12 +08:00
<div class="sub-section insignificant">
2024-08-13 15:26:23 +08:00
<small>
<Trans>
Only for new images while composing new posts.
</Trans>
</small>
2023-12-28 08:29:12 +08:00
</div>
<div class="sub-section insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Note: This feature uses external AI service, powered by{' '}
<a
href="https://github.com/cheeaun/img-alt-api"
target="_blank"
rel="noopener noreferrer"
>
img-alt-api
</a>
. May not work well. Only for images and in English.
</Trans>
</small>
</div>
</li>
)}
{authenticated && supports('@mastodon/grouped-notifications') && (
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.groupedNotificationsAlpha}
onChange={(e) => {
states.settings.groupedNotificationsAlpha =
e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>Server-side grouped notifications</Trans>
</label>
<div class="sub-section insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Alpha-stage feature. Potentially improved grouping window
but basic grouping logic.
</Trans>
</small>
</div>
</li>
)}
{authenticated && (
<li>
<label>
<input
type="checkbox"
checked={
snapStates.settings.shortcutSettingsCloudImportExport
}
onChange={(e) => {
states.settings.shortcutSettingsCloudImportExport =
e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>"Cloud" import/export for shortcuts settings</Trans>{' '}
<Icon icon="cloud" class="more-insignificant" />
</label>
<div class="sub-section insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Very experimental.
<br />
Stored in your own profiles notes. Profile (private)
notes are mainly used for other profiles, and hidden for
own profile.
</Trans>
</small>
</div>
<div class="sub-section insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Note: This feature uses currently-logged-in instance
server API.
</Trans>
</small>
</div>
</li>
)}
2023-04-23 12:08:41 +08:00
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.cloakMode}
onChange={(e) => {
states.settings.cloakMode = e.target.checked;
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>
Cloak mode{' '}
<span class="insignificant">
(<samp>Text</samp> <samp></samp>)
</span>
</Trans>
2023-04-23 12:08:41 +08:00
</label>
2023-04-23 19:47:49 +08:00
<div class="sub-section insignificant">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
Replace text as blocks, useful when taking screenshots, for
privacy reasons.
</Trans>
2023-04-23 19:47:49 +08:00
</small>
</div>
2023-04-23 12:08:41 +08:00
</li>
{authenticated && (
<li>
<button
type="button"
class="light"
onClick={() => {
states.showDrafts = true;
states.showSettings = false;
}}
>
2024-08-13 15:26:23 +08:00
<Trans>Unsent drafts</Trans>
</button>
</li>
)}
2023-02-28 17:12:17 +08:00
</ul>
</section>
{authenticated && <PushNotificationsSection onClose={onClose} />}
2024-08-13 15:26:23 +08:00
<h3>
<Trans>About</Trans>
</h3>
2023-01-17 17:58:04 +08:00
<section>
2023-03-09 11:23:07 +08:00
<div
style={{
display: 'flex',
flexWrap: 'wrap',
2023-03-09 11:23:07 +08:00
gap: 8,
lineHeight: 1.25,
alignItems: 'center',
marginTop: 8,
}}
>
2023-01-26 00:54:30 +08:00
<img
src={logo}
alt=""
2023-03-09 11:23:07 +08:00
width="64"
height="64"
2023-01-26 00:54:30 +08:00
style={{
aspectRatio: '1/1',
verticalAlign: 'middle',
2023-03-09 11:23:07 +08:00
background: '#b7cdf9',
borderRadius: 12,
2023-01-26 00:54:30 +08:00
}}
2023-03-09 11:23:07 +08:00
/>
<div>
<b>Phanpy</b>{' '}
<a
href="https://hachyderm.io/@phanpy"
// target="_blank"
2023-10-02 15:58:59 +08:00
rel="noopener noreferrer"
2023-03-09 11:23:07 +08:00
onClick={(e) => {
e.preventDefault();
states.showAccount = 'phanpy@hachyderm.io';
}}
>
@phanpy
</a>
<br />
2024-08-13 15:26:23 +08:00
<Trans>
<a
href="https://github.com/cheeaun/phanpy"
target="_blank"
rel="noopener noreferrer"
>
Built
</a>{' '}
by{' '}
<a
href="https://mastodon.social/@cheeaun"
// target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.preventDefault();
states.showAccount = 'cheeaun@mastodon.social';
}}
>
@cheeaun
</a>
</Trans>
2023-03-09 11:23:07 +08:00
</div>
</div>
2023-01-30 23:16:00 +08:00
<p>
2023-10-03 15:07:47 +08:00
<a
href="https://github.com/sponsors/cheeaun"
target="_blank"
rel="noopener noreferrer"
>
2024-08-13 15:26:23 +08:00
<Trans>Sponsor</Trans>
2023-10-03 15:07:47 +08:00
</a>{' '}
&middot;{' '}
<a
href="https://www.buymeacoffee.com/cheeaun"
target="_blank"
rel="noopener noreferrer"
>
2024-08-13 15:26:23 +08:00
<Trans>Donate</Trans>
2023-10-03 15:07:47 +08:00
</a>{' '}
&middot;{' '}
2023-01-30 23:16:00 +08:00
<a
2023-12-25 19:25:48 +08:00
href={PRIVACY_POLICY_URL}
2023-01-30 23:16:00 +08:00
target="_blank"
2023-10-02 15:58:59 +08:00
rel="noopener noreferrer"
2023-01-30 23:16:00 +08:00
>
2024-08-13 15:26:23 +08:00
<Trans>Privacy Policy</Trans>
2023-01-30 23:16:00 +08:00
</a>
</p>
2023-01-17 17:58:04 +08:00
{__BUILD_TIME__ && (
<p>
2023-12-25 19:25:48 +08:00
{WEBSITE && (
<>
2024-08-13 15:26:23 +08:00
<Trans>
<span class="insignificant">Site:</span>{' '}
{WEBSITE.replace(/https?:\/\//g, '').replace(/\/$/, '')}
</Trans>
2023-12-25 19:25:48 +08:00
<br />
</>
)}
2024-08-13 15:26:23 +08:00
<Trans>
<span class="insignificant">Version:</span>{' '}
<input
type="text"
class="version-string"
readOnly
size="18" // Manually calculated here
value={`${__BUILD_TIME__.slice(0, 10).replace(/-/g, '.')}${
__COMMIT_HASH__ ? `.${__COMMIT_HASH__}` : ''
}`}
onClick={(e) => {
e.target.select();
// Copy to clipboard
try {
navigator.clipboard.writeText(e.target.value);
showToast(t`Version string copied`);
} catch (e) {
console.warn(e);
showToast(t`Unable to copy version string`);
}
}}
/>{' '}
{!__FAKE_COMMIT_HASH__ && (
<span class="ib insignificant">
(
<a
href={`https://github.com/cheeaun/phanpy/commit/${__COMMIT_HASH__}`}
target="_blank"
rel="noopener noreferrer"
>
<RelativeTime datetime={new Date(__BUILD_TIME__)} />
</a>
)
</span>
)}
</Trans>
2023-01-17 17:58:04 +08:00
</p>
)}
</section>
</main>
2022-12-10 17:14:48 +08:00
</div>
);
2022-12-16 13:27:04 +08:00
}
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 (
<form
ref={pushFormRef}
onChange={() => {
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);
2024-08-13 15:26:23 +08:00
alert(t`Failed to update subscription. Please try again.`);
});
} else {
updateSubscription(params).catch((err) => {
console.warn(err);
2024-08-13 15:26:23 +08:00
alert(t`Failed to update subscription. Please try again.`);
});
}
} else {
removeSubscription().catch((err) => {
console.warn(err);
2024-08-13 15:26:23 +08:00
alert(t`Failed to remove subscription. Please try again.`);
});
}
}, 100);
}}
>
2024-08-13 15:26:23 +08:00
<h3>
<Trans>Push Notifications (beta)</Trans>
</h3>
<section>
<ul>
<li>
<label>
<input
type="checkbox"
disabled={isLoading || needRelogin}
name="policy-allow"
checked={allowNotifications}
onChange={async (e) => {
const { checked } = e.target;
if (checked) {
// Request permission
const permission = await Notification.requestPermission();
if (permission === 'granted') {
setAllowNotifications(true);
} else {
setAllowNotifications(false);
if (permission === 'denied') {
alert(
2024-08-13 15:26:23 +08:00
t`Push notifications are blocked. Please enable them in your browser settings.`,
);
}
}
} else {
setAllowNotifications(false);
}
}}
/>{' '}
2024-08-13 15:26:23 +08:00
<Trans>
Allow from{' '}
<select
name="policy"
disabled={isLoading || needRelogin || !allowNotifications}
>
{[
{
value: 'all',
label: t`anyone`,
},
{
value: 'followed',
label: t`people I follow`,
},
{
value: 'follower',
label: t`followers`,
},
].map((type) => (
<option value={type.value}>{type.label}</option>
))}
</select>
</Trans>
</label>
<div
class="shazam-container no-animation"
style={{
width: '100%',
}}
hidden={!allowNotifications}
>
<div class="shazam-container-inner">
<div class="sub-section">
<ul>
{[
{
value: 'mention',
2024-08-13 15:26:23 +08:00
label: t`Mentions`,
},
{
value: 'favourite',
2024-08-13 15:26:23 +08:00
label: t`Likes`,
},
{
value: 'reblog',
2024-08-13 15:26:23 +08:00
label: t`Boosts`,
},
{
value: 'follow',
2024-08-13 15:26:23 +08:00
label: t`Follows`,
},
{
value: 'followRequest',
2024-08-13 15:26:23 +08:00
label: t`Follow requests`,
},
{
value: 'poll',
2024-08-13 15:26:23 +08:00
label: t`Polls`,
},
{
value: 'update',
2024-08-13 15:26:23 +08:00
label: t`Post edits`,
},
{
value: 'status',
2024-08-13 15:26:23 +08:00
label: t`New posts`,
},
].map((alert) => (
<li>
<label>
<input type="checkbox" name={alert.value} />{' '}
{alert.label}
</label>
</li>
))}
</ul>
</div>
</div>
</div>
{needRelogin && (
<div class="sub-section">
<p>
2024-08-13 15:26:23 +08:00
<Trans>
Push permission was not granted since your last login.
You'll need to{' '}
<Link to={`/login?instance=${instance}`} onClick={onClose}>
<b>log in</b> again to grant push permission
</Link>
.
</Trans>
</p>
</div>
)}
</li>
</ul>
</section>
<p class="section-postnote">
<small>
2024-08-13 15:26:23 +08:00
<Trans>
NOTE: Push notifications only work for <b>one account</b>.
</Trans>
</small>
</p>
</form>
);
}
2022-12-16 13:27:04 +08:00
export default Settings;