mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
Merge pull request #1994 from PR0M3TH3AN/codex/handle-auth-login-failures-and-permissions
Add permission CTA and retry flow for subscriptions & hashtag preferences
This commit is contained in:
6 files changed
+300
-7
No files matched your search
@@ -381,6 +381,24 @@
|
||||
data-profile-mobile-pane
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
id="profilePermissionPrompt"
|
||||
class="card hidden flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<p
|
||||
id="profilePermissionPromptMessage"
|
||||
class="text-sm text-muted"
|
||||
></p>
|
||||
<button
|
||||
id="profilePermissionPromptButton"
|
||||
type="button"
|
||||
class="btn focus-ring"
|
||||
>
|
||||
Enable permissions
|
||||
</button>
|
||||
</div>
|
||||
<div id="profileModalPanes" class="profile-modal__panes">
|
||||
<section
|
||||
id="profilePaneAccount"
|
||||
|
||||
@@ -274,6 +274,10 @@ class Application {
|
||||
this.dmRecipientPubkey = null;
|
||||
this.dmRelayHints = new Map();
|
||||
this.authLoadingState = { profile: "idle", lists: "idle", dms: "idle" };
|
||||
this.permissionPromptShownForSession = false;
|
||||
this.permissionPromptVisible = false;
|
||||
this.permissionPromptInFlight = false;
|
||||
this.permissionPromptPending = new Set();
|
||||
this.relayUiRefreshTimeout = null;
|
||||
this.lastRelayHealthWarningAt = 0;
|
||||
this.appStartedAt = Date.now();
|
||||
@@ -2093,6 +2097,84 @@ class Application {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
resetPermissionPromptState() {
|
||||
this.permissionPromptShownForSession = false;
|
||||
this.permissionPromptVisible = false;
|
||||
this.permissionPromptInFlight = false;
|
||||
if (this.permissionPromptPending instanceof Set) {
|
||||
this.permissionPromptPending.clear();
|
||||
} else {
|
||||
this.permissionPromptPending = new Set();
|
||||
}
|
||||
this.updatePermissionPromptCta();
|
||||
}
|
||||
|
||||
capturePermissionPromptRequirement(source) {
|
||||
const normalized =
|
||||
typeof source === "string" ? source.trim().toLowerCase() : "";
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.permissionPromptPending.add(normalized);
|
||||
|
||||
if (!this.permissionPromptShownForSession) {
|
||||
this.permissionPromptShownForSession = true;
|
||||
this.permissionPromptVisible = true;
|
||||
}
|
||||
|
||||
this.updatePermissionPromptCta();
|
||||
}
|
||||
|
||||
capturePermissionPromptFromError(error) {
|
||||
const code =
|
||||
error && typeof error.code === "string" ? error.code.trim() : "";
|
||||
|
||||
switch (code) {
|
||||
case "subscriptions-permission-required":
|
||||
this.capturePermissionPromptRequirement("subscriptions");
|
||||
break;
|
||||
case "hashtag-preferences-permission-required":
|
||||
this.capturePermissionPromptRequirement("hashtag-preferences");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resolvePermissionPromptMessage() {
|
||||
const needsSubscriptions = this.permissionPromptPending.has("subscriptions");
|
||||
const needsHashtags = this.permissionPromptPending.has("hashtag-preferences");
|
||||
|
||||
if (needsSubscriptions && needsHashtags) {
|
||||
return "Enable permissions to load your subscriptions and hashtag preferences.";
|
||||
}
|
||||
if (needsSubscriptions) {
|
||||
return "Enable permissions to load your subscriptions.";
|
||||
}
|
||||
if (needsHashtags) {
|
||||
return "Enable permissions to load your hashtag preferences.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
updatePermissionPromptCta() {
|
||||
if (!this.profileController?.setPermissionPromptCtaState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldShow =
|
||||
this.permissionPromptVisible && this.permissionPromptPending.size > 0;
|
||||
const message = shouldShow ? this.resolvePermissionPromptMessage() : "";
|
||||
|
||||
this.profileController.setPermissionPromptCtaState({
|
||||
visible: shouldShow,
|
||||
message,
|
||||
buttonLabel: "Enable permissions",
|
||||
busy: this.permissionPromptInFlight,
|
||||
});
|
||||
}
|
||||
|
||||
normalizeHashtagPreferenceList(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
return [];
|
||||
@@ -4040,6 +4122,98 @@ class Application {
|
||||
);
|
||||
}
|
||||
|
||||
async handlePermissionPromptRequest() {
|
||||
if (this.permissionPromptInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activePubkey = this.normalizeHexPubkey(this.pubkey);
|
||||
if (!activePubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const needsSubscriptions = this.permissionPromptPending.has("subscriptions");
|
||||
const needsHashtags = this.permissionPromptPending.has("hashtag-preferences");
|
||||
|
||||
if (!needsSubscriptions && !needsHashtags) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.permissionPromptInFlight = true;
|
||||
this.updatePermissionPromptCta();
|
||||
|
||||
const tasks = [];
|
||||
|
||||
if (needsSubscriptions && subscriptions?.ensureLoaded) {
|
||||
const subscriptionTask = subscriptions
|
||||
.ensureLoaded(activePubkey, { allowPermissionPrompt: true })
|
||||
.then(() => {
|
||||
this.capturePermissionPromptFromError(subscriptions?.lastLoadError);
|
||||
if (
|
||||
subscriptions?.lastLoadError?.code !==
|
||||
"subscriptions-permission-required"
|
||||
) {
|
||||
this.permissionPromptPending.delete("subscriptions");
|
||||
if (this.profileController) {
|
||||
this.profileController.populateSubscriptionsList();
|
||||
}
|
||||
if (typeof subscriptions?.refreshActiveFeed === "function") {
|
||||
return subscriptions.refreshActiveFeed({
|
||||
reason: "permission-prompt",
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to refresh subscriptions after permission prompt:",
|
||||
error,
|
||||
);
|
||||
this.capturePermissionPromptFromError(error);
|
||||
});
|
||||
tasks.push(subscriptionTask);
|
||||
}
|
||||
|
||||
if (needsHashtags && this.hashtagPreferences?.load) {
|
||||
const hashtagTask = this.hashtagPreferences
|
||||
.load(activePubkey, { allowPermissionPrompt: true })
|
||||
.then(() => {
|
||||
this.capturePermissionPromptFromError(
|
||||
this.hashtagPreferences?.lastLoadError,
|
||||
);
|
||||
if (
|
||||
this.hashtagPreferences?.lastLoadError?.code !==
|
||||
"hashtag-preferences-permission-required"
|
||||
) {
|
||||
this.permissionPromptPending.delete("hashtag-preferences");
|
||||
this.updateCachedHashtagPreferences();
|
||||
this.refreshTagPreferenceUi();
|
||||
if (this.profileController) {
|
||||
this.profileController.populateHashtagPreferences();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to refresh hashtag preferences after permission prompt:",
|
||||
error,
|
||||
);
|
||||
this.capturePermissionPromptFromError(error);
|
||||
});
|
||||
tasks.push(hashtagTask);
|
||||
}
|
||||
|
||||
await Promise.allSettled(tasks);
|
||||
|
||||
if (this.permissionPromptPending.size === 0) {
|
||||
this.permissionPromptVisible = false;
|
||||
}
|
||||
|
||||
this.permissionPromptInFlight = false;
|
||||
this.updatePermissionPromptCta();
|
||||
}
|
||||
|
||||
async handleAuthLogin(detail = {}) {
|
||||
const postLoginPromise =
|
||||
detail && typeof detail.postLoginPromise?.then === "function"
|
||||
@@ -4059,6 +4233,8 @@ class Application {
|
||||
this.resetViewLoggingState();
|
||||
}
|
||||
|
||||
this.resetPermissionPromptState();
|
||||
|
||||
// Stop existing feed subscription to prioritize user data sync
|
||||
if (
|
||||
this.videoSubscription &&
|
||||
@@ -4203,13 +4379,20 @@ class Application {
|
||||
tasks.push(
|
||||
subscriptions.ensureLoaded(activePubkey, {
|
||||
allowPermissionPrompt: false,
|
||||
}).catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to load subscriptions during login:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}),
|
||||
})
|
||||
.then(() => {
|
||||
this.capturePermissionPromptFromError(
|
||||
subscriptions?.lastLoadError,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to load subscriptions during login:",
|
||||
error,
|
||||
);
|
||||
this.capturePermissionPromptFromError(error);
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4284,11 +4467,17 @@ class Application {
|
||||
.load(activePubkey, {
|
||||
allowPermissionPrompt: false,
|
||||
})
|
||||
.then(() => {
|
||||
this.capturePermissionPromptFromError(
|
||||
this.hashtagPreferences?.lastLoadError,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to load hashtag preferences during login:",
|
||||
error,
|
||||
);
|
||||
this.capturePermissionPromptFromError(error);
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -4475,6 +4664,7 @@ class Application {
|
||||
this.pendingModalZapOpen = false;
|
||||
|
||||
this.resetHashtagPreferencesState();
|
||||
this.resetPermissionPromptState();
|
||||
this.updateAuthLoadingState({ profile: "idle", lists: "idle", dms: "idle" });
|
||||
|
||||
await this.nwcSettingsService.onLogout({
|
||||
|
||||
@@ -242,6 +242,7 @@ class HashtagPreferencesService {
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
this.subscriptionKey = null;
|
||||
this.lastLoadError = null;
|
||||
|
||||
profileCache.subscribe((event, detail) => {
|
||||
if (event === "profileChanged") {
|
||||
@@ -271,6 +272,7 @@ class HashtagPreferencesService {
|
||||
this.loaded = false;
|
||||
this.backgroundLoading = false;
|
||||
this.preferencesVersion = DEFAULT_VERSION;
|
||||
this.lastLoadError = null;
|
||||
if (this.decryptRetryTimeoutId) {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
this.decryptRetryTimeoutId = null;
|
||||
@@ -539,6 +541,7 @@ class HashtagPreferencesService {
|
||||
async loadInternal(pubkey, options = {}) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
this.lastLoadError = null;
|
||||
let wasLoadedForUser =
|
||||
this.activePubkey &&
|
||||
this.activePubkey === normalized &&
|
||||
@@ -790,6 +793,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
if (!decryptResult.ok) {
|
||||
this.lastLoadError = decryptResult.error || null;
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Failed to decrypt hashtag preferences`,
|
||||
decryptResult.error,
|
||||
|
||||
@@ -339,6 +339,7 @@ class SubscriptionsManager {
|
||||
this.lastRunOptions = null;
|
||||
this.lastResult = null;
|
||||
this.lastContainerId = null;
|
||||
this.lastLoadError = null;
|
||||
this.unsubscribeFromNostrUpdates = null;
|
||||
this.pendingRefreshPromise = null;
|
||||
this.scheduledRefreshDetail = null;
|
||||
@@ -453,6 +454,7 @@ class SubscriptionsManager {
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
const wasBackgroundLoading = this.backgroundLoading;
|
||||
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||
this.lastLoadError = null;
|
||||
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
@@ -603,6 +605,7 @@ class SubscriptionsManager {
|
||||
}
|
||||
|
||||
if (!decryptResult.ok) {
|
||||
this.lastLoadError = decryptResult.error || null;
|
||||
if (decryptResult.error?.code === "subscriptions-decrypt-timeout") {
|
||||
if (!this.loaded && !cachedSnapshot.hasSnapshot) {
|
||||
this.loaded = true;
|
||||
@@ -686,6 +689,7 @@ class SubscriptionsManager {
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.lastLoadError = err || null;
|
||||
userLogger.error("[SubscriptionsManager] Failed to update subs from relays:", err);
|
||||
}
|
||||
}
|
||||
@@ -701,6 +705,7 @@ class SubscriptionsManager {
|
||||
this.lastRunOptions = null;
|
||||
this.lastResult = null;
|
||||
this.hasRenderedOnce = false;
|
||||
this.lastLoadError = null;
|
||||
if (this.decryptRetryTimeoutId) {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
this.decryptRetryTimeoutId = null;
|
||||
|
||||
@@ -604,6 +604,7 @@ export default class ApplicationBootstrap {
|
||||
app.handleProfilePrivacyToggle(payload),
|
||||
onPublishDmRelayPreferences: (payload) =>
|
||||
app.handleProfilePublishDmRelayPreferences(payload),
|
||||
onRequestPermissionPrompt: () => app.handlePermissionPromptRequest(),
|
||||
};
|
||||
|
||||
app.profileController = new ProfileModalController({
|
||||
|
||||
@@ -1195,6 +1195,7 @@ export class ProfileModalController {
|
||||
onTogglePrivacy: callbacks.onTogglePrivacy || noop,
|
||||
onOpenRelays: callbacks.onOpenRelays || noop,
|
||||
onPublishDmRelayPreferences: callbacks.onPublishDmRelayPreferences || noop,
|
||||
onRequestPermissionPrompt: callbacks.onRequestPermissionPrompt || noop,
|
||||
};
|
||||
|
||||
this.profileModal = null;
|
||||
@@ -1331,6 +1332,15 @@ export class ProfileModalController {
|
||||
this.profileHashtagDisinterestRefreshBtn = null;
|
||||
this.subscriptionsStatusText = null;
|
||||
this.subscriptionsBackgroundLoading = false;
|
||||
this.permissionPromptCta = null;
|
||||
this.permissionPromptCtaMessage = null;
|
||||
this.permissionPromptCtaButton = null;
|
||||
this.permissionPromptCtaState = {
|
||||
visible: false,
|
||||
message: "",
|
||||
buttonLabel: "Enable permissions",
|
||||
busy: false,
|
||||
};
|
||||
this.profileSubscriptionsRefreshBtn = null;
|
||||
this.profileFriendsRefreshBtn = null;
|
||||
this.profileBlockedRefreshBtn = null;
|
||||
@@ -1512,6 +1522,12 @@ export class ProfileModalController {
|
||||
this.profileModalPaneWrapper =
|
||||
this.profileModalRoot?.querySelector("[data-profile-mobile-pane]") || null;
|
||||
this.profileModal = this.profileModalRoot;
|
||||
this.permissionPromptCta =
|
||||
document.getElementById("profilePermissionPrompt") || null;
|
||||
this.permissionPromptCtaMessage =
|
||||
document.getElementById("profilePermissionPromptMessage") || null;
|
||||
this.permissionPromptCtaButton =
|
||||
document.getElementById("profilePermissionPromptButton") || null;
|
||||
this.closeButton = document.getElementById("closeProfileModal") || null;
|
||||
this.profileModalBackButton =
|
||||
document.getElementById("profileModalBack") || null;
|
||||
@@ -1735,6 +1751,7 @@ export class ProfileModalController {
|
||||
document.getElementById("profileHashtagDisinterestRefreshBtn") || null;
|
||||
this.subscriptionsStatusText =
|
||||
document.getElementById("subscriptionsStatus") || null;
|
||||
this.applyPermissionPromptCtaState();
|
||||
|
||||
this.profileRelayList = this.relayList;
|
||||
this.profileRelayInput = this.relayInput;
|
||||
@@ -5334,6 +5351,12 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.permissionPromptCtaButton instanceof HTMLElement) {
|
||||
this.permissionPromptCtaButton.addEventListener("click", () => {
|
||||
this.callbacks.onRequestPermissionPrompt({ controller: this });
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(this.navButtons).forEach(([name, button]) => {
|
||||
if (button instanceof HTMLElement) {
|
||||
button.addEventListener("click", () => {
|
||||
@@ -7855,6 +7878,58 @@ export class ProfileModalController {
|
||||
};
|
||||
}
|
||||
|
||||
setPermissionPromptCtaState(nextState = {}) {
|
||||
if (!nextState || typeof nextState !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.permissionPromptCtaState = {
|
||||
...this.permissionPromptCtaState,
|
||||
...nextState,
|
||||
};
|
||||
|
||||
this.applyPermissionPromptCtaState();
|
||||
}
|
||||
|
||||
applyPermissionPromptCtaState() {
|
||||
if (!(this.permissionPromptCta instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackMessage =
|
||||
"Enable permissions to load your subscriptions and hashtag preferences.";
|
||||
const {
|
||||
visible,
|
||||
message,
|
||||
buttonLabel,
|
||||
busy,
|
||||
} = this.permissionPromptCtaState || {};
|
||||
const normalizedMessage =
|
||||
typeof message === "string" && message.trim()
|
||||
? message.trim()
|
||||
: fallbackMessage;
|
||||
|
||||
this.permissionPromptCta.classList.toggle("hidden", !visible);
|
||||
this.permissionPromptCta.setAttribute("aria-hidden", (!visible).toString());
|
||||
|
||||
if (this.permissionPromptCtaMessage instanceof HTMLElement) {
|
||||
this.permissionPromptCtaMessage.textContent = normalizedMessage;
|
||||
}
|
||||
|
||||
if (this.permissionPromptCtaButton instanceof HTMLButtonElement) {
|
||||
const normalizedLabel =
|
||||
typeof buttonLabel === "string" && buttonLabel.trim()
|
||||
? buttonLabel.trim()
|
||||
: "Enable permissions";
|
||||
this.permissionPromptCtaButton.textContent = normalizedLabel;
|
||||
this.permissionPromptCtaButton.disabled = Boolean(busy);
|
||||
this.permissionPromptCtaButton.setAttribute(
|
||||
"aria-busy",
|
||||
busy ? "true" : "false",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setHashtagStatus(message = "", tone = "muted") {
|
||||
if (!(this.hashtagStatusText instanceof HTMLElement)) {
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user