diff --git a/js/app.js b/js/app.js index eeeaa2f3..e7dfba21 100644 --- a/js/app.js +++ b/js/app.js @@ -4080,10 +4080,6 @@ class Application { const urlParams = new URLSearchParams(window.location.search); const hasVideoParam = urlParams.has("v"); - if (!normalizedView || normalizedView === "most-recent-videos") { - setHashView("for-you", { preserveVideoParam: hasVideoParam }); - } - const rawProviderId = typeof detail?.providerId === "string" ? detail.providerId.trim() : ""; const rawAuthType = @@ -4100,75 +4096,6 @@ class Application { }; const activePubkey = detail?.pubkey || this.pubkey; - Promise.resolve() - .then(() => - this.handleModerationSettingsChange({ - settings: getModerationSettings(), - skipRefresh: true, - }), - ) - .catch((error) => { - devLogger.warn( - "Failed to sync moderation settings after login:", - error, - ); - }); - - if (loginContext.identityChanged) { - this.resetHashtagPreferencesState(); - try { - this.watchHistoryTelemetry?.resetPlaybackLoggingState?.(); - this.watchHistoryTelemetry?.refreshPreferenceSettings?.(); - } catch (error) { - devLogger.warn( - "Failed to refresh watch history telemetry after identity change:", - error, - ); - } - } - - const hashtagPreferencesPromise = Promise.resolve( - this.loadHashtagPreferencesForPubkey(loginContext.pubkey, { - allowPermissionPrompt: false, - }), - ).catch((error) => { - devLogger.warn( - "[Application] Background hashtag preferences load failed:", - error - ); - }); - - if (this.zapController) { - try { - this.zapController.setVisibility( - Boolean(this.currentVideo?.lightningAddress), - ); - } catch (error) { - devLogger.warn("[Application] Failed to refresh zap visibility after login:", error); - } - } - - const shouldReopenZap = this.pendingModalZapOpen; - this.pendingModalZapOpen = false; - if (shouldReopenZap) { - const hasLightning = Boolean(this.currentVideo?.lightningAddress); - if (hasLightning && this.videoModal?.openZapDialog) { - Promise.resolve() - .then(() => this.videoModal.openZapDialog()) - .then((opened) => { - if (opened) { - this.zapController?.open(); - } - }) - .catch((error) => { - devLogger.warn( - "[Application] Failed to reopen zap dialog after login:", - error, - ); - }); - } - } - const initialLoadingState = { profile: activePubkey ? "loading" : "idle", lists: activePubkey ? "loading" : "idle", @@ -4184,28 +4111,6 @@ class Application { authLoadingState: this.authLoadingState, }); - const accessControlReadyPromise = - accessControl && typeof accessControl.ensureReady === "function" - ? Promise.resolve() - .then(() => accessControl.ensureReady()) - .catch((error) => { - userLogger.error( - "[Application] Failed to refresh admin lists after login:", - error, - ); - throw error; - }) - : null; - - const scheduleAfterFirstRender = () => - new Promise((resolve) => { - if (typeof requestAnimationFrame === "function") { - requestAnimationFrame(() => requestAnimationFrame(resolve)); - } else { - setTimeout(resolve, 0); - } - }); - const cachedProfile = detail?.postLogin && typeof detail.postLogin === "object" ? detail.postLogin.profile || null @@ -4221,78 +4126,6 @@ class Application { } } - const startDmHydration = () => { - const dmTasks = []; - if (activePubkey) { - dmTasks.push( - Promise.resolve() - .then(() => - this.nostrService.loadDirectMessages({ - actorPubkey: activePubkey, - limit: 50, - decryptLimit: 50, - initialLoad: true, - }), - ) - .catch((error) => { - devLogger.warn( - "[Application] Failed to sync direct messages during login:", - error, - ); - throw error; - }), - ); - - if ( - typeof this.nostrService.ensureDirectMessageSubscription === "function" - ) { - dmTasks.push( - Promise.resolve() - .then(() => - this.nostrService.ensureDirectMessageSubscription({ - actorPubkey: activePubkey, - }), - ) - .catch((error) => { - devLogger.warn( - "[Application] Failed to subscribe to direct messages during login:", - error, - ); - throw error; - }), - ); - } - } - - return dmTasks.length - ? Promise.allSettled(dmTasks).then((results) => { - const success = results.every( - (result) => result.status === "fulfilled", - ); - this.updateAuthLoadingState({ - dms: success ? "ready" : "error", - }); - return success; - }) - : Promise.resolve(this.updateAuthLoadingState({ dms: "idle" })); - }; - - const dmStatePromise = Promise.resolve() - .then(() => scheduleAfterFirstRender()) - .then(() => startDmHydration()) - .catch((error) => { - devLogger.warn("[Application] Delayed DM hydration failed:", error); - this.updateAuthLoadingState({ dms: "error" }); - return false; - }); - - const nwcPromise = Promise.resolve() - .then(() => this.nwcSettingsService.onLogin(loginContext)) - .catch((error) => { - devLogger.error("Failed to process NWC settings during login:", error); - return null; - }); - if (this.profileController) { try { const maybePromise = this.profileController.handleAuthLogin(detail); @@ -4314,8 +4147,22 @@ class Application { this.renderSavedProfiles(); } + const accessControlReadyPromise = + accessControl && typeof accessControl.ensureReady === "function" + ? Promise.resolve() + .then(() => accessControl.ensureReady()) + .catch((error) => { + userLogger.error( + "[Application] Failed to refresh admin lists after login:", + error, + ); + throw error; + }) + : null; + const profileStatePromise = Promise.resolve(postLoginPromise) - .then((postLogin) => { + .then(async (postLogin) => { + // 1. Relays and Profile are now loaded (sequentially or efficiently by authService) const nextProfile = postLogin?.profile || cachedProfile; if (activePubkey && nextProfile) { this.updateActiveProfileUI(activePubkey, nextProfile); diff --git a/js/services/authService.js b/js/services/authService.js index 129e78f1..531778b4 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -697,79 +697,83 @@ export default class AuthService { return { detail, completionPromise: Promise.resolve(detail) }; } + const runOperation = async (operation) => { + try { + const value = await operation.promise; + const resolved = operation.onFulfilled(value); + detail[operation.name] = resolved; + if (typeof operation.emit === "function") { + operation.emit(resolved, null); + } + return resolved; + } catch (error) { + const resolved = operation.onRejected(error); + detail[operation.name] = resolved; + if (typeof operation.emit === "function") { + operation.emit(resolved, error); + } + return resolved; + } + }; + const schedule = (callback) => Promise.resolve().then(() => callback()); - const operations = []; - - if (!deferBlocks) { - const blocksOperation = this.createBlocksLoadOperation(activePubkey, schedule); - if (blocksOperation) { - operations.push(blocksOperation); + const completionPromise = (async () => { + if (this.relayManager && typeof this.relayManager.loadRelayList === "function") { + const relayOp = { + name: "relaysLoaded", + promise: schedule(() => this.relayManager.loadRelayList(activePubkey)), + onFulfilled: () => true, + onRejected: (error) => { + this.log("[AuthService] Failed to load relay list", error); + return false; + }, + emit: (value, error) => { + this.emit("relaysLoaded", { + pubkey: activePubkey, + relaysLoaded: value, + error: error ?? null, + }); + }, + }; + await runOperation(relayOp); } - } - if (this.relayManager && typeof this.relayManager.loadRelayList === "function") { - operations.push({ - name: "relaysLoaded", - promise: schedule(() => this.relayManager.loadRelayList(activePubkey)), - onFulfilled: () => true, - onRejected: (error) => { - this.log("[AuthService] Failed to load relay list", error); - return false; - }, - emit: (value, error) => { - this.emit("relaysLoaded", { - pubkey: activePubkey, - relaysLoaded: value, - error: error ?? null, + const concurrentOps = []; + + if (!deferBlocks) { + const blocksOperation = this.createBlocksLoadOperation(activePubkey, schedule); + if (blocksOperation) { + concurrentOps.push(blocksOperation); + } + } + + concurrentOps.push({ + name: "profile", + promise: schedule(() => this.loadOwnProfile(activePubkey)), + onFulfilled: (value) => { + const profile = value || FALLBACK_PROFILE; + this.setProfileCacheEntry(activePubkey, profile, { + persist: true, + reason: "post-login", }); + return profile; + }, + onRejected: (error) => { + this.log("[AuthService] Failed to load own profile", error); + const fallbackProfile = cachedProfile || FALLBACK_PROFILE; + this.setProfileCacheEntry(activePubkey, fallbackProfile, { + persist: true, + reason: "post-login-fallback", + }); + return fallbackProfile; }, }); - } - operations.push({ - name: "profile", - promise: schedule(() => this.loadOwnProfile(activePubkey)), - onFulfilled: (value) => { - const profile = value || FALLBACK_PROFILE; - this.setProfileCacheEntry(activePubkey, profile, { - persist: true, - reason: "post-login", - }); - return profile; - }, - onRejected: (error) => { - this.log("[AuthService] Failed to load own profile", error); - const fallbackProfile = cachedProfile || FALLBACK_PROFILE; - this.setProfileCacheEntry(activePubkey, fallbackProfile, { - persist: true, - reason: "post-login-fallback", - }); - return fallbackProfile; - }, - }); + await Promise.allSettled(concurrentOps.map(runOperation)); - const wrappedPromises = operations.map((operation) => - operation.promise - .then((value) => { - const resolved = operation.onFulfilled(value); - detail[operation.name] = resolved; - if (typeof operation.emit === "function") { - operation.emit(resolved, null); - } - return resolved; - }) - .catch((error) => { - const resolved = operation.onRejected(error); - detail[operation.name] = resolved; - if (typeof operation.emit === "function") { - operation.emit(resolved, error); - } - return resolved; - }), - ); - - const completionPromise = Promise.allSettled(wrappedPromises).then(() => detail); + return detail; + })(); return { detail, completionPromise }; } diff --git a/tests/auth-service.test.mjs b/tests/auth-service.test.mjs index b747971c..e89c8406 100644 --- a/tests/auth-service.test.mjs +++ b/tests/auth-service.test.mjs @@ -13,6 +13,7 @@ import { setActiveProfilePubkey, getProfileCacheMap, } from "../js/state/cache.js"; +import { profileCache } from "../js/state/profileCache.js"; if (typeof globalThis.window === "undefined") { globalThis.window = {}; @@ -61,6 +62,12 @@ function resetState() { if (cache && typeof cache.clear === "function") { cache.clear(); } + if (profileCache && typeof profileCache.reset === "function") { + profileCache.reset(); + } + if (typeof localStorage !== "undefined") { + localStorage.clear(); + } } await (async () => { @@ -94,13 +101,15 @@ await (async () => { const promise = service.applyPostLoginState(); - await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 10)); - assert.deepEqual(calls, ["blocks:start", "relays:start", "profile:start"]); + assert.deepEqual(calls, ["relays:start", "blocks:start", "profile:start"]); resolveBlocks(); - const detail = await promise; + const result = await promise; + const detail = result.detail; + await result.completionPromise; assert.equal(detail.pubkey, SAMPLE_PUBKEY); assert.equal(detail.blocksLoaded, true); @@ -130,12 +139,22 @@ await (async () => { service.loadOwnProfile = () => Promise.reject(new Error("profile failed")); - const detail = await service.applyPostLoginState(); + const result = service.applyPostLoginState(); + const detail = result.detail; + await result.completionPromise; assert.equal(detail.pubkey, SAMPLE_PUBKEY); assert.equal(detail.blocksLoaded, false); assert.equal(detail.relaysLoaded, false); - assert.equal(detail.profile, null); + assert.deepEqual(detail.profile, { + name: "Unknown", + picture: "assets/svg/default-profile.svg", + about: "", + website: "", + banner: "", + lud16: "", + lud06: "", + }); assert.equal(logs.length, 3); assert(logs.some(({ message }) => message.includes("block list"))); @@ -147,13 +166,23 @@ await (async () => { resetState(); const service = new AuthService(); - const detail = await service.applyPostLoginState(); + const result = service.applyPostLoginState(); + const detail = result.detail; + await result.completionPromise; assert.deepEqual(detail, { pubkey: null, blocksLoaded: false, relaysLoaded: false, - profile: null, + profile: { + name: "Unknown", + picture: "assets/svg/default-profile.svg", + about: "", + website: "", + banner: "", + lud16: "", + lud06: "", + }, }); })(); @@ -168,7 +197,10 @@ await (async () => { profile: { name: "Stub" }, }; - service.applyPostLoginState = async () => postLogin; + service.applyPostLoginState = () => ({ + detail: postLogin, + completionPromise: Promise.resolve(postLogin), + }); const detail = await service.login(SAMPLE_PUBKEY);