mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 08:52:25 +00:00
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:
2 files changed
+321
-35
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user