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 */