From ff68392f59e3aee97e1f6fa4308baf02229a584c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:36:30 +0000 Subject: [PATCH] fix: buffer NIP-29 messages until EOSE to prevent partial renders The loadMessages observable was emitting on every event as they streamed in before EOSE, causing multiple re-renders with partial message lists. This resulted in incorrect scroll positions during initial chat load. Now uses combineLatest with a BehaviorSubject to track EOSE state and only emits the message list after the initial batch is fully loaded. New messages after EOSE continue to trigger immediate updates. https://claude.ai/code/session_0174r1Ddh5e2RuhHf2ZrFiNc --- src/lib/chat/adapters/nip-29-adapter.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/lib/chat/adapters/nip-29-adapter.ts b/src/lib/chat/adapters/nip-29-adapter.ts index 4cb4646..83350c1 100644 --- a/src/lib/chat/adapters/nip-29-adapter.ts +++ b/src/lib/chat/adapters/nip-29-adapter.ts @@ -1,5 +1,10 @@ -import { Observable, firstValueFrom } from "rxjs"; -import { map, first, toArray } from "rxjs/operators"; +import { + Observable, + firstValueFrom, + BehaviorSubject, + combineLatest, +} from "rxjs"; +import { map, first, toArray, filter as filterOp } from "rxjs/operators"; import type { Filter } from "nostr-tools"; import { nip19 } from "nostr-tools"; import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; @@ -332,6 +337,9 @@ export class Nip29Adapter extends ChatProtocolAdapter { const conversationId = `nip-29:${relayUrl}'${groupId}`; this.cleanup(conversationId); + // Track EOSE state - don't emit until initial batch is loaded + const eoseReceived$ = new BehaviorSubject(false); + // Start a persistent subscription to the group relay const subscription = pool .subscription([relayUrl], [filter], { @@ -341,6 +349,7 @@ export class Nip29Adapter extends ChatProtocolAdapter { next: (response) => { if (typeof response === "string") { console.log("[NIP-29] EOSE received"); + eoseReceived$.next(true); } else { console.log( `[NIP-29] Received event k${response.kind}: ${response.id.slice(0, 8)}...`, @@ -352,9 +361,10 @@ export class Nip29Adapter extends ChatProtocolAdapter { // Store subscription for cleanup this.subscriptions.set(conversationId, subscription); - // Return observable from EventStore which will update automatically - return eventStore.timeline(filter).pipe( - map((events) => { + // Return observable that only emits after EOSE (prevents partial renders during initial load) + return combineLatest([eventStore.timeline(filter), eoseReceived$]).pipe( + filterOp(([, eose]) => eose), // Only emit after EOSE received + map(([events]) => { const messages = events.map((event) => { // Convert nutzaps (kind 9321) using nutzapToMessage if (event.kind === 9321) {