+1
src/auto-imports.d.ts
+1
src/auto-imports.d.ts
···
19
19
const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default
20
20
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
21
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
22
+
const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default
22
23
const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default
23
24
}
+18
-4
src/components/UniversalPostRenderer.tsx
+18
-4
src/components/UniversalPostRenderer.tsx
···
41
41
ref?: React.Ref<HTMLDivElement>;
42
42
dataIndexPropPass?: number;
43
43
nopics?: boolean;
44
+
concise?: boolean;
44
45
lightboxCallback?: (d: LightboxProps) => void;
45
46
maxReplies?: number;
46
47
isQuote?: boolean;
···
152
153
ref,
153
154
dataIndexPropPass,
154
155
nopics,
156
+
concise,
155
157
lightboxCallback,
156
158
maxReplies,
157
159
isQuote,
···
536
538
ref={ref}
537
539
dataIndexPropPass={dataIndexPropPass}
538
540
nopics={nopics}
541
+
concise={concise}
539
542
lightboxCallback={lightboxCallback}
540
543
maxReplies={maxReplies}
541
544
isQuote={isQuote}
···
567
570
ref={ref}
568
571
dataIndexPropPass={dataIndexPropPass}
569
572
nopics={nopics}
573
+
concise={concise}
570
574
lightboxCallback={lightboxCallback}
571
575
maxReplies={
572
576
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
···
636
640
ref,
637
641
dataIndexPropPass,
638
642
nopics,
643
+
concise,
639
644
lightboxCallback,
640
645
maxReplies,
641
646
isQuote,
···
657
662
ref?: React.Ref<HTMLDivElement>;
658
663
dataIndexPropPass?: number;
659
664
nopics?: boolean;
665
+
concise?: boolean;
660
666
lightboxCallback?: (d: LightboxProps) => void;
661
667
maxReplies?: number;
662
668
isQuote?: boolean;
···
874
880
ref={ref}
875
881
dataIndexPropPass={dataIndexPropPass}
876
882
nopics={nopics}
883
+
concise={concise}
877
884
lightboxCallback={lightboxCallback}
878
885
maxReplies={maxReplies}
879
886
isQuote={isQuote}
···
1327
1334
ref,
1328
1335
dataIndexPropPass,
1329
1336
nopics,
1337
+
concise,
1330
1338
lightboxCallback,
1331
1339
maxReplies,
1332
1340
}: {
···
1353
1361
ref?: React.Ref<HTMLDivElement>;
1354
1362
dataIndexPropPass?: number;
1355
1363
nopics?: boolean;
1364
+
concise?: boolean;
1356
1365
lightboxCallback?: (d: LightboxProps) => void;
1357
1366
maxReplies?: number;
1358
1367
}) {
···
1759
1768
<div
1760
1769
style={{
1761
1770
fontSize: 16,
1762
-
marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8,
1771
+
marginBottom: !post.embed || concise ? 0 : 8,
1763
1772
whiteSpace: "pre-wrap",
1764
1773
textAlign: "left",
1765
1774
overflowWrap: "anywhere",
1766
1775
wordBreak: "break-word",
1767
-
//color: theme.text,
1776
+
...(concise && {
1777
+
display: "-webkit-box",
1778
+
WebkitBoxOrient: "vertical",
1779
+
WebkitLineClamp: 2,
1780
+
overflow: "hidden",
1781
+
}),
1768
1782
}}
1769
1783
className="text-gray-900 dark:text-gray-100"
1770
1784
>
···
1787
1801
</>
1788
1802
)}
1789
1803
</div>
1790
-
{post.embed && depth < 1 ? (
1804
+
{post.embed && depth < 1 && !concise ? (
1791
1805
<PostEmbeds
1792
1806
embed={post.embed}
1793
1807
//moderation={moderation}
···
1809
1823
</div>
1810
1824
</>
1811
1825
)}
1812
-
<div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}>
1826
+
<div style={{ paddingTop: post.embed && !concise && depth < 1 ? 4 : 0 }}>
1813
1827
<>
1814
1828
{expanded && (
1815
1829
<div
+66
src/routeTree.gen.ts
+66
src/routeTree.gen.ts
···
21
21
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
22
22
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
23
23
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
24
+
import { Route as ProfileDidPostRkeyRepostedByRouteImport } from './routes/profile.$did/post.$rkey.reposted-by'
25
+
import { Route as ProfileDidPostRkeyQuotesRouteImport } from './routes/profile.$did/post.$rkey.quotes'
26
+
import { Route as ProfileDidPostRkeyLikedByRouteImport } from './routes/profile.$did/post.$rkey.liked-by'
24
27
import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i'
25
28
26
29
const SettingsRoute = SettingsRouteImport.update({
···
84
87
path: '/profile/$did/post/$rkey',
85
88
getParentRoute: () => rootRouteImport,
86
89
} as any)
90
+
const ProfileDidPostRkeyRepostedByRoute =
91
+
ProfileDidPostRkeyRepostedByRouteImport.update({
92
+
id: '/reposted-by',
93
+
path: '/reposted-by',
94
+
getParentRoute: () => ProfileDidPostRkeyRoute,
95
+
} as any)
96
+
const ProfileDidPostRkeyQuotesRoute =
97
+
ProfileDidPostRkeyQuotesRouteImport.update({
98
+
id: '/quotes',
99
+
path: '/quotes',
100
+
getParentRoute: () => ProfileDidPostRkeyRoute,
101
+
} as any)
102
+
const ProfileDidPostRkeyLikedByRoute =
103
+
ProfileDidPostRkeyLikedByRouteImport.update({
104
+
id: '/liked-by',
105
+
path: '/liked-by',
106
+
getParentRoute: () => ProfileDidPostRkeyRoute,
107
+
} as any)
87
108
const ProfileDidPostRkeyImageIRoute =
88
109
ProfileDidPostRkeyImageIRouteImport.update({
89
110
id: '/image/$i',
···
102
123
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
103
124
'/profile/$did': typeof ProfileDidIndexRoute
104
125
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
126
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
127
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
128
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
105
129
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
106
130
}
107
131
export interface FileRoutesByTo {
···
115
139
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
116
140
'/profile/$did': typeof ProfileDidIndexRoute
117
141
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
142
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
143
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
144
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
118
145
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
119
146
}
120
147
export interface FileRoutesById {
···
131
158
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
132
159
'/profile/$did/': typeof ProfileDidIndexRoute
133
160
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
161
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
162
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
163
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
134
164
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
135
165
}
136
166
export interface FileRouteTypes {
···
146
176
| '/route-b'
147
177
| '/profile/$did'
148
178
| '/profile/$did/post/$rkey'
179
+
| '/profile/$did/post/$rkey/liked-by'
180
+
| '/profile/$did/post/$rkey/quotes'
181
+
| '/profile/$did/post/$rkey/reposted-by'
149
182
| '/profile/$did/post/$rkey/image/$i'
150
183
fileRoutesByTo: FileRoutesByTo
151
184
to:
···
159
192
| '/route-b'
160
193
| '/profile/$did'
161
194
| '/profile/$did/post/$rkey'
195
+
| '/profile/$did/post/$rkey/liked-by'
196
+
| '/profile/$did/post/$rkey/quotes'
197
+
| '/profile/$did/post/$rkey/reposted-by'
162
198
| '/profile/$did/post/$rkey/image/$i'
163
199
id:
164
200
| '__root__'
···
174
210
| '/_pathlessLayout/_nested-layout/route-b'
175
211
| '/profile/$did/'
176
212
| '/profile/$did/post/$rkey'
213
+
| '/profile/$did/post/$rkey/liked-by'
214
+
| '/profile/$did/post/$rkey/quotes'
215
+
| '/profile/$did/post/$rkey/reposted-by'
177
216
| '/profile/$did/post/$rkey/image/$i'
178
217
fileRoutesById: FileRoutesById
179
218
}
···
275
314
preLoaderRoute: typeof ProfileDidPostRkeyRouteImport
276
315
parentRoute: typeof rootRouteImport
277
316
}
317
+
'/profile/$did/post/$rkey/reposted-by': {
318
+
id: '/profile/$did/post/$rkey/reposted-by'
319
+
path: '/reposted-by'
320
+
fullPath: '/profile/$did/post/$rkey/reposted-by'
321
+
preLoaderRoute: typeof ProfileDidPostRkeyRepostedByRouteImport
322
+
parentRoute: typeof ProfileDidPostRkeyRoute
323
+
}
324
+
'/profile/$did/post/$rkey/quotes': {
325
+
id: '/profile/$did/post/$rkey/quotes'
326
+
path: '/quotes'
327
+
fullPath: '/profile/$did/post/$rkey/quotes'
328
+
preLoaderRoute: typeof ProfileDidPostRkeyQuotesRouteImport
329
+
parentRoute: typeof ProfileDidPostRkeyRoute
330
+
}
331
+
'/profile/$did/post/$rkey/liked-by': {
332
+
id: '/profile/$did/post/$rkey/liked-by'
333
+
path: '/liked-by'
334
+
fullPath: '/profile/$did/post/$rkey/liked-by'
335
+
preLoaderRoute: typeof ProfileDidPostRkeyLikedByRouteImport
336
+
parentRoute: typeof ProfileDidPostRkeyRoute
337
+
}
278
338
'/profile/$did/post/$rkey/image/$i': {
279
339
id: '/profile/$did/post/$rkey/image/$i'
280
340
path: '/image/$i'
···
316
376
)
317
377
318
378
interface ProfileDidPostRkeyRouteChildren {
379
+
ProfileDidPostRkeyLikedByRoute: typeof ProfileDidPostRkeyLikedByRoute
380
+
ProfileDidPostRkeyQuotesRoute: typeof ProfileDidPostRkeyQuotesRoute
381
+
ProfileDidPostRkeyRepostedByRoute: typeof ProfileDidPostRkeyRepostedByRoute
319
382
ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute
320
383
}
321
384
322
385
const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = {
386
+
ProfileDidPostRkeyLikedByRoute: ProfileDidPostRkeyLikedByRoute,
387
+
ProfileDidPostRkeyQuotesRoute: ProfileDidPostRkeyQuotesRoute,
388
+
ProfileDidPostRkeyRepostedByRoute: ProfileDidPostRkeyRepostedByRoute,
323
389
ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute,
324
390
}
325
391
+96
-56
src/routes/notifications.tsx
+96
-56
src/routes/notifications.tsx
···
1
1
import { AtUri } from "@atproto/api";
2
2
import * as TabsPrimitive from "@radix-ui/react-tabs";
3
3
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
4
-
import { createFileRoute, useNavigate } from "@tanstack/react-router";
4
+
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
5
5
import { useAtom } from "jotai";
6
6
import * as React from "react";
7
7
···
47
47
export const Route = createFileRoute("/notifications")({
48
48
component: NotificationsComponent,
49
49
});
50
-
51
50
52
51
export default function NotificationsTabs() {
53
52
const [activeTab, setActiveTab] = React.useState("mentions");
···
225
224
);
226
225
}
227
226
228
-
229
227
function PostInteractionsTab() {
230
228
const { agent } = useAuth();
231
229
const { data: identity } = useQueryIdentity(agent?.did);
···
274
272
);
275
273
}
276
274
275
+
const ORDER: ("like" | "repost" | "reply" | "quote")[] = [
276
+
"like",
277
+
"repost",
278
+
"reply",
279
+
"quote",
280
+
];
281
+
277
282
function PostInteractionsItem({ uri }: { uri: string }) {
278
283
const { data: links } = useQueryConstellation({
279
284
method: "/links/all",
280
285
target: uri,
281
286
});
282
287
283
-
const interactions = React.useMemo(() => {
284
-
const likes =
285
-
links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0;
286
-
const replies =
287
-
links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]?.records || 0;
288
-
const reposts =
289
-
links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0;
290
-
const quotes1 =
291
-
links?.links?.["app.bsky.feed.post"]?.[".embed.record.uri"]?.records || 0;
292
-
const quotes2 =
293
-
links?.links?.["app.bsky.feed.post"]?.[".embed.record.record.uri"]
294
-
?.records || 0;
288
+
const likes =
289
+
links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0;
290
+
const replies =
291
+
links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]?.records || 0;
292
+
const reposts =
293
+
links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0;
294
+
const quotes1 =
295
+
links?.links?.["app.bsky.feed.post"]?.[".embed.record.uri"]?.records || 0;
296
+
const quotes2 =
297
+
links?.links?.["app.bsky.feed.post"]?.[".embed.record.record.uri"]
298
+
?.records || 0;
299
+
const quotes = quotes1 + quotes2;
295
300
296
-
const totals = {
297
-
likes,
298
-
replies,
299
-
reposts,
300
-
quotes: quotes1 + quotes2,
301
-
};
302
-
303
-
const list = (
304
-
[
305
-
["reply", totals.replies],
306
-
["repost", totals.reposts],
307
-
["like", totals.likes],
308
-
["quote", totals.quotes],
309
-
] as const
310
-
).filter(([, count]) => count > 0);
311
-
312
-
return { totals, list };
313
-
}, [links]);
301
+
const all = likes + replies + reposts + quotes;
314
302
315
303
return (
316
-
<div className="flex flex-col border-b pb-8">
317
-
<div className="border rounded-xl mx-4 mt-4 ">
304
+
<div className="flex flex-col">
305
+
<div className="border rounded-xl mx-4 mt-4 overflow-hidden">
318
306
<UniversalPostRendererATURILoader
319
307
isQuote
320
308
key={uri}
321
309
atUri={uri}
322
-
nopics
310
+
nopics={true}
311
+
concise={true}
323
312
/>
324
-
</div>
325
-
<div className="flex flex-col">
326
-
{interactions.list.map(([type, count]) => (
327
-
<InteractionsButton key={type} type={type} uri={uri} count={count} />
328
-
))}
313
+
<div className="flex flex-col divide-x">
314
+
<InteractionsButton
315
+
key={likes}
316
+
type={"like"}
317
+
uri={uri}
318
+
count={likes}
319
+
/>
320
+
<InteractionsButton
321
+
key={reposts}
322
+
type={"repost"}
323
+
uri={uri}
324
+
count={reposts}
325
+
/>
326
+
<InteractionsButton
327
+
key={replies}
328
+
type={"reply"}
329
+
uri={uri}
330
+
count={replies}
331
+
/>
332
+
<InteractionsButton
333
+
key={quotes}
334
+
type={"quote"}
335
+
uri={uri}
336
+
count={quotes}
337
+
/>
338
+
{!all && (
339
+
<div className="text-center text-gray-500 dark:text-gray-400 pb-3 pt-2 border-t">
340
+
No interactions yet.
341
+
</div>
342
+
)}
343
+
</div>
329
344
</div>
330
345
</div>
331
346
);
···
340
355
uri: string;
341
356
count: number;
342
357
}) {
358
+
if (!count) return <></>;
359
+
const aturi = new AtUri(uri);
343
360
return (
344
-
<div className="flex-1 border-t py-2 px-4 flex flex-row items-center gap-2">
361
+
<Link
362
+
to={
363
+
`/profile/$did/post/$rkey` +
364
+
(type === "like"
365
+
? "/liked-by"
366
+
: type === "repost"
367
+
? "/reposted-by"
368
+
: type === "quote"
369
+
? "/quotes"
370
+
: "")
371
+
}
372
+
params={{
373
+
did: aturi.host,
374
+
rkey: aturi.rkey,
375
+
}}
376
+
className="flex-1 border-t py-2 px-4 flex flex-row items-center gap-2 transition-colors hover:bg-gray-100 hover:dark:bg-gray-800"
377
+
>
345
378
{type === "like" ? (
346
379
<MdiCardsHeartOutline height={22} width={22} />
347
380
) : type === "repost" ? (
348
381
<MdiRepeat height={22} width={22} />
349
382
) : type === "reply" ? (
350
383
<MdiCommentOutline height={22} width={22} />
384
+
) : type === "quote" ? (
385
+
<IconMdiMessageReplyTextOutline
386
+
height={22}
387
+
width={22}
388
+
className=" text-gray-400"
389
+
/>
351
390
) : (
352
391
<></>
353
392
)}
354
393
{type}
355
394
{/* bad grammar replys */}
356
395
{count > 1 ? "s" : ""} <div className="flex-1" /> {count}
357
-
</div>
396
+
</Link>
358
397
);
359
398
}
360
399
361
-
function NotificationItem({ notification }: { notification: string }) {
400
+
export function NotificationItem({ notification }: { notification: string }) {
362
401
const aturi = new AtUri(notification);
363
402
const navigate = useNavigate();
364
403
const { data: identity } = useQueryIdentity(aturi.host);
···
381
420
382
421
return (
383
422
<div
384
-
className="flex items-center gap-3 p-4 cursor-pointer border-b flex-row"
423
+
className="flex items-center p-4 cursor-pointer gap-3 justify-around border-b flex-row"
385
424
onClick={() =>
386
425
aturi &&
387
426
navigate({
···
390
429
})
391
430
}
392
431
>
393
-
<div>
432
+
{/* <div>
394
433
{aturi.collection === "app.bsky.graph.follow" ? (
395
434
<IconMdiAccountPlus />
435
+
) : aturi.collection === "app.bsky.feed.like" ? (
436
+
<MdiCardsHeart />
396
437
) : (
397
438
<></>
398
439
)}
399
-
</div>
440
+
</div> */}
400
441
{profile ? (
401
442
<img
402
443
src={avatar || defaultpfp}
···
406
447
) : (
407
448
<div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-700" />
408
449
)}
409
-
<div className="flex flex-col">
410
-
<div className="flex flex-row gap-2">
411
-
<span className="font-medium text-gray-900 dark:text-gray-100">
450
+
<div className="flex flex-col min-w-0">
451
+
<div className="flex flex-row gap-2 overflow-hidden text-ellipsis whitespace-nowrap min-w-0">
452
+
<span className="font-medium text-gray-900 dark:text-gray-100 truncate">
412
453
{profile?.displayName || identity?.handle || "Someone"}
413
454
</span>
414
-
<span className="text-gray-700 dark:text-gray-400">
455
+
<span className="text-gray-700 dark:text-gray-400 truncate">
415
456
@{identity?.handle}
416
457
</span>
417
458
</div>
···
428
469
);
429
470
}
430
471
431
-
432
-
const EmptyState = ({ text }: { text: string }) => (
472
+
export const EmptyState = ({ text }: { text: string }) => (
433
473
<div className="py-10 text-center text-gray-500 dark:text-gray-400">
434
474
{text}
435
475
</div>
436
476
);
437
477
438
-
const LoadingState = ({ text }: { text: string }) => (
478
+
export const LoadingState = ({ text }: { text: string }) => (
439
479
<div className="py-10 text-center text-gray-500 dark:text-gray-400 italic">
440
480
{text}
441
481
</div>
442
482
);
443
483
444
-
const ErrorState = ({ error }: { error: unknown }) => (
484
+
export const ErrorState = ({ error }: { error: unknown }) => (
445
485
<div className="py-10 text-center text-red-600 dark:text-red-400">
446
486
Error: {(error as Error)?.message || "Something went wrong."}
447
487
</div>
448
-
);
488
+
);
+100
src/routes/profile.$did/post.$rkey.liked-by.tsx
+100
src/routes/profile.$did/post.$rkey.liked-by.tsx
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { constellationURLAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
9
+
10
+
import {
11
+
EmptyState,
12
+
ErrorState,
13
+
LoadingState,
14
+
NotificationItem,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/liked-by")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresults = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.like",
34
+
path: ".subject.uri",
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
40
+
const {
41
+
data: infiniteLikesData,
42
+
fetchNextPage,
43
+
hasNextPage,
44
+
isFetchingNextPage,
45
+
isLoading,
46
+
isError,
47
+
error,
48
+
} = infinitequeryresults;
49
+
50
+
const likesAturis = React.useMemo(() => {
51
+
// Get all replies from the standard infinite query
52
+
return (
53
+
infiniteLikesData?.pages.flatMap(
54
+
(page) =>
55
+
page?.linking_records.map(
56
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
57
+
) ?? []
58
+
) ?? []
59
+
);
60
+
}, [infiniteLikesData]);
61
+
62
+
return (
63
+
<>
64
+
<Header
65
+
title={`Liked By`}
66
+
backButtonCallback={() => {
67
+
if (window.history.length > 1) {
68
+
window.history.back();
69
+
} else {
70
+
window.location.assign("/");
71
+
}
72
+
}}
73
+
/>
74
+
75
+
<>
76
+
{(() => {
77
+
if (isLoading) return <LoadingState text="Loading likes..." />;
78
+
if (isError) return <ErrorState error={error} />;
79
+
80
+
if (!likesAturis?.length)
81
+
return <EmptyState text="No likes yet." />;
82
+
})()}
83
+
</>
84
+
85
+
{likesAturis.map((m) => (
86
+
<NotificationItem key={m} notification={m} />
87
+
))}
88
+
89
+
{hasNextPage && (
90
+
<button
91
+
onClick={() => fetchNextPage()}
92
+
disabled={isFetchingNextPage}
93
+
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 disabled:opacity-50"
94
+
>
95
+
{isFetchingNextPage ? "Loading..." : "Load More"}
96
+
</button>
97
+
)}
98
+
</>
99
+
);
100
+
}
+141
src/routes/profile.$did/post.$rkey.quotes.tsx
+141
src/routes/profile.$did/post.$rkey.quotes.tsx
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
8
+
import { constellationURLAtom } from "~/utils/atoms";
9
+
import { type linksRecord,useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
10
+
11
+
import {
12
+
EmptyState,
13
+
ErrorState,
14
+
LoadingState,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/quotes")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresultsWithoutMedia = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.post",
34
+
path: ".embed.record.uri", // embed.record.record.uri and embed.record.uri
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
const infinitequeryresultsWithMedia = useInfiniteQuery({
40
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
41
+
{
42
+
constellation: constellationurl,
43
+
method: "/links",
44
+
target: atUri,
45
+
collection: "app.bsky.feed.post",
46
+
path: ".embed.record.record.uri", // embed.record.record.uri and embed.record.uri
47
+
}
48
+
),
49
+
enabled: !!atUri,
50
+
});
51
+
52
+
const {
53
+
data: infiniteQuotesDataWithoutMedia,
54
+
fetchNextPage: fetchNextPageWithoutMedia,
55
+
hasNextPage: hasNextPageWithoutMedia,
56
+
isFetchingNextPage: isFetchingNextPageWithoutMedia,
57
+
isLoading: isLoadingWithoutMedia,
58
+
isError: isErrorWithoutMedia,
59
+
error: errorWithoutMedia,
60
+
} = infinitequeryresultsWithoutMedia;
61
+
const {
62
+
data: infiniteQuotesDataWithMedia,
63
+
fetchNextPage: fetchNextPageWithMedia,
64
+
hasNextPage: hasNextPageWithMedia,
65
+
isFetchingNextPage: isFetchingNextPageWithMedia,
66
+
isLoading: isLoadingWithMedia,
67
+
isError: isErrorWithMedia,
68
+
error: errorWithMedia,
69
+
} = infinitequeryresultsWithMedia;
70
+
71
+
const fetchNextPage = async () => {
72
+
await Promise.all([
73
+
hasNextPageWithMedia && fetchNextPageWithMedia(),
74
+
hasNextPageWithoutMedia && fetchNextPageWithoutMedia(),
75
+
]);
76
+
};
77
+
78
+
const hasNextPage = hasNextPageWithMedia || hasNextPageWithoutMedia;
79
+
const isFetchingNextPage = isFetchingNextPageWithMedia || isFetchingNextPageWithoutMedia;
80
+
const isLoading = isLoadingWithMedia || isLoadingWithoutMedia;
81
+
82
+
const allQuotes = React.useMemo(() => {
83
+
const withPages = infiniteQuotesDataWithMedia?.pages ?? [];
84
+
const withoutPages = infiniteQuotesDataWithoutMedia?.pages ?? [];
85
+
const maxLen = Math.max(withPages.length, withoutPages.length);
86
+
const merged: linksRecord[] = [];
87
+
88
+
for (let i = 0; i < maxLen; i++) {
89
+
const a = withPages[i]?.linking_records ?? [];
90
+
const b = withoutPages[i]?.linking_records ?? [];
91
+
const mergedPage = [...a, ...b].sort((b, a) => a.rkey.localeCompare(b.rkey));
92
+
merged.push(...mergedPage);
93
+
}
94
+
95
+
return merged;
96
+
}, [infiniteQuotesDataWithMedia?.pages, infiniteQuotesDataWithoutMedia?.pages]);
97
+
98
+
const quotesAturis = React.useMemo(() => {
99
+
return allQuotes.flatMap((r) => `at://${r.did}/${r.collection}/${r.rkey}`);
100
+
}, [allQuotes]);
101
+
102
+
return (
103
+
<>
104
+
<Header
105
+
title={`Quotes`}
106
+
backButtonCallback={() => {
107
+
if (window.history.length > 1) {
108
+
window.history.back();
109
+
} else {
110
+
window.location.assign("/");
111
+
}
112
+
}}
113
+
/>
114
+
115
+
<>
116
+
{(() => {
117
+
if (isLoading) return <LoadingState text="Loading quotes..." />;
118
+
if (isErrorWithMedia) return <ErrorState error={errorWithMedia} />;
119
+
if (isErrorWithoutMedia) return <ErrorState error={errorWithoutMedia} />;
120
+
121
+
if (!quotesAturis?.length)
122
+
return <EmptyState text="No quotes yet." />;
123
+
})()}
124
+
</>
125
+
126
+
{quotesAturis.map((m) => (
127
+
<UniversalPostRendererATURILoader key={m} atUri={m} />
128
+
))}
129
+
130
+
{hasNextPage && (
131
+
<button
132
+
onClick={() => fetchNextPage()}
133
+
disabled={isFetchingNextPage}
134
+
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 disabled:opacity-50"
135
+
>
136
+
{isFetchingNextPage ? "Loading..." : "Load More"}
137
+
</button>
138
+
)}
139
+
</>
140
+
);
141
+
}
+100
src/routes/profile.$did/post.$rkey.reposted-by.tsx
+100
src/routes/profile.$did/post.$rkey.reposted-by.tsx
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { constellationURLAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
9
+
10
+
import {
11
+
EmptyState,
12
+
ErrorState,
13
+
LoadingState,
14
+
NotificationItem,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/reposted-by")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresults = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.repost",
34
+
path: ".subject.uri",
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
40
+
const {
41
+
data: infiniteRepostsData,
42
+
fetchNextPage,
43
+
hasNextPage,
44
+
isFetchingNextPage,
45
+
isLoading,
46
+
isError,
47
+
error,
48
+
} = infinitequeryresults;
49
+
50
+
const repostsAturis = React.useMemo(() => {
51
+
// Get all replies from the standard infinite query
52
+
return (
53
+
infiniteRepostsData?.pages.flatMap(
54
+
(page) =>
55
+
page?.linking_records.map(
56
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
57
+
) ?? []
58
+
) ?? []
59
+
);
60
+
}, [infiniteRepostsData]);
61
+
62
+
return (
63
+
<>
64
+
<Header
65
+
title={`Reposted By`}
66
+
backButtonCallback={() => {
67
+
if (window.history.length > 1) {
68
+
window.history.back();
69
+
} else {
70
+
window.location.assign("/");
71
+
}
72
+
}}
73
+
/>
74
+
75
+
<>
76
+
{(() => {
77
+
if (isLoading) return <LoadingState text="Loading reposts..." />;
78
+
if (isError) return <ErrorState error={error} />;
79
+
80
+
if (!repostsAturis?.length)
81
+
return <EmptyState text="No reposts yet." />;
82
+
})()}
83
+
</>
84
+
85
+
{repostsAturis.map((m) => (
86
+
<NotificationItem key={m} notification={m} />
87
+
))}
88
+
89
+
{hasNextPage && (
90
+
<button
91
+
onClick={() => fetchNextPage()}
92
+
disabled={isFetchingNextPage}
93
+
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 disabled:opacity-50"
94
+
>
95
+
{isFetchingNextPage ? "Loading..." : "Load More"}
96
+
</button>
97
+
)}
98
+
</>
99
+
);
100
+
}
+98
-92
src/routes/profile.$did/post.$rkey.tsx
+98
-92
src/routes/profile.$did/post.$rkey.tsx
···
1
1
import { AtUri } from "@atproto/api";
2
2
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
-
import { createFileRoute, Outlet } from "@tanstack/react-router";
3
+
import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router";
4
4
import { useAtom } from "jotai";
5
5
import React, { useLayoutEffect } from "react";
6
6
···
52
52
nopics?: boolean;
53
53
lightboxCallback?: (d: LightboxProps) => void;
54
54
}) {
55
+
const matchRoute = useMatchRoute()
56
+
const showMainPostRoute = !!matchRoute({ to: '/profile/$did/post/$rkey' }) || !!matchRoute({ to: '/profile/$did/post/$rkey/image/$i' })
57
+
55
58
//const { get, set } = usePersistentStore();
56
59
const queryClient = useQueryClient();
57
60
// const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
···
190
193
data: identity,
191
194
isLoading: isIdentityLoading,
192
195
error: identityError,
193
-
} = useQueryIdentity(did);
196
+
} = useQueryIdentity(showMainPostRoute ? did : undefined);
194
197
195
198
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
196
199
197
200
const atUri = React.useMemo(
198
201
() =>
199
-
resolvedDid
202
+
resolvedDid && showMainPostRoute
200
203
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
201
204
: undefined,
202
-
[resolvedDid, rkey]
205
+
[resolvedDid, rkey, showMainPostRoute]
203
206
);
204
207
205
-
const { data: mainPost } = useQueryPost(atUri);
208
+
const { data: mainPost } = useQueryPost(showMainPostRoute ? atUri : undefined);
206
209
207
210
console.log("atUri",atUri)
208
211
···
215
218
);
216
219
217
220
// @ts-expect-error i hate overloads
218
-
const { data: links } = useQueryConstellation(atUri?{
221
+
const { data: links } = useQueryConstellation(atUri&&showMainPostRoute?{
219
222
method: "/links/all",
220
223
target: atUri,
221
224
} : {
···
248
251
}, [links]);
249
252
250
253
const { data: opreplies } = useQueryConstellation(
251
-
!!opdid && replyCount && replyCount >= 25
254
+
showMainPostRoute && !!opdid && replyCount && replyCount >= 25
252
255
? {
253
256
method: "/links",
254
257
target: atUri,
···
289
292
path: ".reply.parent.uri",
290
293
}
291
294
),
292
-
enabled: !!atUri,
295
+
enabled: !!atUri && showMainPostRoute,
293
296
});
294
297
295
298
const {
···
371
374
const [layoutReady, setLayoutReady] = React.useState(false);
372
375
373
376
useLayoutEffect(() => {
377
+
if (!showMainPostRoute) return
374
378
if (parents.length > 0 && !layoutReady && mainPostRef.current) {
375
379
const mainPostElement = mainPostRef.current;
376
380
···
389
393
// eslint-disable-next-line react-hooks/set-state-in-effect
390
394
setLayoutReady(true);
391
395
}
392
-
}, [parents, layoutReady]);
396
+
}, [parents, layoutReady, showMainPostRoute]);
393
397
394
398
395
399
const [slingshoturl] = useAtom(slingshotURLAtom)
396
400
397
401
React.useEffect(() => {
398
-
if (parentsLoading) {
402
+
if (parentsLoading || !showMainPostRoute) {
399
403
setLayoutReady(false);
400
404
}
401
405
···
403
407
setLayoutReady(true);
404
408
hasPerformedInitialLayout.current = true;
405
409
}
406
-
}, [parentsLoading, mainPost]);
410
+
}, [parentsLoading, mainPost, showMainPostRoute]);
407
411
408
412
React.useEffect(() => {
409
413
if (!mainPost?.value?.reply?.parent?.uri) {
···
444
448
return () => {
445
449
ignore = true;
446
450
};
447
-
}, [mainPost, queryClient]);
451
+
}, [mainPost, queryClient, slingshoturl]);
448
452
449
-
if (!did || !rkey) return <div>Invalid post URI</div>;
450
-
if (isIdentityLoading) return <div>Resolving handle...</div>;
451
-
if (identityError)
453
+
if ((!did || !rkey) && showMainPostRoute) return <div>Invalid post URI</div>;
454
+
if (isIdentityLoading && showMainPostRoute) return <div>Resolving handle...</div>;
455
+
if (identityError && showMainPostRoute)
452
456
return <div style={{ color: "red" }}>{identityError.message}</div>;
453
-
if (!atUri) return <div>Could not construct post URI.</div>;
457
+
if (!atUri && showMainPostRoute) return <div>Could not construct post URI.</div>;
454
458
455
459
return (
456
460
<>
457
461
<Outlet />
458
-
<Header
459
-
title={`Post`}
460
-
backButtonCallback={() => {
461
-
if (window.history.length > 1) {
462
-
window.history.back();
463
-
} else {
464
-
window.location.assign("/");
465
-
}
466
-
}}
467
-
/>
462
+
{showMainPostRoute && (<>
463
+
<Header
464
+
title={`Post`}
465
+
backButtonCallback={() => {
466
+
if (window.history.length > 1) {
467
+
window.history.back();
468
+
} else {
469
+
window.location.assign("/");
470
+
}
471
+
}}
472
+
/>
468
473
469
-
{parentsLoading && (
470
-
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
471
-
<div className="ml-4 w-[42px] flex justify-center">
472
-
<div
473
-
style={{ width: 2, height: "100%", opacity: 0.5 }}
474
-
className="bg-gray-500 dark:bg-gray-400"
475
-
></div>
474
+
{parentsLoading && (
475
+
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
476
+
<div className="ml-4 w-[42px] flex justify-center">
477
+
<div
478
+
style={{ width: 2, height: "100%", opacity: 0.5 }}
479
+
className="bg-gray-500 dark:bg-gray-400"
480
+
></div>
481
+
</div>
482
+
Loading conversation...
476
483
</div>
477
-
Loading conversation...
484
+
)}
485
+
486
+
{/* we should use the reply lines here thats provided by UPR*/}
487
+
<div style={{ maxWidth: 600, padding: 0 }}>
488
+
{parents.map((parent, index) => (
489
+
<UniversalPostRendererATURILoader
490
+
key={parent.uri}
491
+
atUri={parent.uri}
492
+
topReplyLine={index > 0}
493
+
bottomReplyLine={true}
494
+
bottomBorder={false}
495
+
/>
496
+
))}
478
497
</div>
479
-
)}
480
-
481
-
{/* we should use the reply lines here thats provided by UPR*/}
482
-
<div style={{ maxWidth: 600, padding: 0 }}>
483
-
{parents.map((parent, index) => (
498
+
<div ref={mainPostRef}>
484
499
<UniversalPostRendererATURILoader
485
-
key={parent.uri}
486
-
atUri={parent.uri}
487
-
topReplyLine={index > 0}
488
-
bottomReplyLine={true}
489
-
bottomBorder={false}
500
+
atUri={atUri!}
501
+
detailed={true}
502
+
topReplyLine={parentsLoading || parents.length > 0}
503
+
nopics={!!nopics}
504
+
lightboxCallback={lightboxCallback}
490
505
/>
491
-
))}
492
-
</div>
493
-
<div ref={mainPostRef}>
494
-
<UniversalPostRendererATURILoader
495
-
atUri={atUri}
496
-
detailed={true}
497
-
topReplyLine={parentsLoading || parents.length > 0}
498
-
nopics={!!nopics}
499
-
lightboxCallback={lightboxCallback}
500
-
/>
501
-
</div>
502
-
<div
503
-
style={{
504
-
maxWidth: 600,
505
-
//margin: "0px auto 0",
506
-
padding: 0,
507
-
minHeight: "80dvh",
508
-
paddingBottom: "20dvh",
509
-
}}
510
-
>
506
+
</div>
511
507
<div
512
-
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
513
508
style={{
514
-
fontSize: 18,
515
-
margin: "12px 16px 12px 16px",
516
-
fontWeight: 600,
509
+
maxWidth: 600,
510
+
//margin: "0px auto 0",
511
+
padding: 0,
512
+
minHeight: "80dvh",
513
+
paddingBottom: "20dvh",
517
514
}}
518
515
>
519
-
Replies
516
+
<div
517
+
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
518
+
style={{
519
+
fontSize: 18,
520
+
margin: "12px 16px 12px 16px",
521
+
fontWeight: 600,
522
+
}}
523
+
>
524
+
Replies
525
+
</div>
526
+
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
527
+
{replyAturis.length > 0 &&
528
+
replyAturis.map((reply) => {
529
+
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
530
+
return (
531
+
<UniversalPostRendererATURILoader
532
+
key={reply}
533
+
atUri={reply}
534
+
maxReplies={4}
535
+
/>
536
+
);
537
+
})}
538
+
{hasNextPage && (
539
+
<button
540
+
onClick={() => fetchNextPage()}
541
+
disabled={isFetchingNextPage}
542
+
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 disabled:opacity-50"
543
+
>
544
+
{isFetchingNextPage ? "Loading..." : "Load More"}
545
+
</button>
546
+
)}
547
+
</div>
520
548
</div>
521
-
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
522
-
{replyAturis.length > 0 &&
523
-
replyAturis.map((reply) => {
524
-
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
525
-
return (
526
-
<UniversalPostRendererATURILoader
527
-
key={reply}
528
-
atUri={reply}
529
-
maxReplies={4}
530
-
/>
531
-
);
532
-
})}
533
-
{hasNextPage && (
534
-
<button
535
-
onClick={() => fetchNextPage()}
536
-
disabled={isFetchingNextPage}
537
-
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 disabled:opacity-50"
538
-
>
539
-
{isFetchingNextPage ? "Loading..." : "Load More"}
540
-
</button>
541
-
)}
542
-
</div>
543
-
</div>
549
+
</>)}
544
550
</>
545
551
);
546
552
}
+5
-5
src/utils/useQuery.ts
+5
-5
src/utils/useQuery.ts
···
352
352
);
353
353
}
354
354
355
-
type linksRecord = {
355
+
export type linksRecord = {
356
356
did: string;
357
357
collection: string;
358
358
rkey: string;
···
634
634
collection: string
635
635
path: string
636
636
}) {
637
-
console.log(
638
-
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
639
-
query,
640
-
)
637
+
// console.log(
638
+
// 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
639
+
// query,
640
+
// )
641
641
642
642
return infiniteQueryOptions({
643
643
enabled: !!query?.target,