// 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}
`}
`;
}