aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-10 14:21:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-10 14:21:46 +0200
commit2f2a9a6542d2194e50ffba6f382e4fdffd481838 (patch)
tree5437330f9e8b963f8af0904f3c5d80d1afc6dc3b /packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
parent914d2f0af848fb80d70d517901f44420a4e47764 (diff)
downloadmeshbay-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.js180
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>
`;