mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[ui] route all native alert()/confirm() through the site notification system
Every browser alert()/confirm() now uses the app's UI instead of native dialogs (no prompt() existed). Two new pieces: - js/ui/confirmDialog.js — showConfirm(message, opts) -> Promise<boolean>, a styled `bv-modal modal-always-on-top` dialog (Confirm/Cancel/backdrop/Escape; danger variant focuses Cancel). Importable anywhere, so future confirmations use it too. It stacks above other modals and is typable thanks to the recent stacked-modal focus fix. - js/ui/appNotify.js — notifyError/notifyStatus/notifySuccess routing to the app's NotificationController, for modules without a local showError; re-exports showConfirm. Migrated: UploadModal (10 alert -> this.showError, 1 confirm), subscriptionHistory (3 alert -> notify*, 1 confirm), MyVideosController (2), ProfileStorage/Wallet/Relay controllers, profileModalController (2), settingsRestorePrompt (default confirm now showConfirm), nostr/client deleteAllVersions. confirmSyncOverwrite helpers made async (syncConflict already awaits confirmOverwrite). Tests: tests/confirm-dialog.test.mjs (6 — confirm/cancel/backdrop/Escape resolution, DOM cleanup, danger focus, no-DOM fallback). Build + lint clean; affected suites green. (Note: build/lint don't parse JS — caught a multi-line-import split + a wrong import path via module-import checks.) Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
6e85c2a32b
commit
ad83c9f8db
13 files changed
+309
-65
No files matched your search
+5
-3
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import { isDevMode } from "../config.js";
|
||||
import { showConfirm } from "../ui/confirmDialog.js";
|
||||
import { infoHashFromMagnet } from "../magnets.js";
|
||||
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
|
||||
import {
|
||||
@@ -3465,9 +3466,10 @@ export class NostrClient {
|
||||
options && typeof options.video === "object" ? options.video : null;
|
||||
let confirmed = true;
|
||||
|
||||
if (shouldConfirm && typeof window?.confirm === "function") {
|
||||
confirmed = window.confirm(
|
||||
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
||||
if (shouldConfirm) {
|
||||
confirmed = await showConfirm(
|
||||
"Are you sure you want to delete all versions of this video? This action cannot be undone.",
|
||||
{ confirmLabel: "Delete all", danger: true },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// if the user accepts.
|
||||
|
||||
import { storageSyncService as defaultStorageSync } from "./storageSyncService.js";
|
||||
import { showConfirm } from "../ui/confirmDialog.js";
|
||||
|
||||
const OFFERED_KEY = "bitvid:settings-sync:offered:v1";
|
||||
|
||||
@@ -63,10 +64,7 @@ const ITEM_LABELS = {
|
||||
export function createSettingsRestorePrompt({
|
||||
storageSync = defaultStorageSync,
|
||||
walletSync = null,
|
||||
confirm = (message) =>
|
||||
typeof window !== "undefined" && typeof window.confirm === "function"
|
||||
? window.confirm(message)
|
||||
: false,
|
||||
confirm = (message) => showConfirm(message, { confirmLabel: "Restore" }),
|
||||
logger = null,
|
||||
} = {}) {
|
||||
// Items not already enabled / not already offered on THIS device that have a
|
||||
@@ -117,7 +115,7 @@ export function createSettingsRestorePrompt({
|
||||
}
|
||||
|
||||
const list = candidates.map((kind) => ITEM_LABELS[kind]).join(" and ");
|
||||
const accepted = confirm(
|
||||
const accepted = await confirm(
|
||||
`Found an encrypted copy of your ${list} on your Nostr account. ` +
|
||||
`Restore ${candidates.length > 1 ? "them" : "it"} to this device?`
|
||||
);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Single import surface for app notifications, so every alert/confirm goes through the
|
||||
// site's notification system instead of native dialogs.
|
||||
//
|
||||
// - notifyError / notifyStatus / notifySuccess → the app's toast banners
|
||||
// (NotificationController) for components/services without a local showError.
|
||||
// - showConfirm → the styled promise-based confirm dialog (replaces window.confirm).
|
||||
//
|
||||
// Components that already receive a showError/showSuccess callback should keep using it;
|
||||
// this is for modules that don't (e.g. standalone controllers/services).
|
||||
|
||||
import { getApplication } from "../applicationContext.js";
|
||||
import { showConfirm } from "./confirmDialog.js";
|
||||
|
||||
function app() {
|
||||
try {
|
||||
return getApplication();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function notifyError(message) {
|
||||
app()?.showError?.(message);
|
||||
}
|
||||
|
||||
export function notifyStatus(message, options) {
|
||||
app()?.showStatus?.(message, options);
|
||||
}
|
||||
|
||||
export function notifySuccess(message) {
|
||||
app()?.showSuccess?.(message);
|
||||
}
|
||||
|
||||
export { showConfirm };
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "../../nostrClientFacade.js";
|
||||
import { UI_FEEDBACK_DELAY_MS } from "../../constants.js";
|
||||
import { showConfirm } from "../confirmDialog.js";
|
||||
|
||||
const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
|
||||
|
||||
@@ -518,12 +519,12 @@ export class UploadModal {
|
||||
void this.captureVideoMetadata(file);
|
||||
|
||||
if (!this.storageConfigured) {
|
||||
alert("Please configure storage before selecting a file.");
|
||||
this.showError("Please configure storage before selecting a file.");
|
||||
e.target.value = ""; // Clear selection
|
||||
return;
|
||||
}
|
||||
if (!this.isStorageUnlocked) {
|
||||
alert("Please unlock storage before selecting a file.");
|
||||
this.showError("Please unlock storage before selecting a file.");
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
@@ -614,7 +615,7 @@ export class UploadModal {
|
||||
if (this.results.magnet) this.results.magnet.value = "Upload Failed";
|
||||
if (this.results.torrentUrl) this.results.torrentUrl.value = "Upload Failed";
|
||||
|
||||
alert(`Upload failed: ${err.message}`);
|
||||
this.showError(`Upload failed: ${err.message}`);
|
||||
|
||||
if (this.inputs.file) this.inputs.file.value = ""; // Reset
|
||||
}
|
||||
@@ -676,7 +677,7 @@ export class UploadModal {
|
||||
userLogger.error("Thumbnail upload failed:", err);
|
||||
this.thumbnailUploadState.status = 'error';
|
||||
this.updateThumbnailProgress(null, "Failed.");
|
||||
alert("Thumbnail upload failed.");
|
||||
this.showError("Thumbnail upload failed.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,7 +825,7 @@ export class UploadModal {
|
||||
if (this.promptStoredNsecUnlock(pubkey)) {
|
||||
return;
|
||||
}
|
||||
alert("No signer available to unlock storage.");
|
||||
this.showError("No signer available to unlock storage.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -832,7 +833,7 @@ export class UploadModal {
|
||||
if (signer?.type === "extension" || signer?.type === "nip07") {
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
alert("Extension permissions are required to unlock storage.");
|
||||
this.showError("Extension permissions are required to unlock storage.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -846,7 +847,7 @@ export class UploadModal {
|
||||
await this.loadFromStorage();
|
||||
} catch (err) {
|
||||
userLogger.error("Unlock failed", err);
|
||||
alert("Failed to unlock storage: " + err.message);
|
||||
this.showError("Failed to unlock storage: " + err.message);
|
||||
} finally {
|
||||
if (this.toggles.storageUnlock) {
|
||||
this.toggles.storageUnlock.textContent = "Unlock";
|
||||
@@ -945,17 +946,17 @@ export class UploadModal {
|
||||
// Check upload state
|
||||
if (this.activeSource === "upload") {
|
||||
if (this.videoUploadState.status === 'uploading' || this.thumbnailUploadState.status === 'uploading') {
|
||||
alert("Please wait for uploads to complete.");
|
||||
this.showError("Please wait for uploads to complete.");
|
||||
return;
|
||||
}
|
||||
if (this.videoUploadState.status === 'error') {
|
||||
alert("Video upload failed. Please try again.");
|
||||
this.showError("Video upload failed. Please try again.");
|
||||
return;
|
||||
}
|
||||
if (this.videoUploadState.status !== 'complete') {
|
||||
// Fallback: If no file selected, maybe they want to submit without a new file?
|
||||
// (Not supported in this simplified modal, assume file required)
|
||||
alert("Please select a video file and wait for it to upload.");
|
||||
this.showError("Please select a video file and wait for it to upload.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1104,7 +1105,7 @@ export class UploadModal {
|
||||
}
|
||||
|
||||
if (!hasUrl && !hasImeta && hasMagnet) {
|
||||
if (!confirm("Magnet-only uploads require active seeding. Proceed?")) return;
|
||||
if (!(await showConfirm("Magnet-only uploads require active seeding. Proceed?"))) return;
|
||||
}
|
||||
|
||||
await this.publish(metadata);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Promise-based confirm dialog rendered with the site's modal styling, so yes/no
|
||||
// confirmations live in the app's UI system instead of a native window.confirm.
|
||||
//
|
||||
// Importable anywhere (components AND services) — it builds its own
|
||||
// `bv-modal modal-always-on-top` overlay, so it stacks above any open modal and is
|
||||
// keyboard/click dismissible. Returns a Promise<boolean>: true = confirmed.
|
||||
|
||||
function makeButton(label, { ghost = false, danger = false } = {}) {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = ghost ? "btn-ghost focus-ring" : "btn focus-ring";
|
||||
if (danger) {
|
||||
btn.setAttribute("data-variant", "danger");
|
||||
}
|
||||
btn.textContent = label;
|
||||
return btn;
|
||||
}
|
||||
|
||||
export function showConfirm(message, options = {}) {
|
||||
const {
|
||||
title = "",
|
||||
confirmLabel = "Confirm",
|
||||
cancelLabel = "Cancel",
|
||||
danger = false,
|
||||
} = options || {};
|
||||
|
||||
// No DOM (tests without jsdom / SSR): fall back to window.confirm, else decline.
|
||||
if (typeof document === "undefined" || !document.body) {
|
||||
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
||||
return Promise.resolve(window.confirm(String(message || "")));
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className =
|
||||
"bv-modal modal-always-on-top items-start justify-center md:items-center";
|
||||
overlay.setAttribute("role", "dialog");
|
||||
overlay.setAttribute("aria-modal", "true");
|
||||
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.className = "bv-modal-backdrop";
|
||||
overlay.appendChild(backdrop);
|
||||
|
||||
const sheet = document.createElement("div");
|
||||
sheet.className = "modal-sheet w-full max-w-md flex flex-col";
|
||||
sheet.tabIndex = -1;
|
||||
overlay.appendChild(sheet);
|
||||
|
||||
if (title) {
|
||||
const header = document.createElement("div");
|
||||
header.className = "modal-header";
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "text-lg font-bold text-text";
|
||||
heading.textContent = title;
|
||||
header.appendChild(heading);
|
||||
sheet.appendChild(header);
|
||||
}
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "p-5";
|
||||
const text = document.createElement("p");
|
||||
text.className = "text-sm text-muted-strong whitespace-pre-line";
|
||||
text.textContent = String(message || "");
|
||||
body.appendChild(text);
|
||||
sheet.appendChild(body);
|
||||
|
||||
const footer = document.createElement("div");
|
||||
footer.className =
|
||||
"flex items-center justify-end gap-3 border-t border-border/60 p-4";
|
||||
const cancelBtn = makeButton(cancelLabel, { ghost: true });
|
||||
const confirmBtn = makeButton(confirmLabel, { danger });
|
||||
footer.appendChild(cancelBtn);
|
||||
footer.appendChild(confirmBtn);
|
||||
sheet.appendChild(footer);
|
||||
|
||||
let settled = false;
|
||||
const cleanup = (result) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
document.removeEventListener("keydown", onKeydown, true);
|
||||
overlay.remove();
|
||||
resolve(result);
|
||||
};
|
||||
function onKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
cleanup(false);
|
||||
} else if (event.key === "Enter" && document.activeElement === confirmBtn) {
|
||||
cleanup(true);
|
||||
}
|
||||
}
|
||||
|
||||
cancelBtn.addEventListener("click", () => cleanup(false));
|
||||
backdrop.addEventListener("click", () => cleanup(false));
|
||||
confirmBtn.addEventListener("click", () => cleanup(true));
|
||||
document.addEventListener("keydown", onKeydown, true);
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
// Focus the safe action by default (Cancel for destructive prompts).
|
||||
(danger ? cancelBtn : confirmBtn).focus();
|
||||
});
|
||||
}
|
||||
|
||||
export default showConfirm;
|
||||
@@ -6,6 +6,7 @@ import { classifyVideoHealth, isUrlUnderBase } from "./myVideosHealth.js";
|
||||
import { reconcileStorage } from "./myVideosReconcile.js";
|
||||
import { FEATURE_NIP71_MIRROR } from "../../constants.js";
|
||||
import { nip71MirrorService } from "../../services/nip71MirrorService.js";
|
||||
import { showConfirm } from "../confirmDialog.js";
|
||||
import {
|
||||
isMirrorEnabled,
|
||||
setMirrorEnabled,
|
||||
@@ -282,10 +283,13 @@ export class MyVideosController {
|
||||
if (!r2Service || typeof r2Service.deleteStorageKeys !== "function") {
|
||||
return;
|
||||
}
|
||||
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
||||
if (!window.confirm(`Permanently delete this file from your bucket?\n\n${key}`)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await showConfirm(`Permanently delete this file from your bucket?\n\n${key}`, {
|
||||
confirmLabel: "Delete",
|
||||
danger: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
@@ -314,14 +318,13 @@ export class MyVideosController {
|
||||
if (!r2Service || typeof r2Service.deleteStorageKeys !== "function" || !keys.length) {
|
||||
return;
|
||||
}
|
||||
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Permanently delete all ${keys.length} orphaned files from your bucket?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await showConfirm(
|
||||
`Permanently delete all ${keys.length} orphaned files from your bucket?`,
|
||||
{ confirmLabel: "Delete all", danger: true },
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.orphanDeleteAllBtn instanceof HTMLElement) {
|
||||
this.orphanDeleteAllBtn.disabled = true;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
import { showConfirm } from "../confirmDialog.js";
|
||||
import { sanitizeRelayList } from "../../nostr/nip46Client.js";
|
||||
|
||||
const noop = () => {};
|
||||
@@ -556,7 +557,7 @@ export class ProfileRelayController {
|
||||
reason: null,
|
||||
};
|
||||
|
||||
const confirmed = window.confirm("Restore the recommended relay defaults?");
|
||||
const confirmed = await showConfirm("Restore the recommended relay defaults?", { confirmLabel: "Restore" });
|
||||
context.confirmed = confirmed;
|
||||
if (!confirmed) {
|
||||
context.reason = "cancelled";
|
||||
@@ -604,9 +605,10 @@ export class ProfileRelayController {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`Remove ${url} from your relay list?`,
|
||||
);
|
||||
const confirmed = await showConfirm(`Remove ${url} from your relay list?`, {
|
||||
confirmLabel: "Remove",
|
||||
danger: true,
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getActiveSigner } from "../../nostr/client.js";
|
||||
import { DEFAULT_NIP07_ENCRYPTION_METHODS } from "../../nostr/nip07Permissions.js";
|
||||
import { storageSyncService } from "../../services/storageSyncService.js";
|
||||
import { StorageCorsHelp } from "./storageCorsHelp.js";
|
||||
import { showConfirm } from "../confirmDialog.js";
|
||||
import {
|
||||
fillStorageForm,
|
||||
clearCredentialFields,
|
||||
@@ -241,12 +242,10 @@ export class ProfileStorageController {
|
||||
}
|
||||
|
||||
confirmSyncOverwrite() {
|
||||
if (typeof window === "undefined" || typeof window.confirm !== "function") {
|
||||
return true;
|
||||
}
|
||||
return window.confirm(
|
||||
return showConfirm(
|
||||
"A newer copy of your storage settings is on your account (changed on " +
|
||||
"another device). Overwrite it with this one?"
|
||||
"another device). Overwrite it with this one?",
|
||||
{ confirmLabel: "Overwrite", danger: true },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { showConfirm } from "../confirmDialog.js";
|
||||
import { NWC_URI_SCHEME } from "../profileModalContract.js";
|
||||
import { createWalletSyncService } from "../../services/walletSyncService.js";
|
||||
import { runWalletZappabilityCheck } from "./walletZappabilityCheck.js";
|
||||
@@ -104,12 +105,10 @@ export class ProfileWalletController {
|
||||
}
|
||||
|
||||
confirmSyncOverwrite() {
|
||||
if (typeof window === "undefined" || typeof window.confirm !== "function") {
|
||||
return true;
|
||||
}
|
||||
return window.confirm(
|
||||
return showConfirm(
|
||||
"A newer copy of your wallet connection is on your account (changed on " +
|
||||
"another device). Overwrite it with this one?"
|
||||
"another device). Overwrite it with this one?",
|
||||
{ confirmLabel: "Overwrite", danger: true },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,14 +157,12 @@ export class ProfileWalletController {
|
||||
try {
|
||||
if (enabled) {
|
||||
// Spending capability — require explicit confirmation before publishing.
|
||||
const confirmed =
|
||||
typeof window !== "undefined" && typeof window.confirm === "function"
|
||||
? window.confirm(
|
||||
"This publishes an ENCRYPTED copy of your wallet connection to public relays.\n\n" +
|
||||
"A wallet connect URI can SPEND from your wallet. Anyone who controls your " +
|
||||
"Nostr key could decrypt this and spend from this wallet.\n\nContinue?"
|
||||
)
|
||||
: true;
|
||||
const confirmed = await showConfirm(
|
||||
"This publishes an ENCRYPTED copy of your wallet connection to public relays.\n\n" +
|
||||
"A wallet connect URI can SPEND from your wallet. Anyone who controls your " +
|
||||
"Nostr key could decrypt this and spend from this wallet.\n\nContinue?",
|
||||
{ confirmLabel: "Publish", danger: true },
|
||||
);
|
||||
if (!confirmed) {
|
||||
if (this.walletSyncToggle instanceof HTMLInputElement) {
|
||||
this.walletSyncToggle.checked = false;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MAX_WALLET_DEFAULT_ZAP as CONFIG_MAX_WALLET_DEFAULT_ZAP,
|
||||
ENABLE_NIP17_RELAY_WARNING as CONFIG_ENABLE_NIP17_RELAY_WARNING,
|
||||
} from "../config.js";
|
||||
import { showConfirm } from "./confirmDialog.js";
|
||||
import { normalizeDesignSystemContext } from "../designSystem.js";
|
||||
import { RUNTIME_FLAGS } from "../constants.js";
|
||||
import {
|
||||
@@ -1926,14 +1927,17 @@ export class ProfileModalController {
|
||||
}
|
||||
|
||||
if (this.profileSubscriptionsBackupBtn) {
|
||||
this.profileSubscriptionsBackupBtn.addEventListener("click", () => {
|
||||
this.profileSubscriptionsBackupBtn.addEventListener("click", async () => {
|
||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (pubkey) {
|
||||
if (confirm("Create a backup of your current subscription list?")) {
|
||||
if (
|
||||
pubkey &&
|
||||
(await showConfirm("Create a backup of your current subscription list?", {
|
||||
confirmLabel: "Create backup",
|
||||
}))
|
||||
) {
|
||||
this.subscriptionHistoryController.handleCreateBackup(pubkey).then(() => {
|
||||
this.showSuccess("Backup created.");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.showError("Please log in to backup subscriptions.");
|
||||
}
|
||||
@@ -2930,7 +2934,7 @@ export class ProfileModalController {
|
||||
? confirmValue.trim()
|
||||
: "this entry";
|
||||
const prompt = confirmMessage.replace("{npub}", replacement);
|
||||
if (!window.confirm(prompt)) {
|
||||
if (!(await showConfirm(prompt, { danger: true }))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { profileCache } from "../state/profileCache.js";
|
||||
import { formatTimeAgo } from "../utils/formatters.js";
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { getApplication } from "../applicationContext.js";
|
||||
import { notifyError, notifySuccess, showConfirm } from "./appNotify.js";
|
||||
|
||||
export class SubscriptionHistoryController {
|
||||
constructor(options = {}) {
|
||||
@@ -302,9 +303,10 @@ export class SubscriptionHistoryController {
|
||||
async handleRestore() {
|
||||
if (!this.selectedEvent) return;
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to restore this list? It will overwrite your current subscriptions."
|
||||
)
|
||||
!(await showConfirm(
|
||||
"Are you sure you want to restore this list? It will overwrite your current subscriptions.",
|
||||
{ confirmLabel: "Restore", danger: true },
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -313,11 +315,11 @@ export class SubscriptionHistoryController {
|
||||
|
||||
try {
|
||||
await subscriptions.restoreBackup(this.selectedEvent, this.activePubkey);
|
||||
// alert("Subscriptions restored successfully!"); // alert blocks, toast is better if available, but alert is fine for now
|
||||
notifySuccess("Subscriptions restored.");
|
||||
this.hide();
|
||||
} catch (error) {
|
||||
userLogger.error("Restore failed:", error);
|
||||
alert("Failed to restore: " + error.message);
|
||||
notifyError("Failed to restore: " + error.message);
|
||||
} finally {
|
||||
this.restoreBtn.disabled = false;
|
||||
this.restoreBtn.textContent = "Restore as Active";
|
||||
@@ -338,7 +340,7 @@ export class SubscriptionHistoryController {
|
||||
this.fetchHistory();
|
||||
} catch (error) {
|
||||
userLogger.error("Rebroadcast failed:", error);
|
||||
alert("Failed to rebroadcast: " + error.message);
|
||||
notifyError("Failed to rebroadcast: " + error.message);
|
||||
} finally {
|
||||
this.rebroadcastBtn.disabled = false;
|
||||
this.rebroadcastBtn.textContent = "Rebroadcast";
|
||||
@@ -364,7 +366,7 @@ export class SubscriptionHistoryController {
|
||||
}
|
||||
} catch (error) {
|
||||
userLogger.error("Backup failed:", error);
|
||||
alert("Backup failed: " + error.message);
|
||||
notifyError("Backup failed: " + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// showConfirm() replaces native window.confirm with a styled, promise-based dialog so
|
||||
// all confirmations live in the app's UI system. It must resolve true on Confirm, false
|
||||
// on Cancel/backdrop/Escape, clean up its DOM, and degrade gracefully without a DOM.
|
||||
|
||||
import test, { beforeEach, afterEach } from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { showConfirm } from "../js/ui/confirmDialog.js";
|
||||
|
||||
let dom;
|
||||
beforeEach(() => {
|
||||
dom = new JSDOM("<!doctype html><html><body></body></html>");
|
||||
globalThis.document = dom.window.document;
|
||||
globalThis.window = dom.window;
|
||||
globalThis.HTMLElement = dom.window.HTMLElement;
|
||||
globalThis.KeyboardEvent = dom.window.KeyboardEvent;
|
||||
});
|
||||
afterEach(() => {
|
||||
delete globalThis.document;
|
||||
delete globalThis.window;
|
||||
delete globalThis.HTMLElement;
|
||||
delete globalThis.KeyboardEvent;
|
||||
});
|
||||
|
||||
function overlay() {
|
||||
return dom.window.document.querySelector(".bv-modal");
|
||||
}
|
||||
|
||||
test("renders a dialog with the message and resolves TRUE when Confirm is clicked", async () => {
|
||||
const p = showConfirm("Delete this thing?", { confirmLabel: "Delete", danger: true });
|
||||
const modal = overlay();
|
||||
assert.ok(modal, "an overlay was added to the DOM");
|
||||
assert.match(modal.textContent, /Delete this thing\?/);
|
||||
modal.querySelector(".btn").click(); // confirm button (cancel is .btn-ghost)
|
||||
assert.equal(await p, true);
|
||||
assert.equal(overlay(), null, "overlay removed after resolving");
|
||||
});
|
||||
|
||||
test("resolves FALSE when Cancel is clicked", async () => {
|
||||
const p = showConfirm("Sure?");
|
||||
overlay().querySelector(".btn-ghost").click();
|
||||
assert.equal(await p, false);
|
||||
assert.equal(overlay(), null);
|
||||
});
|
||||
|
||||
test("resolves FALSE when the backdrop is clicked", async () => {
|
||||
const p = showConfirm("Sure?");
|
||||
overlay().querySelector(".bv-modal-backdrop").click();
|
||||
assert.equal(await p, false);
|
||||
});
|
||||
|
||||
test("resolves FALSE on Escape", async () => {
|
||||
const p = showConfirm("Sure?");
|
||||
dom.window.document.dispatchEvent(
|
||||
new dom.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
||||
);
|
||||
assert.equal(await p, false);
|
||||
assert.equal(overlay(), null);
|
||||
});
|
||||
|
||||
test("a danger prompt focuses Cancel (the safe action) by default", async () => {
|
||||
const p = showConfirm("Delete?", { danger: true });
|
||||
assert.equal(
|
||||
dom.window.document.activeElement,
|
||||
overlay().querySelector(".btn-ghost"),
|
||||
);
|
||||
overlay().querySelector(".btn-ghost").click();
|
||||
await p;
|
||||
});
|
||||
|
||||
test("without a DOM it declines rather than throwing", async () => {
|
||||
delete globalThis.document;
|
||||
delete globalThis.window;
|
||||
assert.equal(await showConfirm("anything"), false);
|
||||
});
|
||||
@@ -504,6 +504,25 @@ toward freshness and looked identical. Gave each a structural identity:
|
||||
- [ ] Consider removing the CDN/WebTorrent source badge from the card (clutter).
|
||||
- [ ] Run `npm run test:visual` after layout changes; update baselines deliberately.
|
||||
|
||||
### 41. Native browser dialogs → site notification system
|
||||
- [x] **DONE 2026-06-30.** Replaced every `alert()`/`confirm()` (no `prompt()` existed)
|
||||
with the app's notification system so dialogs are styled + consistent.
|
||||
- `alert()` (13) → toasts: `this.showError` where the component already had it
|
||||
(UploadModal), else `notifyError`/`notifySuccess` via a new `js/ui/appNotify.js`
|
||||
(routes to the app's NotificationController).
|
||||
- `confirm()` (~13) → a new promise-based **`showConfirm()`** dialog
|
||||
(`js/ui/confirmDialog.js`) — a `bv-modal modal-always-on-top` overlay returning
|
||||
`Promise<boolean>` (Confirm/Cancel/backdrop/Escape, danger variant focuses
|
||||
Cancel). Importable anywhere, so future confirmations use it too. Stacks above
|
||||
other modals and is typable thanks to the stacked-modal focus fix.
|
||||
- Migrated: UploadModal, MyVideosController, ProfileStorage/Wallet/Relay
|
||||
controllers, profileModalController, subscriptionHistoryController,
|
||||
settingsRestorePrompt (default `confirm` now `showConfirm`), nostr/client
|
||||
(deleteAllVersions). `confirmSyncOverwrite` helpers made async (the sync layer
|
||||
already `await`s `confirmOverwrite`). Tests: `tests/confirm-dialog.test.mjs` (6).
|
||||
Build + lint clean; affected suites green. NOTE: `npm run build`/`lint` do NOT
|
||||
parse JS — verify migrations by importing the module / running its test.
|
||||
|
||||
### 8. Orphan storage garbage-collection tool
|
||||
- [x] **Largely delivered by the My Videos tab** (`9d3a0df0`): lists bucket objects no
|
||||
live note references and offers per-file delete (under the user's prefix only).
|
||||
|
||||
Reference in new issue
Block a user