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) {