Merge pull request #950 from PR0M3TH3AN/codex/add-logout-button-to-profile-modals

Add logout controls to profile modal saved accounts
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-26 20:13:26 -04:00
commit ec62a14928
2 files changed
+321 -35

No files matched your search

+138 -28
View File
@@ -732,6 +732,8 @@ class Application {
onLogout: async () => this.requestLogout(),
onChannelLink: (element) => this.handleProfileChannelLink(element),
onAddAccount: (payload) => this.handleAddProfile(payload),
onRequestLogoutProfile: (payload) =>
this.handleProfileLogoutRequest(payload),
onRequestSwitchProfile: (payload) =>
this.handleProfileSwitchRequest(payload),
onRelayOperation: (payload) =>
@@ -4687,35 +4689,14 @@ class Application {
);
}
let refreshPromise = null;
if (
this.lastIdentityRefreshPromise &&
typeof this.lastIdentityRefreshPromise.then === "function"
) {
refreshPromise = this.lastIdentityRefreshPromise;
}
const refreshCompleted = await this.waitForIdentityRefresh({
reason: "profile-switch",
});
if (refreshPromise) {
try {
await refreshPromise;
} catch (error) {
devLogger.error(
"Failed to refresh UI after switching profiles:",
error,
);
}
} else {
try {
await this.refreshAllVideoGrids({
reason: "profile-switch",
forceMainReload: true,
});
} catch (error) {
devLogger.error(
"Failed to refresh video grids after switching profiles:",
error,
);
}
if (!refreshCompleted) {
devLogger.warn(
"[Application] Fallback identity refresh was required after switching profiles.",
);
}
if (this.watchHistoryTelemetry?.resetPlaybackLoggingState) {
@@ -4744,6 +4725,135 @@ class Application {
return result;
}
async waitForIdentityRefresh({
reason = "identity-refresh",
attempts = 6,
} = {}) {
const maxAttempts = Number.isFinite(attempts)
? Math.max(1, Math.floor(attempts))
: 6;
const waitForTick = () =>
new Promise((resolve) => {
if (typeof queueMicrotask === "function") {
queueMicrotask(resolve);
} else if (typeof setTimeout === "function") {
setTimeout(resolve, 0);
} else {
resolve();
}
});
let promise = null;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const candidate = this.lastIdentityRefreshPromise;
if (candidate && typeof candidate.then === "function") {
promise = candidate;
break;
}
// Yield to allow the auth login flow to schedule the refresh promise.
// eslint-disable-next-line no-await-in-loop
await waitForTick();
}
if (promise && typeof promise.then === "function") {
try {
await promise;
return true;
} catch (error) {
devLogger.error(
"[Application] Identity refresh promise rejected:",
error,
);
}
}
try {
await this.refreshAllVideoGrids({
reason,
forceMainReload: true,
});
} catch (error) {
devLogger.error(
"[Application] Failed to refresh video grids after waiting for identity refresh:",
error,
);
}
return false;
}
async handleProfileLogoutRequest({ pubkey, entry } = {}) {
const candidatePubkey =
typeof pubkey === "string" && pubkey.trim()
? pubkey.trim()
: typeof entry?.pubkey === "string" && entry.pubkey.trim()
? entry.pubkey.trim()
: "";
if (!candidatePubkey) {
return { loggedOut: false, reason: "invalid-pubkey" };
}
const normalizedTarget =
this.normalizeHexPubkey(candidatePubkey) || candidatePubkey;
if (!normalizedTarget) {
return { loggedOut: false, reason: "invalid-pubkey" };
}
const activeNormalized = this.normalizeHexPubkey(getActiveProfilePubkey());
if (activeNormalized && activeNormalized === normalizedTarget) {
const detail = await this.requestLogout();
return {
loggedOut: true,
reason: "active-profile",
active: true,
detail,
};
}
let removalResult;
try {
removalResult = this.authService.removeSavedProfile(candidatePubkey);
} catch (error) {
devLogger.error(
"[Application] Failed to remove saved profile during logout request:",
error,
);
return { loggedOut: false, reason: "remove-failed", error };
}
if (!removalResult?.removed) {
if (removalResult?.error) {
devLogger.warn(
"[Application] removeSavedProfile returned an error during logout request:",
removalResult.error,
);
}
return { loggedOut: false, reason: "not-found" };
}
if (
this.nwcSettingsService &&
typeof this.nwcSettingsService.clearStoredNwcSettings === "function"
) {
try {
await this.nwcSettingsService.clearStoredNwcSettings(normalizedTarget, {
silent: true,
});
} catch (error) {
devLogger.warn(
"[Application] Failed to clear wallet settings for logged-out profile:",
error,
);
}
}
this.renderSavedProfiles();
return { loggedOut: true, removed: true };
}
async handleProfileRelayOperation({
action,
url,
+183 -7
View File
@@ -951,6 +951,7 @@ export class ProfileModalController {
onLogout: callbacks.onLogout || noop,
onChannelLink: callbacks.onChannelLink || noop,
onAddAccount: callbacks.onAddAccount || noop,
onRequestLogoutProfile: callbacks.onRequestLogoutProfile || noop,
onSelectPane: callbacks.onSelectPane || noop,
onPaneShown: callbacks.onPaneShown || noop,
onAddRelay: callbacks.onAddRelay || noop,
@@ -3066,7 +3067,8 @@ export class ProfileModalController {
metaSpan.className = "flex min-w-0 flex-1 flex-col gap-2";
const topLine = document.createElement("div");
topLine.className = "flex flex-wrap items-center gap-3";
topLine.className =
"flex flex-wrap items-center justify-between gap-3";
const providerId = this.getEntryProviderId(entry);
const providerInfo = this.resolveEntryProviderMetadata(entry);
@@ -3076,6 +3078,9 @@ export class ProfileModalController {
providerInfo && providerInfo.badgeVariant,
);
const resolvedProviderId =
providerId || (providerInfo && providerInfo.id) || "";
const label = document.createElement("span");
label.className = `${PROVIDER_BADGE_BASE_CLASS} ${badgeVariant}`;
label.textContent = providerLabel;
@@ -3087,12 +3092,27 @@ export class ProfileModalController {
label.dataset.providerId = providerInfo.id;
}
const actionGroup = document.createElement("div");
actionGroup.className = "flex flex-wrap items-center gap-2";
const action = document.createElement("span");
action.className = "text-xs font-medium text-muted";
action.setAttribute("aria-hidden", "true");
action.textContent = isSelected ? "Selected" : "Switch";
topLine.append(label, action);
actionGroup.appendChild(action);
const logoutButton = this.createSavedProfileLogoutButton({
entry,
providerId: resolvedProviderId || null,
cardButton: button,
displayName: cardDisplayName,
});
if (logoutButton) {
actionGroup.appendChild(logoutButton);
}
topLine.append(label, actionGroup);
const nameSpan = document.createElement("span");
nameSpan.className = "truncate text-sm font-semibold text-primary";
@@ -3115,11 +3135,8 @@ export class ProfileModalController {
: `Switch to ${cardDisplayName}`;
button.setAttribute("aria-label", ariaLabel);
const datasetProviderId =
providerId || (providerInfo && providerInfo.id) || "";
if (datasetProviderId) {
button.dataset.providerId = datasetProviderId;
if (resolvedProviderId) {
button.dataset.providerId = resolvedProviderId;
} else {
delete button.dataset.providerId;
}
@@ -3178,6 +3195,117 @@ export class ProfileModalController {
}
}
async handleSavedProfileLogout({
entry,
providerId,
triggerButton,
cardButton,
displayName,
} = {}) {
if (!entry || typeof entry !== "object") {
this.showError("Failed to logout this account. Please try again.");
return { loggedOut: false, reason: "invalid-entry" };
}
const targetPubkey =
typeof entry.pubkey === "string" && entry.pubkey.trim()
? entry.pubkey.trim()
: "";
if (!targetPubkey) {
this.showError("Failed to logout this account. Please try again.");
return { loggedOut: false, reason: "invalid-pubkey" };
}
if (
!this.callbacks.onRequestLogoutProfile ||
this.callbacks.onRequestLogoutProfile === noop
) {
this.showError("Account logout is not available right now.");
return { loggedOut: false, reason: "logout-unavailable" };
}
const revertUiState = () => {
if (triggerButton instanceof HTMLElement) {
triggerButton.disabled = false;
triggerButton.removeAttribute("aria-busy");
delete triggerButton.dataset.state;
}
if (cardButton instanceof HTMLElement) {
cardButton.disabled = false;
cardButton.removeAttribute("aria-busy");
delete cardButton.dataset.loading;
}
};
if (triggerButton instanceof HTMLElement) {
triggerButton.dataset.state = "loading";
triggerButton.disabled = true;
triggerButton.setAttribute("aria-busy", "true");
}
if (cardButton instanceof HTMLElement) {
cardButton.dataset.loading = "true";
cardButton.disabled = true;
cardButton.setAttribute("aria-busy", "true");
}
let result;
try {
result = await this.callbacks.onRequestLogoutProfile({
controller: this,
pubkey: targetPubkey,
entry,
providerId: this.normalizeProviderId(providerId) || null,
});
} catch (error) {
this.showError("Failed to logout this account. Please try again.");
result = { loggedOut: false, error, reason: "logout-error" };
} finally {
revertUiState();
}
if (!result || typeof result !== "object") {
this.showError("Failed to logout this account. Please try again.");
return { loggedOut: false, reason: "unknown" };
}
if (result.loggedOut || result.removed) {
const successName =
typeof displayName === "string" && displayName.trim()
? displayName.trim()
: null;
const message = successName
? `${successName} logged out.`
: "Account logged out.";
this.profileSwitcherSelectionPubkey = null;
this.showSuccess(message);
this.renderSavedProfiles();
return { ...result, loggedOut: true };
}
if (result.reason === "not-found") {
this.profileSwitcherSelectionPubkey = null;
this.showStatus("This account is no longer connected.");
this.renderSavedProfiles();
return result;
}
if (result.reason === "active-profile") {
return result;
}
if (result.error) {
this.showError("Failed to logout this account. Please try again.");
return result;
}
this.showError("Failed to logout this account. Please try again.");
return result;
}
createCompactProfileSummary({
displayName,
displayNpub,
@@ -3301,6 +3429,54 @@ export class ProfileModalController {
return button;
}
createSavedProfileLogoutButton({
entry,
cardButton,
providerId,
displayName,
} = {}) {
if (this.callbacks.onRequestLogoutProfile === noop) {
return null;
}
if (!entry || typeof entry !== "object" || !entry.pubkey) {
return null;
}
const safeName =
typeof displayName === "string" && displayName.trim()
? displayName.trim()
: "this account";
const button = document.createElement("button");
button.type = "button";
button.className = "btn-ghost focus-ring text-2xs";
button.dataset.variant = "critical";
button.dataset.role = "logout";
button.textContent = "Logout";
button.setAttribute("aria-label", `Log out ${safeName}`);
button.title = `Log out ${safeName}`;
button.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
if (button.dataset.state === "loading") {
return;
}
void this.handleSavedProfileLogout({
entry,
providerId,
triggerButton: button,
cardButton,
displayName: safeName,
});
});
return button;
}
createRemoveButton({
label = "Remove",
confirmMessage,