From 0d327372fe1e6275c7880f824dab5efa3e43ba04 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 20:48:35 +0000 Subject: [PATCH 1/9] Add infinite scroll to AGENTS.md --- AGENTS.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 0fc10c4..db9eaaa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -303,6 +303,70 @@ function usePosts() { } ``` +### Infinite Scroll for Feeds + +For feed-like interfaces, implement infinite scroll using TanStack Query's `useInfiniteQuery` with Nostr's timestamp-based pagination: + +```typescript +import { useNostr } from '@nostrify/react'; +import { useInfiniteQuery } from '@tanstack/react-query'; + +function useInfinitePosts() { + const { nostr } = useNostr(); + + return useInfiniteQuery({ + queryKey: ['posts', 'infinite'], + queryFn: async ({ pageParam, signal }) => { + const filter = { kinds: [1], limit: 20 }; + if (pageParam) filter.until = pageParam; + + const events = await nostr.query([filter], { + signal: AbortSignal.any([signal, AbortSignal.timeout(1500)]) + }); + + return events.sort((a, b) => b.created_at - a.created_at); + }, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) return undefined; + return lastPage[lastPage.length - 1].created_at; + }, + initialPageParam: undefined, + }); +} +``` + +Use with intersection observer for automatic loading: + +```tsx +import { useInView } from 'react-intersection-observer'; + +function PostFeed() { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfinitePosts(); + const { ref, inView } = useInView(); + + useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, fetchNextPage]); + + const posts = data?.pages.flat() || []; + + return ( +
+ {posts.map((post) => ( + + ))} + {hasNextPage && ( +
+ {isFetchingNextPage && } +
+ )} +
+ ); +} +``` + #### Efficient Query Design **Critical**: Always minimize the number of separate queries to avoid rate limiting and improve performance. Combine related queries whenever possible. From cf64f2ca83b1b42c5570eaa0539669ac25968317 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:01:19 +0000 Subject: [PATCH 2/9] Update context for pagination duplication potential. --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index db9eaaa..6d8eb7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -328,7 +328,7 @@ function useInfinitePosts() { }, getNextPageParam: (lastPage) => { if (lastPage.length === 0) return undefined; - return lastPage[lastPage.length - 1].created_at; + return lastPage[lastPage.length - 1].created_at - 1; // Subtract 1 since 'until' is inclusive }, initialPageParam: undefined, }); From dddeb7fc61bea54d93710524275ac70984ca7f08 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:23:33 +0000 Subject: [PATCH 3/9] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Alex Gleason --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 6d8eb7d..8d4c830 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -324,7 +324,7 @@ function useInfinitePosts() { signal: AbortSignal.any([signal, AbortSignal.timeout(1500)]) }); - return events.sort((a, b) => b.created_at - a.created_at); + return events; }, getNextPageParam: (lastPage) => { if (lastPage.length === 0) return undefined; From 2baccf1ea0cc73badf77e40b366f3d58d4dc1711 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:23:55 +0000 Subject: [PATCH 4/9] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Alex Gleason --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 8d4c830..8a1a8eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -315,7 +315,7 @@ function useInfinitePosts() { const { nostr } = useNostr(); return useInfiniteQuery({ - queryKey: ['posts', 'infinite'], + queryKey: ['global-feed'], queryFn: async ({ pageParam, signal }) => { const filter = { kinds: [1], limit: 20 }; if (pageParam) filter.until = pageParam; From 37ff1d69572c907330d6c6dc7b1a259299f6ce3b Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:24:11 +0000 Subject: [PATCH 5/9] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Alex Gleason --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 8a1a8eb..c2052ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -335,7 +335,7 @@ function useInfinitePosts() { } ``` -Use with intersection observer for automatic loading: +Example usage with intersection observer for automatic loading: ```tsx import { useInView } from 'react-intersection-observer'; From 363b2e3ebb9165793b74daa8ee27d01f04e64031 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:25:10 +0000 Subject: [PATCH 6/9] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Alex Gleason --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c2052ea..6dbe628 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -311,7 +311,7 @@ For feed-like interfaces, implement infinite scroll using TanStack Query's `useI import { useNostr } from '@nostrify/react'; import { useInfiniteQuery } from '@tanstack/react-query'; -function useInfinitePosts() { +export function useGlobalFeed() { const { nostr } = useNostr(); return useInfiniteQuery({ From b2cc0aef79dc64d0498afe8a334084bf36da8ffb Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:25:16 +0000 Subject: [PATCH 7/9] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Alex Gleason --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 6dbe628..9b82f6d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -340,7 +340,7 @@ Example usage with intersection observer for automatic loading: ```tsx import { useInView } from 'react-intersection-observer'; -function PostFeed() { +function GlobalFeed() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfinitePosts(); const { ref, inView } = useInView(); From 9114c7db8642b284469271e9232076fc2ba5171f Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:25:34 +0000 Subject: [PATCH 8/9] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Alex Gleason --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 9b82f6d..4104325 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -341,7 +341,7 @@ Example usage with intersection observer for automatic loading: import { useInView } from 'react-intersection-observer'; function GlobalFeed() { - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfinitePosts(); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGlobalFeed(); const { ref, inView } = useInView(); useEffect(() => { From 6458b243055bb450cdc9a0c7f3e7e7a4b0a032d3 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Mon, 1 Sep 2025 21:26:23 +0000 Subject: [PATCH 9/9] Add react-intersection-observer per AGENTS.md --- package-lock.json | 214 ++++------------------------------------------ package.json | 1 + 2 files changed, 17 insertions(+), 198 deletions(-) diff --git a/package-lock.json b/package-lock.json index d48747f..d9a7ff4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-intersection-observer": "^9.16.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", @@ -211,32 +212,6 @@ "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -352,33 +327,6 @@ "node": ">=18" } }, - "node_modules/@edge-runtime/primitives": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-4.1.0.tgz", - "integrity": "sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==", - "dev": true, - "license": "MPL-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@edge-runtime/vm": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.2.0.tgz", - "integrity": "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==", - "dev": true, - "license": "MPL-2.0", - "optional": true, - "peer": true, - "dependencies": { - "@edge-runtime/primitives": "4.1.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -3450,38 +3398,6 @@ } } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4025,20 +3941,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -4567,14 +4469,6 @@ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT" }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4852,17 +4746,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -6008,14 +5891,6 @@ "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6743,6 +6618,21 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7632,59 +7522,6 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, - "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7909,14 +7746,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/vaul": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", @@ -8551,17 +8380,6 @@ "node": ">=8" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d6adff7..98f7eb5 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-intersection-observer": "^9.16.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7",