diff --git a/js/subscriptions.js b/js/subscriptions.js index 4a44bc12..e4d28053 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -215,50 +215,6 @@ const listVideoViewEventsApi = (pointer, options) => const subscribeVideoViewEventsApi = (pointer, options) => subscribeVideoViewEvents(nostrClient, pointer, options); -class TinyEventEmitter { - constructor() { - this.listeners = new Map(); - } - - on(eventName, handler) { - if (typeof eventName !== "string" || typeof handler !== "function") { - return () => {}; - } - - if (!this.listeners.has(eventName)) { - this.listeners.set(eventName, new Set()); - } - - const handlers = this.listeners.get(eventName); - handlers.add(handler); - - return () => { - handlers.delete(handler); - if (!handlers.size) { - this.listeners.delete(eventName); - } - }; - } - - emit(eventName, detail) { - const handlers = this.listeners.get(eventName); - if (!handlers || !handlers.size) { - return; - } - - for (const handler of Array.from(handlers)) { - try { - handler(detail); - } catch (error) { - userLogger.warn( - `[SubscriptionsManager] listener for "${eventName}" threw an error`, - error, - ); - } - } - } -} - /** * Manages the user's subscription list (kind=30000 follow set) *privately*, * using encrypted NIP-51 tag arrays (NIP-04/NIP-44) for the content field. @@ -281,7 +237,6 @@ class SubscriptionsManager { this.scheduledRefreshDetail = null; this.isRunningFeed = false; this.hasRenderedOnce = false; - this.emitter = new TinyEventEmitter(); this.ensureNostrServiceListener(); profileCache.subscribe((event, detail) => { @@ -333,10 +288,6 @@ class SubscriptionsManager { profileCache.set("subscriptions", Array.from(this.subscribedPubkeys)); } - on(eventName, handler) { - return this.emitter.on(eventName, handler); - } - async updateFromRelays(userPubkey) { if (!userPubkey) return; @@ -507,10 +458,6 @@ class SubscriptionsManager { } if (changed) { - this.emitter.emit("change", { - action: "sync", - subscribedPubkeys: Array.from(this.subscribedPubkeys), - }); devLogger.log("[SubscriptionsManager] Subscription list updated from relays, refreshing feed."); this.refreshActiveFeed({ reason: "subscription-update-background" }).catch((error) => { userLogger.warn( @@ -533,7 +480,6 @@ class SubscriptionsManager { this.lastRunOptions = null; this.lastResult = null; this.hasRenderedOnce = false; - this.emitter.emit("change", { action: "reset", subscribedPubkeys: [] }); } async ensureLoaded(actorHex) { @@ -698,11 +644,6 @@ class SubscriptionsManager { } this.subscribedPubkeys.add(normalizedChannel); this.saveToCache(userPubkey); - this.emitter.emit("change", { - action: "add", - channel: normalizedChannel, - subscribedPubkeys: Array.from(this.subscribedPubkeys), - }); await this.publishSubscriptionList(userPubkey); this.refreshActiveFeed({ reason: "subscription-update" }).catch((error) => { userLogger.warn( @@ -728,11 +669,6 @@ class SubscriptionsManager { } this.subscribedPubkeys.delete(normalizedChannel); this.saveToCache(userPubkey); - this.emitter.emit("change", { - action: "remove", - channel: normalizedChannel, - subscribedPubkeys: Array.from(this.subscribedPubkeys), - }); await this.publishSubscriptionList(userPubkey); this.refreshActiveFeed({ reason: "subscription-update" }).catch((error) => { userLogger.warn( diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 7ab78f04..fe57b464 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -1407,24 +1407,6 @@ export class ProfileModalController { this.addAccountButtonState = null; this.adminEmptyMessages = new Map(); this.hashtagPreferencesUnsubscribe = null; - - if ( - this.subscriptionsService && - typeof this.subscriptionsService.on === "function" - ) { - this.subscriptionsService.on("change", () => { - void this.populateSubscriptionsList(); - }); - } - - if ( - this.services.userBlocks && - typeof this.services.userBlocks.on === "function" - ) { - this.services.userBlocks.on("change", () => { - this.populateBlockedList(); - }); - } } async load() { @@ -6783,16 +6765,11 @@ export class ProfileModalController { const shouldStayInMenu = keepMenuView && isMobile; this.setMobileView(shouldStayInMenu ? "menu" : "pane"); - const activeHex = this.normalizeHexPubkey(this.getActivePubkey()); - if (target === "history") { void this.populateProfileWatchHistory(); } else if (target === "relays") { this.populateProfileRelays(); - void this.refreshRelayHealthPanel({ - forceRefresh: true, - reason: "pane-select", - }); + void this.refreshRelayHealthPanel({ forceRefresh: true, reason: "pane-select" }); } else if (target === "messages") { this.resumeProfileMessages(); void this.populateProfileMessages({ reason: "pane-select" }); @@ -6804,14 +6781,8 @@ export class ProfileModalController { } else if (target === "hashtags") { this.populateHashtagPreferences(); } else if (target === "subscriptions") { - if (activeHex && this.subscriptionsService) { - this.subscriptionsService.loadSubscriptions(activeHex).catch(noop); - } void this.populateSubscriptionsList(); } else if (target === "blocked") { - if (activeHex && this.services.userBlocks) { - this.services.userBlocks.loadBlocks(activeHex).catch(noop); - } this.populateBlockedList(); } else if (target === "safety") { this.refreshModerationSettingsUi(); @@ -8037,6 +8008,17 @@ export class ProfileModalController { if (Array.isArray(subscriptions) && subscriptions.length) { sourceEntries = subscriptions; } else { + if (typeof service.ensureLoaded === "function") { + try { + await service.ensureLoaded(activeHex); + } catch (error) { + devLogger.warn( + "[profileModal] Failed to ensure subscriptions before populating subscriptions list:", + error, + ); + } + } + if (typeof service.getSubscribedAuthors === "function") { try { sourceEntries = service.getSubscribedAuthors() || []; @@ -12717,21 +12699,14 @@ export class ProfileModalController { this.setActivePubkey(activePubkey); } - const walletPromise = this.hydrateActiveWalletSettings(activePubkey); - const adminPromise = this.refreshAdminPaneState().catch((error) => { - userLogger.warn("Failed to refresh admin pane after login:", error); - }); + await this.hydrateActiveWalletSettings(activePubkey); this.renderSavedProfiles(); - // Trigger aggressive parallel fetches - if (activePubkey) { - if (this.services.userBlocks) { - this.services.userBlocks.loadBlocks(activePubkey).catch(noop); - } - if (this.subscriptionsService) { - this.subscriptionsService.loadSubscriptions(activePubkey).catch(noop); - } + try { + await this.refreshAdminPaneState(); + } catch (error) { + userLogger.warn("Failed to refresh admin pane after login:", error); } this.populateBlockedList(); @@ -12743,10 +12718,8 @@ export class ProfileModalController { this.handleActiveDmIdentityChanged(activePubkey); void this.refreshDmRelayPreferences({ force: true }); - // Ensure critical state is settled if possible, but don't block initial rendering - Promise.all([walletPromise, adminPromise, postLoginPromise]) + postLoginPromise .then(() => { - // Re-run population to ensure any late-arriving data is reflected this.populateBlockedList(); void this.populateSubscriptionsList(); void this.populateFriendsList(); diff --git a/js/userBlocks.js b/js/userBlocks.js index cbb13a46..dcaa315e 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -737,6 +737,8 @@ class UserBlockListManager { blockedPubkeys: Array.from(this.blockedPubkeys), source: "local", }); + emitStatus({ status: "settled" }); + return; } const applyBlockedPubkeys = (nextValues, meta = {}) => { diff --git a/tests/subscriptions-manager.test.mjs b/tests/subscriptions-manager.test.mjs index 5a13b993..5462966c 100644 --- a/tests/subscriptions-manager.test.mjs +++ b/tests/subscriptions-manager.test.mjs @@ -4,13 +4,12 @@ import assert from "node:assert/strict"; import { JSDOM } from "jsdom"; import { nostrClient } from "../js/nostrClientFacade.js"; -import { getActiveSigner, setActiveSigner, clearActiveSigner } from "../js/nostr/client.js"; +import { getActiveSigner, setActiveSigner } from "../js/nostr/client.js"; import { subscriptions } from "../js/subscriptions.js"; import { setApplication } from "../js/applicationContext.js"; import nostrService from "../js/services/nostrService.js"; import moderationService from "../js/services/moderationService.js"; import { getNostrEventSchema, NOTE_TYPES } from "../js/nostrEventSchemas.js"; -import { profileCache } from "../js/state/profileCache.js"; test("loadSubscriptions aggregates relay results when one rejects", async () => { const SubscriptionsManager = subscriptions.constructor; @@ -72,8 +71,6 @@ test("loadSubscriptions aggregates relay results when one rejects", async () => const decryptCalls = []; globalThis.window.nostr = { - getPublicKey: async () => "0000000000000000000000000000000000000000000000000000000000000123", - signEvent: async () => ({}), nip04: { async decrypt(_pubkey, ciphertext) { decryptCalls.push(ciphertext); @@ -86,7 +83,7 @@ test("loadSubscriptions aggregates relay results when one rejects", async () => }; try { - await manager.loadSubscriptions("0000000000000000000000000000000000000000000000000000000000000123"); + await manager.loadSubscriptions("user-pubkey-123"); assert.deepEqual( Array.from(manager.subscribedPubkeys), @@ -133,9 +130,6 @@ test("loadSubscriptions aggregates relay results when one rejects", async () => delete globalThis.window; } localStorage.clear(); - profileCache.memoryCache.clear(); - profileCache.activePubkey = null; - clearActiveSigner(); } }); @@ -176,7 +170,7 @@ test("loadSubscriptions queries the correct subscription list kind", async () => nostrClient.ensureExtensionPermissions = async () => ({ ok: true }); try { - await manager.loadSubscriptions("0000000000000000000000000000000000000000000000000000000000000002"); + await manager.loadSubscriptions("user-pubkey-legacy"); assert.ok( capturedFilters.length > 0, @@ -216,9 +210,6 @@ test("loadSubscriptions queries the correct subscription list kind", async () => delete globalThis.window; } localStorage.clear(); - profileCache.memoryCache.clear(); - profileCache.activePubkey = null; - clearActiveSigner(); } }); @@ -235,6 +226,12 @@ test("loadSubscriptions falls back to nip44 when hinted", async () => { const originalPool = nostrClient.pool; const originalEnsurePermissions = nostrClient.ensureExtensionPermissions; + const hadWindow = typeof globalThis.window !== "undefined"; + if (!hadWindow) { + globalThis.window = {}; + } + const originalWindowNostr = globalThis.window.nostr; + const relayUrls = ["wss://relay-nip44.example"]; nostrClient.relays = relayUrls; nostrClient.writeRelays = relayUrls; @@ -255,20 +252,23 @@ test("loadSubscriptions falls back to nip44 when hinted", async () => { nostrClient.ensureExtensionPermissions = async () => ({ ok: true }); const decryptCalls = { nip04: 0, nip44: 0 }; - setActiveSigner({ - pubkey: "0000000000000000000000000000000000000000000000000000000000000005", - async nip04Decrypt() { - decryptCalls.nip04 += 1; - throw new Error("nip04 unavailable"); + globalThis.window.nostr = { + nip04: { + async decrypt() { + decryptCalls.nip04 += 1; + throw new Error("nip04 unavailable"); + }, }, - async nip44Decrypt(_pubkey, ciphertext) { - decryptCalls.nip44 += 1; - return JSON.stringify([["p", "pub-nip44", ciphertext]]); + nip44: { + async decrypt(_pubkey, ciphertext) { + decryptCalls.nip44 += 1; + return JSON.stringify([["p", "pub-nip44", ciphertext]]); + }, }, - }); + }; try { - await manager.loadSubscriptions("0000000000000000000000000000000000000000000000000000000000000005"); + await manager.loadSubscriptions("user-pubkey-123"); assert.deepEqual( Array.from(manager.subscribedPubkeys), @@ -292,10 +292,15 @@ test("loadSubscriptions falls back to nip44 when hinted", async () => { nostrClient.writeRelays = originalWriteRelays; nostrClient.pool = originalPool; nostrClient.ensureExtensionPermissions = originalEnsurePermissions; + if (typeof originalWindowNostr === "undefined") { + delete globalThis.window.nostr; + } else { + globalThis.window.nostr = originalWindowNostr; + } + if (!hadWindow) { + delete globalThis.window; + } localStorage.clear(); - profileCache.memoryCache.clear(); - profileCache.activePubkey = null; - clearActiveSigner(); } }); @@ -312,6 +317,12 @@ test("loadSubscriptions handles nip44.v2 decryptors", async () => { const originalPool = nostrClient.pool; const originalEnsurePermissions = nostrClient.ensureExtensionPermissions; + const hadWindow = typeof globalThis.window !== "undefined"; + if (!hadWindow) { + globalThis.window = {}; + } + const originalWindowNostr = globalThis.window.nostr; + const relayUrls = ["wss://relay-nip44-v2.example"]; nostrClient.relays = relayUrls; nostrClient.writeRelays = relayUrls; @@ -332,16 +343,19 @@ test("loadSubscriptions handles nip44.v2 decryptors", async () => { nostrClient.ensureExtensionPermissions = async () => ({ ok: true }); const decryptCalls = { nip04: 0, nip44: 0, nip44_v2: 0 }; - setActiveSigner({ - pubkey: "0000000000000000000000000000000000000000000000000000000000000006", - async nip44Decrypt(_pubkey, ciphertext) { - decryptCalls.nip44_v2 += 1; - return JSON.stringify([["p", "pub-nip44-v2"], ["t", ciphertext]]); + globalThis.window.nostr = { + nip44: { + v2: { + async decrypt(_pubkey, ciphertext) { + decryptCalls.nip44_v2 += 1; + return JSON.stringify([["p", "pub-nip44-v2"], ["t", ciphertext]]); + }, + }, }, - }); + }; try { - await manager.loadSubscriptions("0000000000000000000000000000000000000000000000000000000000000006"); + await manager.loadSubscriptions("user-pubkey-123"); assert.deepEqual( Array.from(manager.subscribedPubkeys), @@ -370,10 +384,15 @@ test("loadSubscriptions handles nip44.v2 decryptors", async () => { nostrClient.writeRelays = originalWriteRelays; nostrClient.pool = originalPool; nostrClient.ensureExtensionPermissions = originalEnsurePermissions; + if (typeof originalWindowNostr === "undefined") { + delete globalThis.window.nostr; + } else { + globalThis.window.nostr = originalWindowNostr; + } + if (!hadWindow) { + delete globalThis.window; + } localStorage.clear(); - profileCache.memoryCache.clear(); - profileCache.activePubkey = null; - clearActiveSigner(); } }); @@ -390,6 +409,12 @@ test("loadSubscriptions prefers nip44 decryptors when both are available", async const originalPool = nostrClient.pool; const originalEnsurePermissions = nostrClient.ensureExtensionPermissions; + const hadWindow = typeof globalThis.window !== "undefined"; + if (!hadWindow) { + globalThis.window = {}; + } + const originalWindowNostr = globalThis.window.nostr; + const relayUrls = ["wss://relay-nip44-pref.example"]; nostrClient.relays = relayUrls; nostrClient.writeRelays = relayUrls; @@ -410,20 +435,25 @@ test("loadSubscriptions prefers nip44 decryptors when both are available", async nostrClient.ensureExtensionPermissions = async () => ({ ok: true }); const decryptCalls = { nip04: 0, nip44: 0 }; - setActiveSigner({ - pubkey: "0000000000000000000000000000000000000000000000000000000000000003", - async nip04Decrypt() { - decryptCalls.nip04 += 1; - return JSON.stringify([["p", "pub-nip04"]]); + globalThis.window.nostr = { + nip04: { + async decrypt() { + decryptCalls.nip04 += 1; + return JSON.stringify([["p", "pub-nip04"]]); + }, }, - async nip44Decrypt(_pubkey, ciphertext) { - decryptCalls.nip44 += 1; - return JSON.stringify([["p", "pub-nip44-pref", ciphertext]]); + nip44: { + v2: { + async decrypt(_pubkey, ciphertext) { + decryptCalls.nip44 += 1; + return JSON.stringify([["p", "pub-nip44-pref", ciphertext]]); + }, + }, }, - }); + }; try { - await manager.loadSubscriptions("0000000000000000000000000000000000000000000000000000000000000003"); + await manager.loadSubscriptions("user-pubkey-123"); assert.deepEqual( Array.from(manager.subscribedPubkeys), @@ -437,10 +467,15 @@ test("loadSubscriptions prefers nip44 decryptors when both are available", async nostrClient.writeRelays = originalWriteRelays; nostrClient.pool = originalPool; nostrClient.ensureExtensionPermissions = originalEnsurePermissions; + if (typeof originalWindowNostr === "undefined") { + delete globalThis.window.nostr; + } else { + globalThis.window.nostr = originalWindowNostr; + } + if (!hadWindow) { + delete globalThis.window; + } localStorage.clear(); - profileCache.memoryCache.clear(); - profileCache.activePubkey = null; - clearActiveSigner(); } }); @@ -491,7 +526,7 @@ test( const decryptCalls = []; setActiveSigner({ - pubkey: "0000000000000000000000000000000000000000000000000000000000000004", + pubkey: "user-pubkey-123", async nip04Decrypt(pubkey, ciphertext) { decryptCalls.push({ pubkey, ciphertext }); return JSON.stringify([["p", "pub-direct"]]); @@ -499,13 +534,13 @@ test( }); try { - await manager.loadSubscriptions("0000000000000000000000000000000000000000000000000000000000000004"); + await manager.loadSubscriptions("user-pubkey-123"); assert.equal(permissionCalls, 0, "should not request extension permissions"); assert.equal(decryptCalls.length, 1, "signer decrypt should be invoked once"); assert.deepEqual( decryptCalls[0], - { pubkey: "0000000000000000000000000000000000000000000000000000000000000004", ciphertext: "cipher-direct" }, + { pubkey: "user-pubkey-123", ciphertext: "cipher-direct" }, "signer decrypt should receive the expected arguments", ); assert.deepEqual( @@ -528,9 +563,6 @@ test( delete globalThis.window; } localStorage.clear(); - profileCache.memoryCache.clear(); - profileCache.activePubkey = null; - clearActiveSigner(); } }, );