+1
-1
index.html
+1
-1
index.html
+101
-29
src/components/UniversalPostRenderer.tsx
+101
-29
src/components/UniversalPostRenderer.tsx
···
14
14
atUri: string;
15
15
onConstellation?: (data: any) => void;
16
16
detailed?: boolean;
17
+
bottomReplyLine?: boolean;
18
+
topReplyLine?: boolean;
19
+
bottomBorder?:boolean;
20
+
feedviewpost?:boolean;
17
21
}
18
22
19
23
export async function cachedGetRecord({
···
113
117
atUri,
114
118
onConstellation,
115
119
detailed = false,
120
+
bottomReplyLine,
121
+
topReplyLine,
122
+
bottomBorder= true,
123
+
feedviewpost = false,
116
124
}: UniversalPostRendererATURILoaderProps) {
117
125
console.log("atUri", atUri);
118
126
const { get, set } = usePersistentStore();
···
359
367
likesCount={likes}
360
368
repostsCount={reposts}
361
369
repliesCount={replies}
370
+
bottomReplyLine={bottomReplyLine}
371
+
topReplyLine={topReplyLine}
372
+
bottomBorder={bottomBorder}
373
+
feedviewpost={feedviewpost}
362
374
/>
363
375
);
364
376
}
···
372
384
repostsCount,
373
385
repliesCount,
374
386
detailed = false,
387
+
bottomReplyLine = false,
388
+
topReplyLine = false,
389
+
bottomBorder= true,
390
+
feedviewpost= false,
375
391
}: {
376
392
postRecord: any;
377
393
profileRecord: any;
···
381
397
repostsCount?: number | null;
382
398
repliesCount?: number | null;
383
399
detailed?: boolean;
400
+
bottomReplyLine?: boolean;
401
+
topReplyLine?: boolean;
402
+
bottomBorder?: boolean;
403
+
feedviewpost?: boolean;
384
404
}) {
385
405
const navigate = useNavigate();
386
406
···
458
478
459
479
const parsedaturi = parseAtUri(aturi);
460
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
+
461
545
return (
462
546
<>
463
547
{/* <p>
···
484
568
});
485
569
}
486
570
}}
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
-
}}
571
+
post={fakepost}
511
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}
512
578
/>
513
579
</>
514
580
);
···
1071
1137
AppBskyFeedDefs,
1072
1138
AppBskyFeedPost,
1073
1139
AppBskyGraphDefs,
1140
+
AtUri,
1074
1141
//AppBskyLabelerDefs,
1075
1142
//AtUri,
1076
1143
//ComAtprotoRepoStrongRef,
···
1171
1238
topReplyLine,
1172
1239
salt,
1173
1240
bottomBorder = true,
1241
+
feedviewpostreplyhandle,
1174
1242
}: {
1175
1243
post: PostView;
1176
1244
// optional for now because i havent ported every use to this yet
···
1187
1255
topReplyLine?: boolean;
1188
1256
salt: string;
1189
1257
bottomBorder?: boolean;
1258
+
feedviewpostreplyhandle?: string;
1190
1259
}) {
1191
1260
const navigate = useNavigate();
1192
1261
const [hasRetweeted, setHasRetweeted] = useState<Boolean>(
···
1319
1388
//opacity: 0.5,
1320
1389
// no flex here
1321
1390
}}
1391
+
className="bg-gray-500 dark:bg-gray-400"
1322
1392
/>
1323
1393
)}
1324
1394
<div
···
1375
1445
//background: theme.textSecondary,
1376
1446
opacity: 0.5,
1377
1447
// no flex here
1448
+
//color: "Red",
1449
+
//zIndex: 99
1378
1450
}}
1379
-
className="text-gray-500 dark:text-gray-400"
1451
+
className="bg-gray-500 dark:bg-gray-400"
1380
1452
/>
1381
1453
)}
1382
1454
{/* <div
···
1482
1554
</div>
1483
1555
</div>
1484
1556
{/* reply indicator */}
1485
-
{false && isReply && (
1557
+
{!!feedviewpostreplyhandle && (
1486
1558
<div
1487
1559
style={{
1488
1560
display: "flex",
···
1494
1566
gap: 4,
1495
1567
alignItems: "center",
1496
1568
//marginLeft: 36,
1497
-
height: !(expanded || isQuote) && isReply ? "1rem" : 0,
1498
-
opacity: !(expanded || isQuote) && isReply ? 1 : 0,
1569
+
height: !(expanded || isQuote) && !!feedviewpostreplyhandle ? "1rem" : 0,
1570
+
opacity: !(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0,
1499
1571
}}
1500
1572
className="text-gray-500 dark:text-gray-400"
1501
1573
>
1502
-
<MdiReply /> Reply to some other post lmao
1574
+
<MdiReply /> Reply to {feedviewpostreplyhandle}
1503
1575
</div>
1504
1576
)}
1505
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
176
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
177
177
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
178
178
Red Dwarf{" "}
179
-
<span className="text-gray-500 dark:text-gray-400 text-sm">
179
+
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
180
180
lite
181
-
</span>
181
+
</span> */}
182
182
</span>
183
183
</div>
184
184
<Link
···
277
277
</button>
278
278
<div className="flex-1"></div>
279
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
280
288
href="https://whey.party/"
281
289
target="_blank"
282
290
rel="noopener noreferrer"
···
339
347
340
348
<div className="flex-1"></div>
341
349
<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.
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.
346
353
</p>
347
354
</aside>
348
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";
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
5
6
6
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
7
7
8
-
export const Route = createFileRoute("/profile/$did/post/$rkey")({
8
+
export const Route = createFileRoute('/profile/$did/post/$rkey')({
9
9
component: RouterWrapper,
10
10
});
11
11
12
12
function RouterWrapper() {
13
13
const { did, rkey } = Route.useParams();
14
14
15
-
return (
16
-
<ProfilePostComponent
17
-
key={`/profile/${did}/post/${rkey}`}
18
-
did={did}
19
-
rkey={rkey}
20
-
/>
21
-
);
15
+
return <ProfilePostComponent key={`/profile/${did}/post/${rkey}`} did={did} rkey={rkey} />;
22
16
}
23
17
24
18
function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) {
···
26
20
const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
27
21
const [loading, setLoading] = React.useState(false);
28
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);
29
27
const [replies, setReplies] = React.useState<any[]>([]);
30
28
31
29
React.useEffect(() => {
···
35
33
setResolvedDid(null);
36
34
return;
37
35
}
38
-
if (did.startsWith("did:")) {
36
+
if (did.startsWith('did:')) {
39
37
setResolvedDid(did);
40
38
return;
41
39
}
···
44
42
const cacheKey = `handleDid:${did}`;
45
43
const now = Date.now();
46
44
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
-
) {
45
+
if (cached && cached.value && cached.time && now - cached.time < HANDLE_DID_CACHE_TIMEOUT) {
53
46
try {
54
47
const data = JSON.parse(cached.value);
55
48
if (!ignore) setResolvedDid(data.did);
···
60
53
try {
61
54
const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(did)}`;
62
55
const res = await fetch(url);
63
-
if (!res.ok) throw new Error("Failed to resolve handle");
56
+
if (!res.ok) throw new Error('Failed to resolve handle');
64
57
const data = await res.json();
65
58
await set(cacheKey, JSON.stringify(data)); // <-- await here
66
59
if (!ignore) setResolvedDid(data.did);
67
60
} catch (e: any) {
68
-
if (!ignore) setError("Failed to resolve handle: " + (e?.message || e));
61
+
if (!ignore) setError('Failed to resolve handle: ' + (e?.message || e));
69
62
} finally {
70
63
setLoading(false);
71
64
}
···
76
69
};
77
70
}, [did, get, set]);
78
71
79
-
const atUri =
80
-
resolvedDid && rkey
81
-
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
82
-
: "";
72
+
const atUri = resolvedDid && rkey ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` : '';
83
73
84
-
const handleConstellation = React.useCallback((data: any) => {}, []);
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]);
85
127
86
128
React.useEffect(() => {
87
129
if (!atUri) return;
88
130
let ignore = false;
89
131
async function fetchReplies() {
90
132
try {
91
-
const url = `https://constellation.microcosm.blue/links?target=${encodeURIComponent(atUri)}&collection=app.bsky.feed.post&path=.reply.parent.uri`;
133
+
const url = `https://constellation.microcosm.blue/links?target=${encodeURIComponent(
134
+
atUri,
135
+
)}&collection=app.bsky.feed.post&path=.reply.parent.uri`;
92
136
const res = await fetch(url);
93
-
if (!res.ok) throw new Error("Failed to fetch replies");
137
+
if (!res.ok) throw new Error('Failed to fetch replies');
94
138
const data = await res.json();
95
139
if (!ignore && data.linking_records) {
96
140
setReplies(data.linking_records.slice(0, 50));
···
107
151
108
152
if (!did || !rkey) return <div>Invalid post URI</div>;
109
153
if (loading) return <div>Resolving handle...</div>;
110
-
if (error) return <div style={{ color: "red" }}>{error}</div>;
154
+
if (error) return <div style={{ color: 'red' }}>{error}</div>;
111
155
if (!atUri) return <div>Invalid post URI</div>;
112
-
113
-
console.log("atUri", atUri);
114
156
115
157
return (
116
158
<>
···
118
160
<Link
119
161
to=".."
120
162
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
121
-
onClick={(e) => {
163
+
onClick={e => {
122
164
e.preventDefault();
123
-
window.history.length > 1
124
-
? window.history.back()
125
-
: window.location.assign("/");
165
+
window.history.length > 1 ? window.history.back() : window.location.assign('/');
126
166
}}
127
167
aria-label="Go back"
128
168
>
···
130
170
</Link>
131
171
<span className="text-xl font-bold ml-2">Post</span>
132
172
</div>
133
-
<UniversalPostRendererATURILoader
134
-
atUri={atUri}
135
-
onConstellation={handleConstellation}
136
-
detailed={true}
137
-
/>
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
+
138
189
{replies.length > 0 && (
139
-
<div style={{ maxWidth: 600, margin: "0px auto 0", padding: 0 }}>
190
+
<div style={{ maxWidth: 600, margin: '0px auto 0', padding: 0 }}>
140
191
<div
141
192
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
-
}}
193
+
style={{ fontSize: 18, margin: '12px 16px 12px 16px', fontWeight: 600 }}
147
194
>
148
195
Replies
149
196
</div>
150
-
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
151
-
{replies.map((reply, i) => {
197
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
198
+
{replies.map(reply => {
152
199
const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
153
-
return (
154
-
<UniversalPostRendererATURILoader
155
-
key={replyAtUri}
156
-
atUri={replyAtUri}
157
-
/>
158
-
);
200
+
return <UniversalPostRendererATURILoader key={replyAtUri} atUri={replyAtUri} />;
159
201
})}
160
202
</div>
161
203
</div>
162
204
)}
163
205
</>
164
206
);
165
-
}
207
+
}