From 3a55e1bf0313e1c11a8ba98cc5734f293cbbe53a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 21:42:49 +0000 Subject: [PATCH] fix: resolve coordinator refactoring regressions breaking DM, lists, and feed pipeline - Remove undefined hashtagPreferencesPromise reference in handleAuthLogin that caused the feed sync Promise.allSettled to silently fail, preventing proper list/profile synchronization before grid refresh - Add missing getModerationSettings and getActiveProfilePubkey dependency injection to authSessionCoordinator, fixing crashes during logout moderation reset and profile switch pubkey resolution - Parallelize NIP-07 permission request with relay readiness instead of gating list loading behind it; parallelize subscriptions and hashtag preference loading (independent of each other, only blocks must sequence) - Add native run() alias to feedEngine so callers don't rely solely on the applicationBootstrap compatibility shim - Remove redundant refreshVisibleModerationUi call from renderVideoList that re-decorated every video immediately after decoration - Avoid re-normalizing moderation settings on every getActiveModerationThresholds call when settings are already an object https://claude.ai/code/session_01FCosieUXjEn95KtUeyGemY --- js/app.js | 2 + js/app/authSessionCoordinator.js | 110 ++++++++++++++++++------------- js/app/feedCoordinator.js | 15 ----- js/app/moderationCoordinator.js | 6 +- js/feedEngine/engine.js | 1 + 5 files changed, 71 insertions(+), 63 deletions(-) diff --git a/js/app.js b/js/app.js index 16adc511..f568078e 100644 --- a/js/app.js +++ b/js/app.js @@ -603,6 +603,8 @@ class Application { assertAnyRelayAccepted, queueSignEvent, bootstrapTrustedSeeds, + getModerationSettings, + getActiveProfilePubkey, })), writable: true, configurable: true, diff --git a/js/app/authSessionCoordinator.js b/js/app/authSessionCoordinator.js index bf67760b..0125d071 100644 --- a/js/app/authSessionCoordinator.js +++ b/js/app/authSessionCoordinator.js @@ -190,27 +190,34 @@ export function createAuthSessionCoordinator(deps) { return null; }); + // PRE-AUTH: Request full permissions once so subsequent services don't + // spam individual prompts. This runs immediately (not gated behind + // relaysReadyPromise) so the user can approve while relays connect. + const permissionPromise = activePubkey + ? Promise.resolve() + .then(async () => { + const signer = await nostrClient.ensureActiveSignerForPubkey( + activePubkey, + ); + if (signer?.type === "extension" || signer?.type === "nip07") { + await nostrClient.ensureExtensionPermissions( + DEFAULT_NIP07_PERMISSION_METHODS, + ); + } + }) + .catch((error) => { + devLogger.warn("[Application] Pre-authorization failed:", error); + }) + : Promise.resolve(); + // Start list loading as soon as relays are ready — lists don't depend on // profile data, only on relay URLs. This runs in parallel with profile // fetching instead of sequentially after it. - const listStatePromise = relaysReadyPromise.then(async () => { - // PRE-AUTH: Request full permissions once so subsequent services don't spam prompts. - if (activePubkey) { - try { - const signer = await nostrClient.ensureActiveSignerForPubkey( - activePubkey, - ); - if (signer?.type === "extension" || signer?.type === "nip07") { - await nostrClient.ensureExtensionPermissions( - DEFAULT_NIP07_PERMISSION_METHODS, - ); - } - } catch (error) { - devLogger.warn("[Application] Pre-authorization failed:", error); - } - } - - // SEQUENTIAL LOAD: Ensure critical lists (blocks) load before content filters. + const listStatePromise = Promise.all([ + relaysReadyPromise, + permissionPromise, + ]).then(async () => { + // SEQUENTIAL LOAD: Blocks must load first (foundation for all filtering). if ( activePubkey && typeof this.authService.loadBlocksForPubkey === "function" @@ -227,23 +234,31 @@ export function createAuthSessionCoordinator(deps) { } } + // Subscriptions and hashtag preferences are independent of each other, + // so load them in parallel after blocks are ready. + const parallelListTasks = []; + if ( activePubkey && subscriptions && typeof subscriptions.ensureLoaded === "function" ) { - try { - await subscriptions.ensureLoaded(activePubkey, { - allowPermissionPrompt: false, - }); - this.capturePermissionPromptFromError(subscriptions?.lastLoadError); - } catch (error) { - devLogger.warn( - "[Application] Failed to load subscriptions during login:", - error, - ); - this.capturePermissionPromptFromError(error); - } + parallelListTasks.push( + subscriptions + .ensureLoaded(activePubkey, { allowPermissionPrompt: false }) + .then(() => { + this.capturePermissionPromptFromError( + subscriptions?.lastLoadError, + ); + }) + .catch((error) => { + devLogger.warn( + "[Application] Failed to load subscriptions during login:", + error, + ); + this.capturePermissionPromptFromError(error); + }), + ); } if ( @@ -251,23 +266,27 @@ export function createAuthSessionCoordinator(deps) { this.hashtagPreferences && typeof this.hashtagPreferences.load === "function" ) { - try { - await this.hashtagPreferences.load(activePubkey, { - allowPermissionPrompt: false, - }); - this.capturePermissionPromptFromError( - this.hashtagPreferences?.lastLoadError, - ); - this.updateCachedHashtagPreferences(); - } catch (error) { - devLogger.warn( - "[Application] Failed to load hashtag preferences during login:", - error, - ); - this.capturePermissionPromptFromError(error); - } + parallelListTasks.push( + this.hashtagPreferences + .load(activePubkey, { allowPermissionPrompt: false }) + .then(() => { + this.capturePermissionPromptFromError( + this.hashtagPreferences?.lastLoadError, + ); + this.updateCachedHashtagPreferences(); + }) + .catch((error) => { + devLogger.warn( + "[Application] Failed to load hashtag preferences during login:", + error, + ); + this.capturePermissionPromptFromError(error); + }), + ); } + await Promise.allSettled(parallelListTasks); + this.updateAuthLoadingState({ lists: "ready" }); return true; }); @@ -370,7 +389,6 @@ export function createAuthSessionCoordinator(deps) { const feedSyncPromise = Promise.race([ Promise.allSettled([ listStatePromise, - hashtagPreferencesPromise, profileStatePromise, ]), new Promise((resolve) => setTimeout(resolve, FEED_SYNC_TIMEOUT_MS)), diff --git a/js/app/feedCoordinator.js b/js/app/feedCoordinator.js index 9bb87249..f0b782ea 100644 --- a/js/app/feedCoordinator.js +++ b/js/app/feedCoordinator.js @@ -1962,21 +1962,6 @@ export function createFeedCoordinator(deps) { this.videoListView.render(decoratedVideos, metadata); this.emitFeedImpressions(decoratedVideos); - - if (typeof this.refreshVisibleModerationUi === "function") { - const renderReason = - metadata && typeof metadata.reason === "string" && metadata.reason - ? `render-${metadata.reason}` - : "render-video-list"; - try { - this.refreshVisibleModerationUi({ reason: renderReason }); - } catch (error) { - devLogger.warn( - "[Application] Failed to refresh moderation UI after rendering video list:", - error, - ); - } - } this.updateModalSimilarContent(); }, diff --git a/js/app/moderationCoordinator.js b/js/app/moderationCoordinator.js index 75a35eab..c8a52bbc 100644 --- a/js/app/moderationCoordinator.js +++ b/js/app/moderationCoordinator.js @@ -179,8 +179,10 @@ export function createModerationCoordinator(deps) { }, getActiveModerationThresholds() { - this.moderationSettings = this.moderationDecorator.normalizeModerationSettings(this.moderationSettings); - return { ...this.moderationSettings }; + if (!this.moderationSettings || typeof this.moderationSettings !== "object") { + this.moderationSettings = this.moderationDecorator.normalizeModerationSettings(this.moderationSettings); + } + return this.moderationSettings; }, decorateVideoModeration(video, feedContext = {}) { diff --git a/js/feedEngine/engine.js b/js/feedEngine/engine.js index 9e61e339..4df3b73b 100644 --- a/js/feedEngine/engine.js +++ b/js/feedEngine/engine.js @@ -246,6 +246,7 @@ export function createFeedEngine({ logger } = {}) { return { registerFeed, runFeed, + run: runFeed, listFeeds, getFeedDefinition, };