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 { isDevMode } from "../config.js";
|
||||||
|
import { showConfirm } from "../ui/confirmDialog.js";
|
||||||
import { infoHashFromMagnet } from "../magnets.js";
|
import { infoHashFromMagnet } from "../magnets.js";
|
||||||
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
|
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
|
||||||
import {
|
import {
|
||||||
@@ -3465,9 +3466,10 @@ export class NostrClient {
|
|||||||
options && typeof options.video === "object" ? options.video : null;
|
options && typeof options.video === "object" ? options.video : null;
|
||||||
let confirmed = true;
|
let confirmed = true;
|
||||||
|
|
||||||
if (shouldConfirm && typeof window?.confirm === "function") {
|
if (shouldConfirm) {
|
||||||
confirmed = window.confirm(
|
confirmed = await showConfirm(
|
||||||
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
"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.
|
// if the user accepts.
|
||||||
|
|
||||||
import { storageSyncService as defaultStorageSync } from "./storageSyncService.js";
|
import { storageSyncService as defaultStorageSync } from "./storageSyncService.js";
|
||||||
|
import { showConfirm } from "../ui/confirmDialog.js";
|
||||||
|
|
||||||
const OFFERED_KEY = "bitvid:settings-sync:offered:v1";
|
const OFFERED_KEY = "bitvid:settings-sync:offered:v1";
|
||||||
|
|
||||||
@@ -63,10 +64,7 @@ const ITEM_LABELS = {
|
|||||||
export function createSettingsRestorePrompt({
|
export function createSettingsRestorePrompt({
|
||||||
storageSync = defaultStorageSync,
|
storageSync = defaultStorageSync,
|
||||||
walletSync = null,
|
walletSync = null,
|
||||||
confirm = (message) =>
|
confirm = (message) => showConfirm(message, { confirmLabel: "Restore" }),
|
||||||
typeof window !== "undefined" && typeof window.confirm === "function"
|
|
||||||
? window.confirm(message)
|
|
||||||
: false,
|
|
||||||
logger = null,
|
logger = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
// Items not already enabled / not already offered on THIS device that have a
|
// 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 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. ` +
|
`Found an encrypted copy of your ${list} on your Nostr account. ` +
|
||||||
`Restore ${candidates.length > 1 ? "them" : "it"} to this device?`
|
`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,
|
requestDefaultExtensionPermissions,
|
||||||
} from "../../nostrClientFacade.js";
|
} from "../../nostrClientFacade.js";
|
||||||
import { UI_FEEDBACK_DELAY_MS } from "../../constants.js";
|
import { UI_FEEDBACK_DELAY_MS } from "../../constants.js";
|
||||||
|
import { showConfirm } from "../confirmDialog.js";
|
||||||
|
|
||||||
const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
|
const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
|
||||||
|
|
||||||
@@ -518,12 +519,12 @@ export class UploadModal {
|
|||||||
void this.captureVideoMetadata(file);
|
void this.captureVideoMetadata(file);
|
||||||
|
|
||||||
if (!this.storageConfigured) {
|
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
|
e.target.value = ""; // Clear selection
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.isStorageUnlocked) {
|
if (!this.isStorageUnlocked) {
|
||||||
alert("Please unlock storage before selecting a file.");
|
this.showError("Please unlock storage before selecting a file.");
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -614,7 +615,7 @@ export class UploadModal {
|
|||||||
if (this.results.magnet) this.results.magnet.value = "Upload Failed";
|
if (this.results.magnet) this.results.magnet.value = "Upload Failed";
|
||||||
if (this.results.torrentUrl) this.results.torrentUrl.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
|
if (this.inputs.file) this.inputs.file.value = ""; // Reset
|
||||||
}
|
}
|
||||||
@@ -676,7 +677,7 @@ export class UploadModal {
|
|||||||
userLogger.error("Thumbnail upload failed:", err);
|
userLogger.error("Thumbnail upload failed:", err);
|
||||||
this.thumbnailUploadState.status = 'error';
|
this.thumbnailUploadState.status = 'error';
|
||||||
this.updateThumbnailProgress(null, "Failed.");
|
this.updateThumbnailProgress(null, "Failed.");
|
||||||
alert("Thumbnail upload failed.");
|
this.showError("Thumbnail upload failed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,7 +825,7 @@ export class UploadModal {
|
|||||||
if (this.promptStoredNsecUnlock(pubkey)) {
|
if (this.promptStoredNsecUnlock(pubkey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alert("No signer available to unlock storage.");
|
this.showError("No signer available to unlock storage.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,7 +833,7 @@ export class UploadModal {
|
|||||||
if (signer?.type === "extension" || signer?.type === "nip07") {
|
if (signer?.type === "extension" || signer?.type === "nip07") {
|
||||||
const permissionResult = await requestDefaultExtensionPermissions();
|
const permissionResult = await requestDefaultExtensionPermissions();
|
||||||
if (!permissionResult?.ok) {
|
if (!permissionResult?.ok) {
|
||||||
alert("Extension permissions are required to unlock storage.");
|
this.showError("Extension permissions are required to unlock storage.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -846,7 +847,7 @@ export class UploadModal {
|
|||||||
await this.loadFromStorage();
|
await this.loadFromStorage();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
userLogger.error("Unlock failed", err);
|
userLogger.error("Unlock failed", err);
|
||||||
alert("Failed to unlock storage: " + err.message);
|
this.showError("Failed to unlock storage: " + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
if (this.toggles.storageUnlock) {
|
if (this.toggles.storageUnlock) {
|
||||||
this.toggles.storageUnlock.textContent = "Unlock";
|
this.toggles.storageUnlock.textContent = "Unlock";
|
||||||
@@ -945,17 +946,17 @@ export class UploadModal {
|
|||||||
// Check upload state
|
// Check upload state
|
||||||
if (this.activeSource === "upload") {
|
if (this.activeSource === "upload") {
|
||||||
if (this.videoUploadState.status === 'uploading' || this.thumbnailUploadState.status === 'uploading') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (this.videoUploadState.status === 'error') {
|
if (this.videoUploadState.status === 'error') {
|
||||||
alert("Video upload failed. Please try again.");
|
this.showError("Video upload failed. Please try again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.videoUploadState.status !== 'complete') {
|
if (this.videoUploadState.status !== 'complete') {
|
||||||
// Fallback: If no file selected, maybe they want to submit without a new file?
|
// Fallback: If no file selected, maybe they want to submit without a new file?
|
||||||
// (Not supported in this simplified modal, assume file required)
|
// (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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1104,7 +1105,7 @@ export class UploadModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasUrl && !hasImeta && hasMagnet) {
|
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);
|
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 { reconcileStorage } from "./myVideosReconcile.js";
|
||||||
import { FEATURE_NIP71_MIRROR } from "../../constants.js";
|
import { FEATURE_NIP71_MIRROR } from "../../constants.js";
|
||||||
import { nip71MirrorService } from "../../services/nip71MirrorService.js";
|
import { nip71MirrorService } from "../../services/nip71MirrorService.js";
|
||||||
|
import { showConfirm } from "../confirmDialog.js";
|
||||||
import {
|
import {
|
||||||
isMirrorEnabled,
|
isMirrorEnabled,
|
||||||
setMirrorEnabled,
|
setMirrorEnabled,
|
||||||
@@ -282,10 +283,13 @@ export class MyVideosController {
|
|||||||
if (!r2Service || typeof r2Service.deleteStorageKeys !== "function") {
|
if (!r2Service || typeof r2Service.deleteStorageKeys !== "function") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
if (
|
||||||
if (!window.confirm(`Permanently delete this file from your bucket?\n\n${key}`)) {
|
!(await showConfirm(`Permanently delete this file from your bucket?\n\n${key}`, {
|
||||||
return;
|
confirmLabel: "Delete",
|
||||||
}
|
danger: true,
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
@@ -314,14 +318,13 @@ export class MyVideosController {
|
|||||||
if (!r2Service || typeof r2Service.deleteStorageKeys !== "function" || !keys.length) {
|
if (!r2Service || typeof r2Service.deleteStorageKeys !== "function" || !keys.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
if (
|
||||||
if (
|
!(await showConfirm(
|
||||||
!window.confirm(
|
`Permanently delete all ${keys.length} orphaned files from your bucket?`,
|
||||||
`Permanently delete all ${keys.length} orphaned files from your bucket?`,
|
{ confirmLabel: "Delete all", danger: true },
|
||||||
)
|
))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this.orphanDeleteAllBtn instanceof HTMLElement) {
|
if (this.orphanDeleteAllBtn instanceof HTMLElement) {
|
||||||
this.orphanDeleteAllBtn.disabled = true;
|
this.orphanDeleteAllBtn.disabled = true;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||||
|
import { showConfirm } from "../confirmDialog.js";
|
||||||
import { sanitizeRelayList } from "../../nostr/nip46Client.js";
|
import { sanitizeRelayList } from "../../nostr/nip46Client.js";
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -556,7 +557,7 @@ export class ProfileRelayController {
|
|||||||
reason: null,
|
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;
|
context.confirmed = confirmed;
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
context.reason = "cancelled";
|
context.reason = "cancelled";
|
||||||
@@ -604,9 +605,10 @@ export class ProfileRelayController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = window.confirm(
|
const confirmed = await showConfirm(`Remove ${url} from your relay list?`, {
|
||||||
`Remove ${url} from your relay list?`,
|
confirmLabel: "Remove",
|
||||||
);
|
danger: true,
|
||||||
|
});
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getActiveSigner } from "../../nostr/client.js";
|
|||||||
import { DEFAULT_NIP07_ENCRYPTION_METHODS } from "../../nostr/nip07Permissions.js";
|
import { DEFAULT_NIP07_ENCRYPTION_METHODS } from "../../nostr/nip07Permissions.js";
|
||||||
import { storageSyncService } from "../../services/storageSyncService.js";
|
import { storageSyncService } from "../../services/storageSyncService.js";
|
||||||
import { StorageCorsHelp } from "./storageCorsHelp.js";
|
import { StorageCorsHelp } from "./storageCorsHelp.js";
|
||||||
|
import { showConfirm } from "../confirmDialog.js";
|
||||||
import {
|
import {
|
||||||
fillStorageForm,
|
fillStorageForm,
|
||||||
clearCredentialFields,
|
clearCredentialFields,
|
||||||
@@ -241,12 +242,10 @@ export class ProfileStorageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirmSyncOverwrite() {
|
confirmSyncOverwrite() {
|
||||||
if (typeof window === "undefined" || typeof window.confirm !== "function") {
|
return showConfirm(
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return window.confirm(
|
|
||||||
"A newer copy of your storage settings is on your account (changed on " +
|
"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 { devLogger } from "../../utils/logger.js";
|
||||||
|
import { showConfirm } from "../confirmDialog.js";
|
||||||
import { NWC_URI_SCHEME } from "../profileModalContract.js";
|
import { NWC_URI_SCHEME } from "../profileModalContract.js";
|
||||||
import { createWalletSyncService } from "../../services/walletSyncService.js";
|
import { createWalletSyncService } from "../../services/walletSyncService.js";
|
||||||
import { runWalletZappabilityCheck } from "./walletZappabilityCheck.js";
|
import { runWalletZappabilityCheck } from "./walletZappabilityCheck.js";
|
||||||
@@ -104,12 +105,10 @@ export class ProfileWalletController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirmSyncOverwrite() {
|
confirmSyncOverwrite() {
|
||||||
if (typeof window === "undefined" || typeof window.confirm !== "function") {
|
return showConfirm(
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return window.confirm(
|
|
||||||
"A newer copy of your wallet connection is on your account (changed on " +
|
"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 {
|
try {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
// Spending capability — require explicit confirmation before publishing.
|
// Spending capability — require explicit confirmation before publishing.
|
||||||
const confirmed =
|
const confirmed = await showConfirm(
|
||||||
typeof window !== "undefined" && typeof window.confirm === "function"
|
"This publishes an ENCRYPTED copy of your wallet connection to public relays.\n\n" +
|
||||||
? window.confirm(
|
"A wallet connect URI can SPEND from your wallet. Anyone who controls your " +
|
||||||
"This publishes an ENCRYPTED copy of your wallet connection to public relays.\n\n" +
|
"Nostr key could decrypt this and spend from this wallet.\n\nContinue?",
|
||||||
"A wallet connect URI can SPEND from your wallet. Anyone who controls your " +
|
{ confirmLabel: "Publish", danger: true },
|
||||||
"Nostr key could decrypt this and spend from this wallet.\n\nContinue?"
|
);
|
||||||
)
|
|
||||||
: true;
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
if (this.walletSyncToggle instanceof HTMLInputElement) {
|
if (this.walletSyncToggle instanceof HTMLInputElement) {
|
||||||
this.walletSyncToggle.checked = false;
|
this.walletSyncToggle.checked = false;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
MAX_WALLET_DEFAULT_ZAP as CONFIG_MAX_WALLET_DEFAULT_ZAP,
|
MAX_WALLET_DEFAULT_ZAP as CONFIG_MAX_WALLET_DEFAULT_ZAP,
|
||||||
ENABLE_NIP17_RELAY_WARNING as CONFIG_ENABLE_NIP17_RELAY_WARNING,
|
ENABLE_NIP17_RELAY_WARNING as CONFIG_ENABLE_NIP17_RELAY_WARNING,
|
||||||
} from "../config.js";
|
} from "../config.js";
|
||||||
|
import { showConfirm } from "./confirmDialog.js";
|
||||||
import { normalizeDesignSystemContext } from "../designSystem.js";
|
import { normalizeDesignSystemContext } from "../designSystem.js";
|
||||||
import { RUNTIME_FLAGS } from "../constants.js";
|
import { RUNTIME_FLAGS } from "../constants.js";
|
||||||
import {
|
import {
|
||||||
@@ -1926,14 +1927,17 @@ export class ProfileModalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.profileSubscriptionsBackupBtn) {
|
if (this.profileSubscriptionsBackupBtn) {
|
||||||
this.profileSubscriptionsBackupBtn.addEventListener("click", () => {
|
this.profileSubscriptionsBackupBtn.addEventListener("click", async () => {
|
||||||
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
const pubkey = this.normalizeHexPubkey(this.getActivePubkey());
|
||||||
if (pubkey) {
|
if (
|
||||||
if (confirm("Create a backup of your current subscription list?")) {
|
pubkey &&
|
||||||
|
(await showConfirm("Create a backup of your current subscription list?", {
|
||||||
|
confirmLabel: "Create backup",
|
||||||
|
}))
|
||||||
|
) {
|
||||||
this.subscriptionHistoryController.handleCreateBackup(pubkey).then(() => {
|
this.subscriptionHistoryController.handleCreateBackup(pubkey).then(() => {
|
||||||
this.showSuccess("Backup created.");
|
this.showSuccess("Backup created.");
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.showError("Please log in to backup subscriptions.");
|
this.showError("Please log in to backup subscriptions.");
|
||||||
}
|
}
|
||||||
@@ -2930,7 +2934,7 @@ export class ProfileModalController {
|
|||||||
? confirmValue.trim()
|
? confirmValue.trim()
|
||||||
: "this entry";
|
: "this entry";
|
||||||
const prompt = confirmMessage.replace("{npub}", replacement);
|
const prompt = confirmMessage.replace("{npub}", replacement);
|
||||||
if (!window.confirm(prompt)) {
|
if (!(await showConfirm(prompt, { danger: true }))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { profileCache } from "../state/profileCache.js";
|
|||||||
import { formatTimeAgo } from "../utils/formatters.js";
|
import { formatTimeAgo } from "../utils/formatters.js";
|
||||||
import { userLogger } from "../utils/logger.js";
|
import { userLogger } from "../utils/logger.js";
|
||||||
import { getApplication } from "../applicationContext.js";
|
import { getApplication } from "../applicationContext.js";
|
||||||
|
import { notifyError, notifySuccess, showConfirm } from "./appNotify.js";
|
||||||
|
|
||||||
export class SubscriptionHistoryController {
|
export class SubscriptionHistoryController {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
@@ -302,9 +303,10 @@ export class SubscriptionHistoryController {
|
|||||||
async handleRestore() {
|
async handleRestore() {
|
||||||
if (!this.selectedEvent) return;
|
if (!this.selectedEvent) return;
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!(await showConfirm(
|
||||||
"Are you sure you want to restore this list? It will overwrite your current subscriptions."
|
"Are you sure you want to restore this list? It will overwrite your current subscriptions.",
|
||||||
)
|
{ confirmLabel: "Restore", danger: true },
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -313,11 +315,11 @@ export class SubscriptionHistoryController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await subscriptions.restoreBackup(this.selectedEvent, this.activePubkey);
|
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();
|
this.hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
userLogger.error("Restore failed:", error);
|
userLogger.error("Restore failed:", error);
|
||||||
alert("Failed to restore: " + error.message);
|
notifyError("Failed to restore: " + error.message);
|
||||||
} finally {
|
} finally {
|
||||||
this.restoreBtn.disabled = false;
|
this.restoreBtn.disabled = false;
|
||||||
this.restoreBtn.textContent = "Restore as Active";
|
this.restoreBtn.textContent = "Restore as Active";
|
||||||
@@ -338,7 +340,7 @@ export class SubscriptionHistoryController {
|
|||||||
this.fetchHistory();
|
this.fetchHistory();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
userLogger.error("Rebroadcast failed:", error);
|
userLogger.error("Rebroadcast failed:", error);
|
||||||
alert("Failed to rebroadcast: " + error.message);
|
notifyError("Failed to rebroadcast: " + error.message);
|
||||||
} finally {
|
} finally {
|
||||||
this.rebroadcastBtn.disabled = false;
|
this.rebroadcastBtn.disabled = false;
|
||||||
this.rebroadcastBtn.textContent = "Rebroadcast";
|
this.rebroadcastBtn.textContent = "Rebroadcast";
|
||||||
@@ -364,7 +366,7 @@ export class SubscriptionHistoryController {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
userLogger.error("Backup failed:", 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).
|
- [ ] Consider removing the CDN/WebTorrent source badge from the card (clutter).
|
||||||
- [ ] Run `npm run test:visual` after layout changes; update baselines deliberately.
|
- [ ] 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
|
### 8. Orphan storage garbage-collection tool
|
||||||
- [x] **Largely delivered by the My Videos tab** (`9d3a0df0`): lists bucket objects no
|
- [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).
|
live note references and offers per-file delete (under the user's prefix only).
|
||||||
|
|||||||
Reference in new issue
Block a user