From dc4345a64b9735c8b0f080f155b5badc02f242f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Fri, 16 Jan 2026 17:04:05 +0100 Subject: [PATCH] perf: Make NIP-17 inbox sync on-demand for optimal login performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit eliminates automatic inbox initialization on login, preventing unwanted network requests and heavy I/O operations. Inbox sync now only activates when users explicitly enable it. ## Problem When logging in, the app would automatically: - Initialize gift wrap service immediately - Auto-enable inbox sync without user consent - Load encrypted content from Dexie - Wait up to 1 second for cache readiness - Fetch inbox relay lists from network - Subscribe to gift wrap events - Open persistent relay connections This caused: - App hangs during login (network/IO blocking) - Unwanted network activity before user opts in - Poor performance on initial load - Unnecessary resource consumption when DMs not needed ## Solution ### 1. On-Demand Initialization (useAccountSync.ts) **Before**: Auto-init and auto-enable on every login **After**: Watch settings$ and only init when user enables ```typescript // Only initialize when user explicitly enables inbox sync const settingsSub = giftWrapService.settings$.subscribe((settings) => { if (settings.enabled && giftWrapService.userPubkey !== pubkey) { giftWrapService.init(pubkey, signer); } }); ``` ### 2. Early Exit for Disabled State (gift-wrap.ts) **Before**: Always loaded cache and relays, then checked enabled flag **After**: Check enabled FIRST, exit early if disabled ```typescript async init(pubkey: string, signer: ISigner | null) { // Set basic properties this.userPubkey = pubkey; this.signer = signer; // Early exit if disabled (prevents expensive operations) if (!this.settings$.value.enabled) { return; } // Only do expensive operations when enabled await getStoredEncryptedContentIds(); await this.waitForCacheReady(); this.loadInboxRelays(); // ... } ``` ### 3. Updated Documentation - Clarified on-demand initialization flow - Updated lifecycle documentation with performance notes - Changed "auto-enable" section to "on-demand" section ## Performance Impact **Login Performance**: - ✅ No automatic Dexie reads - ✅ No cache waiting (up to 1s saved) - ✅ No network requests for inbox relays - ✅ No relay subscriptions until needed - ✅ Instant login when DMs not needed **User Control**: - Users explicitly opt-in via InboxViewer toggle - Clear UI feedback about enabling inbox sync - No surprise network activity **When Enabled**: - Full functionality identical to before - All optimizations from previous commits preserved ## Testing - ✅ All 864 tests pass - ✅ Build succeeds with no errors - ✅ Verified on-demand initialization flow - ✅ Confirmed no auto-init on login ## Files Changed - `src/hooks/useAccountSync.ts` - Watch settings, init only when enabled - `src/services/gift-wrap.ts` - Early exit if disabled, expose userPubkey - `src/components/InboxViewer.tsx` - Updated comments - `docs/gift-wrap-architecture.md` - Updated flow and lifecycle docs Co-Authored-By: Claude Sonnet 4.5 --- docs/gift-wrap-architecture.md | 47 +++++++++++++++++++--------------- src/components/InboxViewer.tsx | 5 ++-- src/hooks/useAccountSync.ts | 38 +++++++++++---------------- src/services/gift-wrap.ts | 21 ++++++++++----- 4 files changed, 59 insertions(+), 52 deletions(-) diff --git a/docs/gift-wrap-architecture.md b/docs/gift-wrap-architecture.md index 0331596..ec2b14d 100644 --- a/docs/gift-wrap-architecture.md +++ b/docs/gift-wrap-architecture.md @@ -124,14 +124,15 @@ Chat Component ### Receiving Messages (Inbox Flow) -1. **Account Login** → `useAccountSync` calls `giftWrapService.init(pubkey, signer)` -2. **Fetch Inbox Relays** → Load kind 10050 from user's outbox relays -3. **Subscribe to Gift Wraps** → Open subscription to inbox relays for `kind 1059` with `#p` = user pubkey -4. **Gift Wrap Arrival** → EventStore receives event → GiftWrapService detects new gift wrap -5. **Decrypt** (if auto-decrypt enabled) → Call `unlockGiftWrap(event, signer)` -6. **Extract Rumor** → Get kind 14 DM from gift wrap inner content -7. **Group into Conversations** → Compute conversation ID from participants → Update `conversations$` observable -8. **UI Update** → InboxViewer/ChatViewer re-renders with new messages +1. **User Enables Inbox Sync** → User toggles "Enable Inbox Sync" in InboxViewer settings +2. **Service Initialization** → `useAccountSync` detects enabled setting and calls `giftWrapService.init(pubkey, signer)` +3. **Fetch Inbox Relays** → Load kind 10050 from user's outbox relays +4. **Subscribe to Gift Wraps** → Open subscription to inbox relays for `kind 1059` with `#p` = user pubkey +5. **Gift Wrap Arrival** → EventStore receives event → GiftWrapService detects new gift wrap +6. **Decrypt** (if auto-decrypt enabled) → Call `unlockGiftWrap(event, signer)` +7. **Extract Rumor** → Get kind 14 DM from gift wrap inner content +8. **Group into Conversations** → Compute conversation ID from participants → Update `conversations$` observable +9. **UI Update** → InboxViewer/ChatViewer re-renders with new messages ### Sending Messages (Outbox Flow) @@ -161,18 +162,19 @@ Chat Component ### Lifecycle -**Init** (on account login): +**Init** (when user enables inbox sync): ```typescript giftWrapService.init(pubkey, signer) - 1. Load persisted encrypted content IDs from Dexie - 2. Wait for cache readiness (prevents race condition) - 3. Subscribe to user's kind 10050 (inbox relays) - 4. Load stored gift wraps from Dexie into EventStore - 5. Subscribe to EventStore timeline for real-time updates - 6. Open persistent relay subscription for new gift wraps + 1. Check if enabled (early return if disabled for performance) + 2. Load persisted encrypted content IDs from Dexie + 3. Wait for cache readiness (prevents race condition) + 4. Subscribe to user's kind 10050 (inbox relays) + 5. Load stored gift wraps from Dexie into EventStore + 6. Subscribe to EventStore timeline for real-time updates + 7. Open persistent relay subscription for new gift wraps ``` -**Cleanup** (on account logout): +**Cleanup** (on account logout or disable): ```typescript giftWrapService.cleanup() 1. Unsubscribe from all observables @@ -180,6 +182,8 @@ giftWrapService.cleanup() 3. Clear in-memory state ``` +**Performance Note**: Init is only called when user explicitly enables inbox sync via InboxViewer toggle. This prevents automatic network requests and heavy I/O operations on login. + ## Cache Strategy ### Encrypted Content Persistence @@ -231,13 +235,16 @@ giftWrapService.cleanup() - Clear UI feedback about why sending is blocked - Relay lists can be fetched in background without blocking UI -### Auto-Enable Inbox Sync +### On-Demand Inbox Sync -**Default**: Inbox sync is **auto-enabled** on first login to ensure users receive DMs. +**Default**: Inbox sync is **disabled** on login for optimal performance. -**Rationale**: Better UX to auto-enable with opt-out than require manual setup. +**Rationale**: Prevents automatic network requests and heavy I/O operations on login. Users must explicitly enable inbox sync to receive DMs. -**User Control**: Settings UI allows disabling inbox sync and auto-decrypt. +**User Control**: +- Enable/disable inbox sync via toggle in InboxViewer +- Configure auto-decrypt behavior in settings +- Service initializes only when explicitly enabled ### Relay List Privacy diff --git a/src/components/InboxViewer.tsx b/src/components/InboxViewer.tsx index c5a27c3..363bc55 100644 --- a/src/components/InboxViewer.tsx +++ b/src/components/InboxViewer.tsx @@ -51,8 +51,9 @@ function InboxViewer() { const [isDecryptingAll, setIsDecryptingAll] = useState(false); - // Note: Gift wrap service is now initialized globally in useAccountSync - // This ensures DM subscriptions are active even when inbox viewer isn't open + // Note: Gift wrap service initializes ON-DEMAND when user enables inbox sync + // This prevents automatic network requests and heavy I/O on login + // Toggle "Enable Inbox Sync" below to start receiving DMs // Update signer when it changes (in case user switches signers) useEffect(() => { diff --git a/src/hooks/useAccountSync.ts b/src/hooks/useAccountSync.ts index 6b9d86a..119c204 100644 --- a/src/hooks/useAccountSync.ts +++ b/src/hooks/useAccountSync.ts @@ -127,12 +127,10 @@ export function useAccountSync() { }; }, [activeAccount?.pubkey, eventStore, setActiveAccountBlossomServers]); - // Initialize gift wrap service for NIP-17 DM subscriptions when account changes + // Initialize gift wrap service ONLY when user enables inbox sync + // This ensures no automatic network requests or heavy operations on login useEffect(() => { if (!activeAccount?.pubkey) { - console.log( - "[useAccountSync] No active account, cleaning up gift wrap service", - ); giftWrapService.cleanup(); return; } @@ -140,28 +138,22 @@ export function useAccountSync() { const pubkey = activeAccount.pubkey; const signer = activeAccount.signer ?? null; - console.log( - `[useAccountSync] Initializing gift wrap service for user ${pubkey.slice(0, 8)}`, - ); - - // Initialize the service (loads inbox relays, sets up subscriptions) - giftWrapService.init(pubkey, signer); - - // Auto-enable inbox sync if not already set - // This ensures users receive DMs without manually enabling inbox - const currentSettings = giftWrapService.settings$.value; - if (!currentSettings.enabled) { - console.log( - "[useAccountSync] Auto-enabling inbox sync for NIP-17 DM subscriptions", - ); - giftWrapService.updateSettings({ enabled: true }); - } + // Watch settings changes and init when enabled + const settingsSub = giftWrapService.settings$.subscribe((settings) => { + if (settings.enabled) { + // Only init if not already initialized for this account + if (giftWrapService.userPubkey !== pubkey) { + console.log( + `[useAccountSync] Initializing gift wrap service for user ${pubkey.slice(0, 8)}`, + ); + giftWrapService.init(pubkey, signer); + } + } + }); // Cleanup on account change or logout return () => { - console.log( - `[useAccountSync] Cleaning up gift wrap service for user ${pubkey.slice(0, 8)}`, - ); + settingsSub.unsubscribe(); giftWrapService.cleanup(); }; }, [activeAccount?.pubkey, activeAccount?.signer]); diff --git a/src/services/gift-wrap.ts b/src/services/gift-wrap.ts index dadfbb0..2d17a9a 100644 --- a/src/services/gift-wrap.ts +++ b/src/services/gift-wrap.ts @@ -81,8 +81,8 @@ function saveSettings(settings: InboxSettings) { } class GiftWrapService { - /** Current user's pubkey */ - private userPubkey: string | null = null; + /** Current user pubkey (null if not initialized) */ + userPubkey: string | null = null; /** Current signer for decryption */ private signer: ISigner | null = null; @@ -192,6 +192,15 @@ class GiftWrapService { this.decryptStates$.next(new Map()); this.pendingCount$.next(0); + // Only perform expensive operations if inbox sync is enabled + // This prevents automatic network requests and heavy I/O on login + if (!this.settings$.value.enabled) { + dmDebug("GiftWrap", "Inbox sync disabled, skipping initialization"); + return; + } + + dmInfo("GiftWrap", `Initializing inbox sync for ${pubkey.slice(0, 8)}`); + // Load persisted encrypted content IDs to know which gift wraps are already decrypted this.persistedIds = await getStoredEncryptedContentIds(); @@ -225,11 +234,9 @@ class GiftWrapService { }); this.subscriptions.push(updateSub); - // If enabled, load stored gift wraps and start syncing - if (this.settings$.value.enabled) { - await this.loadStoredGiftWraps(); - this.startSync(); - } + // Load stored gift wraps and start syncing + await this.loadStoredGiftWraps(); + this.startSync(); } /** Load stored gift wraps from Dexie into EventStore */