diff --git a/index.html b/index.html index b3b1f64..1e84695 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ - Red Dwarf lite + Red Dwarf
diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index 7cb7adf..b50790e 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -14,6 +14,10 @@ export interface UniversalPostRendererATURILoaderProps { atUri: string; onConstellation?: (data: any) => void; detailed?: boolean; + bottomReplyLine?: boolean; + topReplyLine?: boolean; + bottomBorder?:boolean; + feedviewpost?:boolean; } export async function cachedGetRecord({ @@ -113,6 +117,10 @@ export function UniversalPostRendererATURILoader({ atUri, onConstellation, detailed = false, + bottomReplyLine, + topReplyLine, + bottomBorder= true, + feedviewpost = false, }: UniversalPostRendererATURILoaderProps) { console.log("atUri", atUri); const { get, set } = usePersistentStore(); @@ -359,6 +367,10 @@ export function UniversalPostRendererATURILoader({ likesCount={likes} repostsCount={reposts} repliesCount={replies} + bottomReplyLine={bottomReplyLine} + topReplyLine={topReplyLine} + bottomBorder={bottomBorder} + feedviewpost={feedviewpost} /> ); } @@ -372,6 +384,10 @@ export function UniversalPostRendererRawRecordShim({ repostsCount, repliesCount, detailed = false, + bottomReplyLine = false, + topReplyLine = false, + bottomBorder= true, + feedviewpost= false, }: { postRecord: any; profileRecord: any; @@ -381,6 +397,10 @@ export function UniversalPostRendererRawRecordShim({ repostsCount?: number | null; repliesCount?: number | null; detailed?: boolean; + bottomReplyLine?: boolean; + topReplyLine?: boolean; + bottomBorder?: boolean; + feedviewpost?: boolean; }) { const navigate = useNavigate(); @@ -458,6 +478,70 @@ export function UniversalPostRendererRawRecordShim({ const parsedaturi = parseAtUri(aturi); + const fakepost = React.useMemo(() => ({ + $type: "app.bsky.feed.defs#postView", + uri: aturi, + cid: postRecord?.cid || "", + author: { + did: resolved?.did || "", + handle: resolved?.handle || "", + displayName: profileRecord?.value?.displayName || "", + avatar: getAvatarUrl(profileRecord) || "", + viewer: undefined, + labels: profileRecord?.labels || undefined, + verification: undefined, + }, + record: postRecord?.value || {}, + embed: hydratedEmbed ?? undefined, + replyCount: repliesCount ?? 0, + repostCount: repostsCount ?? 0, + likeCount: likesCount ?? 0, + quoteCount: 0, + indexedAt: postRecord?.value?.createdAt || "", + viewer: undefined, + labels: postRecord?.labels || undefined, + threadgate: undefined, + }), [ + aturi, + postRecord, + profileRecord, + hydratedEmbed, + repliesCount, + repostsCount, + likesCount, + resolved, + ]); + + const [feedviewpostreplyhandle, setFeedviewpostreplyhandle] = useState(undefined); + + useEffect(() => { + if(!feedviewpost) return; + let cancelled = false; + + const run = async () => { + const thereply = (fakepost?.record as AppBskyFeedPost.Record)?.reply?.parent?.uri; + const feedviewpostreplydid = thereply ? new AtUri(thereply).host : undefined; + + if (feedviewpostreplydid) { + const opi = await cachedResolveIdentity({ + didOrHandle: feedviewpostreplydid, + get, + set, + }); + + if (!cancelled) { + setFeedviewpostreplyhandle(opi?.handle); + } + } + }; + + run(); + + return () => { + cancelled = true; + }; + }, [fakepost, get, set]); + return ( <> {/*

@@ -484,31 +568,13 @@ export function UniversalPostRendererRawRecordShim({ }); } }} - post={{ - $type: "app.bsky.feed.defs#postView", - uri: aturi, - cid: postRecord?.cid || "", - author: { - did: resolved?.did || "", - handle: resolved?.handle || "", - displayName: profileRecord?.value?.displayName || "", - avatar: getAvatarUrl(profileRecord) || "", - viewer: undefined, - labels: profileRecord?.labels || undefined, - verification: undefined, - }, - record: postRecord?.value || {}, - embed: hydratedEmbed ?? undefined, - replyCount: repliesCount ?? 0, - repostCount: repostsCount ?? 0, - likeCount: likesCount ?? 0, - quoteCount: 0, - indexedAt: postRecord?.value?.createdAt || "", - viewer: undefined, - labels: postRecord?.labels || undefined, - threadgate: undefined, - }} + post={fakepost} salt={aturi} + bottomReplyLine={bottomReplyLine} + topReplyLine={topReplyLine} + bottomBorder={bottomBorder} + //extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}} + feedviewpostreplyhandle={feedviewpostreplyhandle} /> ); @@ -1071,6 +1137,7 @@ import { AppBskyFeedDefs, AppBskyFeedPost, AppBskyGraphDefs, + AtUri, //AppBskyLabelerDefs, //AtUri, //ComAtprotoRepoStrongRef, @@ -1171,6 +1238,7 @@ function UniversalPostRenderer({ topReplyLine, salt, bottomBorder = true, + feedviewpostreplyhandle, }: { post: PostView; // optional for now because i havent ported every use to this yet @@ -1187,6 +1255,7 @@ function UniversalPostRenderer({ topReplyLine?: boolean; salt: string; bottomBorder?: boolean; + feedviewpostreplyhandle?: string; }) { const navigate = useNavigate(); const [hasRetweeted, setHasRetweeted] = useState( @@ -1319,6 +1388,7 @@ function UniversalPostRenderer({ //opacity: 0.5, // no flex here }} + className="bg-gray-500 dark:bg-gray-400" /> )}

)} {/*
{/* reply indicator */} - {false && isReply && ( + {!!feedviewpostreplyhandle && (
- Reply to some other post lmao + Reply to {feedviewpostreplyhandle}
)}
- , - //, + + + ); } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index e3bec84..4468825 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -176,9 +176,9 @@ function RootDocument({ children }: { children: React.ReactNode }) { Red Dwarf Logo Red Dwarf{" "} - + {/* lite - + */}
Post
+ + git repo +

- Red Dwarf lite is a bluesky client that uses Constellation and - direct PDS queries. Red Dwarf (without the lite) would be a - self-hosted bluesky "instance". Stay tuned for the "without the - lite" version. + Red Dwarf is a bluesky client that uses Constellation and + direct PDS queries. Skylite would be a + self-hosted bluesky "instance". Stay tuned for the release of Skylite.

diff --git a/src/routes/profile.$did/index.tsx b/src/routes/profile.$did/index.tsx index 3d09f53..932428e 100644 --- a/src/routes/profile.$did/index.tsx +++ b/src/routes/profile.$did/index.tsx @@ -404,6 +404,7 @@ function ProfileComponent() { ); })} diff --git a/src/routes/profile.$did/post.$rkey.tsx b/src/routes/profile.$did/post.$rkey.tsx index dc2770c..0b97fd3 100644 --- a/src/routes/profile.$did/post.$rkey.tsx +++ b/src/routes/profile.$did/post.$rkey.tsx @@ -1,24 +1,18 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; -import React from "react"; -import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; -import { usePersistentStore } from "~/providers/PersistentStoreProvider"; +import { createFileRoute, Link } from '@tanstack/react-router'; +import React from 'react'; +import { UniversalPostRendererATURILoader, cachedGetRecord } from '~/components/UniversalPostRenderer'; +import { usePersistentStore } from '~/providers/PersistentStoreProvider'; const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour -export const Route = createFileRoute("/profile/$did/post/$rkey")({ +export const Route = createFileRoute('/profile/$did/post/$rkey')({ component: RouterWrapper, }); function RouterWrapper() { const { did, rkey } = Route.useParams(); - return ( - - ); + return ; } function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { @@ -26,6 +20,10 @@ function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { const [resolvedDid, setResolvedDid] = React.useState(null); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); + + const [mainPost, setMainPost] = React.useState(null); + const [parents, setParents] = React.useState([]); + const [parentsLoading, setParentsLoading] = React.useState(false); const [replies, setReplies] = React.useState([]); React.useEffect(() => { @@ -35,7 +33,7 @@ function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { setResolvedDid(null); return; } - if (did.startsWith("did:")) { + if (did.startsWith('did:')) { setResolvedDid(did); return; } @@ -44,12 +42,7 @@ function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { const cacheKey = `handleDid:${did}`; const now = Date.now(); const cached = await get(cacheKey); // <-- await here - if ( - cached && - cached.value && - cached.time && - now - cached.time < HANDLE_DID_CACHE_TIMEOUT - ) { + if (cached && cached.value && cached.time && now - cached.time < HANDLE_DID_CACHE_TIMEOUT) { try { const data = JSON.parse(cached.value); if (!ignore) setResolvedDid(data.did); @@ -60,12 +53,12 @@ function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { try { const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(did)}`; const res = await fetch(url); - if (!res.ok) throw new Error("Failed to resolve handle"); + if (!res.ok) throw new Error('Failed to resolve handle'); const data = await res.json(); await set(cacheKey, JSON.stringify(data)); // <-- await here if (!ignore) setResolvedDid(data.did); } catch (e: any) { - if (!ignore) setError("Failed to resolve handle: " + (e?.message || e)); + if (!ignore) setError('Failed to resolve handle: ' + (e?.message || e)); } finally { setLoading(false); } @@ -76,21 +69,72 @@ function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { }; }, [did, get, set]); - const atUri = - resolvedDid && rkey - ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` - : ""; + const atUri = resolvedDid && rkey ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` : ''; - const handleConstellation = React.useCallback((data: any) => {}, []); + React.useEffect(() => { + if (!atUri) return; + let ignore = false; + async function fetchMainPost() { + try { + const postData = await cachedGetRecord({ atUri, get, set }); + if (!ignore) { + setMainPost(postData); + } + } catch (e) { + console.error('Failed to fetch main post record:', e); + } + } + fetchMainPost(); + return () => { + ignore = true; + }; + }, [atUri, get, set]); + + React.useEffect(() => { + if (!mainPost) return; + let ignore = false; + async function fetchParents() { + setParentsLoading(true); + const parentChain: any[] = []; + let currentParentUri = mainPost.value?.reply?.parent?.uri; + const MAX_PARENTS = 25; // Important to know theres a limit + let safetyCounter = 0; + + while (currentParentUri && safetyCounter < MAX_PARENTS) { + try { + const parentPost = await cachedGetRecord({ atUri: currentParentUri, get, set }); + if (!parentPost) break; + parentChain.push(parentPost); + currentParentUri = parentPost.value?.reply?.parent?.uri; + safetyCounter++; + } catch (error) { + console.error('Failed to fetch a parent post:', error); + break; + } + } + + if (!ignore) { + setParents(parentChain.reverse()); + setParentsLoading(false); + } + } + + fetchParents(); + return () => { + ignore = true; + }; + }, [mainPost, get, set]); React.useEffect(() => { if (!atUri) return; let ignore = false; async function fetchReplies() { try { - const url = `https://constellation.microcosm.blue/links?target=${encodeURIComponent(atUri)}&collection=app.bsky.feed.post&path=.reply.parent.uri`; + const url = `https://constellation.microcosm.blue/links?target=${encodeURIComponent( + atUri, + )}&collection=app.bsky.feed.post&path=.reply.parent.uri`; const res = await fetch(url); - if (!res.ok) throw new Error("Failed to fetch replies"); + if (!res.ok) throw new Error('Failed to fetch replies'); const data = await res.json(); if (!ignore && data.linking_records) { setReplies(data.linking_records.slice(0, 50)); @@ -107,22 +151,18 @@ function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { if (!did || !rkey) return
Invalid post URI
; if (loading) return
Resolving handle...
; - if (error) return
{error}
; + if (error) return
{error}
; if (!atUri) return
Invalid post URI
; - console.log("atUri", atUri); - return ( <>
{ + onClick={e => { e.preventDefault(); - window.history.length > 1 - ? window.history.back() - : window.location.assign("/"); + window.history.length > 1 ? window.history.back() : window.location.assign('/'); }} aria-label="Go back" > @@ -130,36 +170,38 @@ function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) { Post
- + + {parentsLoading &&
Loading conversation...
} + + {/* we should use the reply lines here thats provided by UPR*/} +
+ {parents.map((parent, index) => ( + 0} + bottomReplyLine={true} + bottomBorder={false} + /> + ))} +
+ + 0} /> + {replies.length > 0 && ( -
+
Replies
-
- {replies.map((reply, i) => { +
+ {replies.map(reply => { const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; - return ( - - ); + return ; })}
)} ); -} +} \ No newline at end of file