+28
package-lock.json
+28
package-lock.json
···
15
"@tanstack/react-query-persist-client": "^5.85.6",
16
"@tanstack/react-router": "^1.130.2",
17
"@tanstack/react-router-devtools": "^1.131.5",
18
"@tanstack/router-plugin": "^1.121.2",
19
"idb-keyval": "^6.2.2",
20
"jotai": "^2.13.1",
···
2579
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2580
}
2581
},
2582
"node_modules/@tanstack/router-core": {
2583
"version": "1.131.28",
2584
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz",
···
2731
"version": "0.7.4",
2732
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz",
2733
"integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==",
2734
"license": "MIT",
2735
"funding": {
2736
"type": "github",
···
15
"@tanstack/react-query-persist-client": "^5.85.6",
16
"@tanstack/react-router": "^1.130.2",
17
"@tanstack/react-router-devtools": "^1.131.5",
18
+
"@tanstack/react-virtual": "^3.13.12",
19
"@tanstack/router-plugin": "^1.121.2",
20
"idb-keyval": "^6.2.2",
21
"jotai": "^2.13.1",
···
2580
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2581
}
2582
},
2583
+
"node_modules/@tanstack/react-virtual": {
2584
+
"version": "3.13.12",
2585
+
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
2586
+
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
2587
+
"license": "MIT",
2588
+
"dependencies": {
2589
+
"@tanstack/virtual-core": "3.13.12"
2590
+
},
2591
+
"funding": {
2592
+
"type": "github",
2593
+
"url": "https://github.com/sponsors/tannerlinsley"
2594
+
},
2595
+
"peerDependencies": {
2596
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2597
+
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2598
+
}
2599
+
},
2600
"node_modules/@tanstack/router-core": {
2601
"version": "1.131.28",
2602
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz",
···
2749
"version": "0.7.4",
2750
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz",
2751
"integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==",
2752
+
"license": "MIT",
2753
+
"funding": {
2754
+
"type": "github",
2755
+
"url": "https://github.com/sponsors/tannerlinsley"
2756
+
}
2757
+
},
2758
+
"node_modules/@tanstack/virtual-core": {
2759
+
"version": "3.13.12",
2760
+
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
2761
+
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
2762
"license": "MIT",
2763
"funding": {
2764
"type": "github",
+1
package.json
+1
package.json
+213
-23
src/components/InfiniteCustomFeed.tsx
+213
-23
src/components/InfiniteCustomFeed.tsx
···
1
import * as React from "react";
2
//import { useInView } from "react-intersection-observer";
3
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
4
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
-
import {
6
-
useQueryArbitrary,
7
-
useQueryIdentity,
8
-
useInfiniteQueryFeedSkeleton,
9
-
} from "~/utils/useQuery";
10
11
interface InfiniteCustomFeedProps {
12
feedUri: string;
13
pdsUrl?: string;
14
feedServiceDid?: string;
15
}
16
17
export function InfiniteCustomFeed({
18
feedUri,
19
pdsUrl,
20
feedServiceDid,
21
}: InfiniteCustomFeedProps) {
22
const { agent } = useAuth();
23
const authed = !!agent?.did;
24
25
// const identityresultmaybe = useQueryIdentity(agent?.did);
26
// const identity = identityresultmaybe?.data;
···
56
// }
57
// }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
58
59
if (isLoading) {
60
return <div className="p-4 text-center text-gray-500">Loading feed...</div>;
61
}
···
65
<div className="p-4 text-center text-red-500">Error: {error.message}</div>
66
);
67
}
68
-
69
-
const allPosts =
70
-
data?.pages.flatMap((page) => {
71
-
if (page) return page.feed;
72
-
}) ?? [];
73
74
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
75
return (
···
79
);
80
}
81
82
return (
83
<>
84
-
{allPosts.map((item, i) => {
85
-
if (item)
86
-
return (
87
-
<UniversalPostRendererATURILoader
88
-
key={item.post || i}
89
-
atUri={item.post}
90
-
feedviewpost={true}
91
-
repostedby={!!item.reason?.$type && (item.reason as any)?.repost}
92
-
/>
93
-
);
94
-
})}
95
{/* allPosts?: {allPosts ? "true" : "false"}
96
hasNextPage?: {hasNextPage ? "true" : "false"}
97
isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */}
···
115
className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed"
116
aria-label="Refresh feed"
117
>
118
-
{isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />}
119
</button>
120
</>
121
);
···
138
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
139
></path>
140
</svg>
141
-
);
···
1
+
/* eslint-disable react-hooks/refs */
2
+
import { useWindowVirtualizer } from "@tanstack/react-virtual";
3
+
import { useAtom } from "jotai";
4
import * as React from "react";
5
+
import { useEffect, useLayoutEffect } from "react";
6
+
7
//import { useInView } from "react-intersection-observer";
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
9
import { useAuth } from "~/providers/UnifiedAuthProvider";
10
+
import { feedHeightsAtom, feedScrollIndexAtom } from "~/utils/atoms";
11
+
import { useInfiniteQueryFeedSkeleton } from "~/utils/useQuery";
12
13
interface InfiniteCustomFeedProps {
14
feedUri: string;
15
pdsUrl?: string;
16
feedServiceDid?: string;
17
+
initialScrollIndex?: number;
18
+
//onVisibleIndexChange?: (index: number) => void;
19
}
20
21
export function InfiniteCustomFeed({
22
feedUri,
23
pdsUrl,
24
feedServiceDid,
25
+
initialScrollIndex,
26
+
//onVisibleIndexChange,
27
}: InfiniteCustomFeedProps) {
28
+
const OVERSCAN_COUNT = 10;
29
+
const ESTIMATE_HEIGHT = 150;
30
+
31
const { agent } = useAuth();
32
const authed = !!agent?.did;
33
+
34
+
const listRef = React.useRef<HTMLDivElement | null>(null);
35
+
const [offsetTop, setOffsetTop] = React.useState(0);
36
+
const [scrollIndexes, setScrollIndexes] = useAtom(feedScrollIndexAtom);
37
+
//const initialScrollIndex = scrollIndexes[feedUri];
38
39
// const identityresultmaybe = useQueryIdentity(agent?.did);
40
// const identity = identityresultmaybe?.data;
···
70
// }
71
// }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
72
73
+
const allPosts = React.useMemo(() => {
74
+
const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? [];
75
+
76
+
const seenUris = new Set<string>();
77
+
78
+
return flattenedPosts.filter((item) => {
79
+
if (!item?.post) return false;
80
+
81
+
if (seenUris.has(item.post)) {
82
+
return false;
83
+
}
84
+
85
+
seenUris.add(item.post);
86
+
return true;
87
+
});
88
+
}, [data]);
89
+
90
+
const [feedHeights, setFeedHeights] = useAtom(feedHeightsAtom);
91
+
const currentFeedCache = feedHeights[feedUri] ?? {};
92
+
93
+
const virtualizerRef = React.useRef<ReturnType<
94
+
typeof useWindowVirtualizer
95
+
> | null>(null);
96
+
97
+
const virtualizer = useWindowVirtualizer({
98
+
count: allPosts.length,
99
+
// +
100
+
// (isFetchingNextPage ? 1 : 0) +
101
+
// (hasNextPage && !isFetchingNextPage ? 1 : 0) +
102
+
// (!hasNextPage ? 1 : 0) +
103
+
// 1,
104
+
estimateSize: (index) => {
105
+
const post = allPosts[index];
106
+
if (!post) return ESTIMATE_HEIGHT;
107
+
108
+
if (currentFeedCache[post.post]) {
109
+
return currentFeedCache[post.post];
110
+
}
111
+
112
+
return ESTIMATE_HEIGHT;
113
+
},
114
+
// measureElement: measureElement,
115
+
overscan: OVERSCAN_COUNT,
116
+
scrollMargin: offsetTop,
117
+
});
118
+
// React.useEffect(() => {
119
+
// virtualizer.measure();
120
+
// }, [data]);
121
+
122
+
const measureElement = React.useCallback(
123
+
(node: HTMLElement | null) => {
124
+
if (!node) return;
125
+
126
+
virtualizer.measureElement(node);
127
+
128
+
const postUri = node.dataset.postUri;
129
+
const newHeight = node.offsetHeight;
130
+
131
+
if (postUri && newHeight > 0 && currentFeedCache[postUri] !== newHeight) {
132
+
setFeedHeights((prev) => ({
133
+
...prev,
134
+
[feedUri]: {
135
+
...prev[feedUri],
136
+
[postUri]: newHeight,
137
+
},
138
+
}));
139
+
}
140
+
},
141
+
[virtualizer, setFeedHeights, feedUri, currentFeedCache]
142
+
);
143
+
144
+
virtualizerRef.current = virtualizer;
145
+
146
+
useLayoutEffect(() => {
147
+
const update = () => {
148
+
if (listRef.current) {
149
+
setOffsetTop(listRef.current.offsetTop);
150
+
}
151
+
//if (virtualizerRef.current) {
152
+
// virtualizerRef.current.measure();
153
+
// }
154
+
};
155
+
156
+
update();
157
+
158
+
let debounceTimeout: NodeJS.Timeout;
159
+
160
+
const debouncedUpdate = () => {
161
+
clearTimeout(debounceTimeout);
162
+
debounceTimeout = setTimeout(update, 100);
163
+
};
164
+
165
+
window.addEventListener("resize", debouncedUpdate);
166
+
167
+
return () => {
168
+
window.removeEventListener("resize", debouncedUpdate);
169
+
clearTimeout(debounceTimeout);
170
+
};
171
+
}, []);
172
+
173
+
const hasRestoredScroll = React.useRef(false);
174
+
useLayoutEffect(() => {
175
+
if (
176
+
hasRestoredScroll.current ||
177
+
!initialScrollIndex ||
178
+
initialScrollIndex === 0
179
+
) {
180
+
return;
181
+
}
182
+
183
+
if (initialScrollIndex < allPosts.length) {
184
+
console.log(`Restoring scroll to index: ${initialScrollIndex}`);
185
+
virtualizer.scrollToIndex(initialScrollIndex, {
186
+
align: "start",
187
+
behavior: "auto",
188
+
});
189
+
hasRestoredScroll.current = true;
190
+
}
191
+
}, [initialScrollIndex, allPosts.length, virtualizer]);
192
+
193
+
// React.useEffect(() => {
194
+
// const handleScroll = () => {
195
+
// const topVisibleItem = virtualizer.getVirtualItems()[0];
196
+
// if (topVisibleItem && onVisibleIndexChange) {
197
+
// onVisibleIndexChange(topVisibleItem.index);
198
+
// }
199
+
// };
200
+
201
+
// window.addEventListener('scroll', handleScroll, { passive: true });
202
+
// return () => window.removeEventListener('scroll', handleScroll);
203
+
// }, [virtualizer, onVisibleIndexChange]);
204
+
205
+
useEffect(() => {
206
+
return () => {
207
+
const topVisibleItem = virtualizer.getVirtualItems()[OVERSCAN_COUNT];
208
+
209
+
if (topVisibleItem) {
210
+
console.log(
211
+
`Saving final scroll index ${topVisibleItem.index} for feed ${feedUri}`
212
+
);
213
+
setScrollIndexes((prev) => ({
214
+
...prev,
215
+
[feedUri]: topVisibleItem.index,
216
+
}));
217
+
}
218
+
};
219
+
}, [virtualizer, feedUri, setScrollIndexes]);
220
+
221
if (isLoading) {
222
return <div className="p-4 text-center text-gray-500">Loading feed...</div>;
223
}
···
227
<div className="p-4 text-center text-red-500">Error: {error.message}</div>
228
);
229
}
230
231
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
232
return (
···
236
);
237
}
238
239
+
//if (offsetTop === 0) {
240
+
// return <div ref={listRef}>Calculating...</div>;
241
+
//}
242
+
243
return (
244
<>
245
+
<div ref={listRef}>
246
+
<div
247
+
style={{
248
+
height: `${virtualizer.getTotalSize()}px`,
249
+
width: "100%",
250
+
position: "relative",
251
+
}}
252
+
>
253
+
{virtualizer.getVirtualItems().map((virtualItem) => {
254
+
const item = allPosts[virtualItem.index];
255
+
const i = virtualItem.index;
256
+
if (item)
257
+
return (
258
+
<UniversalPostRendererATURILoader
259
+
key={item.post || i}
260
+
atUri={item.post}
261
+
dataIndexPropPass={i}
262
+
feedviewpost={true}
263
+
ref={measureElement}
264
+
repostedby={
265
+
!!item.reason?.$type && (item.reason as any)?.repost
266
+
}
267
+
style={{
268
+
position: "absolute",
269
+
top: 0,
270
+
left: 0,
271
+
width: "100%",
272
+
//height: `${item.size}px`,
273
+
transform: `translateY(${virtualItem.start - offsetTop}px)`,
274
+
}}
275
+
/>
276
+
);
277
+
})}
278
+
</div>
279
+
</div>
280
+
281
{/* allPosts?: {allPosts ? "true" : "false"}
282
hasNextPage?: {hasNextPage ? "true" : "false"}
283
isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */}
···
301
className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed"
302
aria-label="Refresh feed"
303
>
304
+
{isRefetching ? (
305
+
<RefreshIcon className="h-6 w-6 animate-spin" />
306
+
) : (
307
+
<RefreshIcon className="h-6 w-6" />
308
+
)}
309
</button>
310
</>
311
);
···
328
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
329
></path>
330
</svg>
331
+
);
+32
-1
src/components/UniversalPostRenderer.tsx
+32
-1
src/components/UniversalPostRenderer.tsx
···
28
bottomBorder?: boolean;
29
feedviewpost?: boolean;
30
repostedby?: string;
31
}
32
33
// export async function cachedGetRecord({
···
132
bottomBorder = true,
133
feedviewpost = false,
134
repostedby,
135
}: UniversalPostRendererATURILoaderProps) {
136
// /*mass comment*/ console.log("atUri", atUri);
137
//const { get, set } = usePersistentStore();
···
406
bottomBorder={bottomBorder}
407
feedviewpost={feedviewpost}
408
repostedby={repostedby}
409
/>
410
);
411
}
···
430
bottomBorder = true,
431
feedviewpost = false,
432
repostedby,
433
}: {
434
postRecord: any;
435
profileRecord: any;
···
444
bottomBorder?: boolean;
445
feedviewpost?: boolean;
446
repostedby?: string;
447
}) {
448
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
449
const navigate = useNavigate();
···
638
//extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}}
639
feedviewpostreplyhandle={feedviewpostreplyhandle}
640
repostedby={feedviewpostrepostedbyhandle}
641
/>
642
</>
643
);
···
1079
feedviewpostreplyhandle,
1080
depth = 0,
1081
repostedby,
1082
}: {
1083
post: PostView;
1084
// optional for now because i havent ported every use to this yet
···
1098
feedviewpostreplyhandle?: string;
1099
depth?: number;
1100
repostedby?: string;
1101
}) {
1102
const navigate = useNavigate();
1103
const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom);
···
1171
/* fuck you */
1172
const isMainItem = false;
1173
const setMainItem = (any: any) => {};
1174
return (
1175
<div
1176
key={salt + "-" + (post.uri || emergencySalt)}
1177
onClick={
1178
isMainItem
···
1188
}
1189
: undefined
1190
}
1191
-
style={{
1192
//border: "1px solid #e1e8ed",
1193
//borderRadius: 12,
1194
opacity: "1 !important",
···
1572
/>
1573
</div>
1574
</div>
1575
</div>
1576
);
1577
}
···
28
bottomBorder?: boolean;
29
feedviewpost?: boolean;
30
repostedby?: string;
31
+
style?: React.CSSProperties;
32
+
ref?: React.Ref<HTMLDivElement>;
33
+
dataIndexPropPass?: number;
34
}
35
36
// export async function cachedGetRecord({
···
135
bottomBorder = true,
136
feedviewpost = false,
137
repostedby,
138
+
style,
139
+
ref,
140
+
dataIndexPropPass,
141
}: UniversalPostRendererATURILoaderProps) {
142
// /*mass comment*/ console.log("atUri", atUri);
143
//const { get, set } = usePersistentStore();
···
412
bottomBorder={bottomBorder}
413
feedviewpost={feedviewpost}
414
repostedby={repostedby}
415
+
style={style}
416
+
ref={ref}
417
+
dataIndexPropPass={dataIndexPropPass}
418
/>
419
);
420
}
···
439
bottomBorder = true,
440
feedviewpost = false,
441
repostedby,
442
+
style,
443
+
ref,
444
+
dataIndexPropPass,
445
}: {
446
postRecord: any;
447
profileRecord: any;
···
456
bottomBorder?: boolean;
457
feedviewpost?: boolean;
458
repostedby?: string;
459
+
style?: React.CSSProperties;
460
+
ref?: React.Ref<HTMLDivElement>;
461
+
dataIndexPropPass?: number;
462
}) {
463
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
464
const navigate = useNavigate();
···
653
//extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}}
654
feedviewpostreplyhandle={feedviewpostreplyhandle}
655
repostedby={feedviewpostrepostedbyhandle}
656
+
style={style}
657
+
ref={ref}
658
+
dataIndexPropPass={dataIndexPropPass}
659
/>
660
</>
661
);
···
1097
feedviewpostreplyhandle,
1098
depth = 0,
1099
repostedby,
1100
+
style,
1101
+
ref,
1102
+
dataIndexPropPass,
1103
}: {
1104
post: PostView;
1105
// optional for now because i havent ported every use to this yet
···
1119
feedviewpostreplyhandle?: string;
1120
depth?: number;
1121
repostedby?: string;
1122
+
style?: React.CSSProperties;
1123
+
ref?: React.Ref<HTMLDivElement>;
1124
+
dataIndexPropPass?: number;
1125
}) {
1126
const navigate = useNavigate();
1127
const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom);
···
1195
/* fuck you */
1196
const isMainItem = false;
1197
const setMainItem = (any: any) => {};
1198
+
// eslint-disable-next-line react-hooks/refs
1199
+
console.log("Received ref in UniversalPostRenderer:", ref);
1200
return (
1201
+
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1202
<div
1203
+
//ref={ref}
1204
key={salt + "-" + (post.uri || emergencySalt)}
1205
onClick={
1206
isMainItem
···
1216
}
1217
: undefined
1218
}
1219
+
style={
1220
+
{
1221
+
//...style,
1222
//border: "1px solid #e1e8ed",
1223
//borderRadius: 12,
1224
opacity: "1 !important",
···
1602
/>
1603
</div>
1604
</div>
1605
+
</div>
1606
</div>
1607
);
1608
}
+127
-104
src/routes/index.tsx
+127
-104
src/routes/index.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
-
import { useEffect, useLayoutEffect } from "react";
5
6
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
import {
9
agentAtom,
10
authedAtom,
11
-
feedScrollPositionsAtom,
12
selectedFeedUriAtom,
13
store,
14
} from "~/utils/atoms";
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
import {
17
-
constructArbitraryQuery,
18
-
constructIdentityQuery,
19
-
constructInfiniteFeedSkeletonQuery,
20
-
constructPostQuery,
21
useQueryArbitrary,
22
useQueryIdentity,
23
useQueryPreferences,
24
} from "~/utils/useQuery";
25
26
export const Route = createFileRoute("/")({
27
-
loader: async ({ context }) => {
28
-
const { queryClient } = context;
29
-
const atomauth = store.get(authedAtom);
30
-
const atomagent = store.get(agentAtom);
31
32
-
let identitypds: string | undefined;
33
-
const initialselectedfeed = store.get(selectedFeedUriAtom);
34
-
if (atomagent && atomauth && atomagent?.did) {
35
-
const identityopts = constructIdentityQuery(atomagent.did);
36
-
const identityresultmaybe =
37
-
await queryClient.ensureQueryData(identityopts);
38
-
identitypds = identityresultmaybe?.pds;
39
-
}
40
41
-
const arbitraryopts = constructArbitraryQuery(
42
-
initialselectedfeed ??
43
-
"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
44
-
);
45
-
const feedGengetrecordquery =
46
-
await queryClient.ensureQueryData(arbitraryopts);
47
-
const feedServiceDid = (feedGengetrecordquery?.value as any)?.did;
48
-
//queryClient.ensureInfiniteQueryData()
49
50
-
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({
51
-
feedUri:
52
-
initialselectedfeed ??
53
-
"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
54
-
agent: atomagent ?? undefined,
55
-
isAuthed: atomauth ?? false,
56
-
pdsUrl: identitypds,
57
-
feedServiceDid: feedServiceDid,
58
-
});
59
60
-
const res = await queryClient.ensureInfiniteQueryData({
61
-
queryKey,
62
-
queryFn,
63
-
initialPageParam: undefined as never,
64
-
getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined,
65
-
staleTime: Infinity,
66
-
//refetchOnWindowFocus: false,
67
-
//enabled: true,
68
-
});
69
-
await Promise.all(
70
-
res.pages.map(async (page) => {
71
-
await Promise.all(
72
-
page.feed.map(async (feedviewpost) => {
73
-
if (!feedviewpost.post) return;
74
-
// /*mass comment*/ console.log("preloading: ", feedviewpost.post);
75
-
const opts = constructPostQuery(feedviewpost.post);
76
-
try {
77
-
await queryClient.ensureQueryData(opts);
78
-
} catch (e) {
79
-
// /*mass comment*/ console.log(" failed:", e);
80
-
}
81
-
})
82
-
);
83
-
})
84
-
);
85
-
},
86
component: Home,
87
pendingComponent: PendingHome,
88
});
···
288
// };
289
// }, [authed, agent, loadering, selectedFeed, get, set]);
290
291
-
const [scrollPositions, setScrollPositions] = useAtom(
292
-
feedScrollPositionsAtom
293
-
);
294
295
-
const scrollRef = React.useRef<Record<string, number>>({});
296
297
-
useEffect(() => {
298
-
const onScroll = () => {
299
-
//if (!selectedFeed) return;
300
-
scrollRef.current[selectedFeed ?? "null"] = window.scrollY;
301
-
};
302
-
window.addEventListener("scroll", onScroll, { passive: true });
303
-
return () => window.removeEventListener("scroll", onScroll);
304
-
}, [selectedFeed]);
305
-
const [donerestored, setdonerestored] = React.useState(false);
306
307
-
useEffect(() => {
308
-
return () => {
309
-
if (!donerestored) return;
310
-
// /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current);
311
-
//if (!selectedFeed) return;
312
-
setScrollPositions((prev) => ({
313
-
...prev,
314
-
[selectedFeed ?? "null"]:
315
-
scrollRef.current[selectedFeed ?? "null"] ?? 0,
316
-
}));
317
-
};
318
-
}, [selectedFeed, setScrollPositions, donerestored]);
319
320
-
const [restoringScrollPosition, setRestoringScrollPosition] =
321
-
React.useState(false);
322
323
-
useLayoutEffect(() => {
324
-
setRestoringScrollPosition(true);
325
-
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
326
327
-
const raf = requestAnimationFrame(() => {
328
-
// setRestoringScrollPosition(true);
329
-
// raf = requestAnimationFrame(() => {
330
-
// window.scrollTo({ top: savedPosition, behavior: "instant" });
331
-
// setRestoringScrollPosition(false);
332
-
// setdonerestored(true);
333
-
// });
334
-
window.scrollTo({ top: savedPosition, behavior: "instant" });
335
-
setRestoringScrollPosition(false);
336
-
setdonerestored(true);
337
-
});
338
339
-
return () => cancelAnimationFrame(raf);
340
-
}, [selectedFeed, scrollPositions]);
341
342
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
343
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
···
359
const isReadyForAuthedFeed =
360
authed && agent && identity?.pds && feedServiceDid;
361
const isReadyForUnauthedFeed = !authed && selectedFeed;
362
363
return (
364
<div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
···
416
feedUri={selectedFeed!}
417
pdsUrl={identity?.pds}
418
feedServiceDid={feedServiceDid}
419
/>
420
) : (
421
<div className="p-4 text-center text-gray-500">
···
1
import { createFileRoute } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
+
import { useEffect } from "react";
5
6
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
import {
9
agentAtom,
10
authedAtom,
11
+
feedScrollIndexAtom,
12
selectedFeedUriAtom,
13
store,
14
} from "~/utils/atoms";
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
import {
17
+
//constructArbitraryQuery,
18
+
//constructIdentityQuery,
19
+
//constructInfiniteFeedSkeletonQuery,
20
+
//constructPostQuery,
21
useQueryArbitrary,
22
useQueryIdentity,
23
useQueryPreferences,
24
} from "~/utils/useQuery";
25
26
export const Route = createFileRoute("/")({
27
+
// loader: async ({ context }) => {
28
+
// const { queryClient } = context;
29
+
// const atomauth = store.get(authedAtom);
30
+
// const atomagent = store.get(agentAtom);
31
32
+
// let identitypds: string | undefined;
33
+
// const initialselectedfeed = store.get(selectedFeedUriAtom);
34
+
// if (atomagent && atomauth && atomagent?.did) {
35
+
// const identityopts = constructIdentityQuery(atomagent.did);
36
+
// const identityresultmaybe =
37
+
// await queryClient.ensureQueryData(identityopts);
38
+
// identitypds = identityresultmaybe?.pds;
39
+
// }
40
41
+
// const arbitraryopts = constructArbitraryQuery(
42
+
// initialselectedfeed ??
43
+
// "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
44
+
// );
45
+
// const feedGengetrecordquery =
46
+
// await queryClient.ensureQueryData(arbitraryopts);
47
+
// const feedServiceDid = (feedGengetrecordquery?.value as any)?.did;
48
+
// //queryClient.ensureInfiniteQueryData()
49
50
+
// const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({
51
+
// feedUri:
52
+
// initialselectedfeed ??
53
+
// "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
54
+
// agent: atomagent ?? undefined,
55
+
// isAuthed: atomauth ?? false,
56
+
// pdsUrl: identitypds,
57
+
// feedServiceDid: feedServiceDid,
58
+
// });
59
60
+
// const res = await queryClient.ensureInfiniteQueryData({
61
+
// queryKey,
62
+
// queryFn,
63
+
// initialPageParam: undefined as never,
64
+
// getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined,
65
+
// staleTime: Infinity,
66
+
// //refetchOnWindowFocus: false,
67
+
// //enabled: true,
68
+
// });
69
+
// await Promise.all(
70
+
// res.pages.map(async (page) => {
71
+
// await Promise.all(
72
+
// page.feed.map(async (feedviewpost) => {
73
+
// if (!feedviewpost.post) return;
74
+
// // /*mass comment*/ console.log("preloading: ", feedviewpost.post);
75
+
// const opts = constructPostQuery(feedviewpost.post);
76
+
// try {
77
+
// await queryClient.ensureQueryData(opts);
78
+
// } catch (e) {
79
+
// // /*mass comment*/ console.log(" failed:", e);
80
+
// }
81
+
// })
82
+
// );
83
+
// })
84
+
// );
85
+
// },
86
component: Home,
87
pendingComponent: PendingHome,
88
});
···
288
// };
289
// }, [authed, agent, loadering, selectedFeed, get, set]);
290
291
+
// const [scrollPositions, setScrollPositions] = useAtom(
292
+
// feedScrollPositionsAtom
293
+
// );
294
295
+
const [scrollIndexes] = useAtom(feedScrollIndexAtom);
296
+
297
+
//const latestVisibleIndexRef = React.useRef(0);
298
+
299
+
// const handleVisibleIndexChange = React.useCallback((index: number) => {
300
+
// latestVisibleIndexRef.current = index;
301
+
// }, []);
302
303
+
// React.useEffect(() => {
304
+
// // This return function is the cleanup effect.
305
+
// return () => {
306
+
// if (selectedFeed) {
307
+
// console.log(`Saving scroll index ${latestVisibleIndexRef.current} for feed ${selectedFeed}`);
308
+
// setScrollIndexes((prev) => ({
309
+
// ...prev,
310
+
// [selectedFeed]: latestVisibleIndexRef.current,
311
+
// }));
312
+
// }
313
+
// };
314
+
// }, [selectedFeed, setScrollIndexes]);
315
+
316
+
// useEffect(() => {
317
+
// const onScroll = () => {
318
+
// //if (!selectedFeed) return;
319
+
// scrollRef.current[selectedFeed ?? "null"] = window.scrollY;
320
+
// };
321
+
// window.addEventListener("scroll", onScroll, { passive: true });
322
+
// return () => window.removeEventListener("scroll", onScroll);
323
+
// }, [selectedFeed]);
324
+
// const [donerestored, setdonerestored] = React.useState(false);
325
326
+
// useEffect(() => {
327
+
// return () => {
328
+
// if (!donerestored) return;
329
+
// // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current);
330
+
// //if (!selectedFeed) return;
331
+
// setScrollPositions((prev) => ({
332
+
// ...prev,
333
+
// [selectedFeed ?? "null"]:
334
+
// scrollRef.current[selectedFeed ?? "null"] ?? 0,
335
+
// }));
336
+
// };
337
+
// }, [selectedFeed, setScrollPositions, donerestored]);
338
339
+
// const [restoringScrollPosition, setRestoringScrollPosition] =
340
+
// React.useState(false);
341
342
+
// useLayoutEffect(() => {
343
+
// setRestoringScrollPosition(true);
344
+
// const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
345
346
+
// const raf = requestAnimationFrame(() => {
347
+
// // setRestoringScrollPosition(true);
348
+
// // raf = requestAnimationFrame(() => {
349
+
// // window.scrollTo({ top: savedPosition, behavior: "instant" });
350
+
// // setRestoringScrollPosition(false);
351
+
// // setdonerestored(true);
352
+
// // });
353
+
// window.scrollTo({ top: savedPosition, behavior: "instant" });
354
+
// setRestoringScrollPosition(false);
355
+
// setdonerestored(true);
356
+
// });
357
358
+
// return () => cancelAnimationFrame(raf);
359
+
// }, [selectedFeed, scrollPositions]);
360
361
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
362
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
···
378
const isReadyForAuthedFeed =
379
authed && agent && identity?.pds && feedServiceDid;
380
const isReadyForUnauthedFeed = !authed && selectedFeed;
381
+
382
+
const savedIndex = selectedFeed ? scrollIndexes[selectedFeed] : 0;
383
384
return (
385
<div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
···
437
feedUri={selectedFeed!}
438
pdsUrl={identity?.pds}
439
feedServiceDid={feedServiceDid}
440
+
initialScrollIndex={savedIndex}
441
+
//onVisibleIndexChange={handleVisibleIndexChange}
442
/>
443
) : (
444
<div className="p-4 text-center text-gray-500">
+10
src/utils/atoms.ts
+10
src/utils/atoms.ts
···
11
12
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
13
14
+
/**
15
+
* @deprecated use the Tanstack Virtual index thanks
16
+
*/
17
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
18
'feedscrollpositions',
19
+
{}
20
+
);
21
+
22
+
export const feedScrollIndexAtom = atomWithStorage<Record<string, number>>('feedScrollIndexes',{});
23
+
24
+
export const feedHeightsAtom = atomWithStorage<Record<string, Record<string, number>>>(
25
+
'feedPostHeights',
26
{}
27
);
28