+28
package-lock.json
+28
package-lock.json
···
15
15
"@tanstack/react-query-persist-client": "^5.85.6",
16
16
"@tanstack/react-router": "^1.130.2",
17
17
"@tanstack/react-router-devtools": "^1.131.5",
18
+
"@tanstack/react-virtual": "^3.13.12",
18
19
"@tanstack/router-plugin": "^1.121.2",
19
20
"idb-keyval": "^6.2.2",
20
21
"jotai": "^2.13.1",
···
2579
2580
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2580
2581
}
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
+
},
2582
2600
"node_modules/@tanstack/router-core": {
2583
2601
"version": "1.131.28",
2584
2602
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz",
···
2731
2749
"version": "0.7.4",
2732
2750
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz",
2733
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==",
2734
2762
"license": "MIT",
2735
2763
"funding": {
2736
2764
"type": "github",
+1
package.json
+1
package.json
···
19
19
"@tanstack/react-query-persist-client": "^5.85.6",
20
20
"@tanstack/react-router": "^1.130.2",
21
21
"@tanstack/react-router-devtools": "^1.131.5",
22
+
"@tanstack/react-virtual": "^3.13.12",
22
23
"@tanstack/router-plugin": "^1.121.2",
23
24
"idb-keyval": "^6.2.2",
24
25
"jotai": "^2.13.1",
+213
-23
src/components/InfiniteCustomFeed.tsx
+213
-23
src/components/InfiniteCustomFeed.tsx
···
1
+
/* eslint-disable react-hooks/refs */
2
+
import { useWindowVirtualizer } from "@tanstack/react-virtual";
3
+
import { useAtom } from "jotai";
1
4
import * as React from "react";
5
+
import { useEffect, useLayoutEffect } from "react";
6
+
2
7
//import { useInView } from "react-intersection-observer";
3
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
4
9
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
-
import {
6
-
useQueryArbitrary,
7
-
useQueryIdentity,
8
-
useInfiniteQueryFeedSkeleton,
9
-
} from "~/utils/useQuery";
10
+
import { feedHeightsAtom, feedScrollIndexAtom } from "~/utils/atoms";
11
+
import { useInfiniteQueryFeedSkeleton } from "~/utils/useQuery";
10
12
11
13
interface InfiniteCustomFeedProps {
12
14
feedUri: string;
13
15
pdsUrl?: string;
14
16
feedServiceDid?: string;
17
+
initialScrollIndex?: number;
18
+
//onVisibleIndexChange?: (index: number) => void;
15
19
}
16
20
17
21
export function InfiniteCustomFeed({
18
22
feedUri,
19
23
pdsUrl,
20
24
feedServiceDid,
25
+
initialScrollIndex,
26
+
//onVisibleIndexChange,
21
27
}: InfiniteCustomFeedProps) {
28
+
const OVERSCAN_COUNT = 10;
29
+
const ESTIMATE_HEIGHT = 150;
30
+
22
31
const { agent } = useAuth();
23
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];
24
38
25
39
// const identityresultmaybe = useQueryIdentity(agent?.did);
26
40
// const identity = identityresultmaybe?.data;
···
56
70
// }
57
71
// }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
58
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
+
59
221
if (isLoading) {
60
222
return <div className="p-4 text-center text-gray-500">Loading feed...</div>;
61
223
}
···
65
227
<div className="p-4 text-center text-red-500">Error: {error.message}</div>
66
228
);
67
229
}
68
-
69
-
const allPosts =
70
-
data?.pages.flatMap((page) => {
71
-
if (page) return page.feed;
72
-
}) ?? [];
73
230
74
231
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
75
232
return (
···
79
236
);
80
237
}
81
238
239
+
//if (offsetTop === 0) {
240
+
// return <div ref={listRef}>Calculating...</div>;
241
+
//}
242
+
82
243
return (
83
244
<>
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
-
})}
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
+
95
281
{/* allPosts?: {allPosts ? "true" : "false"}
96
282
hasNextPage?: {hasNextPage ? "true" : "false"}
97
283
isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */}
···
115
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"
116
302
aria-label="Refresh feed"
117
303
>
118
-
{isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />}
304
+
{isRefetching ? (
305
+
<RefreshIcon className="h-6 w-6 animate-spin" />
306
+
) : (
307
+
<RefreshIcon className="h-6 w-6" />
308
+
)}
119
309
</button>
120
310
</>
121
311
);
···
138
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"
139
329
></path>
140
330
</svg>
141
-
);
331
+
);
+32
-1
src/components/UniversalPostRenderer.tsx
+32
-1
src/components/UniversalPostRenderer.tsx
···
28
28
bottomBorder?: boolean;
29
29
feedviewpost?: boolean;
30
30
repostedby?: string;
31
+
style?: React.CSSProperties;
32
+
ref?: React.Ref<HTMLDivElement>;
33
+
dataIndexPropPass?: number;
31
34
}
32
35
33
36
// export async function cachedGetRecord({
···
132
135
bottomBorder = true,
133
136
feedviewpost = false,
134
137
repostedby,
138
+
style,
139
+
ref,
140
+
dataIndexPropPass,
135
141
}: UniversalPostRendererATURILoaderProps) {
136
142
// /*mass comment*/ console.log("atUri", atUri);
137
143
//const { get, set } = usePersistentStore();
···
406
412
bottomBorder={bottomBorder}
407
413
feedviewpost={feedviewpost}
408
414
repostedby={repostedby}
415
+
style={style}
416
+
ref={ref}
417
+
dataIndexPropPass={dataIndexPropPass}
409
418
/>
410
419
);
411
420
}
···
430
439
bottomBorder = true,
431
440
feedviewpost = false,
432
441
repostedby,
442
+
style,
443
+
ref,
444
+
dataIndexPropPass,
433
445
}: {
434
446
postRecord: any;
435
447
profileRecord: any;
···
444
456
bottomBorder?: boolean;
445
457
feedviewpost?: boolean;
446
458
repostedby?: string;
459
+
style?: React.CSSProperties;
460
+
ref?: React.Ref<HTMLDivElement>;
461
+
dataIndexPropPass?: number;
447
462
}) {
448
463
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
449
464
const navigate = useNavigate();
···
638
653
//extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}}
639
654
feedviewpostreplyhandle={feedviewpostreplyhandle}
640
655
repostedby={feedviewpostrepostedbyhandle}
656
+
style={style}
657
+
ref={ref}
658
+
dataIndexPropPass={dataIndexPropPass}
641
659
/>
642
660
</>
643
661
);
···
1079
1097
feedviewpostreplyhandle,
1080
1098
depth = 0,
1081
1099
repostedby,
1100
+
style,
1101
+
ref,
1102
+
dataIndexPropPass,
1082
1103
}: {
1083
1104
post: PostView;
1084
1105
// optional for now because i havent ported every use to this yet
···
1098
1119
feedviewpostreplyhandle?: string;
1099
1120
depth?: number;
1100
1121
repostedby?: string;
1122
+
style?: React.CSSProperties;
1123
+
ref?: React.Ref<HTMLDivElement>;
1124
+
dataIndexPropPass?: number;
1101
1125
}) {
1102
1126
const navigate = useNavigate();
1103
1127
const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom);
···
1171
1195
/* fuck you */
1172
1196
const isMainItem = false;
1173
1197
const setMainItem = (any: any) => {};
1198
+
// eslint-disable-next-line react-hooks/refs
1199
+
console.log("Received ref in UniversalPostRenderer:", ref);
1174
1200
return (
1201
+
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1175
1202
<div
1203
+
//ref={ref}
1176
1204
key={salt + "-" + (post.uri || emergencySalt)}
1177
1205
onClick={
1178
1206
isMainItem
···
1188
1216
}
1189
1217
: undefined
1190
1218
}
1191
-
style={{
1219
+
style={
1220
+
{
1221
+
//...style,
1192
1222
//border: "1px solid #e1e8ed",
1193
1223
//borderRadius: 12,
1194
1224
opacity: "1 !important",
···
1572
1602
/>
1573
1603
</div>
1574
1604
</div>
1605
+
</div>
1575
1606
</div>
1576
1607
);
1577
1608
}
+127
-104
src/routes/index.tsx
+127
-104
src/routes/index.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
2
import { useAtom } from "jotai";
3
3
import * as React from "react";
4
-
import { useEffect, useLayoutEffect } from "react";
4
+
import { useEffect } from "react";
5
5
6
6
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
7
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
8
import {
9
9
agentAtom,
10
10
authedAtom,
11
-
feedScrollPositionsAtom,
11
+
feedScrollIndexAtom,
12
12
selectedFeedUriAtom,
13
13
store,
14
14
} from "~/utils/atoms";
15
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
16
import {
17
-
constructArbitraryQuery,
18
-
constructIdentityQuery,
19
-
constructInfiniteFeedSkeletonQuery,
20
-
constructPostQuery,
17
+
//constructArbitraryQuery,
18
+
//constructIdentityQuery,
19
+
//constructInfiniteFeedSkeletonQuery,
20
+
//constructPostQuery,
21
21
useQueryArbitrary,
22
22
useQueryIdentity,
23
23
useQueryPreferences,
24
24
} from "~/utils/useQuery";
25
25
26
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);
27
+
// loader: async ({ context }) => {
28
+
// const { queryClient } = context;
29
+
// const atomauth = store.get(authedAtom);
30
+
// const atomagent = store.get(agentAtom);
31
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
-
}
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
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()
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
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
-
});
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
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
-
},
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
86
component: Home,
87
87
pendingComponent: PendingHome,
88
88
});
···
288
288
// };
289
289
// }, [authed, agent, loadering, selectedFeed, get, set]);
290
290
291
-
const [scrollPositions, setScrollPositions] = useAtom(
292
-
feedScrollPositionsAtom
293
-
);
291
+
// const [scrollPositions, setScrollPositions] = useAtom(
292
+
// feedScrollPositionsAtom
293
+
// );
294
294
295
-
const scrollRef = React.useRef<Record<string, number>>({});
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
+
// }, []);
296
302
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);
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);
306
325
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]);
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]);
319
338
320
-
const [restoringScrollPosition, setRestoringScrollPosition] =
321
-
React.useState(false);
339
+
// const [restoringScrollPosition, setRestoringScrollPosition] =
340
+
// React.useState(false);
322
341
323
-
useLayoutEffect(() => {
324
-
setRestoringScrollPosition(true);
325
-
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
342
+
// useLayoutEffect(() => {
343
+
// setRestoringScrollPosition(true);
344
+
// const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
326
345
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
-
});
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
+
// });
338
357
339
-
return () => cancelAnimationFrame(raf);
340
-
}, [selectedFeed, scrollPositions]);
358
+
// return () => cancelAnimationFrame(raf);
359
+
// }, [selectedFeed, scrollPositions]);
341
360
342
361
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
343
362
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
···
359
378
const isReadyForAuthedFeed =
360
379
authed && agent && identity?.pds && feedServiceDid;
361
380
const isReadyForUnauthedFeed = !authed && selectedFeed;
381
+
382
+
const savedIndex = selectedFeed ? scrollIndexes[selectedFeed] : 0;
362
383
363
384
return (
364
385
<div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
···
416
437
feedUri={selectedFeed!}
417
438
pdsUrl={identity?.pds}
418
439
feedServiceDid={feedServiceDid}
440
+
initialScrollIndex={savedIndex}
441
+
//onVisibleIndexChange={handleVisibleIndexChange}
419
442
/>
420
443
) : (
421
444
<div className="p-4 text-center text-gray-500">
+10
src/utils/atoms.ts
+10
src/utils/atoms.ts
···
11
11
12
12
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
13
13
14
+
/**
15
+
* @deprecated use the Tanstack Virtual index thanks
16
+
*/
14
17
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
15
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',
16
26
{}
17
27
);
18
28