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
reply indicator and parent chain
rimar1337
6 months ago
c64e32b7
1c939a15
+216
-94
6 changed files
expand all
collapse all
unified
split
index.html
src
components
UniversalPostRenderer.tsx
main.tsx
routes
__root.tsx
profile.$did
index.tsx
post.$rkey.tsx
+1
-1
index.html
···
12
<link rel="apple-touch-icon" href="/redstar.png" />
13
<link rel="manifest" href="/manifest.json" />
14
<link rel="stylesheet" href="/src/styles/app.css" />
15
-
<title>Red Dwarf lite</title>
16
</head>
17
<body>
18
<div id="app"></div>
···
12
<link rel="apple-touch-icon" href="/redstar.png" />
13
<link rel="manifest" href="/manifest.json" />
14
<link rel="stylesheet" href="/src/styles/app.css" />
15
+
<title>Red Dwarf</title>
16
</head>
17
<body>
18
<div id="app"></div>
+101
-29
src/components/UniversalPostRenderer.tsx
···
14
atUri: string;
15
onConstellation?: (data: any) => void;
16
detailed?: boolean;
0
0
0
0
17
}
18
19
export async function cachedGetRecord({
···
113
atUri,
114
onConstellation,
115
detailed = false,
0
0
0
0
116
}: UniversalPostRendererATURILoaderProps) {
117
console.log("atUri", atUri);
118
const { get, set } = usePersistentStore();
···
359
likesCount={likes}
360
repostsCount={reposts}
361
repliesCount={replies}
0
0
0
0
362
/>
363
);
364
}
···
372
repostsCount,
373
repliesCount,
374
detailed = false,
0
0
0
0
375
}: {
376
postRecord: any;
377
profileRecord: any;
···
381
repostsCount?: number | null;
382
repliesCount?: number | null;
383
detailed?: boolean;
0
0
0
0
384
}) {
385
const navigate = useNavigate();
386
···
458
459
const parsedaturi = parseAtUri(aturi);
460
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
461
return (
462
<>
463
{/* <p>
···
484
});
485
}
486
}}
487
-
post={{
488
-
$type: "app.bsky.feed.defs#postView",
489
-
uri: aturi,
490
-
cid: postRecord?.cid || "",
491
-
author: {
492
-
did: resolved?.did || "",
493
-
handle: resolved?.handle || "",
494
-
displayName: profileRecord?.value?.displayName || "",
495
-
avatar: getAvatarUrl(profileRecord) || "",
496
-
viewer: undefined,
497
-
labels: profileRecord?.labels || undefined,
498
-
verification: undefined,
499
-
},
500
-
record: postRecord?.value || {},
501
-
embed: hydratedEmbed ?? undefined,
502
-
replyCount: repliesCount ?? 0,
503
-
repostCount: repostsCount ?? 0,
504
-
likeCount: likesCount ?? 0,
505
-
quoteCount: 0,
506
-
indexedAt: postRecord?.value?.createdAt || "",
507
-
viewer: undefined,
508
-
labels: postRecord?.labels || undefined,
509
-
threadgate: undefined,
510
-
}}
511
salt={aturi}
0
0
0
0
0
512
/>
513
</>
514
);
···
1071
AppBskyFeedDefs,
1072
AppBskyFeedPost,
1073
AppBskyGraphDefs,
0
1074
//AppBskyLabelerDefs,
1075
//AtUri,
1076
//ComAtprotoRepoStrongRef,
···
1171
topReplyLine,
1172
salt,
1173
bottomBorder = true,
0
1174
}: {
1175
post: PostView;
1176
// optional for now because i havent ported every use to this yet
···
1187
topReplyLine?: boolean;
1188
salt: string;
1189
bottomBorder?: boolean;
0
1190
}) {
1191
const navigate = useNavigate();
1192
const [hasRetweeted, setHasRetweeted] = useState<Boolean>(
···
1319
//opacity: 0.5,
1320
// no flex here
1321
}}
0
1322
/>
1323
)}
1324
<div
···
1375
//background: theme.textSecondary,
1376
opacity: 0.5,
1377
// no flex here
0
0
1378
}}
1379
-
className="text-gray-500 dark:text-gray-400"
1380
/>
1381
)}
1382
{/* <div
···
1482
</div>
1483
</div>
1484
{/* reply indicator */}
1485
-
{false && isReply && (
1486
<div
1487
style={{
1488
display: "flex",
···
1494
gap: 4,
1495
alignItems: "center",
1496
//marginLeft: 36,
1497
-
height: !(expanded || isQuote) && isReply ? "1rem" : 0,
1498
-
opacity: !(expanded || isQuote) && isReply ? 1 : 0,
1499
}}
1500
className="text-gray-500 dark:text-gray-400"
1501
>
1502
-
<MdiReply /> Reply to some other post lmao
1503
</div>
1504
)}
1505
<div
···
14
atUri: string;
15
onConstellation?: (data: any) => void;
16
detailed?: boolean;
17
+
bottomReplyLine?: boolean;
18
+
topReplyLine?: boolean;
19
+
bottomBorder?:boolean;
20
+
feedviewpost?:boolean;
21
}
22
23
export async function cachedGetRecord({
···
117
atUri,
118
onConstellation,
119
detailed = false,
120
+
bottomReplyLine,
121
+
topReplyLine,
122
+
bottomBorder= true,
123
+
feedviewpost = false,
124
}: UniversalPostRendererATURILoaderProps) {
125
console.log("atUri", atUri);
126
const { get, set } = usePersistentStore();
···
367
likesCount={likes}
368
repostsCount={reposts}
369
repliesCount={replies}
370
+
bottomReplyLine={bottomReplyLine}
371
+
topReplyLine={topReplyLine}
372
+
bottomBorder={bottomBorder}
373
+
feedviewpost={feedviewpost}
374
/>
375
);
376
}
···
384
repostsCount,
385
repliesCount,
386
detailed = false,
387
+
bottomReplyLine = false,
388
+
topReplyLine = false,
389
+
bottomBorder= true,
390
+
feedviewpost= false,
391
}: {
392
postRecord: any;
393
profileRecord: any;
···
397
repostsCount?: number | null;
398
repliesCount?: number | null;
399
detailed?: boolean;
400
+
bottomReplyLine?: boolean;
401
+
topReplyLine?: boolean;
402
+
bottomBorder?: boolean;
403
+
feedviewpost?: boolean;
404
}) {
405
const navigate = useNavigate();
406
···
478
479
const parsedaturi = parseAtUri(aturi);
480
481
+
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(() => ({
482
+
$type: "app.bsky.feed.defs#postView",
483
+
uri: aturi,
484
+
cid: postRecord?.cid || "",
485
+
author: {
486
+
did: resolved?.did || "",
487
+
handle: resolved?.handle || "",
488
+
displayName: profileRecord?.value?.displayName || "",
489
+
avatar: getAvatarUrl(profileRecord) || "",
490
+
viewer: undefined,
491
+
labels: profileRecord?.labels || undefined,
492
+
verification: undefined,
493
+
},
494
+
record: postRecord?.value || {},
495
+
embed: hydratedEmbed ?? undefined,
496
+
replyCount: repliesCount ?? 0,
497
+
repostCount: repostsCount ?? 0,
498
+
likeCount: likesCount ?? 0,
499
+
quoteCount: 0,
500
+
indexedAt: postRecord?.value?.createdAt || "",
501
+
viewer: undefined,
502
+
labels: postRecord?.labels || undefined,
503
+
threadgate: undefined,
504
+
}), [
505
+
aturi,
506
+
postRecord,
507
+
profileRecord,
508
+
hydratedEmbed,
509
+
repliesCount,
510
+
repostsCount,
511
+
likesCount,
512
+
resolved,
513
+
]);
514
+
515
+
const [feedviewpostreplyhandle, setFeedviewpostreplyhandle] = useState<string | undefined>(undefined);
516
+
517
+
useEffect(() => {
518
+
if(!feedviewpost) return;
519
+
let cancelled = false;
520
+
521
+
const run = async () => {
522
+
const thereply = (fakepost?.record as AppBskyFeedPost.Record)?.reply?.parent?.uri;
523
+
const feedviewpostreplydid = thereply ? new AtUri(thereply).host : undefined;
524
+
525
+
if (feedviewpostreplydid) {
526
+
const opi = await cachedResolveIdentity({
527
+
didOrHandle: feedviewpostreplydid,
528
+
get,
529
+
set,
530
+
});
531
+
532
+
if (!cancelled) {
533
+
setFeedviewpostreplyhandle(opi?.handle);
534
+
}
535
+
}
536
+
};
537
+
538
+
run();
539
+
540
+
return () => {
541
+
cancelled = true;
542
+
};
543
+
}, [fakepost, get, set]);
544
+
545
return (
546
<>
547
{/* <p>
···
568
});
569
}
570
}}
571
+
post={fakepost}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
572
salt={aturi}
573
+
bottomReplyLine={bottomReplyLine}
574
+
topReplyLine={topReplyLine}
575
+
bottomBorder={bottomBorder}
576
+
//extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}}
577
+
feedviewpostreplyhandle={feedviewpostreplyhandle}
578
/>
579
</>
580
);
···
1137
AppBskyFeedDefs,
1138
AppBskyFeedPost,
1139
AppBskyGraphDefs,
1140
+
AtUri,
1141
//AppBskyLabelerDefs,
1142
//AtUri,
1143
//ComAtprotoRepoStrongRef,
···
1238
topReplyLine,
1239
salt,
1240
bottomBorder = true,
1241
+
feedviewpostreplyhandle,
1242
}: {
1243
post: PostView;
1244
// optional for now because i havent ported every use to this yet
···
1255
topReplyLine?: boolean;
1256
salt: string;
1257
bottomBorder?: boolean;
1258
+
feedviewpostreplyhandle?: string;
1259
}) {
1260
const navigate = useNavigate();
1261
const [hasRetweeted, setHasRetweeted] = useState<Boolean>(
···
1388
//opacity: 0.5,
1389
// no flex here
1390
}}
1391
+
className="bg-gray-500 dark:bg-gray-400"
1392
/>
1393
)}
1394
<div
···
1445
//background: theme.textSecondary,
1446
opacity: 0.5,
1447
// no flex here
1448
+
//color: "Red",
1449
+
//zIndex: 99
1450
}}
1451
+
className="bg-gray-500 dark:bg-gray-400"
1452
/>
1453
)}
1454
{/* <div
···
1554
</div>
1555
</div>
1556
{/* reply indicator */}
1557
+
{!!feedviewpostreplyhandle && (
1558
<div
1559
style={{
1560
display: "flex",
···
1566
gap: 4,
1567
alignItems: "center",
1568
//marginLeft: 36,
1569
+
height: !(expanded || isQuote) && !!feedviewpostreplyhandle ? "1rem" : 0,
1570
+
opacity: !(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0,
1571
}}
1572
className="text-gray-500 dark:text-gray-400"
1573
>
1574
+
<MdiReply /> Reply to {feedviewpostreplyhandle}
1575
</div>
1576
)}
1577
<div
+3
-3
src/main.tsx
···
31
const root = ReactDOM.createRoot(rootElement);
32
root.render(
33
// double queries annoys me
34
-
//<StrictMode>
35
-
<RouterProvider router={router} />,
36
-
//</StrictMode>,
37
);
38
}
39
···
31
const root = ReactDOM.createRoot(rootElement);
32
root.render(
33
// double queries annoys me
34
+
<StrictMode>
35
+
<RouterProvider router={router} />
36
+
</StrictMode>
37
);
38
}
39
+13
-6
src/routes/__root.tsx
···
176
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
177
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
178
Red Dwarf{" "}
179
-
<span className="text-gray-500 dark:text-gray-400 text-sm">
180
lite
181
-
</span>
182
</span>
183
</div>
184
<Link
···
277
</button>
278
<div className="flex-1"></div>
279
<a
0
0
0
0
0
0
0
0
280
href="https://whey.party/"
281
target="_blank"
282
rel="noopener noreferrer"
···
339
340
<div className="flex-1"></div>
341
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
342
-
Red Dwarf lite is a bluesky client that uses Constellation and
343
-
direct PDS queries. Red Dwarf (without the lite) would be a
344
-
self-hosted bluesky "instance". Stay tuned for the "without the
345
-
lite" version.
346
</p>
347
</aside>
348
</div>
···
176
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
177
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
178
Red Dwarf{" "}
179
+
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
180
lite
181
+
</span> */}
182
</span>
183
</div>
184
<Link
···
277
</button>
278
<div className="flex-1"></div>
279
<a
280
+
href="https://tangled.sh/@whey.party/red-dwarf"
281
+
target="_blank"
282
+
rel="noopener noreferrer"
283
+
className="mt-1 text-xs text-gray-400 dark:text-gray-500 text-center hover:underline"
284
+
>
285
+
git repo
286
+
</a>
287
+
<a
288
href="https://whey.party/"
289
target="_blank"
290
rel="noopener noreferrer"
···
347
348
<div className="flex-1"></div>
349
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
350
+
Red Dwarf is a bluesky client that uses Constellation and
351
+
direct PDS queries. Skylite would be a
352
+
self-hosted bluesky "instance". Stay tuned for the release of Skylite.
0
353
</p>
354
</aside>
355
</div>
+1
src/routes/profile.$did/index.tsx
···
404
<UniversalPostRendererATURILoader
405
key={post.uri}
406
atUri={post.uri}
0
407
/>
408
);
409
})}
···
404
<UniversalPostRendererATURILoader
405
key={post.uri}
406
atUri={post.uri}
407
+
feedviewpost={true}
408
/>
409
);
410
})}
+97
-55
src/routes/profile.$did/post.$rkey.tsx
···
1
-
import { createFileRoute, Link } from "@tanstack/react-router";
2
-
import React from "react";
3
-
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
4
-
import { usePersistentStore } from "~/providers/PersistentStoreProvider";
5
6
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
7
8
-
export const Route = createFileRoute("/profile/$did/post/$rkey")({
9
component: RouterWrapper,
10
});
11
12
function RouterWrapper() {
13
const { did, rkey } = Route.useParams();
14
15
-
return (
16
-
<ProfilePostComponent
17
-
key={`/profile/${did}/post/${rkey}`}
18
-
did={did}
19
-
rkey={rkey}
20
-
/>
21
-
);
22
}
23
24
function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) {
···
26
const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
27
const [loading, setLoading] = React.useState(false);
28
const [error, setError] = React.useState<string | null>(null);
0
0
0
0
29
const [replies, setReplies] = React.useState<any[]>([]);
30
31
React.useEffect(() => {
···
35
setResolvedDid(null);
36
return;
37
}
38
-
if (did.startsWith("did:")) {
39
setResolvedDid(did);
40
return;
41
}
···
44
const cacheKey = `handleDid:${did}`;
45
const now = Date.now();
46
const cached = await get(cacheKey); // <-- await here
47
-
if (
48
-
cached &&
49
-
cached.value &&
50
-
cached.time &&
51
-
now - cached.time < HANDLE_DID_CACHE_TIMEOUT
52
-
) {
53
try {
54
const data = JSON.parse(cached.value);
55
if (!ignore) setResolvedDid(data.did);
···
60
try {
61
const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(did)}`;
62
const res = await fetch(url);
63
-
if (!res.ok) throw new Error("Failed to resolve handle");
64
const data = await res.json();
65
await set(cacheKey, JSON.stringify(data)); // <-- await here
66
if (!ignore) setResolvedDid(data.did);
67
} catch (e: any) {
68
-
if (!ignore) setError("Failed to resolve handle: " + (e?.message || e));
69
} finally {
70
setLoading(false);
71
}
···
76
};
77
}, [did, get, set]);
78
79
-
const atUri =
80
-
resolvedDid && rkey
81
-
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
82
-
: "";
83
84
-
const handleConstellation = React.useCallback((data: any) => {}, []);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
85
86
React.useEffect(() => {
87
if (!atUri) return;
88
let ignore = false;
89
async function fetchReplies() {
90
try {
91
-
const url = `https://constellation.microcosm.blue/links?target=${encodeURIComponent(atUri)}&collection=app.bsky.feed.post&path=.reply.parent.uri`;
0
0
92
const res = await fetch(url);
93
-
if (!res.ok) throw new Error("Failed to fetch replies");
94
const data = await res.json();
95
if (!ignore && data.linking_records) {
96
setReplies(data.linking_records.slice(0, 50));
···
107
108
if (!did || !rkey) return <div>Invalid post URI</div>;
109
if (loading) return <div>Resolving handle...</div>;
110
-
if (error) return <div style={{ color: "red" }}>{error}</div>;
111
if (!atUri) return <div>Invalid post URI</div>;
112
-
113
-
console.log("atUri", atUri);
114
115
return (
116
<>
···
118
<Link
119
to=".."
120
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
121
-
onClick={(e) => {
122
e.preventDefault();
123
-
window.history.length > 1
124
-
? window.history.back()
125
-
: window.location.assign("/");
126
}}
127
aria-label="Go back"
128
>
···
130
</Link>
131
<span className="text-xl font-bold ml-2">Post</span>
132
</div>
133
-
<UniversalPostRendererATURILoader
134
-
atUri={atUri}
135
-
onConstellation={handleConstellation}
136
-
detailed={true}
137
-
/>
0
0
0
0
0
0
0
0
0
0
0
138
{replies.length > 0 && (
139
-
<div style={{ maxWidth: 600, margin: "0px auto 0", padding: 0 }}>
140
<div
141
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
142
-
style={{
143
-
fontSize: 18,
144
-
margin: "12px 16px 12px 16px",
145
-
fontWeight: 600,
146
-
}}
147
>
148
Replies
149
</div>
150
-
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
151
-
{replies.map((reply, i) => {
152
const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
153
-
return (
154
-
<UniversalPostRendererATURILoader
155
-
key={replyAtUri}
156
-
atUri={replyAtUri}
157
-
/>
158
-
);
159
})}
160
</div>
161
</div>
162
)}
163
</>
164
);
165
-
}
···
1
+
import { createFileRoute, Link } from '@tanstack/react-router';
2
+
import React from 'react';
3
+
import { UniversalPostRendererATURILoader, cachedGetRecord } from '~/components/UniversalPostRenderer';
4
+
import { usePersistentStore } from '~/providers/PersistentStoreProvider';
5
6
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
7
8
+
export const Route = createFileRoute('/profile/$did/post/$rkey')({
9
component: RouterWrapper,
10
});
11
12
function RouterWrapper() {
13
const { did, rkey } = Route.useParams();
14
15
+
return <ProfilePostComponent key={`/profile/${did}/post/${rkey}`} did={did} rkey={rkey} />;
0
0
0
0
0
0
16
}
17
18
function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) {
···
20
const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
21
const [loading, setLoading] = React.useState(false);
22
const [error, setError] = React.useState<string | null>(null);
23
+
24
+
const [mainPost, setMainPost] = React.useState<any | null>(null);
25
+
const [parents, setParents] = React.useState<any[]>([]);
26
+
const [parentsLoading, setParentsLoading] = React.useState(false);
27
const [replies, setReplies] = React.useState<any[]>([]);
28
29
React.useEffect(() => {
···
33
setResolvedDid(null);
34
return;
35
}
36
+
if (did.startsWith('did:')) {
37
setResolvedDid(did);
38
return;
39
}
···
42
const cacheKey = `handleDid:${did}`;
43
const now = Date.now();
44
const cached = await get(cacheKey); // <-- await here
45
+
if (cached && cached.value && cached.time && now - cached.time < HANDLE_DID_CACHE_TIMEOUT) {
0
0
0
0
0
46
try {
47
const data = JSON.parse(cached.value);
48
if (!ignore) setResolvedDid(data.did);
···
53
try {
54
const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(did)}`;
55
const res = await fetch(url);
56
+
if (!res.ok) throw new Error('Failed to resolve handle');
57
const data = await res.json();
58
await set(cacheKey, JSON.stringify(data)); // <-- await here
59
if (!ignore) setResolvedDid(data.did);
60
} catch (e: any) {
61
+
if (!ignore) setError('Failed to resolve handle: ' + (e?.message || e));
62
} finally {
63
setLoading(false);
64
}
···
69
};
70
}, [did, get, set]);
71
72
+
const atUri = resolvedDid && rkey ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` : '';
0
0
0
73
74
+
React.useEffect(() => {
75
+
if (!atUri) return;
76
+
let ignore = false;
77
+
async function fetchMainPost() {
78
+
try {
79
+
const postData = await cachedGetRecord({ atUri, get, set });
80
+
if (!ignore) {
81
+
setMainPost(postData);
82
+
}
83
+
} catch (e) {
84
+
console.error('Failed to fetch main post record:', e);
85
+
}
86
+
}
87
+
fetchMainPost();
88
+
return () => {
89
+
ignore = true;
90
+
};
91
+
}, [atUri, get, set]);
92
+
93
+
React.useEffect(() => {
94
+
if (!mainPost) return;
95
+
let ignore = false;
96
+
async function fetchParents() {
97
+
setParentsLoading(true);
98
+
const parentChain: any[] = [];
99
+
let currentParentUri = mainPost.value?.reply?.parent?.uri;
100
+
const MAX_PARENTS = 25; // Important to know theres a limit
101
+
let safetyCounter = 0;
102
+
103
+
while (currentParentUri && safetyCounter < MAX_PARENTS) {
104
+
try {
105
+
const parentPost = await cachedGetRecord({ atUri: currentParentUri, get, set });
106
+
if (!parentPost) break;
107
+
parentChain.push(parentPost);
108
+
currentParentUri = parentPost.value?.reply?.parent?.uri;
109
+
safetyCounter++;
110
+
} catch (error) {
111
+
console.error('Failed to fetch a parent post:', error);
112
+
break;
113
+
}
114
+
}
115
+
116
+
if (!ignore) {
117
+
setParents(parentChain.reverse());
118
+
setParentsLoading(false);
119
+
}
120
+
}
121
+
122
+
fetchParents();
123
+
return () => {
124
+
ignore = true;
125
+
};
126
+
}, [mainPost, get, set]);
127
128
React.useEffect(() => {
129
if (!atUri) return;
130
let ignore = false;
131
async function fetchReplies() {
132
try {
133
+
const url = `https://constellation.microcosm.blue/links?target=${encodeURIComponent(
134
+
atUri,
135
+
)}&collection=app.bsky.feed.post&path=.reply.parent.uri`;
136
const res = await fetch(url);
137
+
if (!res.ok) throw new Error('Failed to fetch replies');
138
const data = await res.json();
139
if (!ignore && data.linking_records) {
140
setReplies(data.linking_records.slice(0, 50));
···
151
152
if (!did || !rkey) return <div>Invalid post URI</div>;
153
if (loading) return <div>Resolving handle...</div>;
154
+
if (error) return <div style={{ color: 'red' }}>{error}</div>;
155
if (!atUri) return <div>Invalid post URI</div>;
0
0
156
157
return (
158
<>
···
160
<Link
161
to=".."
162
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
163
+
onClick={e => {
164
e.preventDefault();
165
+
window.history.length > 1 ? window.history.back() : window.location.assign('/');
0
0
166
}}
167
aria-label="Go back"
168
>
···
170
</Link>
171
<span className="text-xl font-bold ml-2">Post</span>
172
</div>
173
+
174
+
{parentsLoading && <div className="p-4 text-center text-gray-500 dark:text-gray-400">Loading conversation...</div>}
175
+
176
+
{/* we should use the reply lines here thats provided by UPR*/}
177
+
<div style={{ maxWidth: 600, margin: '0px auto 0', padding: 0 }}>
178
+
{parents.map((parent, index) => (
179
+
<UniversalPostRendererATURILoader key={parent.uri} atUri={parent.uri}
180
+
topReplyLine={index > 0}
181
+
bottomReplyLine={true}
182
+
bottomBorder={false}
183
+
/>
184
+
))}
185
+
</div>
186
+
187
+
<UniversalPostRendererATURILoader atUri={atUri} detailed={true} topReplyLine={parents.length > 0} />
188
+
189
{replies.length > 0 && (
190
+
<div style={{ maxWidth: 600, margin: '0px auto 0', padding: 0 }}>
191
<div
192
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
193
+
style={{ fontSize: 18, margin: '12px 16px 12px 16px', fontWeight: 600 }}
0
0
0
0
194
>
195
Replies
196
</div>
197
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
198
+
{replies.map(reply => {
199
const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
200
+
return <UniversalPostRendererATURILoader key={replyAtUri} atUri={replyAtUri} />;
0
0
0
0
0
201
})}
202
</div>
203
</div>
204
)}
205
</>
206
);
207
+
}