diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 14:21:46 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 14:21:46 +0200 |
| commit | 2f2a9a6542d2194e50ffba6f382e4fdffd481838 (patch) | |
| tree | 5437330f9e8b963f8af0904f3c5d80d1afc6dc3b /packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js | |
| parent | 914d2f0af848fb80d70d517901f44420a4e47764 (diff) | |
| download | meshbay-2f2a9a6542d2194e50ffba6f382e4fdffd481838.tar.gz | |
feat(android): one backup destination, a group the account owns alone
Chosen once at the top of Android Sync for every kind; photos and contacts
go into <folder>/<account>-photos and -contacts. Owner and sole member are
checked at set-up and before every run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js | 180 |
1 files changed, 41 insertions, 139 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js index 7df83b9..cec8b43 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js @@ -1,19 +1,17 @@ // The contacts section of the Android Sync page, on the Android application -// only (docs/MESHBAY_DESIGN.md §9.13). Loaded on demand: no other client has it. +// only (docs/MESHBAY_DESIGN.md §9.13). Loaded on demand: no other client has +// it. Where the copies go is the page's destination section; this one only +// turns the backup on or off. import { html, useState, useEffect, useCallback } from './vendor/htm-preact.js'; import { t } from './i18n.js'; -import { ask } from './ask.js'; -import { - KINDS, subscribe, runDocSync, privateGroups, probeWritable, failureText, -} from './phone-sync.js'; +import { KINDS, subscribe, runDocSync, failureText } from './phone-sync.js'; // What each kind says in its own words; everything else is shared. const TEXT = { contacts: { intro: 'contactsync.intro', permission: 'contactsync.permission_needed', - confirm: 'contactsync.confirm', }, }; @@ -28,11 +26,11 @@ function stateLine(st, status) { } } -export function DocSyncSection({ kind, user, groups }) { +export function DocSyncSection({ kind, destination }) { const { sync, heading } = KINDS[kind]; const [status, setStatus] = useState(null); const [run, setRun] = useState({ phase: 'idle' }); - const [editing, setEditing] = useState(false); + const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const refresh = useCallback(() => sync.status().then(setStatus).catch(() => {}), [sync]); @@ -43,12 +41,22 @@ export function DocSyncSection({ kind, user, groups }) { }), [kind, refresh]); if (!status) return null; - const cfg = status.config; - const busy = run.phase === 'sending' || run.phase === 'connecting'; + const running = run.phase === 'sending' || run.phase === 'connecting'; - const backUpNow = async () => { + const turnOn = async () => { + setBusy(true); setError(''); - try { await runDocSync(kind, { force: true }); } catch (e) { setError(e.message); } + try { + let st = status; + if (st.permission !== 'granted') st = await sync.permit(); + if (st.permission !== 'granted') { setStatus(st); return; } + setStatus(await sync.configure({})); + runDocSync(kind); + } catch (e) { + setError(e.message); + } finally { + setBusy(false); + } }; const turnOff = async () => { @@ -56,21 +64,30 @@ export function DocSyncSection({ kind, user, groups }) { try { setStatus(await sync.configure(null)); } catch (e) { setError(e.message); } }; + const backUpNow = async () => { + setError(''); + try { await runDocSync(kind, { force: true }); } catch (e) { setError(e.message); } + }; + return html` <div class="settings-section"> <h3 class="settings-heading">${t(heading)}</h3> - ${!cfg && !editing && html` + ${!status.on && html` <p class="settings-hint">${t(TEXT[kind].intro)}</p> - <button class="admin-btn" onClick=${() => setEditing(true)}>${t('photosync.setup')}</button> + ${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`} + ${destination && status.permission !== 'granted' && html` + <p class="settings-hint">${t(TEXT[kind].permission)}</p>`} + ${destination && html` + <button class="admin-btn" disabled=${busy} onClick=${turnOn}>${t('phonesync.turn_on')}</button>`} `} - ${cfg && !editing && html` - <div class="settings-row"> - <span class="settings-label settings-name"> - ${t('phonesync.summary', { group: cfg.groupName, dir: status.dir })} - </span> - </div> - ${cfg.account !== user.username && html` - <p class="settings-hint">${t('photosync.other_account', { account: cfg.account })}</p>`} + ${status.on && html` + ${destination && html` + <div class="settings-row"> + <span class="settings-label settings-name"> + ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })} + </span> + </div>`} + ${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`} <p class="settings-hint"> ${t('phonesync.copies_sent', { n: status.sent })} ${' · '} @@ -84,126 +101,11 @@ export function DocSyncSection({ kind, user, groups }) { ? html`<p class="error-msg">${failureText(status.failure)}</p>` : stateLine(run, status) && html`<p class="settings-hint">${stateLine(run, status)}</p>`} <div class="settings-row" style="gap:8px; justify-content:flex-start; flex-wrap:wrap"> - <button class="admin-btn" disabled=${busy} onClick=${backUpNow}>${t('photosync.now')}</button> - <button class="btn-secondary" onClick=${() => setEditing(true)}>${t('photosync.change')}</button> + <button class="admin-btn" disabled=${running || !destination} + onClick=${backUpNow}>${t('photosync.now')}</button> <button class="btn-secondary" onClick=${turnOff}>${t('photosync.turn_off')}</button> </div> `} - ${editing && html` - <${Setup} kind=${kind} user=${user} groups=${groups} status=${status} - onPermission=${setStatus} - onDone=${(next) => { setEditing(false); if (next) { setStatus(next); runDocSync(kind); } }} />`} - ${error && html`<p class="error-msg">${error}</p>`} - </div> - `; -} - -function Setup({ kind, user, groups, status, onPermission, onDone }) { - const { sync, suffix } = KINDS[kind]; - const cfg = status.config && status.config.account === user.username ? status.config : null; - const [candidates, setCandidates] = useState(null); // null while the hub is asked - const [groupId, setGroupId] = useState(cfg ? cfg.groupId : ''); - const [folders, setFolders] = useState(null); - const [probe, setProbe] = useState(''); // '', 'checking', 'offline' - const [folder, setFolder] = useState(cfg ? cfg.folder : ''); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(''); - const permitted = status.permission === 'granted'; - - // Only groups this account is alone in: what is sent is readable by every - // member, and an address book or a conversation is not the group's. - useEffect(() => { - let gone = false; - privateGroups(groups).then((list) => { - if (gone) return; - setCandidates(list); - setGroupId((g) => (list.some((x) => x.id === g) ? g : '')); - }); - return () => { gone = true; }; - }, [groups]); - - useEffect(() => { - setFolders(null); - if (!groupId) return undefined; - let gone = false; - setProbe('checking'); - probeWritable(groupId).then((r) => { - if (gone) return; - setProbe(''); - setFolders(r.folders); - setFolder((f) => (r.folders.includes(f) ? f : (r.folders[0] || ''))); - }).catch(() => { if (!gone) setProbe('offline'); }); - return () => { gone = true; }; - }, [groupId]); - - const allow = async () => { - setError(''); - try { onPermission(await sync.permit()); } catch (e) { setError(e.message); } - }; - - const dir = folder ? `${folder}/${user.username}-${suffix}` : ''; - - const start = async () => { - const group = (candidates || []).find((g) => g.id === groupId); - if (!group || !folder) return; - setBusy(true); - setError(''); - try { - if (!cfg || cfg.groupId !== groupId || cfg.folder !== folder) { - const yes = await ask(t(TEXT[kind].confirm, { group: group.name, dir })); - if (!yes) { setBusy(false); return; } - } - onDone(await sync.configure({ - account: user.username, groupId, groupName: group.name, - owner: group.owner_username || '', folder, - })); - } catch (e) { - setError(e.message); - setBusy(false); - } - }; - - return html` - <div> - ${!permitted && html` - <p class="settings-hint">${t(TEXT[kind].permission)}</p> - <div class="settings-row" style="gap:8px; justify-content:flex-start"> - <button class="admin-btn" onClick=${allow}>${t('photosync.permission_allow')}</button> - <button class="btn-secondary" onClick=${() => onDone(null)}>${t('dialog.cancel')}</button> - </div> - `} - ${permitted && html` - ${candidates === null && html`<p class="settings-hint">${t('phonesync.checking_groups')}</p>`} - ${candidates && !candidates.length && html` - <p class="settings-hint">${t('phonesync.no_private_group')}</p>`} - ${candidates && candidates.length > 0 && html` - <div class="settings-row"> - <span class="settings-label">${t('photosync.group')}</span> - <select class="settings-select" value=${groupId} - onChange=${(e) => setGroupId(e.target.value)}> - <option value="">${t('photosync.choose_group')}</option> - ${candidates.map((g) => html`<option key=${g.id} value=${g.id}>${g.name}</option>`)} - </select> - </div> - `} - ${probe === 'checking' && html`<p class="settings-hint">${t('photosync.checking_group')}</p>`} - ${probe === 'offline' && html`<p class="settings-hint">${t('photosync.group_offline')}</p>`} - ${folders && !folders.length && html`<p class="settings-hint">${t('phonesync.no_folder')}</p>`} - ${folders && folders.length > 0 && html` - <div class="settings-row"> - <span class="settings-label">${t('photosync.folder')}</span> - <select class="settings-select" value=${folder} onChange=${(e) => setFolder(e.target.value)}> - ${folders.map((f) => html`<option key=${f} value=${f}>${f}</option>`)} - </select> - </div> - <p class="settings-hint settings-name">${t('phonesync.destination', { dir })}</p> - `} - <div class="settings-row" style="gap:8px; justify-content:flex-start"> - <button class="admin-btn" onClick=${start} - disabled=${busy || !groupId || !folder}>${t('photosync.start')}</button> - <button class="btn-secondary" onClick=${() => onDone(null)}>${t('dialog.cancel')}</button> - </div> - `} ${error && html`<p class="error-msg">${error}</p>`} </div> `; |