+208
-39
src/components/UniversalPostRenderer.tsx
+208
-39
src/components/UniversalPostRenderer.tsx
···
11
11
useQueryIdentity,
12
12
useQueryPost,
13
13
useQueryProfile,
14
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
14
15
} from "~/utils/useQuery";
15
16
16
17
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
···
33
34
ref?: React.Ref<HTMLDivElement>;
34
35
dataIndexPropPass?: number;
35
36
nopics?: boolean;
36
-
lightboxCallback?: (d:LightboxProps) => void;
37
+
lightboxCallback?: (d: LightboxProps) => void;
38
+
maxReplies?: number;
37
39
}
38
40
39
41
// export async function cachedGetRecord({
···
143
145
dataIndexPropPass,
144
146
nopics,
145
147
lightboxCallback,
148
+
maxReplies,
146
149
}: UniversalPostRendererATURILoaderProps) {
147
150
// /*mass comment*/ console.log("atUri", atUri);
148
151
//const { get, set } = usePersistentStore();
···
388
391
);
389
392
}, [links]);
390
393
394
+
// const { data: repliesData } = useQueryConstellation({
395
+
// method: "/links",
396
+
// target: atUri,
397
+
// collection: "app.bsky.feed.post",
398
+
// path: ".reply.parent.uri",
399
+
// });
400
+
401
+
const infinitequeryresults = useInfiniteQuery({
402
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
403
+
{
404
+
method: "/links",
405
+
target: atUri,
406
+
collection: "app.bsky.feed.post",
407
+
path: ".reply.parent.uri",
408
+
}
409
+
),
410
+
enabled: !!atUri && !!maxReplies,
411
+
});
412
+
413
+
const {
414
+
data: repliesData,
415
+
// fetchNextPage,
416
+
// hasNextPage,
417
+
// isFetchingNextPage,
418
+
} = infinitequeryresults;
419
+
420
+
// auto-fetch all pages
421
+
useEffect(() => {
422
+
if (!maxReplies) return;
423
+
if (
424
+
infinitequeryresults.hasNextPage &&
425
+
!infinitequeryresults.isFetchingNextPage
426
+
) {
427
+
console.log("Fetching the next page...");
428
+
infinitequeryresults.fetchNextPage();
429
+
}
430
+
}, [infinitequeryresults]);
431
+
432
+
const replyAturis = repliesData
433
+
? repliesData.pages.flatMap((page) =>
434
+
page
435
+
? page.linking_records.map((record) => {
436
+
const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
437
+
return aturi;
438
+
})
439
+
: []
440
+
)
441
+
: [];
442
+
443
+
//const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined);
444
+
445
+
const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => {
446
+
if (!replyAturis || replyAturis.length === 0 || !maxReplies)
447
+
return {
448
+
oldestOpsReply: undefined,
449
+
oldestOpsReplyElseNewestNonOpsReply: undefined,
450
+
};
451
+
452
+
const opdid = new AtUri(
453
+
//postQuery?.value.reply?.root.uri ?? postQuery?.uri ?? atUri
454
+
atUri
455
+
).host;
456
+
457
+
const opReplies = replyAturis.filter(
458
+
(aturi) => new AtUri(aturi).host === opdid
459
+
);
460
+
461
+
if (opReplies.length > 0) {
462
+
const opreply = opReplies[opReplies.length - 1];
463
+
//setOldestOpsReply(opreply);
464
+
return {
465
+
oldestOpsReply: opreply,
466
+
oldestOpsReplyElseNewestNonOpsReply: opreply,
467
+
};
468
+
} else {
469
+
return {
470
+
oldestOpsReply: undefined,
471
+
oldestOpsReplyElseNewestNonOpsReply: replyAturis[0],
472
+
};
473
+
}
474
+
})();
475
+
391
476
// const navigateToProfile = (e: React.MouseEvent) => {
392
477
// e.stopPropagation();
393
478
// if (resolved?.did) {
···
403
488
}
404
489
405
490
return (
406
-
<UniversalPostRendererRawRecordShim
407
-
detailed={detailed}
408
-
postRecord={postQuery}
409
-
profileRecord={opProfile}
410
-
aturi={atUri}
411
-
resolved={resolved}
412
-
likesCount={likes}
413
-
repostsCount={reposts}
414
-
repliesCount={replies}
415
-
bottomReplyLine={bottomReplyLine}
416
-
topReplyLine={topReplyLine}
417
-
bottomBorder={bottomBorder}
418
-
feedviewpost={feedviewpost}
419
-
repostedby={repostedby}
420
-
style={style}
421
-
ref={ref}
422
-
dataIndexPropPass={dataIndexPropPass}
423
-
nopics={nopics}
424
-
lightboxCallback={lightboxCallback}
425
-
/>
491
+
<>
492
+
{/* <span>uprrs {maxReplies} {!!maxReplies&&!!oldestOpsReplyElseNewestNonOpsReply ? "true" : "false"}</span> */}
493
+
<UniversalPostRendererRawRecordShim
494
+
detailed={detailed}
495
+
postRecord={postQuery}
496
+
profileRecord={opProfile}
497
+
aturi={atUri}
498
+
resolved={resolved}
499
+
likesCount={likes}
500
+
repostsCount={reposts}
501
+
repliesCount={replies}
502
+
bottomReplyLine={
503
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
504
+
? true
505
+
: maxReplies && !oldestOpsReplyElseNewestNonOpsReply
506
+
? false
507
+
: bottomReplyLine
508
+
}
509
+
topReplyLine={topReplyLine}
510
+
//bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder}
511
+
bottomBorder={
512
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
513
+
? false
514
+
: maxReplies === 0
515
+
? false
516
+
: bottomBorder
517
+
}
518
+
feedviewpost={feedviewpost}
519
+
repostedby={repostedby}
520
+
//style={{...style, background: oldestOpsReply === atUri ? "Red" : undefined}}
521
+
style={style}
522
+
ref={ref}
523
+
dataIndexPropPass={dataIndexPropPass}
524
+
nopics={nopics}
525
+
lightboxCallback={lightboxCallback}
526
+
maxReplies={maxReplies}
527
+
/>
528
+
{oldestOpsReplyElseNewestNonOpsReply && (
529
+
<>
530
+
{/* <span>hello {maxReplies}</span> */}
531
+
<UniversalPostRendererATURILoader
532
+
//detailed={detailed}
533
+
atUri={oldestOpsReplyElseNewestNonOpsReply}
534
+
bottomReplyLine={(maxReplies ?? 0) > 0}
535
+
topReplyLine={(maxReplies ?? 0) > 1}
536
+
bottomBorder={bottomBorder}
537
+
feedviewpost={feedviewpost}
538
+
repostedby={repostedby}
539
+
style={style}
540
+
ref={ref}
541
+
dataIndexPropPass={dataIndexPropPass}
542
+
nopics={nopics}
543
+
lightboxCallback={lightboxCallback}
544
+
maxReplies={
545
+
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
546
+
}
547
+
/>
548
+
{maxReplies && maxReplies - 1 === 0 && (
549
+
<MoreReplies atUri={oldestOpsReplyElseNewestNonOpsReply} />
550
+
)}
551
+
</>
552
+
)}
553
+
</>
554
+
);
555
+
}
556
+
557
+
function MoreReplies({ atUri }: { atUri: string }) {
558
+
const navigate = useNavigate();
559
+
const aturio = new AtUri(atUri);
560
+
return (
561
+
<div
562
+
onClick={() =>
563
+
navigate({
564
+
to: "/profile/$did/post/$rkey",
565
+
params: { did: aturio.host, rkey: aturio.rkey },
566
+
})
567
+
}
568
+
className="border-b border-gray-300 dark:border-gray-800 flex flex-row px-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"
569
+
>
570
+
<div className="w-[42px] h-12 flex flex-col items-center justify-center">
571
+
<div
572
+
style={{
573
+
width: 2,
574
+
height: "100%",
575
+
backgroundImage:
576
+
"repeating-linear-gradient(to bottom, var(--color-gray-500) 0, var(--color-gray-500) 4px, transparent 4px, transparent 8px)",
577
+
opacity: 0.5,
578
+
}}
579
+
className="dark:bg-[repeating-linear-gradient(to_bottom,var(--color-gray-500)_0,var(--color-gray-400)_4px,transparent_4px,transparent_8px)]"
580
+
//className="border-gray-400 dark:border-gray-500"
581
+
/>
582
+
</div>
583
+
584
+
<div className="flex items-center pl-3 text-sm text-gray-500 dark:text-gray-400 select-none">
585
+
More Replies
586
+
</div>
587
+
</div>
426
588
);
427
589
}
428
590
···
451
613
dataIndexPropPass,
452
614
nopics,
453
615
lightboxCallback,
616
+
maxReplies,
454
617
}: {
455
618
postRecord: any;
456
619
profileRecord: any;
···
469
632
ref?: React.Ref<HTMLDivElement>;
470
633
dataIndexPropPass?: number;
471
634
nopics?: boolean;
472
-
lightboxCallback?: (d:LightboxProps) => void;
635
+
lightboxCallback?: (d: LightboxProps) => void;
636
+
maxReplies?: number;
473
637
}) {
474
638
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
475
639
const navigate = useNavigate();
···
669
833
dataIndexPropPass={dataIndexPropPass}
670
834
nopics={nopics}
671
835
lightboxCallback={lightboxCallback}
836
+
maxReplies={maxReplies}
672
837
/>
673
838
</>
674
839
);
···
982
1147
PostView,
983
1148
//ThreadViewPost,
984
1149
} from "@atproto/api/dist/client/types/app/bsky/feed/defs";
1150
+
import { useInfiniteQuery } from "@tanstack/react-query";
985
1151
import { useEffect, useRef, useState } from "react";
986
1152
import ReactPlayer from "react-player";
987
1153
···
1115
1281
ref,
1116
1282
dataIndexPropPass,
1117
1283
nopics,
1118
-
lightboxCallback
1284
+
lightboxCallback,
1285
+
maxReplies,
1119
1286
}: {
1120
1287
post: PostView;
1121
1288
// optional for now because i havent ported every use to this yet
···
1139
1306
ref?: React.Ref<HTMLDivElement>;
1140
1307
dataIndexPropPass?: number;
1141
1308
nopics?: boolean;
1142
-
lightboxCallback?: (d:LightboxProps) => void;
1309
+
lightboxCallback?: (d: LightboxProps) => void;
1310
+
maxReplies?: number;
1143
1311
}) {
1144
1312
const parsed = new AtUri(post.uri);
1145
1313
const navigate = useNavigate();
···
1496
1664
>
1497
1665
{fedi ? (
1498
1666
<>
1499
-
<span className="dangerousFediContent"
1667
+
<span
1668
+
className="dangerousFediContent"
1500
1669
dangerouslySetInnerHTML={{
1501
1670
__html: DOMPurify.sanitize(fedi),
1502
1671
}}
···
1728
1897
navigate,
1729
1898
postid,
1730
1899
nopics,
1731
-
lightboxCallback
1900
+
lightboxCallback,
1732
1901
}: {
1733
1902
embed?: Embed;
1734
1903
moderation?: ModerationDecision;
···
1739
1908
navigate: (_: any) => void;
1740
1909
postid?: { did: string; rkey: string };
1741
1910
nopics?: boolean;
1742
-
lightboxCallback?: (d:LightboxProps) => void;
1911
+
lightboxCallback?: (d: LightboxProps) => void;
1743
1912
}) {
1744
1913
//const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
1745
-
function setLightboxIndex(number:number) {
1914
+
function setLightboxIndex(number: number) {
1746
1915
navigate({
1747
-
to: "/profile/$did/post/$rkey/image/$i",
1748
-
params: {
1749
-
did: postid?.did,
1750
-
rkey: postid?.rkey,
1751
-
i: number.toString(),
1752
-
},
1753
-
});
1916
+
to: "/profile/$did/post/$rkey/image/$i",
1917
+
params: {
1918
+
did: postid?.did,
1919
+
rkey: postid?.rkey,
1920
+
i: number.toString(),
1921
+
},
1922
+
});
1754
1923
}
1755
1924
if (
1756
1925
AppBskyEmbedRecordWithMedia.isView(embed) &&
···
1962
2131
src: img.fullsize,
1963
2132
alt: img.alt,
1964
2133
}));
1965
-
console.log("rendering images")
2134
+
console.log("rendering images");
1966
2135
if (lightboxCallback) {
1967
-
lightboxCallback({images: lightboxImages})
1968
-
console.log("rendering images")
1969
-
};
2136
+
lightboxCallback({ images: lightboxImages });
2137
+
console.log("rendering images");
2138
+
}
1970
2139
1971
2140
if (nopics) return;
1972
2141
+72
-15
src/routes/profile.$did/post.$rkey.tsx
+72
-15
src/routes/profile.$did/post.$rkey.tsx
···
1
-
import { useQueryClient } from "@tanstack/react-query";
1
+
import { AtUri } from "@atproto/api";
2
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
2
3
import { createFileRoute, Outlet } from "@tanstack/react-router";
3
-
import React, { useLayoutEffect } from "react";
4
+
import React, { useEffect, useLayoutEffect } from "react";
4
5
5
6
import { Header } from "~/components/Header";
6
7
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
7
8
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
8
9
import {
9
10
constructPostQuery,
10
-
useQueryConstellation,
11
11
useQueryIdentity,
12
12
useQueryPost,
13
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
13
14
} from "~/utils/useQuery";
14
15
15
16
import type { LightboxProps } from "./post.$rkey.image.$i";
···
198
199
199
200
const { data: mainPost } = useQueryPost(atUri);
200
201
201
-
const { data: repliesData } = useQueryConstellation({
202
-
method: "/links",
203
-
target: atUri,
204
-
collection: "app.bsky.feed.post",
205
-
path: ".reply.parent.uri",
202
+
// const { data: repliesData } = useQueryConstellation({
203
+
// method: "/links",
204
+
// target: atUri,
205
+
// collection: "app.bsky.feed.post",
206
+
// path: ".reply.parent.uri",
207
+
// });
208
+
// const replies = repliesData?.linking_records.slice(0, 50) ?? [];
209
+
const infinitequeryresults = useInfiniteQuery({
210
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
211
+
{
212
+
method: "/links",
213
+
target: atUri,
214
+
collection: "app.bsky.feed.post",
215
+
path: ".reply.parent.uri",
216
+
}
217
+
),
218
+
enabled: !!atUri,
206
219
});
207
-
const replies = repliesData?.linking_records.slice(0, 50) ?? [];
220
+
221
+
const {
222
+
data: repliesData,
223
+
// fetchNextPage,
224
+
// hasNextPage,
225
+
// isFetchingNextPage,
226
+
} = infinitequeryresults;
227
+
228
+
// auto-fetch all pages
229
+
useEffect(() => {
230
+
if (
231
+
infinitequeryresults.hasNextPage &&
232
+
!infinitequeryresults.isFetchingNextPage
233
+
) {
234
+
console.log("Fetching the next page...");
235
+
infinitequeryresults.fetchNextPage();
236
+
}
237
+
}, [infinitequeryresults]);
238
+
239
+
const replyAturis = repliesData
240
+
? repliesData.pages.flatMap((page) =>
241
+
page
242
+
? page.linking_records.map((record) => {
243
+
const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
244
+
return aturi;
245
+
})
246
+
: []
247
+
)
248
+
: [];
249
+
250
+
const opdid = new AtUri(atUri).host;
251
+
252
+
// Find oldest OP reply
253
+
const oldestOpsIndex = replyAturis.findIndex(
254
+
(aturi) => new AtUri(aturi).host === opdid
255
+
);
256
+
257
+
// Reorder: move oldest OP reply to the front
258
+
if (oldestOpsIndex > 0) {
259
+
const [oldestOpsReply] = replyAturis.splice(oldestOpsIndex, 1);
260
+
replyAturis.unshift(oldestOpsReply);
261
+
}
262
+
208
263
209
264
const [parents, setParents] = React.useState<any[]>([]);
210
265
const [parentsLoading, setParentsLoading] = React.useState(false);
···
351
406
maxWidth: 600,
352
407
//margin: "0px auto 0",
353
408
padding: 0,
354
-
minHeight: "100dvh",
409
+
minHeight: "80dvh",
410
+
paddingBottom: "20dvh"
355
411
}}
356
412
>
357
413
<div
···
365
421
Replies
366
422
</div>
367
423
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
368
-
{replies.length > 0 &&
369
-
replies.map((reply) => {
370
-
const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
424
+
{replyAturis.length > 0 &&
425
+
replyAturis.map((reply) => {
426
+
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
371
427
return (
372
428
<UniversalPostRendererATURILoader
373
-
key={replyAtUri}
374
-
atUri={replyAtUri}
429
+
key={reply}
430
+
atUri={reply}
431
+
maxReplies={4}
375
432
/>
376
433
);
377
434
})}
+55
src/utils/useQuery.ts
+55
src/utils/useQuery.ts
···
1
1
import * as ATPAPI from "@atproto/api";
2
2
import {
3
+
infiniteQueryOptions,
3
4
type QueryFunctionContext,
4
5
queryOptions,
5
6
useInfiniteQuery,
···
614
615
refetchOnWindowFocus: false,
615
616
enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true),
616
617
});
618
+
}
619
+
620
+
621
+
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
622
+
method: '/links'
623
+
target?: string
624
+
collection: string
625
+
path: string
626
+
}) {
627
+
const constellationHost = 'constellation.microcosm.blue'
628
+
console.log(
629
+
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
630
+
query,
631
+
)
632
+
633
+
return infiniteQueryOptions({
634
+
enabled: !!query?.target,
635
+
queryKey: [
636
+
'reddwarf_constellation',
637
+
query?.method,
638
+
query?.target,
639
+
query?.collection,
640
+
query?.path,
641
+
] as const,
642
+
643
+
queryFn: async ({pageParam}: {pageParam?: string}) => {
644
+
if (!query || !query?.target) return undefined
645
+
646
+
const method = query.method
647
+
const target = query.target
648
+
const collection = query.collection
649
+
const path = query.path
650
+
const cursor = pageParam
651
+
652
+
const res = await fetch(
653
+
`https://${constellationHost}${method}?target=${encodeURIComponent(target)}${
654
+
collection ? `&collection=${encodeURIComponent(collection)}` : ''
655
+
}${path ? `&path=${encodeURIComponent(path)}` : ''}${
656
+
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
657
+
}`,
658
+
)
659
+
660
+
if (!res.ok) throw new Error('Failed to fetch')
661
+
662
+
return (await res.json()) as linksRecordsResponse
663
+
},
664
+
665
+
getNextPageParam: lastPage => {
666
+
return (lastPage as any)?.cursor ?? undefined
667
+
},
668
+
initialPageParam: undefined,
669
+
staleTime: 5 * 60 * 1000,
670
+
gcTime: 5 * 60 * 1000,
671
+
})
617
672
}