aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/whatsapp-sync-settings.js
blob: 2bd2c44c4bedab0ecdbe423e6c88605e0f6094f3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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>
  `;
}