tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
profile tabs
rimar1337
4 months ago
0883da1a
9d9b2b83
+450
-171
3 changed files
expand all
collapse all
unified
split
src
routes
notifications.tsx
profile.$did
index.tsx
utils
useQuery.ts
+13
-94
src/routes/notifications.tsx
···
1
1
import { AtUri } from "@atproto/api";
2
2
-
import * as TabsPrimitive from "@radix-ui/react-tabs";
3
2
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
4
3
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
5
4
import { useAtom } from "jotai";
6
5
import * as React from "react";
7
7
-
import { useEffect, useLayoutEffect } from "react";
8
6
9
7
import defaultpfp from "~/../public/favicon.png";
10
8
import { Header } from "~/components/Header";
9
9
+
import { ReusableTabRoute, useReusableTabScrollRestore } from "~/components/ReusableTabRoute";
11
10
import {
12
11
MdiCardsHeartOutline,
13
12
MdiCommentOutline,
···
18
17
import {
19
18
constellationURLAtom,
20
19
imgCDNAtom,
21
21
-
isAtTopAtom,
22
22
-
notificationsScrollAtom,
23
20
} from "~/utils/atoms";
24
21
import {
25
22
useInfiniteQueryAuthorFeed,
···
55
52
});
56
53
57
54
export default function NotificationsTabs() {
58
58
-
const [notifState, setNotifState] = useAtom(notificationsScrollAtom);
59
59
-
const activeTab = notifState.activeTab;
60
60
-
const [isAtTop] = useAtom(isAtTopAtom);
61
61
-
62
62
-
const handleValueChange = (newTab: string) => {
63
63
-
console.log(newTab);
64
64
-
setNotifState((prev) => {
65
65
-
const wow = {
66
66
-
...prev,
67
67
-
scrollPositions: {
68
68
-
...prev.scrollPositions,
69
69
-
[prev.activeTab]: window.scrollY,
70
70
-
},
71
71
-
activeTab: newTab,
72
72
-
};
73
73
-
//console.log(wow);
74
74
-
return wow;
75
75
-
});
76
76
-
};
77
77
-
78
78
-
useLayoutEffect(() => {
79
79
-
return () => {
80
80
-
setNotifState((prev) => {
81
81
-
const wow = {
82
82
-
...prev,
83
83
-
scrollPositions: {
84
84
-
...prev.scrollPositions,
85
85
-
[activeTab]: window.scrollY,
86
86
-
},
87
87
-
};
88
88
-
//console.log(wow);
89
89
-
return wow;
90
90
-
});
91
91
-
};
92
92
-
// eslint-disable-next-line react-hooks/exhaustive-deps
93
93
-
}, []);
94
94
-
95
55
return (
96
96
-
<TabsPrimitive.Root
97
97
-
value={activeTab}
98
98
-
onValueChange={handleValueChange}
99
99
-
className={`w-full`}
100
100
-
>
101
101
-
<TabsPrimitive.List
102
102
-
className={`flex sticky top-[52px] bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-[9] border-0 sm:border-b ${!isAtTop && "shadow-sm"} sm:shadow-none sm:dark:bg-gray-950 sm:bg-white border-gray-200 dark:border-gray-700`}
103
103
-
>
104
104
-
<TabsPrimitive.Trigger
105
105
-
value="mentions"
106
106
-
className="m3tab"
107
107
-
// styling is in app.css
108
108
-
>
109
109
-
Mentions
110
110
-
</TabsPrimitive.Trigger>
111
111
-
<TabsPrimitive.Trigger value="follows" className="m3tab">
112
112
-
Follows
113
113
-
</TabsPrimitive.Trigger>
114
114
-
<TabsPrimitive.Trigger value="postInteractions" className="m3tab">
115
115
-
Post Interactions
116
116
-
</TabsPrimitive.Trigger>
117
117
-
</TabsPrimitive.List>
118
118
-
119
119
-
<TabsPrimitive.Content value="mentions" className="flex-1">
120
120
-
{activeTab === "mentions" && <MentionsTab />}
121
121
-
</TabsPrimitive.Content>
122
122
-
123
123
-
<TabsPrimitive.Content value="follows" className="flex-1">
124
124
-
{activeTab === "follows" && <FollowsTab />}
125
125
-
</TabsPrimitive.Content>
126
126
-
127
127
-
<TabsPrimitive.Content value="postInteractions" className="flex-1">
128
128
-
{activeTab === "postInteractions" && <PostInteractionsTab />}
129
129
-
</TabsPrimitive.Content>
130
130
-
</TabsPrimitive.Root>
56
56
+
<ReusableTabRoute
57
57
+
route={`Notifications`}
58
58
+
tabs={{
59
59
+
Mentions: <MentionsTab />,
60
60
+
Follows: <FollowsTab />,
61
61
+
"Post Interactions": <PostInteractionsTab />,
62
62
+
}}
63
63
+
/>
131
64
);
132
65
}
133
66
···
169
102
);
170
103
}, [infiniteMentionsData]);
171
104
172
172
-
const [notifState] = useAtom(notificationsScrollAtom);
173
173
-
const activeTab = notifState.activeTab;
174
174
-
useEffect(() => {
175
175
-
const savedY = notifState.scrollPositions[activeTab] ?? 0;
176
176
-
window.scrollTo(0, savedY);
177
177
-
}, [activeTab, notifState.scrollPositions]);
105
105
+
106
106
+
useReusableTabScrollRestore("Notifications");
178
107
179
108
if (isLoading) return <LoadingState text="Loading mentions..." />;
180
109
if (isError) return <ErrorState error={error} />;
···
238
167
);
239
168
}, [infiniteFollowsData]);
240
169
241
241
-
const [notifState] = useAtom(notificationsScrollAtom);
242
242
-
const activeTab = notifState.activeTab;
243
243
-
useEffect(() => {
244
244
-
const savedY = notifState.scrollPositions[activeTab] ?? 0;
245
245
-
window.scrollTo(0, savedY);
246
246
-
}, [activeTab, notifState.scrollPositions]);
170
170
+
useReusableTabScrollRestore("Notifications");
247
171
248
172
if (isLoading) return <LoadingState text="Loading mentions..." />;
249
173
if (isError) return <ErrorState error={error} />;
···
298
222
[postsData]
299
223
);
300
224
301
301
-
const [notifState] = useAtom(notificationsScrollAtom);
302
302
-
const activeTab = notifState.activeTab;
303
303
-
useEffect(() => {
304
304
-
const savedY = notifState.scrollPositions[activeTab] ?? 0;
305
305
-
window.scrollTo(0, savedY);
306
306
-
}, [activeTab, notifState.scrollPositions]);
225
225
+
useReusableTabScrollRestore("Notifications");
307
226
308
227
return (
309
228
<>
+432
-72
src/routes/profile.$did/index.tsx
···
1
1
import { RichText } from "@atproto/api";
2
2
+
import * as ATPAPI from "@atproto/api";
2
3
import { useQueryClient } from "@tanstack/react-query";
3
4
import { createFileRoute, useNavigate } from "@tanstack/react-router";
4
5
import { useAtom } from "jotai";
5
6
import React, { type ReactNode, useEffect, useState } from "react";
6
7
8
8
+
import defaultpfp from "~/../public/favicon.png";
7
9
import { Header } from "~/components/Header";
8
10
import {
11
11
+
ReusableTabRoute,
12
12
+
useReusableTabScrollRestore,
13
13
+
} from "~/components/ReusableTabRoute";
14
14
+
import {
9
15
renderTextWithFacets,
10
16
UniversalPostRendererATURILoader,
11
17
} from "~/components/UniversalPostRenderer";
···
18
24
} from "~/utils/followState";
19
25
import {
20
26
useInfiniteQueryAuthorFeed,
27
27
+
useQueryConstellation,
21
28
useQueryIdentity,
22
29
useQueryProfile,
23
30
} from "~/utils/useQuery";
···
29
36
function ProfileComponent() {
30
37
// booo bad this is not always the did it might be a handle, use identity.did instead
31
38
const { did } = Route.useParams();
39
39
+
const { agent } = useAuth();
32
40
const navigate = useNavigate();
33
41
const queryClient = useQueryClient();
34
42
const {
···
47
55
const { data: profileRecord } = useQueryProfile(profileUri);
48
56
const profile = profileRecord?.value;
49
57
50
50
-
const {
51
51
-
data: postsData,
52
52
-
fetchNextPage,
53
53
-
hasNextPage,
54
54
-
isFetchingNextPage,
55
55
-
isLoading: arePostsLoading,
56
56
-
} = useInfiniteQueryAuthorFeed(resolvedDid, pdsUrl);
57
57
-
58
58
-
React.useEffect(() => {
59
59
-
if (postsData) {
60
60
-
postsData.pages.forEach((page) => {
61
61
-
page.records.forEach((record) => {
62
62
-
if (!queryClient.getQueryData(["post", record.uri])) {
63
63
-
queryClient.setQueryData(["post", record.uri], record);
64
64
-
}
65
65
-
});
66
66
-
});
67
67
-
}
68
68
-
}, [postsData, queryClient]);
69
69
-
70
70
-
const posts = React.useMemo(
71
71
-
() => postsData?.pages.flatMap((page) => page.records) ?? [],
72
72
-
[postsData]
73
73
-
);
74
74
-
75
58
const [imgcdn] = useAtom(imgCDNAtom);
76
59
77
60
function getAvatarUrl(p: typeof profile) {
···
90
73
const handle = resolvedHandle ? `@${resolvedHandle}` : resolvedDid || did;
91
74
const description = profile?.description || "";
92
75
93
93
-
if (isIdentityLoading) {
94
94
-
return (
95
95
-
<div className="p-4 text-center text-gray-500">Resolving profile...</div>
96
96
-
);
97
97
-
}
98
98
-
99
99
-
if (identityError) {
100
100
-
return (
101
101
-
<div className="p-4 text-center text-red-500">
102
102
-
Error: {identityError.message}
103
103
-
</div>
104
104
-
);
105
105
-
}
106
106
-
107
107
-
if (!resolvedDid) {
108
108
-
return (
109
109
-
<div className="p-4 text-center text-gray-500">Profile not found.</div>
110
110
-
);
111
111
-
}
76
76
+
const isReady = !!resolvedDid && !isIdentityLoading && !!profileRecord;
112
77
113
78
return (
114
114
-
<>
79
79
+
<div className="">
115
80
<Header
116
81
title={`Profile`}
117
82
backButtonCallback={() => {
···
121
86
window.location.assign("/");
122
87
}
123
88
}}
89
89
+
bottomBorderDisabled={true}
124
90
/>
125
91
{/* <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
126
92
<Link
···
191
157
</div>
192
158
</div>
193
159
194
194
-
{/* Posts Section */}
195
195
-
<div className="max-w-2xl mx-auto">
196
196
-
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
197
197
-
Posts
160
160
+
{/* this should not be rendered until its ready (the top profile layout is stable) */}
161
161
+
{isReady ? (
162
162
+
<ReusableTabRoute
163
163
+
route={`Profile` + did}
164
164
+
tabs={{
165
165
+
Posts: <PostsTab did={did} />,
166
166
+
Reposts: <RepostsTab did={did} />,
167
167
+
Feeds: <FeedsTab did={did} />,
168
168
+
Lists: <ListsTab did={did} />,
169
169
+
...(identity?.did === agent?.did
170
170
+
? { Likes: <SelfLikesTab did={did} /> }
171
171
+
: {}),
172
172
+
}}
173
173
+
/>
174
174
+
) : isIdentityLoading ? (
175
175
+
<div className="p-4 text-center text-gray-500">
176
176
+
Resolving profile...
198
177
</div>
199
199
-
<div>
200
200
-
{posts.map((post) => (
178
178
+
) : identityError ? (
179
179
+
<div className="p-4 text-center text-red-500">
180
180
+
Error: {identityError.message}
181
181
+
</div>
182
182
+
) : !resolvedDid ? (
183
183
+
<div className="p-4 text-center text-gray-500">Profile not found.</div>
184
184
+
) : (
185
185
+
<div className="p-4 text-center text-gray-500">
186
186
+
Loading profile content...
187
187
+
</div>
188
188
+
)}
189
189
+
</div>
190
190
+
);
191
191
+
}
192
192
+
193
193
+
function PostsTab({ did }: { did: string }) {
194
194
+
useReusableTabScrollRestore(`Profile` + did);
195
195
+
const queryClient = useQueryClient();
196
196
+
const {
197
197
+
data: identity,
198
198
+
isLoading: isIdentityLoading,
199
199
+
error: identityError,
200
200
+
} = useQueryIdentity(did);
201
201
+
202
202
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
203
203
+
204
204
+
const {
205
205
+
data: postsData,
206
206
+
fetchNextPage,
207
207
+
hasNextPage,
208
208
+
isFetchingNextPage,
209
209
+
isLoading: arePostsLoading,
210
210
+
} = useInfiniteQueryAuthorFeed(resolvedDid, identity?.pds);
211
211
+
212
212
+
React.useEffect(() => {
213
213
+
if (postsData) {
214
214
+
postsData.pages.forEach((page) => {
215
215
+
page.records.forEach((record) => {
216
216
+
if (!queryClient.getQueryData(["post", record.uri])) {
217
217
+
queryClient.setQueryData(["post", record.uri], record);
218
218
+
}
219
219
+
});
220
220
+
});
221
221
+
}
222
222
+
}, [postsData, queryClient]);
223
223
+
224
224
+
const posts = React.useMemo(
225
225
+
() => postsData?.pages.flatMap((page) => page.records) ?? [],
226
226
+
[postsData]
227
227
+
);
228
228
+
229
229
+
return (
230
230
+
<>
231
231
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
232
232
+
Posts
233
233
+
</div>
234
234
+
<div>
235
235
+
{posts.map((post) => (
236
236
+
<UniversalPostRendererATURILoader
237
237
+
key={post.uri}
238
238
+
atUri={post.uri}
239
239
+
feedviewpost={true}
240
240
+
/>
241
241
+
))}
242
242
+
</div>
243
243
+
244
244
+
{/* Loading and "Load More" states */}
245
245
+
{arePostsLoading && posts.length === 0 && (
246
246
+
<div className="p-4 text-center text-gray-500">Loading posts...</div>
247
247
+
)}
248
248
+
{isFetchingNextPage && (
249
249
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
250
250
+
)}
251
251
+
{hasNextPage && !isFetchingNextPage && (
252
252
+
<button
253
253
+
onClick={() => fetchNextPage()}
254
254
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
255
255
+
>
256
256
+
Load More Posts
257
257
+
</button>
258
258
+
)}
259
259
+
{posts.length === 0 && !arePostsLoading && (
260
260
+
<div className="p-4 text-center text-gray-500">No posts found.</div>
261
261
+
)}
262
262
+
</>
263
263
+
);
264
264
+
}
265
265
+
266
266
+
function RepostsTab({ did }: { did: string }) {
267
267
+
useReusableTabScrollRestore(`Profile` + did);
268
268
+
const {
269
269
+
data: identity,
270
270
+
isLoading: isIdentityLoading,
271
271
+
error: identityError,
272
272
+
} = useQueryIdentity(did);
273
273
+
274
274
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
275
275
+
276
276
+
const {
277
277
+
data: repostsData,
278
278
+
fetchNextPage,
279
279
+
hasNextPage,
280
280
+
isFetchingNextPage,
281
281
+
isLoading: arePostsLoading,
282
282
+
} = useInfiniteQueryAuthorFeed(
283
283
+
resolvedDid,
284
284
+
identity?.pds,
285
285
+
"app.bsky.feed.repost"
286
286
+
);
287
287
+
288
288
+
const reposts = React.useMemo(
289
289
+
() => repostsData?.pages.flatMap((page) => page.records) ?? [],
290
290
+
[repostsData]
291
291
+
);
292
292
+
293
293
+
return (
294
294
+
<>
295
295
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
296
296
+
Reposts
297
297
+
</div>
298
298
+
<div>
299
299
+
{reposts.map((repost) => {
300
300
+
if (
301
301
+
!repost ||
302
302
+
!repost?.value ||
303
303
+
!repost?.value?.subject ||
304
304
+
// @ts-expect-error blehhhhh
305
305
+
!repost?.value?.subject?.uri
306
306
+
)
307
307
+
return;
308
308
+
const repostRecord =
309
309
+
repost.value as unknown as ATPAPI.AppBskyFeedRepost.Record;
310
310
+
return (
201
311
<UniversalPostRendererATURILoader
202
202
-
key={post.uri}
203
203
-
atUri={post.uri}
312
312
+
key={repostRecord.subject.uri}
313
313
+
atUri={repostRecord.subject.uri}
204
314
feedviewpost={true}
315
315
+
repostedby={repost.uri}
205
316
/>
206
206
-
))}
317
317
+
);
318
318
+
})}
319
319
+
</div>
320
320
+
321
321
+
{/* Loading and "Load More" states */}
322
322
+
{arePostsLoading && reposts.length === 0 && (
323
323
+
<div className="p-4 text-center text-gray-500">Loading posts...</div>
324
324
+
)}
325
325
+
{isFetchingNextPage && (
326
326
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
327
327
+
)}
328
328
+
{hasNextPage && !isFetchingNextPage && (
329
329
+
<button
330
330
+
onClick={() => fetchNextPage()}
331
331
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
332
332
+
>
333
333
+
Load More Posts
334
334
+
</button>
335
335
+
)}
336
336
+
{reposts.length === 0 && !arePostsLoading && (
337
337
+
<div className="p-4 text-center text-gray-500">No posts found.</div>
338
338
+
)}
339
339
+
</>
340
340
+
);
341
341
+
}
342
342
+
343
343
+
function FeedsTab({ did }: { did: string }) {
344
344
+
useReusableTabScrollRestore(`Profile` + did);
345
345
+
const {
346
346
+
data: identity,
347
347
+
isLoading: isIdentityLoading,
348
348
+
error: identityError,
349
349
+
} = useQueryIdentity(did);
350
350
+
351
351
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
352
352
+
353
353
+
const {
354
354
+
data: feedsData,
355
355
+
fetchNextPage,
356
356
+
hasNextPage,
357
357
+
isFetchingNextPage,
358
358
+
isLoading: arePostsLoading,
359
359
+
} = useInfiniteQueryAuthorFeed(
360
360
+
resolvedDid,
361
361
+
identity?.pds,
362
362
+
"app.bsky.feed.generator"
363
363
+
);
364
364
+
365
365
+
const feeds = React.useMemo(
366
366
+
() => feedsData?.pages.flatMap((page) => page.records) ?? [],
367
367
+
[feedsData]
368
368
+
);
369
369
+
370
370
+
return (
371
371
+
<>
372
372
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
373
373
+
Feeds
374
374
+
</div>
375
375
+
<div>
376
376
+
{feeds.map((feed) => {
377
377
+
if (!feed || !feed?.value) return;
378
378
+
const feedGenRecord =
379
379
+
feed.value as unknown as ATPAPI.AppBskyFeedGenerator.Record;
380
380
+
return <FeedItemRender feed={feed as any} key={feed.uri} />;
381
381
+
})}
382
382
+
</div>
383
383
+
384
384
+
{/* Loading and "Load More" states */}
385
385
+
{arePostsLoading && feeds.length === 0 && (
386
386
+
<div className="p-4 text-center text-gray-500">Loading feeds...</div>
387
387
+
)}
388
388
+
{isFetchingNextPage && (
389
389
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
390
390
+
)}
391
391
+
{hasNextPage && !isFetchingNextPage && (
392
392
+
<button
393
393
+
onClick={() => fetchNextPage()}
394
394
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
395
395
+
>
396
396
+
Load More Feeds
397
397
+
</button>
398
398
+
)}
399
399
+
{feeds.length === 0 && !arePostsLoading && (
400
400
+
<div className="p-4 text-center text-gray-500">No feeds found.</div>
401
401
+
)}
402
402
+
</>
403
403
+
);
404
404
+
}
405
405
+
406
406
+
function FeedItemRender({
407
407
+
feed,
408
408
+
listmode
409
409
+
}: {
410
410
+
feed: { uri: string; cid: string; value: ATPAPI.AppBskyFeedGenerator.Record };
411
411
+
listmode?: boolean;
412
412
+
}) {
413
413
+
const name = listmode ? feed.value?.name as string : feed.value?.displayName as string;
414
414
+
const aturi = new ATPAPI.AtUri(feed.uri);
415
415
+
const {data: identity} = useQueryIdentity(aturi.host);
416
416
+
const resolvedDid = identity?.did;
417
417
+
const [imgcdn] = useAtom(imgCDNAtom);
418
418
+
419
419
+
function getAvatarThumbnailUrl(f: typeof feed) {
420
420
+
const link = f?.value.avatar?.ref?.["$link"];
421
421
+
if (!link || !resolvedDid) return null;
422
422
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
423
423
+
}
424
424
+
425
425
+
// @ts-expect-error overloads sucks
426
426
+
const {data: likes} = useQueryConstellation(!listmode ? {
427
427
+
target: feed.uri,
428
428
+
method: "/links/count",
429
429
+
collection: "app.bsky.feed.like",
430
430
+
path: ".subject.uri"
431
431
+
} : undefined)
432
432
+
433
433
+
return (
434
434
+
<div className="px-4 py-4 border-b flex flex-col gap-1">
435
435
+
<div className="flex flex-row gap-3">
436
436
+
<div className="min-w-10 min-h-10">
437
437
+
<img src={getAvatarThumbnailUrl(feed) || defaultpfp} className="h-10 w-10 rounded border" />
207
438
</div>
439
439
+
<div className="flex flex-col">
440
440
+
<span className="">{name}</span>
441
441
+
<span className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">{feed.value.did || aturi.rkey}</span>
442
442
+
</div>
443
443
+
<div className="flex-1" />
444
444
+
{/* <div className="button bg-red-500 rounded-full min-w-[60px]" /> */}
445
445
+
</div>
446
446
+
<span className=" text-sm">{feed.value?.description}</span>
447
447
+
{!listmode && (<span className=" text-sm dark:text-gray-400 text-gray-500">Liked by {(likes as unknown as any)?.total as number || 0} users</span>)}
448
448
+
</div>
449
449
+
);
450
450
+
}
208
451
209
209
-
{/* Loading and "Load More" states */}
210
210
-
{arePostsLoading && posts.length === 0 && (
211
211
-
<div className="p-4 text-center text-gray-500">Loading posts...</div>
212
212
-
)}
213
213
-
{isFetchingNextPage && (
214
214
-
<div className="p-4 text-center text-gray-500">Loading more...</div>
215
215
-
)}
216
216
-
{hasNextPage && !isFetchingNextPage && (
217
217
-
<button
218
218
-
onClick={() => fetchNextPage()}
219
219
-
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
220
220
-
>
221
221
-
Load More Posts
222
222
-
</button>
223
223
-
)}
224
224
-
{posts.length === 0 && !arePostsLoading && (
225
225
-
<div className="p-4 text-center text-gray-500">No posts found.</div>
226
226
-
)}
452
452
+
453
453
+
function ListsTab({ did }: { did: string }) {
454
454
+
useReusableTabScrollRestore(`Profile` + did);
455
455
+
const {
456
456
+
data: identity,
457
457
+
isLoading: isIdentityLoading,
458
458
+
error: identityError,
459
459
+
} = useQueryIdentity(did);
460
460
+
461
461
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
462
462
+
463
463
+
const {
464
464
+
data: feedsData,
465
465
+
fetchNextPage,
466
466
+
hasNextPage,
467
467
+
isFetchingNextPage,
468
468
+
isLoading: arePostsLoading,
469
469
+
} = useInfiniteQueryAuthorFeed(
470
470
+
resolvedDid,
471
471
+
identity?.pds,
472
472
+
"app.bsky.graph.list"
473
473
+
);
474
474
+
475
475
+
const feeds = React.useMemo(
476
476
+
() => feedsData?.pages.flatMap((page) => page.records) ?? [],
477
477
+
[feedsData]
478
478
+
);
479
479
+
480
480
+
return (
481
481
+
<>
482
482
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
483
483
+
Feeds
227
484
</div>
485
485
+
<div>
486
486
+
{feeds.map((feed) => {
487
487
+
if (!feed || !feed?.value) return;
488
488
+
const feedGenRecord =
489
489
+
feed.value as unknown as ATPAPI.AppBskyFeedGenerator.Record;
490
490
+
return <FeedItemRender listmode={true} feed={feed as any} key={feed.uri} />;
491
491
+
})}
492
492
+
</div>
493
493
+
494
494
+
{/* Loading and "Load More" states */}
495
495
+
{arePostsLoading && feeds.length === 0 && (
496
496
+
<div className="p-4 text-center text-gray-500">Loading lists...</div>
497
497
+
)}
498
498
+
{isFetchingNextPage && (
499
499
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
500
500
+
)}
501
501
+
{hasNextPage && !isFetchingNextPage && (
502
502
+
<button
503
503
+
onClick={() => fetchNextPage()}
504
504
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
505
505
+
>
506
506
+
Load More Lists
507
507
+
</button>
508
508
+
)}
509
509
+
{feeds.length === 0 && !arePostsLoading && (
510
510
+
<div className="p-4 text-center text-gray-500">No lists found.</div>
511
511
+
)}
512
512
+
</>
513
513
+
);
514
514
+
}
515
515
+
516
516
+
function SelfLikesTab({ did }: { did: string }) {
517
517
+
useReusableTabScrollRestore(`Profile` + did);
518
518
+
const {
519
519
+
data: identity,
520
520
+
isLoading: isIdentityLoading,
521
521
+
error: identityError,
522
522
+
} = useQueryIdentity(did);
523
523
+
524
524
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
525
525
+
526
526
+
const {
527
527
+
data: repostsData,
528
528
+
fetchNextPage,
529
529
+
hasNextPage,
530
530
+
isFetchingNextPage,
531
531
+
isLoading: arePostsLoading,
532
532
+
} = useInfiniteQueryAuthorFeed(
533
533
+
resolvedDid,
534
534
+
identity?.pds,
535
535
+
"app.bsky.feed.like"
536
536
+
);
537
537
+
538
538
+
const reposts = React.useMemo(
539
539
+
() => repostsData?.pages.flatMap((page) => page.records) ?? [],
540
540
+
[repostsData]
541
541
+
);
542
542
+
543
543
+
return (
544
544
+
<>
545
545
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
546
546
+
Likes
547
547
+
</div>
548
548
+
<div>
549
549
+
{reposts.map((repost) => {
550
550
+
if (
551
551
+
!repost ||
552
552
+
!repost?.value ||
553
553
+
!repost?.value?.subject ||
554
554
+
// @ts-expect-error blehhhhh
555
555
+
!repost?.value?.subject?.uri
556
556
+
)
557
557
+
return;
558
558
+
const repostRecord =
559
559
+
repost.value as unknown as ATPAPI.AppBskyFeedLike.Record;
560
560
+
return (
561
561
+
<UniversalPostRendererATURILoader
562
562
+
key={repostRecord.subject.uri}
563
563
+
atUri={repostRecord.subject.uri}
564
564
+
feedviewpost={true}
565
565
+
/>
566
566
+
);
567
567
+
})}
568
568
+
</div>
569
569
+
570
570
+
{/* Loading and "Load More" states */}
571
571
+
{arePostsLoading && reposts.length === 0 && (
572
572
+
<div className="p-4 text-center text-gray-500">Loading posts...</div>
573
573
+
)}
574
574
+
{isFetchingNextPage && (
575
575
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
576
576
+
)}
577
577
+
{hasNextPage && !isFetchingNextPage && (
578
578
+
<button
579
579
+
onClick={() => fetchNextPage()}
580
580
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
581
581
+
>
582
582
+
Load More Posts
583
583
+
</button>
584
584
+
)}
585
585
+
{reposts.length === 0 && !arePostsLoading && (
586
586
+
<div className="p-4 text-center text-gray-500">No posts found.</div>
587
587
+
)}
228
588
</>
229
589
);
230
590
}
+5
-5
src/utils/useQuery.ts
···
534
534
}[];
535
535
};
536
536
537
537
-
export function constructAuthorFeedQuery(did: string, pdsUrl: string) {
537
537
+
export function constructAuthorFeedQuery(did: string, pdsUrl: string, collection: string = "app.bsky.feed.post") {
538
538
return queryOptions({
539
539
-
queryKey: ['authorFeed', did],
539
539
+
queryKey: ['authorFeed', did, collection],
540
540
queryFn: async ({ pageParam }: QueryFunctionContext) => {
541
541
const limit = 25;
542
542
543
543
const cursor = pageParam as string | undefined;
544
544
const cursorParam = cursor ? `&cursor=${cursor}` : '';
545
545
546
546
-
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=app.bsky.feed.post&limit=${limit}${cursorParam}`;
546
546
+
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`;
547
547
548
548
const res = await fetch(url);
549
549
if (!res.ok) throw new Error("Failed to fetch author's posts");
···
553
553
});
554
554
}
555
555
556
556
-
export function useInfiniteQueryAuthorFeed(did: string | undefined, pdsUrl: string | undefined) {
557
557
-
const { queryKey, queryFn } = constructAuthorFeedQuery(did!, pdsUrl!);
556
556
+
export function useInfiniteQueryAuthorFeed(did: string | undefined, pdsUrl: string | undefined, collection?: string) {
557
557
+
const { queryKey, queryFn } = constructAuthorFeedQuery(did!, pdsUrl!, collection);
558
558
559
559
return useInfiniteQuery({
560
560
queryKey,