summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/admin-page.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/ask.js70
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/chat-app.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/explore-page.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/file-utils.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/files-app.js12
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js15
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/node-page.js15
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/profile-page.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css3
21 files changed, 152 insertions, 30 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js b/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
index d5cc7a8..b3d71dd 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
@@ -2,6 +2,7 @@ import {
html, useState, useEffect, useCallback,
} from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask, tell } from './ask.js';
import { hubFetch } from './hub-client.js';
import { GroupName } from './group-name.js';
@@ -129,12 +130,12 @@ export function AdminPage({ token, role }) {
const deleteUser = useCallback(async (u) => {
// Suspension is the reversible tool and stays one click away; this one is
// not, so it names the account and says what it cannot reach.
- if (!confirm(t('admin.delete_confirm', { user: u.username }))) return;
+ if (!await ask(t('admin.delete_confirm', { user: u.username }))) return;
try {
await hubFetch(`/v1/admin/users/${u.id}`, { method: 'DELETE', token });
loadUsers(userSearch);
} catch (err) {
- alert(err.message);
+ tell(err.message);
}
}, [token, userSearch, loadUsers]);
@@ -157,7 +158,7 @@ export function AdminPage({ token, role }) {
// Suspending is the reversible tool and stays one click away; revoking
// pushes a signed revocation to every node hosting the group and there is
// no undo from here, so it names the group and asks first.
- if (!confirm(t('admin.revoke_group_confirm', { group: g.name }))) return;
+ if (!await ask(t('admin.revoke_group_confirm', { group: g.name }))) return;
try {
await hubFetch('/v1/admin/revoke',
{ method: 'POST', body: { target: 'group', target_id: g.id }, token });
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/ask.js b/packages/meshbay-hub/src/meshbay_hub/static/ask.js
new file mode 100644
index 0000000..8a8b895
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/ask.js
@@ -0,0 +1,70 @@
+import { html, render, useEffect, useRef } from './vendor/htm-preact.js';
+import { t } from './i18n.js';
+
+/**
+ * `confirm` and `alert`, drawn by the page instead of the browser.
+ *
+ * In the desktop client a native `confirm()` leaves the window unable to type:
+ * once it closes, `document.hasFocus()` stays false, a click still moves
+ * `activeElement` to a field, and every keystroke after it goes nowhere — until
+ * the window loses and regains focus. Found as "the Create group fields are
+ * frozen" right after removing a member, and reproduced against Electron 44 by
+ * real X input events (`test_no_native_dialogs_in_the_spa.py`). `prompt` was
+ * already banned for throwing; the other two fail more quietly, and later.
+ *
+ * Both return a promise, so a call site reads as it did: `if (!(await
+ * ask(msg))) return;`.
+ */
+
+function Dialog({ message, cancellable, onDone }) {
+ const okRef = useRef(null);
+ useEffect(() => { if (okRef.current) okRef.current.focus(); }, []);
+
+ const onKeyDown = (e) => {
+ if (e.key === 'Escape') { e.preventDefault(); onDone(!cancellable); }
+ };
+
+ return html`
+ <div class="video-overlay" onClick=${(e) => {
+ if (e.target.classList.contains('video-overlay')) onDone(!cancellable);
+ }}>
+ <form class="music-detail playlist-modal" role="alertdialog" aria-modal="true"
+ onKeyDown=${onKeyDown}
+ onSubmit=${(e) => { e.preventDefault(); onDone(true); }}>
+ <div class="playlist-modal-body">
+ <div class="ask-message">${message}</div>
+ <div class="playlist-modal-actions">
+ ${cancellable && html`
+ <button type="button" class="tb-btn" onClick=${() => onDone(false)}>
+ ${t('dialog.cancel')}</button>`}
+ <button type="submit" class="admin-btn" ref=${okRef}>${t('dialog.ok')}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ `;
+}
+
+function open(message, cancellable) {
+ return new Promise((resolve) => {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const previous = document.activeElement;
+ let settled = false;
+ const onDone = (value) => {
+ if (settled) return;
+ settled = true;
+ render(null, host);
+ host.remove();
+ if (previous && previous.isConnected && typeof previous.focus === 'function') {
+ previous.focus();
+ }
+ resolve(value);
+ };
+ render(html`<${Dialog} message=${String(message)} cancellable=${cancellable}
+ onDone=${onDone} />`, host);
+ });
+}
+
+export const ask = (message) => open(message, true);
+export const tell = (message) => open(message, false);
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js b/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
index 6e9de5f..0057218 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
@@ -2,6 +2,7 @@ import {
html, useState, useEffect, useLayoutEffect, useCallback, useRef,
} from './vendor/htm-preact.js';
import { t, getLocale } from './i18n.js';
+import { tell } from './ask.js';
import { Icon } from './icon.js';
import { formatSize, CHUNK_SIZE, pipelinedDownload } from './file-utils.js';
@@ -516,7 +517,7 @@ function ChatPanel({ transportRef, username, userId, entries, gekRef,
}]);
jumpToBottom();
} catch (err) {
- alert(err.message);
+ tell(err.message);
} finally {
setAttaching(false);
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/explore-page.js b/packages/meshbay-hub/src/meshbay_hub/static/explore-page.js
index bb47487..36b63a1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/explore-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/explore-page.js
@@ -2,6 +2,7 @@ import {
html, useState, useEffect, useCallback,
} from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { tell } from './ask.js';
import { hubFetch, navigate } from './hub-client.js';
import * as platform from './platform.js';
import { GroupName } from './group-name.js';
@@ -40,7 +41,7 @@ export function ExplorePage({ token, myGroupIds, allowPublicGroups = true }) {
if (err.message.includes('Already a member')) {
navigate(`/group/${gid}`);
} else {
- alert(err.message);
+ tell(err.message);
}
} finally {
setJoining(null);
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
index decf916..bdfba6e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
@@ -1,6 +1,7 @@
import * as downloads from './downloads.js';
import * as platform from './platform.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import { ZipStream, entriesUnder } from './zipstream.js';
const FILE_ICONS = {
@@ -556,7 +557,7 @@ async function downloadDirectory(transfers, transport, gek, entries, dir, { setE
accept: { 'application/zip': ['.zip'] } }],
}, 0);
if (target === false) return false;
- if (!target && !confirm(t('group.zip_no_stream', {
+ if (!target && !await ask(t('group.zip_no_stream', {
size: formatSize(totalBytes), name: suggested,
}))) {
return false;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
index 3f4212e..6aabea3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
@@ -2,6 +2,7 @@ import {
html, useState, useEffect, useRef, useCallback,
} from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import { Icon } from './icon.js';
import { entriesUnder } from './zipstream.js';
import { transfers } from './transfers.js';
@@ -280,9 +281,10 @@ function FilesPanel({
* The name comes from a field in the toolbar rather than `window.prompt`,
* which **throws** in Electron — "prompt() is not supported" — and threw
* outside this function's try, so clicking the button did nothing at all:
- * no folder, no error, nothing in the interface to react to. `confirm()` and
- * `alert()` do work there and are used elsewhere; `prompt` is the one
- * Chromium leaves to the embedder and Electron declines to implement.
+ * no folder, no error, nothing in the interface to react to. `prompt` is the
+ * one Chromium leaves to the embedder and Electron declines to implement;
+ * `confirm()` and `alert()` open but leave the window unable to type after
+ * them, which is why `ask.js` exists.
*
* An inline field is better anyway — it can show the refusal next to the
* input instead of after the dialog has closed.
@@ -636,10 +638,10 @@ function FilesPanel({
}), { disabled: selectedDirs.length === 0 })}
${mayEverDelete && !readOnly && action('trash',
deletableCount ? t('group.delete_n', { n: deletableCount }) : t('group.delete'),
- () => {
+ async () => {
const names = [...deletableFiles.map(e => e.name),
...(operatorPaired ? selectedDirs : [])];
- if (!confirm(t('group.delete_n_confirm', { n: names.length,
+ if (!await ask(t('group.delete_n_confirm', { n: names.length,
names: names.join(', ') }))) return;
run(() => {
for (const e of deletableFiles) deleteFile(e);
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index d7a1a66..5f811e8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -2,6 +2,7 @@ import {
html, useState, useEffect, useCallback, useMemo, useRef,
} from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import { Icon } from './icon.js';
import { CollapsibleSection, ToggleSwitch } from './settings-ui.js';
import { hubFetch, navigate } from './hub-client.js';
@@ -189,7 +190,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
}
return;
}
- if (!confirm(t('node.root_remove_confirm', { name: rootName }))) return;
+ if (!await ask(t('node.root_remove_confirm', { name: rootName }))) return;
const ok = await run(async () => {
if (overMnp) await transport.removeRoot(groupId, rootName, signFn);
else if (overLoopback) {
@@ -531,7 +532,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
}, [approveCode, userId, transportRef, loadDevices]);
const revokeDevice = useCallback(async (device) => {
- if (!confirm(t('device.revoke_confirm'))) return;
+ if (!await ask(t('device.revoke_confirm'))) return;
setDeviceMsg('');
try {
await transportRef.current.revokeDevice(
@@ -1046,7 +1047,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
${isOwner
? html`
<button class="admin-btn danger" onClick=${async () => {
- if (!confirm(t('group.delete_group_confirm', {
+ if (!await ask(t('group.delete_group_confirm', {
name: group.owner_username ? `${group.name}@${group.owner_username}` : group.name,
}))) return;
try {
@@ -1056,7 +1057,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
await platform.node.call('POST', '/api/groups/detach',
{ name: nodeGroupName });
} catch (detachErr) {
- if (!confirm(t('settings_node.detach_failed_continue'))) return;
+ if (!await ask(t('settings_node.detach_failed_continue'))) return;
}
}
await hubFetch('/v1/groups/' + groupId, { method: 'DELETE', token });
@@ -1067,7 +1068,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
`
: html`
<button class="admin-btn danger" onClick=${async () => {
- if (!confirm(t('group.leave_confirm', {
+ if (!await ask(t('group.leave_confirm', {
name: group.owner_username ? `${group.name}@${group.owner_username}` : group.name,
}))) return;
try {
@@ -1146,8 +1147,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
<td class="admin-actions">
${isAdmin && m.user_id !== adminId && html`
<button class="admin-btn danger" disabled=${removing === m.user_id}
- onClick=${() => {
- if (!confirm(t('members.remove_confirm', { user: m.username }))) return;
+ onClick=${async () => {
+ if (!await ask(t('members.remove_confirm', { user: m.username }))) return;
removeMember(m);
}}>
${removing === m.user_id ? '...' : t('members.remove')}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 989b5ca..e742787 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -1140,4 +1140,8 @@ export default {
'node.root_rw': 'Lesen/Schreiben',
'node.root_ro': 'Nur lesen',
'node.removable': 'wechselbar',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'Abbrechen',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 5042b31..87e5a4a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -1121,4 +1121,8 @@ export default {
'node.tab_peers': 'Peers',
'node.tab_audit': 'Audit',
'node.tab_settings': 'Settings',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'Cancel',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 80beb15..9e4040d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -1134,4 +1134,8 @@ export default {
'node.root_rw': 'lectura-escritura',
'node.root_ro': 'solo lectura',
'node.removable': 'extraíble',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'Aceptar',
+ 'dialog.cancel': 'Cancelar',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index bc2e221..aaa3e19 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -1149,4 +1149,8 @@ export default {
'node.tab_peers': 'Peers',
'node.tab_audit': 'Audit',
'node.tab_settings': 'Settings',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'Annuler',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 34296e1..a976e09 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -1148,4 +1148,8 @@ export default {
'node.root_rw': 'lettura-scrittura',
'node.root_ro': 'sola lettura',
'node.removable': 'rimovibile',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'Annulla',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index d256d1b..2660e7f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -1132,4 +1132,8 @@ export default {
'node.root_rw': '読み書き',
'node.root_ro': '読み取り専用',
'node.removable': 'リムーバブル',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'キャンセル',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 2c5c854..81ed678 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -1150,4 +1150,8 @@ export default {
'node.root_rw': 'lezen-schrijven',
'node.root_ro': 'alleen-lezen',
'node.removable': 'verwijderbaar',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'Annuleren',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index f2cfb8d..bd91a07 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -1176,4 +1176,8 @@ export default {
'node.root_rw': 'odczyt-zapis',
'node.root_ro': 'tylko odczyt',
'node.removable': 'wymienny',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'Anuluj',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 9d51a9c..490f2f9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -1135,4 +1135,8 @@ export default {
'node.root_rw': 'leitura-escrita',
'node.root_ro': 'somente leitura',
'node.removable': 'removível',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': 'OK',
+ 'dialog.cancel': 'Cancelar',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 5ca0571..8609297 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -1121,4 +1121,8 @@ export default {
'node.root_rw': '读写',
'node.root_ro': '只读',
'node.removable': '可移除',
+
+ // In-page confirm/alert (ask.js)
+ 'dialog.ok': '确定',
+ 'dialog.cancel': '取消',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
index 0b05dad..a59c100 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
@@ -2,6 +2,7 @@ import {
html, useState, useEffect, useCallback, useRef,
} from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import * as platform from './platform.js';
import { Icon } from './icon.js';
import { HUB } from './hub-client.js';
@@ -286,7 +287,7 @@ export function NodePage({ groups, token, username }) {
}, [refresh]);
const removeRoot = useCallback(async (groupId, rootName) => {
- if (!confirm(t('node.root_remove_confirm', { name: rootName }))) return;
+ if (!await ask(t('node.root_remove_confirm', { name: rootName }))) return;
setBusy(true);
setActionMsg('');
try {
@@ -315,7 +316,7 @@ export function NodePage({ groups, token, username }) {
}, []);
const unpinMember = useCallback(async (userId) => {
- if (!confirm(t('node.unpin_confirm', { name: userId }))) return;
+ if (!await ask(t('node.unpin_confirm', { name: userId }))) return;
setBusy(true);
setActionMsg('');
try {
@@ -330,7 +331,7 @@ export function NodePage({ groups, token, username }) {
}, [rosterGroup, loadRoster]);
const rotateGek = useCallback(async (groupId) => {
- if (!confirm(t('node.gek_rotate_confirm'))) return;
+ if (!await ask(t('node.gek_rotate_confirm'))) return;
setBusy(true);
setActionMsg('');
try {
@@ -358,7 +359,7 @@ export function NodePage({ groups, token, username }) {
const clearDenylist = useCallback(async (subject) => {
const label = subject || t('node.denylist_clear_all');
- if (!confirm(t('node.denylist_clear_confirm', { subject: label }))) return;
+ if (!await ask(t('node.denylist_clear_confirm', { subject: label }))) return;
setBusy(true);
setActionMsg('');
try {
@@ -373,7 +374,7 @@ export function NodePage({ groups, token, username }) {
}, [loadDenylist]);
const detachGroup = useCallback(async (name) => {
- if (!confirm(t('node.detach_confirm', { name }))) return;
+ if (!await ask(t('node.detach_confirm', { name }))) return;
setBusy(true);
setActionMsg('');
try {
@@ -640,7 +641,7 @@ export function NodePage({ groups, token, username }) {
}, []);
const unpinNodeMember = useCallback(async (userId) => {
- if (!confirm(t('node.unpin_confirm', { name: userId }))) return;
+ if (!await ask(t('node.unpin_confirm', { name: userId }))) return;
setBusy(true);
setActionMsg('');
try {
@@ -655,7 +656,7 @@ export function NodePage({ groups, token, username }) {
}, [loadNodeRoster]);
const unlinkNode = useCallback(async () => {
- if (!confirm(t('node.unlink_confirm'))) return;
+ if (!await ask(t('node.unlink_confirm'))) return;
setUnlinkBusy(true);
setActionMsg('');
try {
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js b/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
index 64d1c62..a65d26d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
@@ -2,6 +2,7 @@ import {
html, useState, useCallback, useEffect,
} from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import { Icon } from './icon.js';
import { Menu, useMenu } from './menu.js';
import * as P from './playlists.js';
@@ -123,10 +124,9 @@ function PlaylistMenuButton({ userId, lists, reload, onPlayQueue, onSync, cached
}, [userId, reload, say]);
const deletePlaylist = useCallback(async (p) => {
- // `confirm` and not a component: Electron implements it, a dozen places in
- // this SPA already use it, and a deletion is a tombstone rather than
- // something that can be undone from the interface.
- if (!window.confirm(t('playlists.confirm_delete', { name: p.name }))) return;
+ // Asked first: a deletion is a tombstone rather than something that can be
+ // undone from the interface.
+ if (!await ask(t('playlists.confirm_delete', { name: p.name }))) return;
await P.deletePlaylist(userId, p.id);
await reload();
say(t('playlists.deleted', { name: p.name }));
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
index a19b543..ed7a2fa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
@@ -2,6 +2,7 @@ import {
html, useState, useEffect, useCallback,
} from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import { Icon } from './icon.js';
import {
hubFetch, HUB, session, setAuth,
@@ -48,7 +49,7 @@ export function ProfilePage({ user, onLogout }) {
// Every session of this account stops renewing, this one included — the
// case it exists for is a browser left signed in somewhere else.
const signOutEverywhere = useCallback(async () => {
- if (!confirm(t('settings.sign_out_everywhere_confirm'))) return;
+ if (!await ask(t('settings.sign_out_everywhere_confirm'))) return;
setRevoking(true);
setRevokeError('');
try {
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index e012a1a..5f16a6c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -5229,6 +5229,9 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
flex-wrap: wrap;
}
+/* ask.js — the page's own confirm/alert, in the playlist modal's frame. */
+.ask-message { white-space: pre-line; line-height: 1.5; overflow-wrap: anywhere; }
+
/* The result of an action, said once and then gone. Not a dialog: adding an
album to a playlist is not a thing anyone should have to dismiss. Fixed
above the player bar, which is itself pinned to the bottom — `--music-bar-h`