Merge pull request #336 from PR0M3TH3AN/codex/refactor-profilemodalcontroller-event-handling

Refactor profile modal event handling to use controller
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-07 10:26:33 -04:00
commit 19bcc8ca6b
2 files changed
+448 -470

No files matched your search

+79 -363
View File
@@ -1531,18 +1531,11 @@ class Application {
*/
async initProfileModal() {
try {
console.log("Starting profile modal initialization...");
const resp = await fetch("components/profile-modal.html");
if (!resp.ok) {
// If you don't have a profile modal, comment this entire method out.
throw new Error(`HTTP error! status: ${resp.status}`);
}
const html = await resp.text();
const modalContainer = document.getElementById("modalContainer");
if (!modalContainer) {
throw new Error("Modal container element not found!");
}
if (!this.profileModalController) {
this.profileModalController = new ProfileModalController({
modalContainer,
@@ -1556,376 +1549,99 @@ class Application {
state: this.getProfileModalStateBridge(),
});
}
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
removeTrackingScripts(wrapper);
modalContainer.appendChild(wrapper);
// Now references
this.profileModal = document.getElementById("profileModal") || null;
this.closeProfileModal =
document.getElementById("closeProfileModal") || null;
this.profileLogoutBtn =
document.getElementById("profileLogoutBtn") || null;
this.profileModalAvatar =
document.getElementById("profileModalAvatar") || null;
this.profileModalName =
document.getElementById("profileModalName") || null;
this.profileModalNpub =
document.getElementById("profileModalNpub") || null;
this.profileChannelLink =
document.getElementById("profileChannelLink") || null;
this.profileSwitcherList =
document.getElementById("profileSwitcherList") || null;
this.profileAddAccountBtn =
document.getElementById("profileAddAccountBtn") || null;
const topLevelProfileAvatar =
document.getElementById("profileAvatar") || null;
if (topLevelProfileAvatar) {
this.profileAvatar = topLevelProfileAvatar;
}
this.profileNavButtons.account =
document.getElementById("profileNavAccount") || null;
this.profileNavButtons.relays =
document.getElementById("profileNavRelays") || null;
this.profileNavButtons.wallet =
document.getElementById("profileNavWallet") || null;
this.profileNavButtons.blocked =
document.getElementById("profileNavBlocked") || null;
this.profileNavButtons.history =
document.getElementById("profileNavHistory") || null;
this.profileNavButtons.admin =
document.getElementById("profileNavAdmin") || null;
this.profilePaneElements.account =
document.getElementById("profilePaneAccount") || null;
this.profilePaneElements.relays =
document.getElementById("profilePaneRelays") || null;
this.profilePaneElements.wallet =
document.getElementById("profilePaneWallet") || null;
this.profilePaneElements.blocked =
document.getElementById("profilePaneBlocked") || null;
this.profilePaneElements.history =
document.getElementById("profilePaneHistory") || null;
this.profilePaneElements.admin =
document.getElementById("profilePaneAdmin") || null;
this.profileRelayList = document.getElementById("relayList") || null;
this.profileBlockedList = document.getElementById("blockedList") || null;
this.profileBlockedEmpty =
document.getElementById("blockedEmpty") || null;
this.profileBlockedInput =
document.getElementById("blockedInput") || null;
this.profileAddBlockedBtn =
document.getElementById("addBlockedBtn") || null;
this.profileRelayInput = document.getElementById("relayInput") || null;
this.profileAddRelayBtn = document.getElementById("addRelayBtn") || null;
this.profileRestoreRelaysBtn =
document.getElementById("restoreRelaysBtn") || null;
this.profileWalletUriInput =
document.getElementById("profileWalletUri") || null;
this.profileWalletDefaultZapInput =
document.getElementById("profileWalletDefaultZap") || null;
this.profileWalletSaveButton =
document.getElementById("profileWalletSave") || null;
this.profileWalletTestButton =
document.getElementById("profileWalletTest") || null;
this.profileWalletDisconnectButton =
document.getElementById("profileWalletDisconnect") || null;
this.profileWalletStatusText =
document.getElementById("profileWalletStatus") || null;
if (!this.profileHistoryRenderer) {
this.profileHistoryRenderer = createWatchHistoryRenderer({
viewSelector: "#profilePaneHistory",
gridSelector: "#profileHistoryGrid",
loadingSelector: "#profileHistoryLoading",
statusSelector: "#profileHistoryStatus",
emptySelector: "#profileHistoryEmpty",
sentinelSelector: "#profileHistorySentinel",
scrollContainerSelector: "#profileHistoryScroll",
errorBannerSelector: "#profileHistoryError",
clearButtonSelector: "#profileHistoryClear",
republishButtonSelector: "#profileHistoryRepublish",
featureBannerSelector: "#profileHistoryFeatureBanner",
toastRegionSelector: "#profileHistoryToastRegion",
sessionWarningSelector: "#profileHistorySessionWarning",
metadataToggleSelector: "#profileHistoryMetadataToggle",
metadataThumbSelector: "#profileHistoryMetadataThumb",
metadataLabelSelector: "#profileHistoryMetadataLabel",
metadataDescriptionSelector: "#profileHistoryMetadataDescription",
emptyCopy: "You haven’t watched any videos yet.",
remove: (payload) => this.handleWatchHistoryRemoval(payload),
getActor: async () => {
if (this.pubkey) {
return this.pubkey;
}
if (
typeof nostrClient?.sessionActor?.pubkey === "string" &&
nostrClient.sessionActor.pubkey
) {
return nostrClient.sessionActor.pubkey;
}
return this.pubkey || undefined;
},
});
}
this.adminModeratorsSection =
document.getElementById("adminModeratorsSection") || null;
this.adminModeratorsEmpty =
document.getElementById("adminModeratorsEmpty") || null;
this.adminModeratorList =
document.getElementById("adminModeratorList") || null;
this.adminModeratorInput =
document.getElementById("adminModeratorInput") || null;
this.adminAddModeratorBtn =
document.getElementById("adminAddModeratorBtn") || null;
this.adminWhitelistSection =
document.getElementById("adminWhitelistSection") || null;
this.adminWhitelistEmpty =
document.getElementById("adminWhitelistEmpty") || null;
this.adminWhitelistList =
document.getElementById("adminWhitelistList") || null;
this.adminWhitelistInput =
document.getElementById("adminWhitelistInput") || null;
this.adminAddWhitelistBtn =
document.getElementById("adminAddWhitelistBtn") || null;
this.adminBlacklistSection =
document.getElementById("adminBlacklistSection") || null;
this.adminBlacklistEmpty =
document.getElementById("adminBlacklistEmpty") || null;
this.adminBlacklistList =
document.getElementById("adminBlacklistList") || null;
this.adminBlacklistInput =
document.getElementById("adminBlacklistInput") || null;
this.adminAddBlacklistBtn =
document.getElementById("adminAddBlacklistBtn") || null;
// Wire up
if (this.closeProfileModal && !this.closeProfileModal.dataset.bound) {
this.closeProfileModal.dataset.bound = "true";
this.closeProfileModal.addEventListener("click", () => {
this.hideProfileModal();
});
}
if (this.profileLogoutBtn && !this.profileLogoutBtn.dataset.bound) {
this.profileLogoutBtn.dataset.bound = "true";
this.profileLogoutBtn.addEventListener("click", async () => {
try {
await this.authService.logout();
} catch (error) {
console.error("Logout failed:", error);
this.showError("Failed to logout. Please try again.");
}
this.hideProfileModal();
});
}
if (this.profileChannelLink && !this.profileChannelLink.dataset.bound) {
this.profileChannelLink.dataset.bound = "true";
this.profileChannelLink.addEventListener("click", (event) => {
event.preventDefault();
const targetNpub = this.profileChannelLink?.dataset?.targetNpub;
if (!targetNpub) {
return;
}
this.hideProfileModal();
window.location.hash = `#view=channel-profile&npub=${targetNpub}`;
});
}
if (
this.profileAddAccountBtn &&
this.profileAddAccountBtn.dataset.bound !== "true"
) {
this.profileAddAccountBtn.dataset.bound = "true";
this.profileAddAccountBtn.addEventListener("click", () => {
this.handleAddProfile();
});
}
Object.entries(this.profileNavButtons).forEach(([name, button]) => {
if (!button || button.dataset.navBound === "true") {
return;
const alreadyLoaded =
this.profileModalController.profileModal instanceof HTMLElement;
if (!alreadyLoaded) {
const loaded = await this.profileModalController.load();
if (!loaded) {
return false;
}
button.dataset.navBound = "true";
button.addEventListener("click", () => {
this.selectProfilePane(name);
});
} else {
this.profileModalController.cacheDomReferences();
}
this.profileModal = this.profileModalController.profileModal;
this.closeProfileModal = this.profileModalController.closeButton;
this.profileLogoutBtn = this.profileModalController.logoutButton;
this.profileChannelLink = this.profileModalController.channelLink;
this.profileModalAvatar = this.profileModalController.profileModalAvatar;
this.profileModalName = this.profileModalController.profileModalName;
this.profileModalNpub = this.profileModalController.profileModalNpub;
this.profileSwitcherList = this.profileModalController.profileSwitcherList;
this.profileAddAccountBtn = this.profileModalController.addAccountButton;
this.profileAvatar = this.profileModalController.profileAvatar;
Object.entries(this.profileNavButtons).forEach(([key]) => {
this.profileNavButtons[key] =
this.profileModalController.navButtons[key] || null;
});
if (this.profileAddRelayBtn && !this.profileAddRelayBtn.dataset.bound) {
this.profileAddRelayBtn.dataset.bound = "true";
this.profileAddRelayBtn.addEventListener("click", () => {
this.handleAddRelay();
});
}
if (
this.profileRestoreRelaysBtn &&
this.profileRestoreRelaysBtn.dataset.bound !== "true"
) {
this.profileRestoreRelaysBtn.dataset.bound = "true";
this.profileRestoreRelaysBtn.addEventListener("click", () => {
this.handleRestoreRelays();
});
}
if (
this.profileRelayInput &&
this.profileRelayInput.dataset.bound !== "true"
) {
this.profileRelayInput.dataset.bound = "true";
this.profileRelayInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
this.handleAddRelay();
}
});
}
Object.entries(this.profilePaneElements).forEach(([key]) => {
this.profilePaneElements[key] =
this.profileModalController.panes[key] || null;
});
if (
this.profileAddBlockedBtn &&
this.profileAddBlockedBtn.dataset.bound !== "true"
) {
this.profileAddBlockedBtn.dataset.bound = "true";
this.profileAddBlockedBtn.addEventListener("click", () => {
this.handleAddBlockedCreator();
});
}
if (
this.profileBlockedInput &&
this.profileBlockedInput.dataset.bound !== "true"
) {
this.profileBlockedInput.dataset.bound = "true";
this.profileBlockedInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
this.handleAddBlockedCreator();
}
});
}
this.profileRelayList = this.profileModalController.profileRelayList;
this.profileRelayInput = this.profileModalController.profileRelayInput;
this.profileAddRelayBtn = this.profileModalController.profileAddRelayBtn;
this.profileRestoreRelaysBtn =
this.profileModalController.profileRestoreRelaysBtn;
if (
this.profileWalletUriInput &&
this.profileWalletUriInput.dataset.bound !== "true"
) {
this.profileWalletUriInput.dataset.bound = "true";
this.profileWalletUriInput.addEventListener("input", () => {
this.applyWalletControlState();
});
}
if (
this.profileWalletDefaultZapInput &&
this.profileWalletDefaultZapInput.dataset.bound !== "true"
) {
this.profileWalletDefaultZapInput.dataset.bound = "true";
this.profileWalletDefaultZapInput.addEventListener("input", () => {
this.applyWalletControlState();
});
}
if (
this.profileWalletSaveButton &&
this.profileWalletSaveButton.dataset.bound !== "true"
) {
this.profileWalletSaveButton.dataset.bound = "true";
this.profileWalletSaveButton.addEventListener("click", () => {
this.handleWalletSave();
});
}
if (
this.profileWalletTestButton &&
this.profileWalletTestButton.dataset.bound !== "true"
) {
this.profileWalletTestButton.dataset.bound = "true";
this.profileWalletTestButton.addEventListener("click", () => {
this.handleWalletTest();
});
}
if (
this.profileWalletDisconnectButton &&
this.profileWalletDisconnectButton.dataset.bound !== "true"
) {
this.profileWalletDisconnectButton.dataset.bound = "true";
this.profileWalletDisconnectButton.addEventListener("click", () => {
this.handleWalletDisconnect();
});
}
this.profileBlockedList = this.profileModalController.profileBlockedList;
this.profileBlockedEmpty = this.profileModalController.profileBlockedEmpty;
this.profileBlockedInput = this.profileModalController.profileBlockedInput;
this.profileAddBlockedBtn = this.profileModalController.profileAddBlockedBtn;
if (
this.adminAddModeratorBtn &&
this.adminAddModeratorBtn.dataset.bound !== "true"
) {
this.adminAddModeratorBtn.dataset.bound = "true";
this.adminAddModeratorBtn.addEventListener("click", () => {
this.handleAddModerator();
});
}
if (
this.adminModeratorInput &&
this.adminModeratorInput.dataset.bound !== "true"
) {
this.adminModeratorInput.dataset.bound = "true";
this.adminModeratorInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
this.handleAddModerator();
}
});
}
this.profileWalletUriInput = this.profileModalController.walletUriInput;
this.profileWalletDefaultZapInput =
this.profileModalController.walletDefaultZapInput;
this.profileWalletSaveButton = this.profileModalController.walletSaveButton;
this.profileWalletTestButton = this.profileModalController.walletTestButton;
this.profileWalletDisconnectButton =
this.profileModalController.walletDisconnectButton;
this.profileWalletStatusText =
this.profileModalController.profileWalletStatusText;
if (
this.adminAddWhitelistBtn &&
this.adminAddWhitelistBtn.dataset.bound !== "true"
) {
this.adminAddWhitelistBtn.dataset.bound = "true";
this.adminAddWhitelistBtn.addEventListener("click", () => {
this.handleAdminListMutation("whitelist", "add");
});
}
if (
this.adminWhitelistInput &&
this.adminWhitelistInput.dataset.bound !== "true"
) {
this.adminWhitelistInput.dataset.bound = "true";
this.adminWhitelistInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
this.handleAdminListMutation("whitelist", "add");
}
});
}
this.profileHistoryRenderer =
this.profileModalController.profileHistoryRenderer;
if (
this.adminAddBlacklistBtn &&
this.adminAddBlacklistBtn.dataset.bound !== "true"
) {
this.adminAddBlacklistBtn.dataset.bound = "true";
this.adminAddBlacklistBtn.addEventListener("click", () => {
this.handleAdminListMutation("blacklist", "add");
});
}
if (
this.adminBlacklistInput &&
this.adminBlacklistInput.dataset.bound !== "true"
) {
this.adminBlacklistInput.dataset.bound = "true";
this.adminBlacklistInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
this.handleAdminListMutation("blacklist", "add");
}
});
}
this.adminModeratorsSection =
this.profileModalController.adminModeratorsSection;
this.adminModeratorsEmpty =
this.profileModalController.adminModeratorsEmpty;
this.adminModeratorList =
this.profileModalController.adminModeratorList;
this.adminModeratorInput =
this.profileModalController.adminModeratorInput;
this.adminAddModeratorBtn =
this.profileModalController.adminAddModeratorButton;
this.selectProfilePane("account");
this.populateProfileRelays();
this.populateBlockedList();
this.refreshWalletPaneState();
await this.refreshAdminPaneState();
this.adminWhitelistSection =
this.profileModalController.adminWhitelistSection;
this.adminWhitelistEmpty =
this.profileModalController.adminWhitelistEmpty;
this.adminWhitelistList =
this.profileModalController.adminWhitelistList;
this.adminWhitelistInput =
this.profileModalController.adminWhitelistInput;
this.adminAddWhitelistBtn =
this.profileModalController.adminAddWhitelistButton;
this.renderSavedProfiles();
this.adminBlacklistSection =
this.profileModalController.adminBlacklistSection;
this.adminBlacklistEmpty =
this.profileModalController.adminBlacklistEmpty;
this.adminBlacklistList =
this.profileModalController.adminBlacklistList;
this.adminBlacklistInput =
this.profileModalController.adminBlacklistInput;
this.adminAddBlacklistBtn =
this.profileModalController.adminAddBlacklistButton;
console.log("Profile modal initialization successful");
return true;
} catch (error) {
console.error("initProfileModal failed:", error);
// Not critical if missing
return false;
}
}
+369 -107
View File
@@ -446,10 +446,17 @@ export class ProfileModalController {
}
const html = await response.text();
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
this.removeTrackingScripts(wrapper);
this.modalContainer.appendChild(wrapper);
const template = document.createElement("template");
template.innerHTML = html;
this.removeTrackingScripts(template.content);
const modalRoot = template.content.querySelector("#profileModal");
if (!(modalRoot instanceof HTMLElement)) {
throw new Error("profile modal markup missing expected #profileModal root");
}
this.modalContainer.appendChild(template.content);
this.profileModal = modalRoot;
this.cacheDomReferences();
this.registerEventListeners();
@@ -1166,38 +1173,55 @@ export class ProfileModalController {
skipPublishIfUnchanged = true,
unchangedMessage = null,
} = {}) {
const operationContext = {
ok: false,
changed: false,
reason: null,
error: null,
publishResult: null,
operationResult: null,
};
const activePubkey = this.normalizeHexPubkey(this.getActivePubkey());
if (!activePubkey) {
this.showError("Please login to manage your relays.");
return;
operationContext.reason = "no-active-pubkey";
return operationContext;
}
if (typeof operation !== "function") {
return;
operationContext.reason = "invalid-operation";
return operationContext;
}
const previous = this.services.relayManager.snapshot();
let result;
try {
result = operation();
operationContext.operationResult = result;
} catch (error) {
const message =
error && typeof error.message === "string" && error.message.trim()
? error.message.trim()
: "Failed to update relay preferences.";
operationContext.reason = "operation-error";
operationContext.error = error;
this.showError(message);
return;
return operationContext;
}
const changed = !!result?.changed;
operationContext.changed = changed;
if (!changed && skipPublishIfUnchanged) {
const reason = result?.reason || "unchanged";
operationContext.reason = reason;
if (result?.reason === "duplicate") {
this.showSuccess("Relay is already configured.");
} else if (typeof unchangedMessage === "string" && unchangedMessage) {
this.showSuccess(unchangedMessage);
}
this.populateProfileRelays();
return;
return operationContext;
}
this.populateProfileRelays();
@@ -1209,9 +1233,12 @@ export class ProfileModalController {
if (!publishResult?.ok) {
throw new Error("No relays accepted the update.");
}
operationContext.ok = true;
operationContext.publishResult = publishResult;
if (successMessage) {
this.showSuccess(successMessage);
}
return operationContext;
} catch (error) {
this.services.relayManager.setEntries(previous, { allowEmpty: false });
this.populateProfileRelays();
@@ -1219,28 +1246,37 @@ export class ProfileModalController {
error && typeof error.message === "string" && error.message.trim()
? error.message.trim()
: "Failed to publish relay configuration. Please try again.";
operationContext.reason = "publish-failed";
operationContext.error = error;
this.showError(message);
return operationContext;
}
}
async handleAddRelay() {
const activePubkey = this.normalizeHexPubkey(this.getActivePubkey());
if (!activePubkey) {
this.showError("Please login to manage your relays.");
return;
}
const rawValue =
typeof this.profileRelayInput?.value === "string"
? this.profileRelayInput.value
: "";
const trimmed = rawValue.trim();
const context = {
input: this.profileRelayInput,
rawValue,
url: trimmed,
result: null,
success: false,
reason: null,
};
if (!trimmed) {
this.showError("Enter a relay URL to add.");
return;
context.reason = "empty";
this.callbacks.onAddRelay(context, this);
return context;
}
await this.handleRelayOperation(
const operationResult = await this.handleRelayOperation(
() => this.services.relayManager.addRelay(trimmed),
{
successMessage: "Relay saved.",
@@ -1252,22 +1288,31 @@ export class ProfileModalController {
this.profileRelayInput.value = "";
}
this.callbacks.onAddRelay(this.profileRelayInput, this);
context.result = operationResult;
context.success = !!operationResult?.ok;
context.reason = operationResult?.reason || null;
this.callbacks.onAddRelay(context, this);
return context;
}
async handleRestoreRelays() {
const activePubkey = this.normalizeHexPubkey(this.getActivePubkey());
if (!activePubkey) {
this.showError("Please login to manage your relays.");
return;
}
const context = {
confirmed: false,
result: null,
success: false,
reason: null,
};
const confirmed = window.confirm("Restore the recommended relay defaults?");
context.confirmed = confirmed;
if (!confirmed) {
return;
context.reason = "cancelled";
this.callbacks.onRestoreRelays(context, this);
return context;
}
await this.handleRelayOperation(
const operationResult = await this.handleRelayOperation(
() => this.services.relayManager.restoreDefaults(),
{
successMessage: "Relay defaults restored.",
@@ -1275,7 +1320,12 @@ export class ProfileModalController {
},
);
this.callbacks.onRestoreRelays(this);
context.result = operationResult;
context.success = !!operationResult?.ok;
context.reason = operationResult?.reason || null;
this.callbacks.onRestoreRelays(context, this);
return context;
}
async handleRelayModeToggle(url) {
@@ -1428,22 +1478,38 @@ export class ProfileModalController {
}
async handleAddBlockedCreator() {
if (!this.profileBlockedInput) {
return;
}
const input = this.profileBlockedInput || null;
const rawValue = typeof input?.value === "string" ? input.value : "";
const trimmed = rawValue.trim();
const rawValue = this.profileBlockedInput.value;
const trimmed = typeof rawValue === "string" ? rawValue.trim() : "";
const context = {
input,
rawValue,
value: trimmed,
success: false,
reason: null,
error: null,
};
if (!input) {
context.reason = "missing-input";
this.callbacks.onAddBlocked(context, this);
return context;
}
if (!trimmed) {
this.showError("Enter an npub to block.");
return;
context.reason = "empty";
this.callbacks.onAddBlocked(context, this);
return context;
}
const activePubkey = this.normalizeHexPubkey(this.getActivePubkey());
if (!activePubkey) {
this.showError("Please login to manage your block list.");
return;
context.reason = "no-active-pubkey";
this.callbacks.onAddBlocked(context, this);
return context;
}
const actorHex = activePubkey;
@@ -1453,30 +1519,41 @@ export class ProfileModalController {
targetHex = this.safeDecodeNpub(trimmed) || "";
if (!targetHex) {
this.showError("Invalid npub. Please double-check and try again.");
return;
context.reason = "invalid-npub";
this.callbacks.onAddBlocked(context, this);
return context;
}
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
targetHex = trimmed.toLowerCase();
} else {
this.showError("Enter a valid npub or hex pubkey.");
return;
context.reason = "invalid-value";
this.callbacks.onAddBlocked(context, this);
return context;
}
if (targetHex === actorHex) {
this.showError("You cannot block yourself.");
return;
context.reason = "self";
this.callbacks.onAddBlocked(context, this);
return context;
}
context.targetHex = targetHex;
try {
await this.services.userBlocks.ensureLoaded(actorHex);
if (this.services.userBlocks.isBlocked(targetHex)) {
this.showSuccess("You already blocked this creator.");
context.reason = "already-blocked";
} else {
await this.services.userBlocks.addBlock(targetHex, actorHex);
this.showSuccess(
"Creator blocked. You won't see their videos anymore.",
);
context.success = true;
context.reason = "blocked";
}
this.profileBlockedInput.value = "";
@@ -1484,6 +1561,8 @@ export class ProfileModalController {
await this.services.loadVideos();
} catch (error) {
console.error("Failed to add creator to personal block list:", error);
context.error = error;
context.reason = error?.code || "service-error";
const message =
error?.code === "nip04-missing"
? "Your Nostr extension must support NIP-04 to manage private lists."
@@ -1491,7 +1570,8 @@ export class ProfileModalController {
this.showError(message);
}
this.callbacks.onAddBlocked(this.profileBlockedInput, this);
this.callbacks.onAddBlocked(context, this);
return context;
}
async handleRemoveBlockedCreator(candidate) {
@@ -1751,28 +1831,47 @@ export class ProfileModalController {
}
async handleWalletSave() {
const { uri, defaultZap, error } = this.getWalletFormValues();
const context = {
uri,
defaultZap: defaultZap ?? null,
sanitizedUri: null,
success: false,
reason: null,
error: error || null,
status: null,
variant: null,
};
if (this.isWalletBusy()) {
return;
context.reason = "busy";
this.callbacks.onWalletSave(context, this);
return context;
}
const { uri, defaultZap, error } = this.getWalletFormValues();
if (error) {
this.updateWalletStatus(error, "error");
this.showError(error);
context.reason = "invalid-default-zap";
if (this.walletDefaultZapInput instanceof HTMLElement) {
this.walletDefaultZapInput.focus();
}
return;
this.callbacks.onWalletSave(context, this);
return context;
}
const { valid, sanitized, message } = this.validateWalletUri(uri);
context.sanitizedUri = sanitized;
if (!valid) {
this.updateWalletStatus(message, "error");
this.showError(message);
context.reason = "invalid-uri";
context.error = message;
if (this.walletUriInput instanceof HTMLElement) {
this.walletUriInput.focus();
}
return;
this.callbacks.onWalletSave(context, this);
return context;
}
const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey());
@@ -1780,7 +1879,10 @@ export class ProfileModalController {
const loginMessage = "Sign in to save wallet settings.";
this.updateWalletStatus(loginMessage, "error");
this.showError(loginMessage);
return;
context.reason = "no-active-pubkey";
context.error = loginMessage;
this.callbacks.onWalletSave(context, this);
return context;
}
this.setWalletPaneBusy(true);
@@ -1796,11 +1898,14 @@ export class ProfileModalController {
finalStatus = "Wallet settings saved.";
finalVariant = "success";
this.showSuccess("Wallet settings saved.");
context.reason = "saved";
} else {
finalStatus = "Wallet connection removed.";
finalVariant = "info";
this.showStatus("Wallet connection removed.");
context.reason = "cleared";
}
context.success = true;
} catch (error) {
const fallbackMessage = "Failed to save wallet settings.";
const detail =
@@ -1809,6 +1914,8 @@ export class ProfileModalController {
: fallbackMessage;
finalStatus = detail;
finalVariant = "error";
context.error = detail;
context.reason = error?.code || "service-error";
this.showError(detail);
} finally {
this.setWalletPaneBusy(false);
@@ -1816,34 +1923,59 @@ export class ProfileModalController {
if (finalStatus) {
this.updateWalletStatus(finalStatus, finalVariant);
}
context.status = finalStatus;
context.variant = finalVariant;
this.callbacks.onWalletSave(context, this);
}
return context;
}
async handleWalletTest() {
const { uri, defaultZap, error } = this.getWalletFormValues();
const context = {
uri,
defaultZap: defaultZap ?? null,
sanitizedUri: null,
success: false,
reason: null,
error: error || null,
status: null,
variant: null,
result: null,
};
if (this.isWalletBusy()) {
return;
context.reason = "busy";
this.callbacks.onWalletTest(context, this);
return context.result;
}
const { uri, defaultZap, error } = this.getWalletFormValues();
if (error) {
this.updateWalletStatus(error, "error");
this.showError(error);
context.reason = "invalid-default-zap";
if (this.walletDefaultZapInput instanceof HTMLElement) {
this.walletDefaultZapInput.focus();
}
return;
this.callbacks.onWalletTest(context, this);
return context.result;
}
const { valid, sanitized, message } = this.validateWalletUri(uri, {
requireValue: true,
});
context.sanitizedUri = sanitized;
if (!valid) {
this.updateWalletStatus(message, "error");
this.showError(message);
context.reason = "invalid-uri";
context.error = message;
if (this.walletUriInput instanceof HTMLElement) {
this.walletUriInput.focus();
}
return;
this.callbacks.onWalletTest(context, this);
return context.result;
}
const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey());
@@ -1851,7 +1983,10 @@ export class ProfileModalController {
const loginMessage = "Sign in to test your wallet connection.";
this.updateWalletStatus(loginMessage, "error");
this.showError(loginMessage);
return;
context.reason = "no-active-pubkey";
context.error = loginMessage;
this.callbacks.onWalletTest(context, this);
return context.result;
}
this.setWalletPaneBusy(true);
@@ -1865,12 +2000,14 @@ export class ProfileModalController {
finalStatus = "Wallet connection confirmed.";
finalVariant = "success";
this.showSuccess("Wallet connection confirmed.");
context.result = result;
context.success = true;
context.reason = "tested";
const currentSettings = this.services.getActiveNwcSettings();
if (currentSettings.nwcUri === sanitized) {
await this.services.updateActiveNwcSettings({ lastChecked: Date.now() });
}
return result;
} catch (error) {
const fallbackMessage = "Failed to reach wallet.";
const detail =
@@ -1879,20 +2016,36 @@ export class ProfileModalController {
: fallbackMessage;
finalStatus = detail;
finalVariant = "error";
context.error = detail;
context.reason = error?.code || "service-error";
this.showError(detail);
return null;
} finally {
this.setWalletPaneBusy(false);
this.refreshWalletPaneState();
if (finalStatus) {
this.updateWalletStatus(finalStatus, finalVariant);
}
context.status = finalStatus;
context.variant = finalVariant;
this.callbacks.onWalletTest(context, this);
}
return context.result;
}
async handleWalletDisconnect() {
const context = {
success: false,
reason: null,
error: null,
status: null,
variant: null,
};
if (this.isWalletBusy()) {
return;
context.reason = "busy";
this.callbacks.onWalletDisconnect(context, this);
return context;
}
const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey());
@@ -1900,7 +2053,10 @@ export class ProfileModalController {
const loginMessage = "Sign in to disconnect your wallet.";
this.updateWalletStatus(loginMessage, "error");
this.showError(loginMessage);
return;
context.reason = "no-active-pubkey";
context.error = loginMessage;
this.callbacks.onWalletDisconnect(context, this);
return context;
}
this.setWalletPaneBusy(true);
@@ -1912,6 +2068,8 @@ export class ProfileModalController {
);
finalStatus = "Wallet disconnected.";
this.showStatus("Wallet disconnected.");
context.success = true;
context.reason = "disconnected";
} catch (error) {
const fallbackMessage = "Failed to disconnect wallet.";
const detail =
@@ -1920,6 +2078,8 @@ export class ProfileModalController {
: fallbackMessage;
finalStatus = detail;
finalVariant = "error";
context.error = detail;
context.reason = error?.code || "service-error";
this.showError(detail);
} finally {
this.setWalletPaneBusy(false);
@@ -1927,7 +2087,12 @@ export class ProfileModalController {
if (finalStatus) {
this.updateWalletStatus(finalStatus, finalVariant);
}
context.status = finalStatus;
context.variant = finalVariant;
this.callbacks.onWalletDisconnect(context, this);
}
return context;
}
storeAdminEmptyMessages() {
@@ -2211,6 +2376,20 @@ export class ProfileModalController {
}
async handleAddModerator() {
const input = this.adminModeratorInput || null;
const rawValue = typeof input?.value === "string" ? input.value : "";
const trimmed = rawValue.trim();
const context = {
input,
rawValue,
value: trimmed,
actorNpub: null,
success: false,
reason: null,
error: null,
result: null,
};
let preloadError = null;
try {
await this.services.accessControl.ensureReady();
@@ -2221,18 +2400,25 @@ export class ProfileModalController {
if (preloadError) {
this.showError(this.describeAdminError(preloadError.code || "storage-error"));
return;
context.reason = preloadError.code || "storage-error";
context.error = preloadError;
this.callbacks.onAdminAddModerator(context, this);
return context;
}
const actorNpub = this.ensureAdminActor(true);
if (!actorNpub || !this.adminModeratorInput) {
return;
context.actorNpub = actorNpub;
if (!actorNpub || !input) {
context.reason = actorNpub ? "missing-input" : "unauthorized";
this.callbacks.onAdminAddModerator(context, this);
return context;
}
const value = this.adminModeratorInput.value.trim();
if (!value) {
if (!trimmed) {
this.showError("Enter an npub to add as a moderator.");
return;
context.reason = "empty";
this.callbacks.onAdminAddModerator(context, this);
return context;
}
if (this.adminAddModeratorButton) {
@@ -2241,24 +2427,51 @@ export class ProfileModalController {
}
try {
const result = await this.services.accessControl.addModerator(actorNpub, value);
const result = await this.services.accessControl.addModerator(
actorNpub,
trimmed,
);
context.result = result;
if (!result.ok) {
this.showError(this.describeAdminError(result.error));
return;
context.reason = result.error || "service-error";
return context;
}
this.adminModeratorInput.value = "";
this.showSuccess("Moderator added successfully.");
await this.services.onAccessControlUpdated();
context.success = true;
context.reason = "added";
} finally {
if (this.adminAddModeratorButton) {
this.adminAddModeratorButton.disabled = false;
this.adminAddModeratorButton.removeAttribute("aria-busy");
}
this.callbacks.onAdminAddModerator(context, this);
}
return context;
}
async handleRemoveModerator(npub, button) {
const context = {
npub,
button,
actorNpub: null,
success: false,
reason: null,
error: null,
result: null,
};
const releaseButton = () => {
if (button instanceof HTMLElement) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
};
let preloadError = null;
try {
await this.services.accessControl.ensureReady();
@@ -2269,40 +2482,97 @@ export class ProfileModalController {
if (preloadError) {
this.showError(this.describeAdminError(preloadError.code || "storage-error"));
if (button instanceof HTMLElement) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
context.reason = preloadError.code || "storage-error";
context.error = preloadError;
releaseButton();
this.callbacks.onAdminRemoveModerator(context, this);
return context;
}
const actorNpub = this.ensureAdminActor(true);
context.actorNpub = actorNpub;
if (!actorNpub) {
if (button instanceof HTMLElement) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
context.reason = "unauthorized";
releaseButton();
this.callbacks.onAdminRemoveModerator(context, this);
return context;
}
const result = await this.services.accessControl.removeModerator(
actorNpub,
npub,
);
context.result = result;
if (!result.ok) {
this.showError(this.describeAdminError(result.error));
if (button instanceof HTMLElement) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
return;
context.reason = result.error || "service-error";
releaseButton();
this.callbacks.onAdminRemoveModerator(context, this);
return context;
}
this.showSuccess("Moderator removed.");
await this.services.onAccessControlUpdated();
context.success = true;
context.reason = "removed";
releaseButton();
this.callbacks.onAdminRemoveModerator(context, this);
return context;
}
async handleAdminListMutation(listType, action, explicitNpub = null, sourceButton = null) {
const isWhitelist = listType === "whitelist";
const input = isWhitelist ? this.adminWhitelistInput : this.adminBlacklistInput;
const addButton = isWhitelist ? this.adminAddWhitelistButton : this.adminAddBlacklistButton;
const isAdd = action === "add";
let buttonToToggle = sourceButton || (isAdd ? addButton : null);
const context = {
listType,
action,
explicitNpub,
sourceButton,
actorNpub: null,
targetNpub: null,
success: false,
reason: null,
error: null,
result: null,
notificationResult: null,
notificationError: null,
};
const callbackMap = {
whitelist: {
add: this.callbacks.onAdminAddWhitelist,
remove: this.callbacks.onAdminRemoveWhitelist,
},
blacklist: {
add: this.callbacks.onAdminAddBlacklist,
remove: this.callbacks.onAdminRemoveBlacklist,
},
};
const adminCallback = callbackMap[listType]?.[action] || noop;
const setBusy = (element, busy) => {
if (!(element instanceof HTMLElement)) {
return;
}
element.disabled = !!busy;
if (busy) {
element.setAttribute("aria-busy", "true");
} else {
element.removeAttribute("aria-busy");
}
};
const finalize = () => {
setBusy(buttonToToggle, false);
adminCallback(context, this);
};
let preloadError = null;
try {
await this.services.accessControl.ensureReady();
@@ -2313,46 +2583,35 @@ export class ProfileModalController {
if (preloadError) {
this.showError(this.describeAdminError(preloadError.code || "storage-error"));
if (sourceButton instanceof HTMLElement) {
sourceButton.disabled = false;
sourceButton.removeAttribute("aria-busy");
}
return;
context.reason = preloadError.code || "storage-error";
context.error = preloadError;
finalize();
return context;
}
const actorNpub = this.ensureAdminActor(false);
context.actorNpub = actorNpub;
if (!actorNpub) {
if (sourceButton instanceof HTMLElement) {
sourceButton.disabled = false;
sourceButton.removeAttribute("aria-busy");
}
return;
context.reason = "unauthorized";
finalize();
return context;
}
const isWhitelist = listType === "whitelist";
const input = isWhitelist ? this.adminWhitelistInput : this.adminBlacklistInput;
const addButton = isWhitelist ? this.adminAddWhitelistButton : this.adminAddBlacklistButton;
const isAdd = action === "add";
let target = typeof explicitNpub === "string" ? explicitNpub.trim() : "";
if (!target && input instanceof HTMLInputElement) {
target = input.value.trim();
}
context.targetNpub = target;
if (isAdd && !target) {
this.showError("Enter an npub before adding it to the list.");
if (sourceButton instanceof HTMLElement) {
sourceButton.disabled = false;
sourceButton.removeAttribute("aria-busy");
}
return;
context.reason = "empty";
finalize();
return context;
}
const buttonToToggle = sourceButton || (isAdd ? addButton : null);
if (buttonToToggle instanceof HTMLElement) {
buttonToToggle.disabled = true;
buttonToToggle.setAttribute("aria-busy", "true");
}
buttonToToggle = buttonToToggle || (isAdd ? addButton : null);
setBusy(buttonToToggle, true);
let result;
if (isWhitelist) {
@@ -2365,13 +2624,13 @@ export class ProfileModalController {
: await this.services.accessControl.removeFromBlacklist(actorNpub, target);
}
context.result = result;
if (!result.ok) {
this.showError(this.describeAdminError(result.error));
if (buttonToToggle instanceof HTMLElement) {
buttonToToggle.disabled = false;
buttonToToggle.removeAttribute("aria-busy");
}
return;
context.reason = result.error || "service-error";
finalize();
return context;
}
if (isAdd && input instanceof HTMLInputElement) {
@@ -2388,10 +2647,8 @@ export class ProfileModalController {
this.showSuccess(successMessage);
await this.services.onAccessControlUpdated();
if (buttonToToggle instanceof HTMLElement) {
buttonToToggle.disabled = false;
buttonToToggle.removeAttribute("aria-busy");
}
context.success = true;
context.reason = isAdd ? "added" : "removed";
if (isAdd) {
try {
@@ -2400,6 +2657,7 @@ export class ProfileModalController {
actorNpub,
targetNpub: target,
});
context.notificationResult = notifyResult;
if (!notifyResult?.ok) {
const errorMessage = this.describeNotificationError(notifyResult?.error);
if (errorMessage) {
@@ -2413,6 +2671,7 @@ export class ProfileModalController {
}
}
} catch (error) {
context.notificationError = error;
console.error("Failed to send list notification DM:", error);
if (isDevMode) {
console.warn(
@@ -2422,6 +2681,9 @@ export class ProfileModalController {
}
}
}
finalize();
return context;
}
describeAdminError(code) {