From 2f2a9a6542d2194e50ffba6f382e4fdffd481838 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 10 Oct 2026 14:21:46 +0200 Subject: 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 /-photos and -contacts. Owner and sole member are checked at set-up and before every run. Co-Authored-By: Claude Opus 5.5 --- .../src/meshbay_hub/static/phone-sync-settings.js | 180 +++++---------------- 1 file changed, 41 insertions(+), 139 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js') 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`

${t(heading)}

- ${!cfg && !editing && html` + ${!status.on && html`

${t(TEXT[kind].intro)}

- + ${!destination && html`

${t('androidsync.dest_needed')}

`} + ${destination && status.permission !== 'granted' && html` +

${t(TEXT[kind].permission)}

`} + ${destination && html` + `} `} - ${cfg && !editing && html` -
- - ${t('phonesync.summary', { group: cfg.groupName, dir: status.dir })} - -
- ${cfg.account !== user.username && html` -

${t('photosync.other_account', { account: cfg.account })}

`} + ${status.on && html` + ${destination && html` +
+ + ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })} + +
`} + ${!destination && html`

${t('androidsync.dest_needed')}

`}

${t('phonesync.copies_sent', { n: status.sent })} ${' · '} @@ -84,126 +101,11 @@ export function DocSyncSection({ kind, user, groups }) { ? html`

${failureText(status.failure)}

` : stateLine(run, status) && html`

${stateLine(run, status)}

`}
- - +
`} - ${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`

${error}

`} -
- `; -} - -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` -
- ${!permitted && html` -

${t(TEXT[kind].permission)}

-
- - -
- `} - ${permitted && html` - ${candidates === null && html`

${t('phonesync.checking_groups')}

`} - ${candidates && !candidates.length && html` -

${t('phonesync.no_private_group')}

`} - ${candidates && candidates.length > 0 && html` -
- ${t('photosync.group')} - -
- `} - ${probe === 'checking' && html`

${t('photosync.checking_group')}

`} - ${probe === 'offline' && html`

${t('photosync.group_offline')}

`} - ${folders && !folders.length && html`

${t('phonesync.no_folder')}

`} - ${folders && folders.length > 0 && html` -
- ${t('photosync.folder')} - -
-

${t('phonesync.destination', { dir })}

- `} -
- - -
- `} ${error && html`

${error}

`}
`; -- cgit v1.2.3