diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 16:29:20 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 16:29:20 +0200 |
| commit | 7957fa686eb6e8cf57bcb62ff4e6d2785693030b (patch) | |
| tree | 562195088fa40bc4db85193956bdd8aaf00e31f5 /packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js | |
| parent | 24547e4efd3a45b36b3bea8c29cfc687230a6d3a (diff) | |
| download | meshbay-7957fa686eb6e8cf57bcb62ff4e6d2785693030b.tar.gz | |
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
<folder>/<account>-whatsapp, and names the restore set in the manifest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.js | 125 |
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..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` + <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> + `; +} |