aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js125
1 files changed, 125 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js
new file mode 100644
index 0000000..2bd2c44
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js
@@ -0,0 +1,125 @@
+// The WhatsApp 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. WhatsApp's chats are only ever in its own encrypted backups, which this
+// copies for a restore onto a new phone; its media too, if asked.
+
+import { html, useState, useEffect, useCallback } from './vendor/htm-preact.js';
+import { t } from './i18n.js';
+import { ask } from './ask.js';
+import * as platform from './platform.js';
+import { subscribeWhatsApp, runWhatsAppSync } from './drive-sync.js';
+import { failureText } from './phone-sync.js';
+
+function stateLine(st, status) {
+ switch (st.phase) {
+ case 'waiting_wifi': return t('photosync.state_waiting_wifi');
+ case 'connecting': return t('photosync.state_connecting');
+ case 'sending': return t('photosync.state_sending', { done: st.done, total: st.total });
+ 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 WhatsAppSyncSection({ destination }) {
+ const sync = platform.whatsappSync;
+ 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(() => subscribeWhatsApp((st) => {
+ setRun(st);
+ if (st.phase !== 'sending' && st.phase !== 'connecting') refresh();
+ }), [refresh]);
+
+ if (!status) return null;
+ const chosen = (status.folders || []).length > 0;
+ const media = Boolean(status.options && status.options.media);
+ const running = run.phase === 'sending' || run.phase === 'connecting';
+
+ const act = async (fn) => {
+ setBusy(true);
+ setError('');
+ try {
+ const next = await fn();
+ if (next) setStatus(next);
+ } catch (e) {
+ setError(/Refused: not_whatsapp/.test(e.message || '') ? t('whatsappsync.refused') : e.message);
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const turnOn = (withMedia) => act(async () => {
+ const next = await sync.configure({ media: withMedia });
+ runWhatsAppSync({ force: true });
+ return next;
+ });
+
+ const backUpNow = () => act(async () => {
+ if (!status.unmetered && !await ask(t('drivesync.metered_confirm'))) return null;
+ await runWhatsAppSync({ force: true, metered: !status.unmetered });
+ return null;
+ });
+
+ return html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('whatsappsync.heading')}</h3>
+ ${!status.on && html`<p class="settings-hint">${t('whatsappsync.intro')}</p>`}
+ ${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`}
+ ${destination && html`
+ <div class="settings-row">
+ <span class="settings-label settings-name">
+ ${chosen ? t('whatsappsync.folder_chosen') : t('whatsappsync.folder_needed')}
+ </span>
+ <button class="btn-secondary" disabled=${busy}
+ onClick=${() => act(() => sync.chooseFolder())}>
+ ${chosen ? t('photosync.change') : t('whatsappsync.folder_choose')}</button>
+ </div>
+ ${!chosen && html`<p class="settings-hint">${t('whatsappsync.folder_hint')}</p>`}
+ <label class="settings-choice">
+ <input type="checkbox" checked=${media} disabled=${busy || !chosen}
+ onChange=${() => (status.on ? turnOn(!media)
+ : act(async () => ({ ...status, options: { media: !media } })))} />
+ <span class="settings-name">
+ <strong>${t('whatsappsync.media')}</strong>
+ <span class="settings-hint">${t('whatsappsync.media_hint')}</span>
+ </span>
+ </label>
+ `}
+ ${destination && chosen && !status.on && html`
+ <button class="admin-btn" disabled=${busy}
+ onClick=${() => turnOn(media)}>${t('phonesync.turn_on')}</button>
+ `}
+ ${destination && status.on && html`
+ <div class="settings-row" style="margin-top:8px">
+ <span class="settings-label settings-name">
+ ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })}
+ </span>
+ </div>
+ <p class="settings-hint">
+ ${t('drivesync.sent_total', { n: status.sent })}
+ ${' · '}
+ ${status.lastCompleted
+ ? t('photosync.last_run', { when: new Date(status.lastCompleted).toLocaleString() })
+ : t('photosync.never')}
+ </p>
+ ${status.failure
+ ? html`<p class="error-msg">${failureText(status.failure)}</p>`
+ : stateLine(run, status) && html`<p class="settings-hint">${stateLine(run, status)}</p>`}
+ <p class="settings-hint">${t('whatsappsync.restore_hint')}</p>
+ <div class="settings-row" style="gap:8px; justify-content:flex-start; flex-wrap:wrap">
+ <button class="admin-btn" disabled=${running || busy || !chosen}
+ onClick=${backUpNow}>${t('photosync.now')}</button>
+ <button class="btn-secondary" disabled=${busy}
+ onClick=${() => act(() => sync.configure(null))}>${t('photosync.turn_off')}</button>
+ </div>
+ `}
+ ${error && html`<p class="error-msg">${error}</p>`}
+ </div>
+ `;
+}