diff --git a/js/app.js b/js/app.js index 9aefbfa6..29816a9f 100644 --- a/js/app.js +++ b/js/app.js @@ -2698,7 +2698,7 @@ class Application { } const messageContext = - reason === "login" && postLoginResult?.blocksLoaded !== false + reason === "login" && postLoginResult?.blocksLoaded === true ? "Applying your filters…" : "Refreshing videos…"; @@ -4285,56 +4285,96 @@ class Application { authLoadingState: this.authLoadingState, }); - const dmTasks = []; - if (activePubkey) { - dmTasks.push( - Promise.resolve() - .then(() => - this.nostrService.loadDirectMessages({ - actorPubkey: activePubkey, - limit: 50, - initialLoad: true, - }), - ) - .catch((error) => { - devLogger.warn( - "[Application] Failed to sync direct messages during login:", - error, - ); - throw error; - }), - ); + 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; - if (typeof this.nostrService.ensureDirectMessageSubscription === "function") { + const scheduleAfterFirstRender = () => + new Promise((resolve) => { + if (typeof requestAnimationFrame === "function") { + requestAnimationFrame(() => requestAnimationFrame(resolve)); + } else { + setTimeout(resolve, 0); + } + }); + + const tier1ReadyPromise = Promise.allSettled( + [postLoginPromise, accessControlReadyPromise].filter(Boolean), + ); + + const startDmHydration = () => { + const dmTasks = []; + if (activePubkey) { dmTasks.push( Promise.resolve() .then(() => - this.nostrService.ensureDirectMessageSubscription({ + this.nostrService.loadDirectMessages({ actorPubkey: activePubkey, + limit: 50, + decryptLimit: 50, + initialLoad: true, }), ) .catch((error) => { devLogger.warn( - "[Application] Failed to subscribe to direct messages during login:", + "[Application] Failed to sync direct messages during login:", error, ); throw error; }), ); - } - } - const dmStatePromise = dmTasks.length - ? Promise.allSettled(dmTasks).then((results) => { - const success = results.every( - (result) => result.status === "fulfilled", + 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; + }), ); - this.updateAuthLoadingState({ - dms: success ? "ready" : "error", - }); - return success; - }) - : Promise.resolve(this.updateAuthLoadingState({ dms: "idle" })); + } + } + + 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 = tier1ReadyPromise + .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)) @@ -4382,46 +4422,32 @@ class Application { }); const listTasks = []; - 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; - if (activePubkey) { + if (activePubkey && typeof this.authService.loadBlocksForPubkey === "function") { listTasks.push( - postLoginPromise.then((postLogin) => { - const blocksLoaded = postLogin?.blocksLoaded !== false; - const relaysLoaded = postLogin?.relaysLoaded !== false; - if (!blocksLoaded || !relaysLoaded) { - const listError = new Error("Post-login list hydration incomplete."); - listError.blocksLoaded = blocksLoaded; - listError.relaysLoaded = relaysLoaded; - throw listError; - } - return { blocksLoaded, relaysLoaded }; - }), + tier1ReadyPromise + .then(() => this.authService.loadBlocksForPubkey(activePubkey)) + .catch((error) => { + devLogger.warn( + "[Application] Failed to load blocks during login:", + error, + ); + throw error; + }), ); } - if (accessControlReadyPromise) { - listTasks.push(accessControlReadyPromise); - } - if ( activePubkey && subscriptions && typeof subscriptions.ensureLoaded === "function" ) { listTasks.push( - subscriptions - .ensureLoaded(activePubkey, { allowPermissionPrompt: false }) + tier1ReadyPromise + .then(() => + subscriptions.ensureLoaded(activePubkey, { + allowPermissionPrompt: false, + }), + ) .catch((error) => { devLogger.warn( "[Application] Failed to load subscriptions during login:", diff --git a/js/services/authService.js b/js/services/authService.js index 592b9268..df45f56d 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -623,7 +623,9 @@ export default class AuthService { postLoginError: null, }; - const { detail: postLoginDetail, completionPromise } = this.applyPostLoginState(); + const { detail: postLoginDetail, completionPromise } = this.applyPostLoginState({ + deferBlocks: true, + }); detail.postLogin = postLoginDetail; const postLoginPromise = Promise.resolve(completionPromise) @@ -680,13 +682,13 @@ export default class AuthService { return detail; } - applyPostLoginState() { + applyPostLoginState({ deferBlocks = false } = {}) { const activePubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey(); const cachedEntry = activePubkey ? this.getProfileCacheEntry(activePubkey) : null; const cachedProfile = cachedEntry?.profile || FALLBACK_PROFILE; const detail = { pubkey: activePubkey || null, - blocksLoaded: false, + blocksLoaded: deferBlocks ? null : false, relaysLoaded: false, profile: cachedProfile, }; @@ -699,23 +701,11 @@ export default class AuthService { const operations = []; - if (this.userBlocks && typeof this.userBlocks.loadBlocks === "function") { - operations.push({ - name: "blocksLoaded", - promise: schedule(() => this.userBlocks.loadBlocks(activePubkey)), - onFulfilled: () => true, - onRejected: (error) => { - this.log("[AuthService] Failed to load block list", error); - return false; - }, - emit: (value, error) => { - this.emit("blocksLoaded", { - pubkey: activePubkey, - blocksLoaded: value, - error: error ?? null, - }); - }, - }); + if (!deferBlocks) { + const blocksOperation = this.createBlocksLoadOperation(activePubkey, schedule); + if (blocksOperation) { + operations.push(blocksOperation); + } } if (this.relayManager && typeof this.relayManager.loadRelayList === "function") { @@ -784,6 +774,53 @@ export default class AuthService { return { detail, completionPromise }; } + createBlocksLoadOperation(activePubkey, schedule = (callback) => Promise.resolve().then(() => callback())) { + if (!this.userBlocks || typeof this.userBlocks.loadBlocks !== "function") { + return null; + } + + return { + name: "blocksLoaded", + promise: schedule(() => this.userBlocks.loadBlocks(activePubkey)), + onFulfilled: () => true, + onRejected: (error) => { + this.log("[AuthService] Failed to load block list", error); + return false; + }, + emit: (value, error) => { + this.emit("blocksLoaded", { + pubkey: activePubkey, + blocksLoaded: value, + error: error ?? null, + }); + }, + }; + } + + async loadBlocksForPubkey(pubkey) { + const normalized = this.normalizeHexPubkey(pubkey) || + (typeof pubkey === "string" ? pubkey.trim() : ""); + if (!normalized) { + return false; + } + + const operation = this.createBlocksLoadOperation(normalized); + if (!operation) { + return false; + } + + try { + const value = await operation.promise; + const resolved = operation.onFulfilled(value); + operation.emit?.(resolved, null); + return resolved; + } catch (error) { + const resolved = operation.onRejected(error); + operation.emit?.(resolved, error); + return resolved; + } + } + async logout() { if (this.nostrClient && typeof this.nostrClient.logout === "function") { try { diff --git a/js/services/nostrService.js b/js/services/nostrService.js index e751cb60..40bbec66 100644 --- a/js/services/nostrService.js +++ b/js/services/nostrService.js @@ -982,6 +982,8 @@ export class NostrService { : Number.isFinite(decryptLimitCandidate) && decryptLimitCandidate > 0 ? Math.floor(decryptLimitCandidate) : null; + const sinceCandidate = Number(options.since); + const untilCandidate = Number(options.until); if (resolvedDecryptLimit && resolvedDecryptLimit > resolvedLimit) { resolvedDecryptLimit = resolvedLimit; @@ -1006,6 +1008,27 @@ export class NostrService { this.dmActorPubkey = normalizedActor; + let resolvedSince = + Number.isFinite(sinceCandidate) && sinceCandidate >= 0 + ? Math.floor(sinceCandidate) + : null; + const resolvedUntil = + Number.isFinite(untilCandidate) && untilCandidate >= 0 + ? Math.floor(untilCandidate) + : null; + if (resolvedSince === null && initialLoad) { + let latestTimestamp = 0; + for (const message of Array.isArray(this.dmMessages) ? this.dmMessages : []) { + const timestamp = Number(message?.timestamp); + if (Number.isFinite(timestamp) && timestamp > latestTimestamp) { + latestTimestamp = timestamp; + } + } + if (latestTimestamp > 0) { + resolvedSince = Math.max(latestTimestamp - 60, 0); + } + } + let messages = []; try { messages = await this.nostrClient.listDirectMessages(normalizedActor, { @@ -1013,6 +1036,8 @@ export class NostrService { ...options, limit: resolvedLimit, decryptLimit: resolvedDecryptLimit, + ...(resolvedSince !== null ? { since: resolvedSince } : {}), + ...(resolvedUntil !== null ? { until: resolvedUntil } : {}), onMessage: (message) => { this.applyDirectMessage(message, { reason: "load-incremental" }); }, diff --git a/js/ui/videoListViewController.js b/js/ui/videoListViewController.js index 46ae2f44..2879e96a 100644 --- a/js/ui/videoListViewController.js +++ b/js/ui/videoListViewController.js @@ -79,7 +79,7 @@ export default class VideoListViewController { } const messageContext = - reason === "login" && postLoginResult?.blocksLoaded !== false + reason === "login" && postLoginResult?.blocksLoaded === true ? "Applying your filters…" : "Refreshing videos…";