From 7957fa686eb6e8cf57bcb62ff4e6d2785693030b Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 10 Oct 2026 16:29:20 +0200 Subject: feat(android): back up WhatsApp's own chat backups, and its media if asked A WhatsApp section takes WhatsApp's folder through the picker, sends its encrypted Databases and Backups (not the dated copies of earlier weeks) into /-whatsapp, and names the restore set in the manifest. Co-Authored-By: Claude Opus 5.5 --- .../meshbay_hub/static/whatsapp-sync-settings.js | 125 +++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js (limited to 'packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js') 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..23aeb32 --- /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(); + 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` +
+

${t('whatsappsync.heading')}

+ ${!status.on && html`

${t('whatsappsync.intro')}

`} + ${!destination && html`

${t('androidsync.dest_needed')}

`} + ${destination && html` +
+ + ${chosen ? t('whatsappsync.folder_chosen') : t('whatsappsync.folder_needed')} + + +
+ ${!chosen && html`

${t('whatsappsync.folder_hint')}

`} + + `} + ${destination && chosen && !status.on && html` + + `} + ${destination && status.on && html` +
+ + ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })} + +
+

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

+ ${status.failure + ? html`

${failureText(status.failure)}

` + : stateLine(run, status) && html`

${stateLine(run, status)}

`} +

${t('whatsappsync.restore_hint')}

+
+ + +
+ `} + ${error && html`

${error}

`} +
+ `; +} -- cgit v1.2.3