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
linear threadded replies
rimar1337
4 months ago
9f8a63c5
92264fde
+335
-54
3 changed files
expand all
collapse all
unified
split
src
components
UniversalPostRenderer.tsx
routes
profile.$did
post.$rkey.tsx
utils
useQuery.ts
+208
-39
src/components/UniversalPostRenderer.tsx
···
11
11
useQueryIdentity,
12
12
useQueryPost,
13
13
useQueryProfile,
14
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
36
-
lightboxCallback?: (d:LightboxProps) => void;
37
37
+
lightboxCallback?: (d: LightboxProps) => void;
38
38
+
maxReplies?: number;
37
39
}
38
40
39
41
// export async function cachedGetRecord({
···
143
145
dataIndexPropPass,
144
146
nopics,
145
147
lightboxCallback,
148
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
394
+
// const { data: repliesData } = useQueryConstellation({
395
395
+
// method: "/links",
396
396
+
// target: atUri,
397
397
+
// collection: "app.bsky.feed.post",
398
398
+
// path: ".reply.parent.uri",
399
399
+
// });
400
400
+
401
401
+
const infinitequeryresults = useInfiniteQuery({
402
402
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
403
403
+
{
404
404
+
method: "/links",
405
405
+
target: atUri,
406
406
+
collection: "app.bsky.feed.post",
407
407
+
path: ".reply.parent.uri",
408
408
+
}
409
409
+
),
410
410
+
enabled: !!atUri && !!maxReplies,
411
411
+
});
412
412
+
413
413
+
const {
414
414
+
data: repliesData,
415
415
+
// fetchNextPage,
416
416
+
// hasNextPage,
417
417
+
// isFetchingNextPage,
418
418
+
} = infinitequeryresults;
419
419
+
420
420
+
// auto-fetch all pages
421
421
+
useEffect(() => {
422
422
+
if (!maxReplies) return;
423
423
+
if (
424
424
+
infinitequeryresults.hasNextPage &&
425
425
+
!infinitequeryresults.isFetchingNextPage
426
426
+
) {
427
427
+
console.log("Fetching the next page...");
428
428
+
infinitequeryresults.fetchNextPage();
429
429
+
}
430
430
+
}, [infinitequeryresults]);
431
431
+
432
432
+
const replyAturis = repliesData
433
433
+
? repliesData.pages.flatMap((page) =>
434
434
+
page
435
435
+
? page.linking_records.map((record) => {
436
436
+
const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
437
437
+
return aturi;
438
438
+
})
439
439
+
: []
440
440
+
)
441
441
+
: [];
442
442
+
443
443
+
//const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined);
444
444
+
445
445
+
const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => {
446
446
+
if (!replyAturis || replyAturis.length === 0 || !maxReplies)
447
447
+
return {
448
448
+
oldestOpsReply: undefined,
449
449
+
oldestOpsReplyElseNewestNonOpsReply: undefined,
450
450
+
};
451
451
+
452
452
+
const opdid = new AtUri(
453
453
+
//postQuery?.value.reply?.root.uri ?? postQuery?.uri ?? atUri
454
454
+
atUri
455
455
+
).host;
456
456
+
457
457
+
const opReplies = replyAturis.filter(
458
458
+
(aturi) => new AtUri(aturi).host === opdid
459
459
+
);
460
460
+
461
461
+
if (opReplies.length > 0) {
462
462
+
const opreply = opReplies[opReplies.length - 1];
463
463
+
//setOldestOpsReply(opreply);
464
464
+
return {
465
465
+
oldestOpsReply: opreply,
466
466
+
oldestOpsReplyElseNewestNonOpsReply: opreply,
467
467
+
};
468
468
+
} else {
469
469
+
return {
470
470
+
oldestOpsReply: undefined,
471
471
+
oldestOpsReplyElseNewestNonOpsReply: replyAturis[0],
472
472
+
};
473
473
+
}
474
474
+
})();
475
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
406
-
<UniversalPostRendererRawRecordShim
407
407
-
detailed={detailed}
408
408
-
postRecord={postQuery}
409
409
-
profileRecord={opProfile}
410
410
-
aturi={atUri}
411
411
-
resolved={resolved}
412
412
-
likesCount={likes}
413
413
-
repostsCount={reposts}
414
414
-
repliesCount={replies}
415
415
-
bottomReplyLine={bottomReplyLine}
416
416
-
topReplyLine={topReplyLine}
417
417
-
bottomBorder={bottomBorder}
418
418
-
feedviewpost={feedviewpost}
419
419
-
repostedby={repostedby}
420
420
-
style={style}
421
421
-
ref={ref}
422
422
-
dataIndexPropPass={dataIndexPropPass}
423
423
-
nopics={nopics}
424
424
-
lightboxCallback={lightboxCallback}
425
425
-
/>
491
491
+
<>
492
492
+
{/* <span>uprrs {maxReplies} {!!maxReplies&&!!oldestOpsReplyElseNewestNonOpsReply ? "true" : "false"}</span> */}
493
493
+
<UniversalPostRendererRawRecordShim
494
494
+
detailed={detailed}
495
495
+
postRecord={postQuery}
496
496
+
profileRecord={opProfile}
497
497
+
aturi={atUri}
498
498
+
resolved={resolved}
499
499
+
likesCount={likes}
500
500
+
repostsCount={reposts}
501
501
+
repliesCount={replies}
502
502
+
bottomReplyLine={
503
503
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
504
504
+
? true
505
505
+
: maxReplies && !oldestOpsReplyElseNewestNonOpsReply
506
506
+
? false
507
507
+
: bottomReplyLine
508
508
+
}
509
509
+
topReplyLine={topReplyLine}
510
510
+
//bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder}
511
511
+
bottomBorder={
512
512
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
513
513
+
? false
514
514
+
: maxReplies === 0
515
515
+
? false
516
516
+
: bottomBorder
517
517
+
}
518
518
+
feedviewpost={feedviewpost}
519
519
+
repostedby={repostedby}
520
520
+
//style={{...style, background: oldestOpsReply === atUri ? "Red" : undefined}}
521
521
+
style={style}
522
522
+
ref={ref}
523
523
+
dataIndexPropPass={dataIndexPropPass}
524
524
+
nopics={nopics}
525
525
+
lightboxCallback={lightboxCallback}
526
526
+
maxReplies={maxReplies}
527
527
+
/>
528
528
+
{oldestOpsReplyElseNewestNonOpsReply && (
529
529
+
<>
530
530
+
{/* <span>hello {maxReplies}</span> */}
531
531
+
<UniversalPostRendererATURILoader
532
532
+
//detailed={detailed}
533
533
+
atUri={oldestOpsReplyElseNewestNonOpsReply}
534
534
+
bottomReplyLine={(maxReplies ?? 0) > 0}
535
535
+
topReplyLine={(maxReplies ?? 0) > 1}
536
536
+
bottomBorder={bottomBorder}
537
537
+
feedviewpost={feedviewpost}
538
538
+
repostedby={repostedby}
539
539
+
style={style}
540
540
+
ref={ref}
541
541
+
dataIndexPropPass={dataIndexPropPass}
542
542
+
nopics={nopics}
543
543
+
lightboxCallback={lightboxCallback}
544
544
+
maxReplies={
545
545
+
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
546
546
+
}
547
547
+
/>
548
548
+
{maxReplies && maxReplies - 1 === 0 && (
549
549
+
<MoreReplies atUri={oldestOpsReplyElseNewestNonOpsReply} />
550
550
+
)}
551
551
+
</>
552
552
+
)}
553
553
+
</>
554
554
+
);
555
555
+
}
556
556
+
557
557
+
function MoreReplies({ atUri }: { atUri: string }) {
558
558
+
const navigate = useNavigate();
559
559
+
const aturio = new AtUri(atUri);
560
560
+
return (
561
561
+
<div
562
562
+
onClick={() =>
563
563
+
navigate({
564
564
+
to: "/profile/$did/post/$rkey",
565
565
+
params: { did: aturio.host, rkey: aturio.rkey },
566
566
+
})
567
567
+
}
568
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
569
+
>
570
570
+
<div className="w-[42px] h-12 flex flex-col items-center justify-center">
571
571
+
<div
572
572
+
style={{
573
573
+
width: 2,
574
574
+
height: "100%",
575
575
+
backgroundImage:
576
576
+
"repeating-linear-gradient(to bottom, var(--color-gray-500) 0, var(--color-gray-500) 4px, transparent 4px, transparent 8px)",
577
577
+
opacity: 0.5,
578
578
+
}}
579
579
+
className="dark:bg-[repeating-linear-gradient(to_bottom,var(--color-gray-500)_0,var(--color-gray-400)_4px,transparent_4px,transparent_8px)]"
580
580
+
//className="border-gray-400 dark:border-gray-500"
581
581
+
/>
582
582
+
</div>
583
583
+
584
584
+
<div className="flex items-center pl-3 text-sm text-gray-500 dark:text-gray-400 select-none">
585
585
+
More Replies
586
586
+
</div>
587
587
+
</div>
426
588
);
427
589
}
428
590
···
451
613
dataIndexPropPass,
452
614
nopics,
453
615
lightboxCallback,
616
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
472
-
lightboxCallback?: (d:LightboxProps) => void;
635
635
+
lightboxCallback?: (d: LightboxProps) => void;
636
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
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
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
1118
-
lightboxCallback
1284
1284
+
lightboxCallback,
1285
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
1142
-
lightboxCallback?: (d:LightboxProps) => void;
1309
1309
+
lightboxCallback?: (d: LightboxProps) => void;
1310
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
1499
-
<span className="dangerousFediContent"
1667
1667
+
<span
1668
1668
+
className="dangerousFediContent"
1500
1669
dangerouslySetInnerHTML={{
1501
1670
__html: DOMPurify.sanitize(fedi),
1502
1671
}}
···
1728
1897
navigate,
1729
1898
postid,
1730
1899
nopics,
1731
1731
-
lightboxCallback
1900
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
1742
-
lightboxCallback?: (d:LightboxProps) => void;
1911
1911
+
lightboxCallback?: (d: LightboxProps) => void;
1743
1912
}) {
1744
1913
//const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
1745
1745
-
function setLightboxIndex(number:number) {
1914
1914
+
function setLightboxIndex(number: number) {
1746
1915
navigate({
1747
1747
-
to: "/profile/$did/post/$rkey/image/$i",
1748
1748
-
params: {
1749
1749
-
did: postid?.did,
1750
1750
-
rkey: postid?.rkey,
1751
1751
-
i: number.toString(),
1752
1752
-
},
1753
1753
-
});
1916
1916
+
to: "/profile/$did/post/$rkey/image/$i",
1917
1917
+
params: {
1918
1918
+
did: postid?.did,
1919
1919
+
rkey: postid?.rkey,
1920
1920
+
i: number.toString(),
1921
1921
+
},
1922
1922
+
});
1754
1923
}
1755
1924
if (
1756
1925
AppBskyEmbedRecordWithMedia.isView(embed) &&
···
1962
2131
src: img.fullsize,
1963
2132
alt: img.alt,
1964
2133
}));
1965
1965
-
console.log("rendering images")
2134
2134
+
console.log("rendering images");
1966
2135
if (lightboxCallback) {
1967
1967
-
lightboxCallback({images: lightboxImages})
1968
1968
-
console.log("rendering images")
1969
1969
-
};
2136
2136
+
lightboxCallback({ images: lightboxImages });
2137
2137
+
console.log("rendering images");
2138
2138
+
}
1970
2139
1971
2140
if (nopics) return;
1972
2141
+72
-15
src/routes/profile.$did/post.$rkey.tsx
···
1
1
-
import { useQueryClient } from "@tanstack/react-query";
1
1
+
import { AtUri } from "@atproto/api";
2
2
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
2
3
import { createFileRoute, Outlet } from "@tanstack/react-router";
3
3
-
import React, { useLayoutEffect } from "react";
4
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
10
-
useQueryConstellation,
11
11
useQueryIdentity,
12
12
useQueryPost,
13
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
201
-
const { data: repliesData } = useQueryConstellation({
202
202
-
method: "/links",
203
203
-
target: atUri,
204
204
-
collection: "app.bsky.feed.post",
205
205
-
path: ".reply.parent.uri",
202
202
+
// const { data: repliesData } = useQueryConstellation({
203
203
+
// method: "/links",
204
204
+
// target: atUri,
205
205
+
// collection: "app.bsky.feed.post",
206
206
+
// path: ".reply.parent.uri",
207
207
+
// });
208
208
+
// const replies = repliesData?.linking_records.slice(0, 50) ?? [];
209
209
+
const infinitequeryresults = useInfiniteQuery({
210
210
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
211
211
+
{
212
212
+
method: "/links",
213
213
+
target: atUri,
214
214
+
collection: "app.bsky.feed.post",
215
215
+
path: ".reply.parent.uri",
216
216
+
}
217
217
+
),
218
218
+
enabled: !!atUri,
206
219
});
207
207
-
const replies = repliesData?.linking_records.slice(0, 50) ?? [];
220
220
+
221
221
+
const {
222
222
+
data: repliesData,
223
223
+
// fetchNextPage,
224
224
+
// hasNextPage,
225
225
+
// isFetchingNextPage,
226
226
+
} = infinitequeryresults;
227
227
+
228
228
+
// auto-fetch all pages
229
229
+
useEffect(() => {
230
230
+
if (
231
231
+
infinitequeryresults.hasNextPage &&
232
232
+
!infinitequeryresults.isFetchingNextPage
233
233
+
) {
234
234
+
console.log("Fetching the next page...");
235
235
+
infinitequeryresults.fetchNextPage();
236
236
+
}
237
237
+
}, [infinitequeryresults]);
238
238
+
239
239
+
const replyAturis = repliesData
240
240
+
? repliesData.pages.flatMap((page) =>
241
241
+
page
242
242
+
? page.linking_records.map((record) => {
243
243
+
const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
244
244
+
return aturi;
245
245
+
})
246
246
+
: []
247
247
+
)
248
248
+
: [];
249
249
+
250
250
+
const opdid = new AtUri(atUri).host;
251
251
+
252
252
+
// Find oldest OP reply
253
253
+
const oldestOpsIndex = replyAturis.findIndex(
254
254
+
(aturi) => new AtUri(aturi).host === opdid
255
255
+
);
256
256
+
257
257
+
// Reorder: move oldest OP reply to the front
258
258
+
if (oldestOpsIndex > 0) {
259
259
+
const [oldestOpsReply] = replyAturis.splice(oldestOpsIndex, 1);
260
260
+
replyAturis.unshift(oldestOpsReply);
261
261
+
}
262
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
354
-
minHeight: "100dvh",
409
409
+
minHeight: "80dvh",
410
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
368
-
{replies.length > 0 &&
369
369
-
replies.map((reply) => {
370
370
-
const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
424
424
+
{replyAturis.length > 0 &&
425
425
+
replyAturis.map((reply) => {
426
426
+
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
371
427
return (
372
428
<UniversalPostRendererATURILoader
373
373
-
key={replyAtUri}
374
374
-
atUri={replyAtUri}
429
429
+
key={reply}
430
430
+
atUri={reply}
431
431
+
maxReplies={4}
375
432
/>
376
433
);
377
434
})}
+55
src/utils/useQuery.ts
···
1
1
import * as ATPAPI from "@atproto/api";
2
2
import {
3
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
618
+
}
619
619
+
620
620
+
621
621
+
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
622
622
+
method: '/links'
623
623
+
target?: string
624
624
+
collection: string
625
625
+
path: string
626
626
+
}) {
627
627
+
const constellationHost = 'constellation.microcosm.blue'
628
628
+
console.log(
629
629
+
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
630
630
+
query,
631
631
+
)
632
632
+
633
633
+
return infiniteQueryOptions({
634
634
+
enabled: !!query?.target,
635
635
+
queryKey: [
636
636
+
'reddwarf_constellation',
637
637
+
query?.method,
638
638
+
query?.target,
639
639
+
query?.collection,
640
640
+
query?.path,
641
641
+
] as const,
642
642
+
643
643
+
queryFn: async ({pageParam}: {pageParam?: string}) => {
644
644
+
if (!query || !query?.target) return undefined
645
645
+
646
646
+
const method = query.method
647
647
+
const target = query.target
648
648
+
const collection = query.collection
649
649
+
const path = query.path
650
650
+
const cursor = pageParam
651
651
+
652
652
+
const res = await fetch(
653
653
+
`https://${constellationHost}${method}?target=${encodeURIComponent(target)}${
654
654
+
collection ? `&collection=${encodeURIComponent(collection)}` : ''
655
655
+
}${path ? `&path=${encodeURIComponent(path)}` : ''}${
656
656
+
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
657
657
+
}`,
658
658
+
)
659
659
+
660
660
+
if (!res.ok) throw new Error('Failed to fetch')
661
661
+
662
662
+
return (await res.json()) as linksRecordsResponse
663
663
+
},
664
664
+
665
665
+
getNextPageParam: lastPage => {
666
666
+
return (lastPage as any)?.cursor ?? undefined
667
667
+
},
668
668
+
initialPageParam: undefined,
669
669
+
staleTime: 5 * 60 * 1000,
670
670
+
gcTime: 5 * 60 * 1000,
671
671
+
})
617
672
}