From f6e80064ff8c4869a6ba2cef908cc54326948463 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 30 Aug 2026 23:57:57 +0200 Subject: refactor(ui): extract Settings, Profile and Admin pages from app.js Admin page is lazy-loaded so non-admin users never fetch it. Co-Authored-By: Claude Opus 4.6 --- .../src/meshbay_hub/static/settings-page.js | 316 +++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/settings-page.js (limited to 'packages/meshbay-hub/src/meshbay_hub/static/settings-page.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js b/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js new file mode 100644 index 0000000..c6a73da --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js @@ -0,0 +1,316 @@ +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'; + +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 (musicbay.md §2.2): 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 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); + }) + .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 [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` +
+

${t('settings.title')}

+ +
+

${t('settings.downloads')}

+ ${(downloads.SUPPORTED || platform.folder.available) && html` + + +
+ + ${dlDir ? t('settings.dl_folder', + { name: dlDir.name || String(dlDir) }) + : t('settings.dl_no_folder')} + + + + ${dlDir && !dlDir.isDefault && html` + + `} + +
+ ${dlError && html`

${dlError}

`} + `} +
+ +
+

${t('settings.appearance')}

+
+ ${t('settings.theme')} + +
+
+ ${t('settings.language')} + +
+
+ +
+

${t('settings.groups')}

+
+ ${t('settings.notif_global_disable')} + +
+ ${!globalMute && html` +

${t('settings.notif_global_hint')}

+ ${groups.map(g => html` +
+ ${g.name} + +
+ `)} + `} +
+ +
+

${t('settings.defaults')}

+
+ ${t('settings.default_tab')} + +
+

${t('settings.default_tab_hint')}

+
+ ${t('settings.music_keep_screen_on')} + +
+

${t('settings.music_keep_screen_on_hint')}

+
+ + ${platform.isNative && html` +
+

${t('settings.hub_heading')}

+
+ ${t('settings.hub_current')} + ${platform.hubBase() || '—'} +
+

${t('settings.hub_hint')}

+
{ + e.preventDefault(); + setHubError(''); + try { + await window.meshbay.setHubBase(hubInput.trim()); + } catch (err) { setHubError(platform.bridgeMessage(err)); } + }} style="display:flex;gap:8px"> + setHubInput(e.target.value)} /> + +
+ ${hubError && html`

${hubError}

`} +
+ `} + + ${keyBackend && html` +
+

${t('settings.keys_heading')}

+
+ ${t('settings.keys_where')} + ${keyBackend} +
+ ${keyBackend === 'unprotected_fallback' && html` +

${t('settings.keys_unprotected')}

+ `} + ${keyBackend === 'unavailable' && html` +

${t('settings.keys_unavailable')}

+ `} +
+ `} + +
+

${t('settings.about')}

+
+ ${t('settings.version')} + ${hubInfo ? hubInfo.hub : '—'} +
+
+ ${t('settings.protocol')} + + ${hubInfo ? `MNP ${hubInfo.mnp} / MHP ${hubInfo.mhp}` : '—'} + +
+
+
+ `; +} -- cgit v1.2.3