aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
diff options
context:
space:
mode:
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.js120
1 files changed, 120 insertions, 0 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
new file mode 100644
index 0000000..2c1ebc0
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
@@ -0,0 +1,120 @@
+// The contacts, calendar and messages sections of the Android Sync page, on
+// the Android application 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; these
+// only turn each backup on or off.
+
+import { html, useState, useEffect, useCallback } from './vendor/htm-preact.js';
+import { t } from './i18n.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',
+ },
+ calendar: {
+ intro: 'calendarsync.intro',
+ permission: 'calendarsync.permission_needed',
+ },
+ messages: {
+ intro: 'messagesync.intro',
+ permission: 'messagesync.permission_needed',
+ },
+};
+
+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, destination }) {
+ const { sync, heading } = KINDS[kind];
+ const [status, setStatus] = useState(null);
+ const [run, setRun] = useState({ phase: 'idle' });
+ const [busy, setBusy] = 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 running = run.phase === 'sending' || run.phase === 'connecting';
+
+ const turnOn = async () => {
+ setBusy(true);
+ setError('');
+ 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 () => {
+ setError('');
+ 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>
+ ${!status.on && html`
+ <p class="settings-hint">${t(TEXT[kind].intro)}</p>
+ ${!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>`}
+ `}
+ ${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 })}
+ ${' · '}
+ ${status.lastCompleted
+ ? t('photosync.last_run', { when: new Date(status.lastCompleted).toLocaleString() })
+ : t('photosync.never')}
+ </p>
+ ${status.lastSent && html`
+ <p class="settings-hint settings-name">${t('phonesync.last_file', { name: status.lastSent })}</p>`}
+ ${status.failure
+ ? 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=${running || !destination}
+ onClick=${backUpNow}>${t('photosync.now')}</button>
+ <button class="btn-secondary" onClick=${turnOff}>${t('photosync.turn_off')}</button>
+ </div>
+ `}
+ ${error && html`<p class="error-msg">${error}</p>`}
+ </div>
+ `;
+}