import { html, useState, useEffect, useCallback, } from './vendor/htm-preact.js'; import { t, getLocale, setLocale, LOCALES } from './i18n.js'; import * as downloads from './downloads.js'; import * as platform from './platform.js'; import { hubFetch } from './hub-client.js'; import { APPS } from './apps.js'; import { PAGE_SIZE_PREF, PAGE_SIZE_DEFAULT, PAGE_SIZE_STEP, PAGE_SIZE_MAX, pageSizeFrom, } from './pager.js'; const PAGE_SIZES = Array.from( { length: PAGE_SIZE_MAX / PAGE_SIZE_STEP }, (_, i) => (i + 1) * PAGE_SIZE_STEP); export function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange }) { const [locale, setLoc] = useState(getLocale); const [muted, setMuted] = useState( () => Object.fromEntries((groups || []).map(g => [g.id, !!g.muted]))); const [globalMute, setGlobalMute] = useState(false); const [defaultTab, setDefaultTab] = useState('chat'); // Off by default (docs/MESHBAY_DESIGN.md §9.8): the ordinary expectation, matching // Spotify/Deezer, is that the phone locks on its own idle timer while // listening. This is for whoever would rather trade battery for it — // e.g. to ride out the WebRTC screen-lock reconnect gap without waiting // on the automatic recovery at all. const [keepScreenOnAudio, setKeepScreenOnAudio] = useState(false); const [pageSize, setPageSize] = useState(PAGE_SIZE_DEFAULT); const onLocaleChange = useCallback((e) => { const code = e.target.value; setLocale(code); setLoc(code); window.location.reload(); }, []); const onThemeSelect = useCallback((e) => { onThemeChange(e.target.value); }, [onThemeChange]); useEffect(() => { hubFetch('/v1/users/me/preferences', { token: user.token }) .then(prefs => { if (prefs.notifications_disabled === 'true') setGlobalMute(true); if (prefs.default_tab) setDefaultTab(prefs.default_tab); if (prefs.music_keep_screen_on === 'true') setKeepScreenOnAudio(true); setPageSize(pageSizeFrom(prefs)); }) .catch(() => {}); }, [user.token]); const toggleKeepScreenOnAudio = useCallback(async () => { const next = !keepScreenOnAudio; setKeepScreenOnAudio(next); try { await hubFetch('/v1/users/me/preferences/music_keep_screen_on', { method: 'PUT', token: user.token, body: { value: next ? 'true' : 'false' }, }); // A string, matching what a fresh page load reads from the hub // (prefs.music_keep_screen_on === 'true' above) — music-player.js // compares against that same string, and userPrefs is one shared bag // fed from both this immediate update and that load. if (onPrefsChange) onPrefsChange({ music_keep_screen_on: next ? 'true' : 'false' }); } catch (err) { setKeepScreenOnAudio(!next); } }, [keepScreenOnAudio, user.token, onPrefsChange]); const toggleGlobalMute = useCallback(async () => { const next = !globalMute; setGlobalMute(next); try { await hubFetch('/v1/users/me/preferences/notifications_disabled', { method: 'PUT', token: user.token, body: { value: next ? 'true' : 'false' }, }); if (onPrefsChange) onPrefsChange({ notifications_disabled: next }); } catch (err) { setGlobalMute(!next); } }, [globalMute, user.token, onPrefsChange]); const toggleMute = useCallback(async (gid) => { const next = !muted[gid]; setMuted(prev => ({ ...prev, [gid]: next })); try { await hubFetch(`/v1/groups/${gid}/mute`, { method: 'POST', token: user.token, body: { muted: next }, }); } catch (err) { setMuted(prev => ({ ...prev, [gid]: !next })); } }, [muted, user.token]); const changeDefaultTab = useCallback(async (e) => { const val = e.target.value; setDefaultTab(val); try { await hubFetch('/v1/users/me/preferences/default_tab', { method: 'PUT', token: user.token, body: { value: val }, }); if (onPrefsChange) onPrefsChange({ default_tab: val }); } catch { setDefaultTab(defaultTab); } }, [defaultTab, user.token, onPrefsChange]); const changePageSize = useCallback(async (e) => { const val = String(e.target.value); setPageSize(Number(val)); try { await hubFetch(`/v1/users/me/preferences/${PAGE_SIZE_PREF}`, { method: 'PUT', token: user.token, body: { value: val }, }); if (onPrefsChange) onPrefsChange({ [PAGE_SIZE_PREF]: val }); } catch { setPageSize(pageSize); } }, [pageSize, user.token, onPrefsChange]); const [dlMode, setDlMode] = useState(() => downloads.getMode()); const [dlDir, setDlDir] = useState(null); const [dlError, setDlError] = useState(''); // Read from the hub rather than written here: the two constants that used to // sit in this markup said 0.1.0 and MNP 0.1 long after both had moved on. const [hubInfo, setHubInfo] = useState(null); // On a desktop build, whether the OS is really holding the keys. Electron's // safeStorage falls back to a fixed key when no keyring is running — a // headless session, a minimal desktop — and does it silently. Somebody who // believes the OS is protecting their keys deserves to be told when it is not. const [keyBackend, setKeyBackend] = useState(''); // Changing the hub after the first run. Without this a typo on the first // screen was permanent: the prompt only appears when no hub is set, so a // wrong one left editing a JSON file by hand as the only way out. const [hubInput, setHubInput] = useState(''); const [hubError, setHubError] = useState(''); useEffect(() => { if (!platform.secrets.available) return; platform.secrets.backend().then(setKeyBackend).catch(() => {}); }, []); useEffect(() => { hubFetch('/v1/hub/version').then(setHubInfo).catch(() => {}); }, []); useEffect(() => { // The desktop build remembers a path; the browser remembers a handle. Both // answer "where do downloads go", and the row below renders either. if (platform.folder.available) platform.folder.get().then(setDlDir); else downloads.savedDirectory().then(setDlDir); }, []); const pickFolder = useCallback(async () => { try { if (platform.folder.available) { const dir = await platform.folder.choose(); if (dir) setDlDir(dir); return; } const handle = await downloads.chooseDirectory(); setDlDir(handle); } catch (err) { // Reported where the folder controls are. This used to be written into // the node-key status, two sections away, where nobody was looking. if (err.name !== 'AbortError') setDlError(err.message); } }, []); return html`
${dlError}
`} `}${t('settings.notif_global_hint')}
${groups.map(g => html`${t('settings.default_tab_hint')}
${t('settings.media_page_size_hint')}
${t('settings.music_keep_screen_on_hint')}
${t('settings.hub_hint')}
${hubError && html`${hubError}
`}${t('settings.keys_unprotected')}
`} ${keyBackend === 'unavailable' && html`${t('settings.keys_unavailable')}
`}