// 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. 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'; // 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', }, }; function stateLine(st, status) { switch (st.phase) { case 'connecting': return t('photosync.state_connecting'); case 'sending': return t('phonesync.state_sending'); case 'offline': return t('photosync.state_offline'); case 'interrupted': return t('photosync.state_interrupted'); case 'up_to_date': return t('photosync.state_up_to_date'); default: return status && status.lastCompleted ? t('photosync.state_up_to_date') : ''; } } export function DocSyncSection({ kind, user, groups }) { const { sync, heading } = KINDS[kind]; const [status, setStatus] = useState(null); const [run, setRun] = useState({ phase: 'idle' }); const [editing, setEditing] = useState(false); const [error, setError] = useState(''); const refresh = useCallback(() => sync.status().then(setStatus).catch(() => {}), [sync]); useEffect(() => { refresh(); }, [refresh]); useEffect(() => subscribe(kind, (st) => { setRun(st); if (st.phase !== 'sending' && st.phase !== 'connecting') refresh(); }), [kind, refresh]); if (!status) return null; const cfg = status.config; const busy = run.phase === 'sending' || run.phase === 'connecting'; const backUpNow = async () => { setError(''); try { await runDocSync(kind, { force: true }); } catch (e) { setError(e.message); } }; const turnOff = async () => { setError(''); try { setStatus(await sync.configure(null)); } catch (e) { setError(e.message); } }; return html`

${t(heading)}

${!cfg && !editing && html`

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

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

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

`}

${t('phonesync.copies_sent', { n: status.sent })} ${' · '} ${status.lastCompleted ? t('photosync.last_run', { when: new Date(status.lastCompleted).toLocaleString() }) : t('photosync.never')}

${status.lastSent && html`

${t('phonesync.last_file', { name: status.lastSent })}

`} ${status.failure ? 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}

`}
`; }