tangled
alpha
login
or
join now
whey.party
/
red-dwarf
99
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
99
fork
atom
overview
issues
25
pulls
pipelines
minimal working virtual
rimar1337
6 months ago
9356009e
833207ed
+411
-128
6 changed files
expand all
collapse all
unified
split
package-lock.json
package.json
src
components
InfiniteCustomFeed.tsx
UniversalPostRenderer.tsx
routes
index.tsx
utils
atoms.ts
+28
package-lock.json
reviewed
···
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
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
2583
+
"node_modules/@tanstack/react-virtual": {
2584
2584
+
"version": "3.13.12",
2585
2585
+
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
2586
2586
+
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
2587
2587
+
"license": "MIT",
2588
2588
+
"dependencies": {
2589
2589
+
"@tanstack/virtual-core": "3.13.12"
2590
2590
+
},
2591
2591
+
"funding": {
2592
2592
+
"type": "github",
2593
2593
+
"url": "https://github.com/sponsors/tannerlinsley"
2594
2594
+
},
2595
2595
+
"peerDependencies": {
2596
2596
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2597
2597
+
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2598
2598
+
}
2599
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
2752
+
"license": "MIT",
2753
2753
+
"funding": {
2754
2754
+
"type": "github",
2755
2755
+
"url": "https://github.com/sponsors/tannerlinsley"
2756
2756
+
}
2757
2757
+
},
2758
2758
+
"node_modules/@tanstack/virtual-core": {
2759
2759
+
"version": "3.13.12",
2760
2760
+
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
2761
2761
+
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
2734
2762
"license": "MIT",
2735
2763
"funding": {
2736
2764
"type": "github",
+1
package.json
reviewed
···
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
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
reviewed
···
1
1
+
/* eslint-disable react-hooks/refs */
2
2
+
import { useWindowVirtualizer } from "@tanstack/react-virtual";
3
3
+
import { useAtom } from "jotai";
1
4
import * as React from "react";
5
5
+
import { useEffect, useLayoutEffect } from "react";
6
6
+
2
7
//import { useInView } from "react-intersection-observer";
3
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
4
9
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
5
-
import {
6
6
-
useQueryArbitrary,
7
7
-
useQueryIdentity,
8
8
-
useInfiniteQueryFeedSkeleton,
9
9
-
} from "~/utils/useQuery";
10
10
+
import { feedHeightsAtom, feedScrollIndexAtom } from "~/utils/atoms";
11
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
17
+
initialScrollIndex?: number;
18
18
+
//onVisibleIndexChange?: (index: number) => void;
15
19
}
16
20
17
21
export function InfiniteCustomFeed({
18
22
feedUri,
19
23
pdsUrl,
20
24
feedServiceDid,
25
25
+
initialScrollIndex,
26
26
+
//onVisibleIndexChange,
21
27
}: InfiniteCustomFeedProps) {
28
28
+
const OVERSCAN_COUNT = 10;
29
29
+
const ESTIMATE_HEIGHT = 150;
30
30
+
22
31
const { agent } = useAuth();
23
32
const authed = !!agent?.did;
33
33
+
34
34
+
const listRef = React.useRef<HTMLDivElement | null>(null);
35
35
+
const [offsetTop, setOffsetTop] = React.useState(0);
36
36
+
const [scrollIndexes, setScrollIndexes] = useAtom(feedScrollIndexAtom);
37
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
73
+
const allPosts = React.useMemo(() => {
74
74
+
const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? [];
75
75
+
76
76
+
const seenUris = new Set<string>();
77
77
+
78
78
+
return flattenedPosts.filter((item) => {
79
79
+
if (!item?.post) return false;
80
80
+
81
81
+
if (seenUris.has(item.post)) {
82
82
+
return false;
83
83
+
}
84
84
+
85
85
+
seenUris.add(item.post);
86
86
+
return true;
87
87
+
});
88
88
+
}, [data]);
89
89
+
90
90
+
const [feedHeights, setFeedHeights] = useAtom(feedHeightsAtom);
91
91
+
const currentFeedCache = feedHeights[feedUri] ?? {};
92
92
+
93
93
+
const virtualizerRef = React.useRef<ReturnType<
94
94
+
typeof useWindowVirtualizer
95
95
+
> | null>(null);
96
96
+
97
97
+
const virtualizer = useWindowVirtualizer({
98
98
+
count: allPosts.length,
99
99
+
// +
100
100
+
// (isFetchingNextPage ? 1 : 0) +
101
101
+
// (hasNextPage && !isFetchingNextPage ? 1 : 0) +
102
102
+
// (!hasNextPage ? 1 : 0) +
103
103
+
// 1,
104
104
+
estimateSize: (index) => {
105
105
+
const post = allPosts[index];
106
106
+
if (!post) return ESTIMATE_HEIGHT;
107
107
+
108
108
+
if (currentFeedCache[post.post]) {
109
109
+
return currentFeedCache[post.post];
110
110
+
}
111
111
+
112
112
+
return ESTIMATE_HEIGHT;
113
113
+
},
114
114
+
// measureElement: measureElement,
115
115
+
overscan: OVERSCAN_COUNT,
116
116
+
scrollMargin: offsetTop,
117
117
+
});
118
118
+
// React.useEffect(() => {
119
119
+
// virtualizer.measure();
120
120
+
// }, [data]);
121
121
+
122
122
+
const measureElement = React.useCallback(
123
123
+
(node: HTMLElement | null) => {
124
124
+
if (!node) return;
125
125
+
126
126
+
virtualizer.measureElement(node);
127
127
+
128
128
+
const postUri = node.dataset.postUri;
129
129
+
const newHeight = node.offsetHeight;
130
130
+
131
131
+
if (postUri && newHeight > 0 && currentFeedCache[postUri] !== newHeight) {
132
132
+
setFeedHeights((prev) => ({
133
133
+
...prev,
134
134
+
[feedUri]: {
135
135
+
...prev[feedUri],
136
136
+
[postUri]: newHeight,
137
137
+
},
138
138
+
}));
139
139
+
}
140
140
+
},
141
141
+
[virtualizer, setFeedHeights, feedUri, currentFeedCache]
142
142
+
);
143
143
+
144
144
+
virtualizerRef.current = virtualizer;
145
145
+
146
146
+
useLayoutEffect(() => {
147
147
+
const update = () => {
148
148
+
if (listRef.current) {
149
149
+
setOffsetTop(listRef.current.offsetTop);
150
150
+
}
151
151
+
//if (virtualizerRef.current) {
152
152
+
// virtualizerRef.current.measure();
153
153
+
// }
154
154
+
};
155
155
+
156
156
+
update();
157
157
+
158
158
+
let debounceTimeout: NodeJS.Timeout;
159
159
+
160
160
+
const debouncedUpdate = () => {
161
161
+
clearTimeout(debounceTimeout);
162
162
+
debounceTimeout = setTimeout(update, 100);
163
163
+
};
164
164
+
165
165
+
window.addEventListener("resize", debouncedUpdate);
166
166
+
167
167
+
return () => {
168
168
+
window.removeEventListener("resize", debouncedUpdate);
169
169
+
clearTimeout(debounceTimeout);
170
170
+
};
171
171
+
}, []);
172
172
+
173
173
+
const hasRestoredScroll = React.useRef(false);
174
174
+
useLayoutEffect(() => {
175
175
+
if (
176
176
+
hasRestoredScroll.current ||
177
177
+
!initialScrollIndex ||
178
178
+
initialScrollIndex === 0
179
179
+
) {
180
180
+
return;
181
181
+
}
182
182
+
183
183
+
if (initialScrollIndex < allPosts.length) {
184
184
+
console.log(`Restoring scroll to index: ${initialScrollIndex}`);
185
185
+
virtualizer.scrollToIndex(initialScrollIndex, {
186
186
+
align: "start",
187
187
+
behavior: "auto",
188
188
+
});
189
189
+
hasRestoredScroll.current = true;
190
190
+
}
191
191
+
}, [initialScrollIndex, allPosts.length, virtualizer]);
192
192
+
193
193
+
// React.useEffect(() => {
194
194
+
// const handleScroll = () => {
195
195
+
// const topVisibleItem = virtualizer.getVirtualItems()[0];
196
196
+
// if (topVisibleItem && onVisibleIndexChange) {
197
197
+
// onVisibleIndexChange(topVisibleItem.index);
198
198
+
// }
199
199
+
// };
200
200
+
201
201
+
// window.addEventListener('scroll', handleScroll, { passive: true });
202
202
+
// return () => window.removeEventListener('scroll', handleScroll);
203
203
+
// }, [virtualizer, onVisibleIndexChange]);
204
204
+
205
205
+
useEffect(() => {
206
206
+
return () => {
207
207
+
const topVisibleItem = virtualizer.getVirtualItems()[OVERSCAN_COUNT];
208
208
+
209
209
+
if (topVisibleItem) {
210
210
+
console.log(
211
211
+
`Saving final scroll index ${topVisibleItem.index} for feed ${feedUri}`
212
212
+
);
213
213
+
setScrollIndexes((prev) => ({
214
214
+
...prev,
215
215
+
[feedUri]: topVisibleItem.index,
216
216
+
}));
217
217
+
}
218
218
+
};
219
219
+
}, [virtualizer, feedUri, setScrollIndexes]);
220
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
68
-
69
69
-
const allPosts =
70
70
-
data?.pages.flatMap((page) => {
71
71
-
if (page) return page.feed;
72
72
-
}) ?? [];
73
230
74
231
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
75
232
return (
···
79
236
);
80
237
}
81
238
239
239
+
//if (offsetTop === 0) {
240
240
+
// return <div ref={listRef}>Calculating...</div>;
241
241
+
//}
242
242
+
82
243
return (
83
244
<>
84
84
-
{allPosts.map((item, i) => {
85
85
-
if (item)
86
86
-
return (
87
87
-
<UniversalPostRendererATURILoader
88
88
-
key={item.post || i}
89
89
-
atUri={item.post}
90
90
-
feedviewpost={true}
91
91
-
repostedby={!!item.reason?.$type && (item.reason as any)?.repost}
92
92
-
/>
93
93
-
);
94
94
-
})}
245
245
+
<div ref={listRef}>
246
246
+
<div
247
247
+
style={{
248
248
+
height: `${virtualizer.getTotalSize()}px`,
249
249
+
width: "100%",
250
250
+
position: "relative",
251
251
+
}}
252
252
+
>
253
253
+
{virtualizer.getVirtualItems().map((virtualItem) => {
254
254
+
const item = allPosts[virtualItem.index];
255
255
+
const i = virtualItem.index;
256
256
+
if (item)
257
257
+
return (
258
258
+
<UniversalPostRendererATURILoader
259
259
+
key={item.post || i}
260
260
+
atUri={item.post}
261
261
+
dataIndexPropPass={i}
262
262
+
feedviewpost={true}
263
263
+
ref={measureElement}
264
264
+
repostedby={
265
265
+
!!item.reason?.$type && (item.reason as any)?.repost
266
266
+
}
267
267
+
style={{
268
268
+
position: "absolute",
269
269
+
top: 0,
270
270
+
left: 0,
271
271
+
width: "100%",
272
272
+
//height: `${item.size}px`,
273
273
+
transform: `translateY(${virtualItem.start - offsetTop}px)`,
274
274
+
}}
275
275
+
/>
276
276
+
);
277
277
+
})}
278
278
+
</div>
279
279
+
</div>
280
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
118
-
{isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />}
304
304
+
{isRefetching ? (
305
305
+
<RefreshIcon className="h-6 w-6 animate-spin" />
306
306
+
) : (
307
307
+
<RefreshIcon className="h-6 w-6" />
308
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
141
-
);
331
331
+
);
+32
-1
src/components/UniversalPostRenderer.tsx
reviewed
···
28
28
bottomBorder?: boolean;
29
29
feedviewpost?: boolean;
30
30
repostedby?: string;
31
31
+
style?: React.CSSProperties;
32
32
+
ref?: React.Ref<HTMLDivElement>;
33
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
138
+
style,
139
139
+
ref,
140
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
415
+
style={style}
416
416
+
ref={ref}
417
417
+
dataIndexPropPass={dataIndexPropPass}
409
418
/>
410
419
);
411
420
}
···
430
439
bottomBorder = true,
431
440
feedviewpost = false,
432
441
repostedby,
442
442
+
style,
443
443
+
ref,
444
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
459
+
style?: React.CSSProperties;
460
460
+
ref?: React.Ref<HTMLDivElement>;
461
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
656
+
style={style}
657
657
+
ref={ref}
658
658
+
dataIndexPropPass={dataIndexPropPass}
641
659
/>
642
660
</>
643
661
);
···
1079
1097
feedviewpostreplyhandle,
1080
1098
depth = 0,
1081
1099
repostedby,
1100
1100
+
style,
1101
1101
+
ref,
1102
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
1122
+
style?: React.CSSProperties;
1123
1123
+
ref?: React.Ref<HTMLDivElement>;
1124
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
1198
+
// eslint-disable-next-line react-hooks/refs
1199
1199
+
console.log("Received ref in UniversalPostRenderer:", ref);
1174
1200
return (
1201
1201
+
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1175
1202
<div
1203
1203
+
//ref={ref}
1176
1204
key={salt + "-" + (post.uri || emergencySalt)}
1177
1205
onClick={
1178
1206
isMainItem
···
1188
1216
}
1189
1217
: undefined
1190
1218
}
1191
1191
-
style={{
1219
1219
+
style={
1220
1220
+
{
1221
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
1605
+
</div>
1575
1606
</div>
1576
1607
);
1577
1608
}
+127
-104
src/routes/index.tsx
reviewed
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
2
import { useAtom } from "jotai";
3
3
import * as React from "react";
4
4
-
import { useEffect, useLayoutEffect } from "react";
4
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
11
-
feedScrollPositionsAtom,
11
11
+
feedScrollIndexAtom,
12
12
selectedFeedUriAtom,
13
13
store,
14
14
} from "~/utils/atoms";
15
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
16
import {
17
17
-
constructArbitraryQuery,
18
18
-
constructIdentityQuery,
19
19
-
constructInfiniteFeedSkeletonQuery,
20
20
-
constructPostQuery,
17
17
+
//constructArbitraryQuery,
18
18
+
//constructIdentityQuery,
19
19
+
//constructInfiniteFeedSkeletonQuery,
20
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
27
-
loader: async ({ context }) => {
28
28
-
const { queryClient } = context;
29
29
-
const atomauth = store.get(authedAtom);
30
30
-
const atomagent = store.get(agentAtom);
27
27
+
// loader: async ({ context }) => {
28
28
+
// const { queryClient } = context;
29
29
+
// const atomauth = store.get(authedAtom);
30
30
+
// const atomagent = store.get(agentAtom);
31
31
32
32
-
let identitypds: string | undefined;
33
33
-
const initialselectedfeed = store.get(selectedFeedUriAtom);
34
34
-
if (atomagent && atomauth && atomagent?.did) {
35
35
-
const identityopts = constructIdentityQuery(atomagent.did);
36
36
-
const identityresultmaybe =
37
37
-
await queryClient.ensureQueryData(identityopts);
38
38
-
identitypds = identityresultmaybe?.pds;
39
39
-
}
32
32
+
// let identitypds: string | undefined;
33
33
+
// const initialselectedfeed = store.get(selectedFeedUriAtom);
34
34
+
// if (atomagent && atomauth && atomagent?.did) {
35
35
+
// const identityopts = constructIdentityQuery(atomagent.did);
36
36
+
// const identityresultmaybe =
37
37
+
// await queryClient.ensureQueryData(identityopts);
38
38
+
// identitypds = identityresultmaybe?.pds;
39
39
+
// }
40
40
41
41
-
const arbitraryopts = constructArbitraryQuery(
42
42
-
initialselectedfeed ??
43
43
-
"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
44
44
-
);
45
45
-
const feedGengetrecordquery =
46
46
-
await queryClient.ensureQueryData(arbitraryopts);
47
47
-
const feedServiceDid = (feedGengetrecordquery?.value as any)?.did;
48
48
-
//queryClient.ensureInfiniteQueryData()
41
41
+
// const arbitraryopts = constructArbitraryQuery(
42
42
+
// initialselectedfeed ??
43
43
+
// "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
44
44
+
// );
45
45
+
// const feedGengetrecordquery =
46
46
+
// await queryClient.ensureQueryData(arbitraryopts);
47
47
+
// const feedServiceDid = (feedGengetrecordquery?.value as any)?.did;
48
48
+
// //queryClient.ensureInfiniteQueryData()
49
49
50
50
-
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({
51
51
-
feedUri:
52
52
-
initialselectedfeed ??
53
53
-
"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
54
54
-
agent: atomagent ?? undefined,
55
55
-
isAuthed: atomauth ?? false,
56
56
-
pdsUrl: identitypds,
57
57
-
feedServiceDid: feedServiceDid,
58
58
-
});
50
50
+
// const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({
51
51
+
// feedUri:
52
52
+
// initialselectedfeed ??
53
53
+
// "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
54
54
+
// agent: atomagent ?? undefined,
55
55
+
// isAuthed: atomauth ?? false,
56
56
+
// pdsUrl: identitypds,
57
57
+
// feedServiceDid: feedServiceDid,
58
58
+
// });
59
59
60
60
-
const res = await queryClient.ensureInfiniteQueryData({
61
61
-
queryKey,
62
62
-
queryFn,
63
63
-
initialPageParam: undefined as never,
64
64
-
getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined,
65
65
-
staleTime: Infinity,
66
66
-
//refetchOnWindowFocus: false,
67
67
-
//enabled: true,
68
68
-
});
69
69
-
await Promise.all(
70
70
-
res.pages.map(async (page) => {
71
71
-
await Promise.all(
72
72
-
page.feed.map(async (feedviewpost) => {
73
73
-
if (!feedviewpost.post) return;
74
74
-
// /*mass comment*/ console.log("preloading: ", feedviewpost.post);
75
75
-
const opts = constructPostQuery(feedviewpost.post);
76
76
-
try {
77
77
-
await queryClient.ensureQueryData(opts);
78
78
-
} catch (e) {
79
79
-
// /*mass comment*/ console.log(" failed:", e);
80
80
-
}
81
81
-
})
82
82
-
);
83
83
-
})
84
84
-
);
85
85
-
},
60
60
+
// const res = await queryClient.ensureInfiniteQueryData({
61
61
+
// queryKey,
62
62
+
// queryFn,
63
63
+
// initialPageParam: undefined as never,
64
64
+
// getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined,
65
65
+
// staleTime: Infinity,
66
66
+
// //refetchOnWindowFocus: false,
67
67
+
// //enabled: true,
68
68
+
// });
69
69
+
// await Promise.all(
70
70
+
// res.pages.map(async (page) => {
71
71
+
// await Promise.all(
72
72
+
// page.feed.map(async (feedviewpost) => {
73
73
+
// if (!feedviewpost.post) return;
74
74
+
// // /*mass comment*/ console.log("preloading: ", feedviewpost.post);
75
75
+
// const opts = constructPostQuery(feedviewpost.post);
76
76
+
// try {
77
77
+
// await queryClient.ensureQueryData(opts);
78
78
+
// } catch (e) {
79
79
+
// // /*mass comment*/ console.log(" failed:", e);
80
80
+
// }
81
81
+
// })
82
82
+
// );
83
83
+
// })
84
84
+
// );
85
85
+
// },
86
86
component: Home,
87
87
pendingComponent: PendingHome,
88
88
});
···
288
288
// };
289
289
// }, [authed, agent, loadering, selectedFeed, get, set]);
290
290
291
291
-
const [scrollPositions, setScrollPositions] = useAtom(
292
292
-
feedScrollPositionsAtom
293
293
-
);
291
291
+
// const [scrollPositions, setScrollPositions] = useAtom(
292
292
+
// feedScrollPositionsAtom
293
293
+
// );
294
294
295
295
-
const scrollRef = React.useRef<Record<string, number>>({});
295
295
+
const [scrollIndexes] = useAtom(feedScrollIndexAtom);
296
296
+
297
297
+
//const latestVisibleIndexRef = React.useRef(0);
298
298
+
299
299
+
// const handleVisibleIndexChange = React.useCallback((index: number) => {
300
300
+
// latestVisibleIndexRef.current = index;
301
301
+
// }, []);
296
302
297
297
-
useEffect(() => {
298
298
-
const onScroll = () => {
299
299
-
//if (!selectedFeed) return;
300
300
-
scrollRef.current[selectedFeed ?? "null"] = window.scrollY;
301
301
-
};
302
302
-
window.addEventListener("scroll", onScroll, { passive: true });
303
303
-
return () => window.removeEventListener("scroll", onScroll);
304
304
-
}, [selectedFeed]);
305
305
-
const [donerestored, setdonerestored] = React.useState(false);
303
303
+
// React.useEffect(() => {
304
304
+
// // This return function is the cleanup effect.
305
305
+
// return () => {
306
306
+
// if (selectedFeed) {
307
307
+
// console.log(`Saving scroll index ${latestVisibleIndexRef.current} for feed ${selectedFeed}`);
308
308
+
// setScrollIndexes((prev) => ({
309
309
+
// ...prev,
310
310
+
// [selectedFeed]: latestVisibleIndexRef.current,
311
311
+
// }));
312
312
+
// }
313
313
+
// };
314
314
+
// }, [selectedFeed, setScrollIndexes]);
315
315
+
316
316
+
// useEffect(() => {
317
317
+
// const onScroll = () => {
318
318
+
// //if (!selectedFeed) return;
319
319
+
// scrollRef.current[selectedFeed ?? "null"] = window.scrollY;
320
320
+
// };
321
321
+
// window.addEventListener("scroll", onScroll, { passive: true });
322
322
+
// return () => window.removeEventListener("scroll", onScroll);
323
323
+
// }, [selectedFeed]);
324
324
+
// const [donerestored, setdonerestored] = React.useState(false);
306
325
307
307
-
useEffect(() => {
308
308
-
return () => {
309
309
-
if (!donerestored) return;
310
310
-
// /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current);
311
311
-
//if (!selectedFeed) return;
312
312
-
setScrollPositions((prev) => ({
313
313
-
...prev,
314
314
-
[selectedFeed ?? "null"]:
315
315
-
scrollRef.current[selectedFeed ?? "null"] ?? 0,
316
316
-
}));
317
317
-
};
318
318
-
}, [selectedFeed, setScrollPositions, donerestored]);
326
326
+
// useEffect(() => {
327
327
+
// return () => {
328
328
+
// if (!donerestored) return;
329
329
+
// // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current);
330
330
+
// //if (!selectedFeed) return;
331
331
+
// setScrollPositions((prev) => ({
332
332
+
// ...prev,
333
333
+
// [selectedFeed ?? "null"]:
334
334
+
// scrollRef.current[selectedFeed ?? "null"] ?? 0,
335
335
+
// }));
336
336
+
// };
337
337
+
// }, [selectedFeed, setScrollPositions, donerestored]);
319
338
320
320
-
const [restoringScrollPosition, setRestoringScrollPosition] =
321
321
-
React.useState(false);
339
339
+
// const [restoringScrollPosition, setRestoringScrollPosition] =
340
340
+
// React.useState(false);
322
341
323
323
-
useLayoutEffect(() => {
324
324
-
setRestoringScrollPosition(true);
325
325
-
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
342
342
+
// useLayoutEffect(() => {
343
343
+
// setRestoringScrollPosition(true);
344
344
+
// const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
326
345
327
327
-
const raf = requestAnimationFrame(() => {
328
328
-
// setRestoringScrollPosition(true);
329
329
-
// raf = requestAnimationFrame(() => {
330
330
-
// window.scrollTo({ top: savedPosition, behavior: "instant" });
331
331
-
// setRestoringScrollPosition(false);
332
332
-
// setdonerestored(true);
333
333
-
// });
334
334
-
window.scrollTo({ top: savedPosition, behavior: "instant" });
335
335
-
setRestoringScrollPosition(false);
336
336
-
setdonerestored(true);
337
337
-
});
346
346
+
// const raf = requestAnimationFrame(() => {
347
347
+
// // setRestoringScrollPosition(true);
348
348
+
// // raf = requestAnimationFrame(() => {
349
349
+
// // window.scrollTo({ top: savedPosition, behavior: "instant" });
350
350
+
// // setRestoringScrollPosition(false);
351
351
+
// // setdonerestored(true);
352
352
+
// // });
353
353
+
// window.scrollTo({ top: savedPosition, behavior: "instant" });
354
354
+
// setRestoringScrollPosition(false);
355
355
+
// setdonerestored(true);
356
356
+
// });
338
357
339
339
-
return () => cancelAnimationFrame(raf);
340
340
-
}, [selectedFeed, scrollPositions]);
358
358
+
// return () => cancelAnimationFrame(raf);
359
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
381
+
382
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
440
+
initialScrollIndex={savedIndex}
441
441
+
//onVisibleIndexChange={handleVisibleIndexChange}
419
442
/>
420
443
) : (
421
444
<div className="p-4 text-center text-gray-500">
+10
src/utils/atoms.ts
reviewed
···
11
11
12
12
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
13
13
14
14
+
/**
15
15
+
* @deprecated use the Tanstack Virtual index thanks
16
16
+
*/
14
17
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
15
18
'feedscrollpositions',
19
19
+
{}
20
20
+
);
21
21
+
22
22
+
export const feedScrollIndexAtom = atomWithStorage<Record<string, number>>('feedScrollIndexes',{});
23
23
+
24
24
+
export const feedHeightsAtom = atomWithStorage<Record<string, Record<string, number>>>(
25
25
+
'feedPostHeights',
16
26
{}
17
27
);
18
28