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