+1
-1
index.html
+1
-1
index.html
+101
-29
src/components/UniversalPostRenderer.tsx
+101
-29
src/components/UniversalPostRenderer.tsx
···
14
atUri: string;
15
onConstellation?: (data: any) => void;
16
detailed?: boolean;
17
}
18
19
export async function cachedGetRecord({
···
113
atUri,
114
onConstellation,
115
detailed = false,
116
}: UniversalPostRendererATURILoaderProps) {
117
console.log("atUri", atUri);
118
const { get, set } = usePersistentStore();
···
359
likesCount={likes}
360
repostsCount={reposts}
361
repliesCount={replies}
362
/>
363
);
364
}
···
372
repostsCount,
373
repliesCount,
374
detailed = false,
375
}: {
376
postRecord: any;
377
profileRecord: any;
···
381
repostsCount?: number | null;
382
repliesCount?: number | null;
383
detailed?: boolean;
384
}) {
385
const navigate = useNavigate();
386
···
458
459
const parsedaturi = parseAtUri(aturi);
460
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}
512
/>
513
</>
514
);
···
1071
AppBskyFeedDefs,
1072
AppBskyFeedPost,
1073
AppBskyGraphDefs,
1074
//AppBskyLabelerDefs,
1075
//AtUri,
1076
//ComAtprotoRepoStrongRef,
···
1171
topReplyLine,
1172
salt,
1173
bottomBorder = true,
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;
1190
}) {
1191
const navigate = useNavigate();
1192
const [hasRetweeted, setHasRetweeted] = useState<Boolean>(
···
1319
//opacity: 0.5,
1320
// no flex here
1321
}}
1322
/>
1323
)}
1324
<div
···
1375
//background: theme.textSecondary,
1376
opacity: 0.5,
1377
// no flex here
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}
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
+3
-3
src/main.tsx
+13
-6
src/routes/__root.tsx
+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
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.
353
</p>
354
</aside>
355
</div>
+1
src/routes/profile.$did/index.tsx
+1
src/routes/profile.$did/index.tsx
+97
-55
src/routes/profile.$did/post.$rkey.tsx
+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);
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) => {}, []);
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`;
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
-
/>
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} />;
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) {
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}` : '';
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>;
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('/');
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 }}
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} />;
201
})}
202
</div>
203
</div>
204
)}
205
</>
206
);
207
+
}