diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-21 19:53:33 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-21 19:53:33 +0200 |
| commit | 9f3445d03f106ee3ebd8b4b1bd546a08d9169af7 (patch) | |
| tree | fabae2b4c9e2fee75a73c5ba1479075be908a584 /packages/meshbay-hub/src | |
| parent | 24b563d0073849d0ba74c3fce9822a70c98e9d9f (diff) | |
| download | meshbay-9f3445d03f106ee3ebd8b4b1bd546a08d9169af7.tar.gz | |
fix: ask in the page instead of native confirm/alert
A native confirm() or alert() leaves the desktop client unable to type
until the window is refocused. ask.js draws both in the page; the SPA
test now bans all three browser dialogs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src')
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` |