diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 41dac26..b816d90 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -2,7 +2,7 @@ import { useMemo, useState, memo, useCallback, useRef } from "react"; import { use$ } from "applesauce-react/hooks"; import { from } from "rxjs"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; -import { Reply, Zap } from "lucide-react"; +import { Loader2, Reply, Zap } from "lucide-react"; import { getZapRequest } from "applesauce-common/helpers/zap"; import accountManager from "@/services/accounts"; import eventStore from "@/services/event-store"; @@ -358,6 +358,10 @@ export function ChatViewer({ // Track reply context (which message is being replied to) const [replyTo, setReplyTo] = useState(); + // State for loading older messages + const [isLoadingOlder, setIsLoadingOlder] = useState(false); + const [hasMore, setHasMore] = useState(true); + // Ref to Virtuoso for programmatic scrolling const virtuosoRef = useRef(null); @@ -399,6 +403,32 @@ export function ChatViewer({ [messages], ); + // Handle loading older messages + const handleLoadOlder = useCallback(async () => { + if (!conversation || !messages || messages.length === 0 || isLoadingOlder) { + return; + } + + setIsLoadingOlder(true); + try { + // Get the timestamp of the oldest message + const oldestMessage = messages[0]; + const olderMessages = await adapter.loadMoreMessages( + conversation, + oldestMessage.timestamp, + ); + + // If we got fewer messages than expected, there might be no more + if (olderMessages.length < 50) { + setHasMore(false); + } + } catch (error) { + console.error("Failed to load older messages:", error); + } finally { + setIsLoadingOlder(false); + } + }, [conversation, messages, adapter, isLoadingOlder]); + // Handle NIP badge click const handleNipClick = useCallback(() => { if (conversation?.protocol === "nip-29") { @@ -564,6 +594,27 @@ export function ChatViewer({ data={messagesWithMarkers} initialTopMostItemIndex={messagesWithMarkers.length - 1} followOutput="smooth" + components={{ + Header: () => + hasMore ? ( +
+ +
+ ) : null, + }} itemContent={(_index, item) => { if (item.type === "day-marker") { return ( diff --git a/src/lib/chat/adapters/nip-29-adapter.ts b/src/lib/chat/adapters/nip-29-adapter.ts index 663523b..6536b30 100644 --- a/src/lib/chat/adapters/nip-29-adapter.ts +++ b/src/lib/chat/adapters/nip-29-adapter.ts @@ -1,5 +1,5 @@ -import { Observable } from "rxjs"; -import { map, first } from "rxjs/operators"; +import { Observable, firstValueFrom } from "rxjs"; +import { map, first, toArray } from "rxjs/operators"; import type { Filter } from "nostr-tools"; import { nip19 } from "nostr-tools"; import { ChatProtocolAdapter, type SendMessageOptions } from "./base-adapter"; @@ -369,11 +369,44 @@ export class Nip29Adapter extends ChatProtocolAdapter { * Load more historical messages (pagination) */ async loadMoreMessages( - _conversation: Conversation, - _before: number, + conversation: Conversation, + before: number, ): Promise { - // For now, return empty - pagination to be implemented in Phase 6 - return []; + const groupId = conversation.metadata?.groupId; + const relayUrl = conversation.metadata?.relayUrl; + + if (!groupId || !relayUrl) { + throw new Error("Group ID and relay URL required"); + } + + console.log( + `[NIP-29] Loading older messages for ${groupId} before ${before}`, + ); + + // Same filter as loadMessages but with until for pagination + const filter: Filter = { + kinds: [9, 9000, 9001, 9321], + "#h": [groupId], + until: before, + limit: 50, + }; + + // One-shot request to fetch older messages + const events = await firstValueFrom( + pool.request([relayUrl], [filter], { eventStore }).pipe(toArray()), + ); + + console.log(`[NIP-29] Loaded ${events.length} older events`); + + // Convert events to messages + const messages = events.map((event) => { + if (event.kind === 9321) { + return this.nutzapToMessage(event, conversation.id); + } + return this.eventToMessage(event, conversation.id); + }); + + return messages.sort((a, b) => a.timestamp - b.timestamp); } /** diff --git a/src/lib/chat/adapters/nip-53-adapter.ts b/src/lib/chat/adapters/nip-53-adapter.ts index 9d05b40..20a8ee7 100644 --- a/src/lib/chat/adapters/nip-53-adapter.ts +++ b/src/lib/chat/adapters/nip-53-adapter.ts @@ -1,5 +1,5 @@ -import { Observable } from "rxjs"; -import { map, first } from "rxjs/operators"; +import { Observable, firstValueFrom } from "rxjs"; +import { map, first, toArray } from "rxjs/operators"; import type { Filter } from "nostr-tools"; import { nip19 } from "nostr-tools"; import { ChatProtocolAdapter, type SendMessageOptions } from "./base-adapter"; @@ -311,11 +311,64 @@ export class Nip53Adapter extends ChatProtocolAdapter { * Load more historical messages (pagination) */ async loadMoreMessages( - _conversation: Conversation, - _before: number, + conversation: Conversation, + before: number, ): Promise { - // Pagination to be implemented later - return []; + const activityAddress = conversation.metadata?.activityAddress; + const liveActivity = conversation.metadata?.liveActivity as + | { + relays?: string[]; + } + | undefined; + + if (!activityAddress) { + throw new Error("Activity address required"); + } + + const { pubkey, identifier } = activityAddress; + const aTagValue = `30311:${pubkey}:${identifier}`; + + // Get relays from live activity metadata or fall back to relayUrl + const relays = liveActivity?.relays || []; + if (relays.length === 0 && conversation.metadata?.relayUrl) { + relays.push(conversation.metadata.relayUrl); + } + + if (relays.length === 0) { + throw new Error("No relays available for live chat"); + } + + console.log( + `[NIP-53] Loading older messages for ${aTagValue} before ${before}`, + ); + + // Same filter as loadMessages but with until for pagination + const filter: Filter = { + kinds: [1311, 9735], + "#a": [aTagValue], + until: before, + limit: 50, + }; + + // One-shot request to fetch older messages + const events = await firstValueFrom( + pool.request(relays, [filter], { eventStore }).pipe(toArray()), + ); + + console.log(`[NIP-53] Loaded ${events.length} older events`); + + // Convert events to messages + const messages = events + .map((event) => { + if (event.kind === 9735) { + if (!isValidZap(event)) return null; + return this.zapToMessage(event, conversation.id); + } + return this.eventToMessage(event, conversation.id); + }) + .filter((msg): msg is Message => msg !== null); + + return messages.sort((a, b) => a.timestamp - b.timestamp); } /**