aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js173
1 files changed, 169 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js b/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js
index 5145515..b4d85d0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js
@@ -1,21 +1,186 @@
// The Android Sync page: what this phone sends to its groups, on the Android
// application only (docs/MESHBAY_DESIGN.md §9.12, §9.13). Loaded on demand,
// like the sections it holds: no other client has it.
+//
+// Where everything goes is chosen once, at the top, and every section below
+// sends into its own folder there.
-import { html } from './vendor/htm-preact.js';
+import { html, useState, useEffect } from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import * as platform from './platform.js';
import { PhotoSyncSection } from './photo-sync-settings.js';
import { DocSyncSection } from './phone-sync-settings.js';
+import { runPhotoSync } from './photo-sync.js';
+import { runDocSync, KINDS } from './phone-sync.js';
+import {
+ destination, privateGroups, probeGroup, photosTabShows,
+} from './sync-destination.js';
+
+/** The folder each kind this phone has goes into, named as the phone names it. */
+function kindDirs(folder, account) {
+ const suffixes = [
+ ...(platform.photoSync.available ? ['photos'] : []),
+ ...Object.values(KINDS).filter((k) => k.sync.available).map((k) => k.suffix),
+ ];
+ return suffixes.map((s) => `${folder}/${account}-${s}`);
+}
export function AndroidSyncPage({ user, groups }) {
+ const [dest, setDest] = useState(undefined); // undefined while asked, null when none
+ useEffect(() => { destination.get().then(setDest).catch(() => setDest(null)); }, []);
+
+ if (dest === undefined) {
+ return html`<div><h2>${t('androidsync.title')}</h2>
+ <p class="page-message"><span class="spinner"></span></p></div>`;
+ }
+ const mine = dest && dest.account === user.username ? dest : null;
+ const place = mine ? `${mine.groupId}\n${mine.folder}` : 'none';
+
+ const moved = (next) => {
+ setDest(next);
+ // Each kind that is on starts again in the new place, now.
+ if (platform.photoSync.available) runPhotoSync();
+ for (const kind of Object.keys(KINDS)) if (KINDS[kind].sync.available) runDocSync(kind);
+ };
+
return html`
<div>
<h2>${t('androidsync.title')}</h2>
+ <${DestinationSection} user=${user} groups=${groups || []} dest=${dest} onChange=${moved} />
${platform.photoSync.available && html`
- <${PhotoSyncSection} user=${user} groups=${groups || []} />`}
- ${platform.contactSync.available && html`
- <${DocSyncSection} kind="contacts" user=${user} groups=${groups || []} />`}
+ <${PhotoSyncSection} key=${'photos' + place} destination=${mine} />`}
+ ${Object.keys(KINDS).filter((k) => KINDS[k].sync.available).map((kind) => html`
+ <${DocSyncSection} key=${kind + place} kind=${kind} destination=${mine} />`)}
+ </div>
+ `;
+}
+
+function DestinationSection({ user, groups, dest, onChange }) {
+ const [editing, setEditing] = useState(false);
+ const mine = dest && dest.account === user.username ? dest : null;
+ return html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('androidsync.dest_heading')}</h3>
+ ${!editing && mine && html`
+ <div class="settings-row">
+ <span class="settings-label settings-name">
+ ${t('androidsync.dest_summary', { group: mine.groupName, folder: mine.folder })}
+ </span>
+ </div>
+ <p class="settings-hint settings-name">
+ ${t('androidsync.dest_dirs', { dirs: kindDirs(mine.folder, user.username).join(', ') })}
+ </p>
+ <button class="btn-secondary" onClick=${() => setEditing(true)}>${t('photosync.change')}</button>
+ `}
+ ${!editing && !mine && html`
+ <p class="settings-hint">
+ ${dest ? t('photosync.other_account', { account: dest.account }) : t('androidsync.dest_intro')}
+ </p>
+ <button class="admin-btn" onClick=${() => setEditing(true)}>${t('photosync.setup')}</button>
+ `}
+ ${editing && html`
+ <${DestinationSetup} user=${user} groups=${groups} current=${mine}
+ onDone=${(next) => { setEditing(false); if (next) onChange(next); }} />`}
+ </div>
+ `;
+}
+
+function DestinationSetup({ user, groups, current, onDone }) {
+ const [candidates, setCandidates] = useState(null); // null while the hub is asked
+ const [groupId, setGroupId] = useState(current ? current.groupId : '');
+ const [found, setFound] = useState(null); // { folders, photoDirectories }
+ const [probe, setProbe] = useState(''); // '', 'checking', 'offline'
+ const [folder, setFolder] = useState(current ? current.folder : '');
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState('');
+
+ // Only groups this account owns and is alone in: what is sent there is
+ // readable by every member, and none of it is the group's.
+ useEffect(() => {
+ let gone = false;
+ privateGroups(groups).then((list) => {
+ if (gone) return;
+ setCandidates(list);
+ setGroupId((g) => (list.some((x) => x.id === g) ? g : ''));
+ });
+ return () => { gone = true; };
+ }, [groups]);
+
+ useEffect(() => {
+ setFound(null);
+ if (!groupId) return undefined;
+ let gone = false;
+ setProbe('checking');
+ probeGroup(groupId).then((r) => {
+ if (gone) return;
+ setProbe('');
+ setFound(r);
+ setFolder((f) => (r.folders.includes(f) ? f : (r.folders[0] || '')));
+ }).catch(() => { if (!gone) setProbe('offline'); });
+ return () => { gone = true; };
+ }, [groupId]);
+
+ const photosDir = folder ? `${folder}/${user.username}-photos` : '';
+ const hiddenFromPhotos = platform.photoSync.available && found && folder
+ && !photosTabShows(photosDir, found.photoDirectories);
+
+ const save = async () => {
+ const group = (candidates || []).find((g) => g.id === groupId);
+ if (!group || !folder) return;
+ setBusy(true);
+ setError('');
+ try {
+ // Moving is a fresh start for every kind: said before it happens.
+ if (current && (current.groupId !== groupId || current.folder !== folder)) {
+ const yes = await ask(t('androidsync.dest_change_confirm', { group: group.name, folder }));
+ if (!yes) { setBusy(false); return; }
+ }
+ onDone(await destination.set({
+ account: user.username, groupId, groupName: group.name, folder,
+ }));
+ } catch (e) {
+ setError(e.message);
+ setBusy(false);
+ }
+ };
+
+ return html`
+ <div>
+ ${candidates === null && html`<p class="settings-hint">${t('phonesync.checking_groups')}</p>`}
+ ${candidates && !candidates.length && html`
+ <p class="settings-hint">${t('phonesync.no_private_group')}</p>`}
+ ${candidates && candidates.length > 0 && html`
+ <div class="settings-row">
+ <span class="settings-label">${t('photosync.group')}</span>
+ <select class="settings-select" value=${groupId}
+ onChange=${(e) => setGroupId(e.target.value)}>
+ <option value="">${t('photosync.choose_group')}</option>
+ ${candidates.map((g) => html`<option key=${g.id} value=${g.id}>${g.name}</option>`)}
+ </select>
+ </div>
+ `}
+ ${probe === 'checking' && html`<p class="settings-hint">${t('photosync.checking_group')}</p>`}
+ ${probe === 'offline' && html`<p class="settings-hint">${t('photosync.group_offline')}</p>`}
+ ${found && !found.folders.length && html`<p class="settings-hint">${t('phonesync.no_folder')}</p>`}
+ ${found && found.folders.length > 0 && html`
+ <div class="settings-row">
+ <span class="settings-label">${t('photosync.folder')}</span>
+ <select class="settings-select" value=${folder} onChange=${(e) => setFolder(e.target.value)}>
+ ${found.folders.map((f) => html`<option key=${f} value=${f}>${f}</option>`)}
+ </select>
+ </div>
+ ${folder && html`<p class="settings-hint settings-name">
+ ${t('androidsync.dest_dirs', { dirs: kindDirs(folder, user.username).join(', ') })}</p>`}
+ ${hiddenFromPhotos && html`
+ <p class="settings-hint settings-name">${t('androidsync.dest_not_in_photos', { dir: photosDir })}</p>`}
+ `}
+ <div class="settings-row" style="gap:8px; justify-content:flex-start">
+ <button class="admin-btn" onClick=${save}
+ disabled=${busy || !groupId || !folder}>${t('androidsync.dest_save')}</button>
+ <button class="btn-secondary" onClick=${() => onDone(null)}>${t('dialog.cancel')}</button>
+ </div>
+ ${error && html`<p class="error-msg">${error}</p>`}
</div>
`;
}