an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

feed page with bad hack

rimar1337 ca2ab70a 0883da1a

Changed files
+199 -25
src
+6 -1
src/components/InfiniteCustomFeed.tsx
··· 14 14 feedUri: string; 15 15 pdsUrl?: string; 16 16 feedServiceDid?: string; 17 + authedOverride?: boolean; 18 + unauthedfeedurl?: string; 17 19 } 18 20 19 21 export function InfiniteCustomFeed({ 20 22 feedUri, 21 23 pdsUrl, 22 24 feedServiceDid, 25 + authedOverride, 26 + unauthedfeedurl, 23 27 }: InfiniteCustomFeedProps) { 24 28 const { agent } = useAuth(); 25 - const authed = !!agent?.did; 29 + const authed = authedOverride || !!agent?.did; 26 30 27 31 // const identityresultmaybe = useQueryIdentity(agent?.did); 28 32 // const identity = identityresultmaybe?.data; ··· 45 49 isAuthed: authed ?? false, 46 50 pdsUrl: pdsUrl, 47 51 feedServiceDid: feedServiceDid, 52 + unauthedfeedurl: unauthedfeedurl, 48 53 }); 49 54 const queryClient = useQueryClient(); 50 55
+12 -1
src/components/UniversalPostRenderer.tsx
··· 1204 1204 1205 1205 import defaultpfp from "~/../public/favicon.png"; 1206 1206 import { useAuth } from "~/providers/UnifiedAuthProvider"; 1207 - import { FollowButton, Mutual } from "~/routes/profile.$did"; 1207 + import { FeedItemRenderAturiLoader, FollowButton, Mutual } from "~/routes/profile.$did"; 1208 1208 import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; 1209 1209 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 1210 1210 // import type { ··· 2179 2179 } 2180 2180 2181 2181 if (AppBskyEmbedRecord.isView(embed)) { 2182 + // hey im really lazy and im gonna do it the bad way 2183 + const reallybaduri = (embed?.record as any)?.uri as string | undefined; 2184 + const reallybadaturi = reallybaduri ? new AtUri(reallybaduri) : undefined; 2185 + 2182 2186 // custom feed embed (i.e. generator view) 2183 2187 if (AppBskyFeedDefs.isGeneratorView(embed.record)) { 2184 2188 // stopgap sorry ··· 2188 2192 // <MaybeFeedCard view={embed.record} /> 2189 2193 // </div> 2190 2194 // ) 2195 + } else if (!!reallybaduri && !!reallybadaturi && reallybadaturi.collection === "app.bsky.feed.generator") { 2196 + return <div className="rounded-xl border"><FeedItemRenderAturiLoader aturi={reallybaduri} disableBottomBorder/></div> 2191 2197 } 2192 2198 2193 2199 // list embed ··· 2199 2205 // <MaybeListCard view={embed.record} /> 2200 2206 // </div> 2201 2207 // ) 2208 + } else if (!!reallybaduri && !!reallybadaturi && reallybadaturi.collection === "app.bsky.graph.list") { 2209 + return <div className="rounded-xl border"><FeedItemRenderAturiLoader aturi={reallybaduri} disableBottomBorder listmode /></div> 2202 2210 } 2203 2211 2204 2212 // starter pack embed ··· 2210 2218 // <StarterPackCard starterPack={embed.record} /> 2211 2219 // </div> 2212 2220 // ) 2221 + } else if (!!reallybaduri && !!reallybadaturi && reallybadaturi.collection === "app.bsky.graph.starterpack") { 2222 + return <div className="rounded-xl border"><FeedItemRenderAturiLoader aturi={reallybaduri} disableBottomBorder listmode /></div> 2213 2223 } 2214 2224 2215 2225 // quote post ··· 2269 2279 </div> 2270 2280 ); 2271 2281 } else { 2282 + console.log("what the hell is a ", embed); 2272 2283 return <>sorry</>; 2273 2284 } 2274 2285 //return <QuotePostRenderer record={embed.record} moderation={moderation} />;
+21
src/routeTree.gen.ts
··· 21 21 import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' 22 22 import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' 23 23 import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey' 24 + import { Route as ProfileDidFeedRkeyRouteImport } from './routes/profile.$did/feed.$rkey' 24 25 import { Route as ProfileDidPostRkeyRepostedByRouteImport } from './routes/profile.$did/post.$rkey.reposted-by' 25 26 import { Route as ProfileDidPostRkeyQuotesRouteImport } from './routes/profile.$did/post.$rkey.quotes' 26 27 import { Route as ProfileDidPostRkeyLikedByRouteImport } from './routes/profile.$did/post.$rkey.liked-by' ··· 85 86 const ProfileDidPostRkeyRoute = ProfileDidPostRkeyRouteImport.update({ 86 87 id: '/profile/$did/post/$rkey', 87 88 path: '/profile/$did/post/$rkey', 89 + getParentRoute: () => rootRouteImport, 90 + } as any) 91 + const ProfileDidFeedRkeyRoute = ProfileDidFeedRkeyRouteImport.update({ 92 + id: '/profile/$did/feed/$rkey', 93 + path: '/profile/$did/feed/$rkey', 88 94 getParentRoute: () => rootRouteImport, 89 95 } as any) 90 96 const ProfileDidPostRkeyRepostedByRoute = ··· 122 128 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 123 129 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 124 130 '/profile/$did': typeof ProfileDidIndexRoute 131 + '/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute 125 132 '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 126 133 '/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute 127 134 '/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute ··· 138 145 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 139 146 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 140 147 '/profile/$did': typeof ProfileDidIndexRoute 148 + '/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute 141 149 '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 142 150 '/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute 143 151 '/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute ··· 157 165 '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 158 166 '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 159 167 '/profile/$did/': typeof ProfileDidIndexRoute 168 + '/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute 160 169 '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 161 170 '/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute 162 171 '/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute ··· 175 184 | '/route-a' 176 185 | '/route-b' 177 186 | '/profile/$did' 187 + | '/profile/$did/feed/$rkey' 178 188 | '/profile/$did/post/$rkey' 179 189 | '/profile/$did/post/$rkey/liked-by' 180 190 | '/profile/$did/post/$rkey/quotes' ··· 191 201 | '/route-a' 192 202 | '/route-b' 193 203 | '/profile/$did' 204 + | '/profile/$did/feed/$rkey' 194 205 | '/profile/$did/post/$rkey' 195 206 | '/profile/$did/post/$rkey/liked-by' 196 207 | '/profile/$did/post/$rkey/quotes' ··· 209 220 | '/_pathlessLayout/_nested-layout/route-a' 210 221 | '/_pathlessLayout/_nested-layout/route-b' 211 222 | '/profile/$did/' 223 + | '/profile/$did/feed/$rkey' 212 224 | '/profile/$did/post/$rkey' 213 225 | '/profile/$did/post/$rkey/liked-by' 214 226 | '/profile/$did/post/$rkey/quotes' ··· 225 237 SettingsRoute: typeof SettingsRoute 226 238 CallbackIndexRoute: typeof CallbackIndexRoute 227 239 ProfileDidIndexRoute: typeof ProfileDidIndexRoute 240 + ProfileDidFeedRkeyRoute: typeof ProfileDidFeedRkeyRoute 228 241 ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren 229 242 } 230 243 ··· 314 327 preLoaderRoute: typeof ProfileDidPostRkeyRouteImport 315 328 parentRoute: typeof rootRouteImport 316 329 } 330 + '/profile/$did/feed/$rkey': { 331 + id: '/profile/$did/feed/$rkey' 332 + path: '/profile/$did/feed/$rkey' 333 + fullPath: '/profile/$did/feed/$rkey' 334 + preLoaderRoute: typeof ProfileDidFeedRkeyRouteImport 335 + parentRoute: typeof rootRouteImport 336 + } 317 337 '/profile/$did/post/$rkey/reposted-by': { 318 338 id: '/profile/$did/post/$rkey/reposted-by' 319 339 path: '/reposted-by' ··· 401 421 SettingsRoute: SettingsRoute, 402 422 CallbackIndexRoute: CallbackIndexRoute, 403 423 ProfileDidIndexRoute: ProfileDidIndexRoute, 424 + ProfileDidFeedRkeyRoute: ProfileDidFeedRkeyRoute, 404 425 ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren, 405 426 } 406 427 export const routeTree = rootRouteImport
+90
src/routes/profile.$did/feed.$rkey.tsx
··· 1 + import * as ATPAPI from "@atproto/api"; 2 + import { AtUri } from "@atproto/api"; 3 + import { createFileRoute } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 5 + 6 + import { Header } from "~/components/Header"; 7 + import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8 + import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 + import { quickAuthAtom } from "~/utils/atoms"; 10 + import { useQueryArbitrary, useQueryIdentity } from "~/utils/useQuery"; 11 + 12 + export const Route = createFileRoute("/profile/$did/feed/$rkey")({ 13 + component: FeedRoute, 14 + }); 15 + 16 + function FeedRoute() { 17 + const { did, rkey } = Route.useParams(); 18 + const { agent, status } = useAuth(); 19 + const { data: identitydata } = useQueryIdentity(did); 20 + const { data: identity } = useQueryIdentity(agent?.did); 21 + const uri = `at://${identitydata?.did || did}/app.bsky.feed.generator/${rkey}`; 22 + const aturi = new AtUri(uri); 23 + const { data: feeddata } = useQueryArbitrary(uri); 24 + 25 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 26 + const isAuthRestoring = quickAuth ? status === "loading" : false; 27 + 28 + const authed = status === "signedIn"; 29 + 30 + const feedServiceDid = !isAuthRestoring 31 + ? ((feeddata?.value as any)?.did as string | undefined) 32 + : undefined; 33 + 34 + // const { 35 + // data: feedData, 36 + // isLoading: isFeedLoading, 37 + // error: feedError, 38 + // } = useQueryFeedSkeleton({ 39 + // feedUri: selectedFeed!, 40 + // agent: agent ?? undefined, 41 + // isAuthed: authed ?? false, 42 + // pdsUrl: identity?.pds, 43 + // feedServiceDid: feedServiceDid, 44 + // }); 45 + 46 + // const feed = feedData?.feed || []; 47 + 48 + const isReadyForAuthedFeed = 49 + !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid; 50 + const isReadyForUnauthedFeed = !isAuthRestoring && !authed; 51 + 52 + const feed: ATPAPI.AppBskyFeedGenerator.Record | undefined = feeddata?.value; 53 + 54 + const web = feedServiceDid?.replace(/^did:web:/, "") || ""; 55 + 56 + return ( 57 + <> 58 + <Header 59 + title={feed?.displayName || aturi.rkey} 60 + backButtonCallback={() => { 61 + if (window.history.length > 1) { 62 + window.history.back(); 63 + } else { 64 + window.location.assign("/"); 65 + } 66 + }} 67 + /> 68 + 69 + {isAuthRestoring || 70 + (authed && (!identity?.pds || !feedServiceDid) && ( 71 + <div className="p-4 text-center text-gray-500"> 72 + Preparing your feed... 73 + </div> 74 + ))} 75 + 76 + {!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? ( 77 + <InfiniteCustomFeed 78 + key={uri} 79 + feedUri={uri} 80 + pdsUrl={identity?.pds} 81 + feedServiceDid={feedServiceDid} 82 + authedOverride={true} 83 + unauthedfeedurl={web} 84 + /> 85 + ) : ( 86 + <div className="p-4 text-center text-gray-500">Loading.......</div> 87 + )} 88 + </> 89 + ); 90 + }
+63 -19
src/routes/profile.$did/index.tsx
··· 1 1 import { RichText } from "@atproto/api"; 2 2 import * as ATPAPI from "@atproto/api"; 3 3 import { useQueryClient } from "@tanstack/react-query"; 4 - import { createFileRoute, useNavigate } from "@tanstack/react-router"; 4 + import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; 5 5 import { useAtom } from "jotai"; 6 6 import React, { type ReactNode, useEffect, useState } from "react"; 7 7 ··· 24 24 } from "~/utils/followState"; 25 25 import { 26 26 useInfiniteQueryAuthorFeed, 27 + useQueryArbitrary, 27 28 useQueryConstellation, 28 29 useQueryIdentity, 29 30 useQueryProfile, ··· 403 404 ); 404 405 } 405 406 406 - function FeedItemRender({ 407 + export function FeedItemRenderAturiLoader({ 408 + aturi, 409 + listmode, 410 + disableBottomBorder, 411 + }: { 412 + aturi: string; 413 + listmode?: boolean; 414 + disableBottomBorder?: boolean; 415 + }) { 416 + const { data: record } = useQueryArbitrary(aturi); 417 + 418 + if (!record) return; 419 + return ( 420 + <FeedItemRender 421 + listmode={listmode} 422 + feed={record} 423 + disableBottomBorder={disableBottomBorder} 424 + /> 425 + ); 426 + } 427 + 428 + export function FeedItemRender({ 407 429 feed, 408 - listmode 430 + listmode, 431 + disableBottomBorder, 409 432 }: { 410 - feed: { uri: string; cid: string; value: ATPAPI.AppBskyFeedGenerator.Record }; 433 + feed: { uri: string; cid: string; value: any }; 411 434 listmode?: boolean; 435 + disableBottomBorder?: boolean; 412 436 }) { 413 - const name = listmode ? feed.value?.name as string : feed.value?.displayName as string; 437 + const name = listmode 438 + ? (feed.value?.name as string) 439 + : (feed.value?.displayName as string); 414 440 const aturi = new ATPAPI.AtUri(feed.uri); 415 - const {data: identity} = useQueryIdentity(aturi.host); 441 + const { data: identity } = useQueryIdentity(aturi.host); 416 442 const resolvedDid = identity?.did; 417 443 const [imgcdn] = useAtom(imgCDNAtom); 418 444 ··· 422 448 return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 423 449 } 424 450 451 + const { data: likes } = useQueryConstellation( 425 452 // @ts-expect-error overloads sucks 426 - const {data: likes} = useQueryConstellation(!listmode ? { 427 - target: feed.uri, 428 - method: "/links/count", 429 - collection: "app.bsky.feed.like", 430 - path: ".subject.uri" 431 - } : undefined) 453 + !listmode 454 + ? { 455 + target: feed.uri, 456 + method: "/links/count", 457 + collection: "app.bsky.feed.like", 458 + path: ".subject.uri", 459 + } 460 + : undefined 461 + ); 432 462 433 463 return ( 434 - <div className="px-4 py-4 border-b flex flex-col gap-1"> 464 + <Link 465 + className={`px-4 py-4 ${!disableBottomBorder && "border-b"} flex flex-col gap-1`} 466 + to="/profile/$did/feed/$rkey" 467 + params={{ did: aturi.host, rkey: aturi.rkey }} 468 + > 435 469 <div className="flex flex-row gap-3"> 436 470 <div className="min-w-10 min-h-10"> 437 - <img src={getAvatarThumbnailUrl(feed) || defaultpfp} className="h-10 w-10 rounded border" /> 471 + <img 472 + src={getAvatarThumbnailUrl(feed) || defaultpfp} 473 + className="h-10 w-10 rounded border" 474 + /> 438 475 </div> 439 476 <div className="flex flex-col"> 440 477 <span className="">{name}</span> 441 - <span className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">{feed.value.did || aturi.rkey}</span> 478 + <span className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center"> 479 + {feed.value.did || aturi.rkey} 480 + </span> 442 481 </div> 443 482 <div className="flex-1" /> 444 483 {/* <div className="button bg-red-500 rounded-full min-w-[60px]" /> */} 445 484 </div> 446 485 <span className=" text-sm">{feed.value?.description}</span> 447 - {!listmode && (<span className=" text-sm dark:text-gray-400 text-gray-500">Liked by {(likes as unknown as any)?.total as number || 0} users</span>)} 448 - </div> 486 + {!listmode && ( 487 + <span className=" text-sm dark:text-gray-400 text-gray-500"> 488 + Liked by {((likes as unknown as any)?.total as number) || 0} users 489 + </span> 490 + )} 491 + </Link> 449 492 ); 450 493 } 451 - 452 494 453 495 function ListsTab({ did }: { did: string }) { 454 496 useReusableTabScrollRestore(`Profile` + did); ··· 487 529 if (!feed || !feed?.value) return; 488 530 const feedGenRecord = 489 531 feed.value as unknown as ATPAPI.AppBskyFeedGenerator.Record; 490 - return <FeedItemRender listmode={true} feed={feed as any} key={feed.uri} />; 532 + return ( 533 + <FeedItemRender listmode={true} feed={feed as any} key={feed.uri} /> 534 + ); 491 535 })} 492 536 </div> 493 537
+7 -4
src/utils/useQuery.ts
··· 573 573 isAuthed: boolean; 574 574 pdsUrl?: string; 575 575 feedServiceDid?: string; 576 + // todo the hell is a unauthedfeedurl 577 + unauthedfeedurl?: string; 576 578 }) { 577 - const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options; 579 + const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } = options; 578 580 579 581 return queryOptions({ 580 582 queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }], ··· 582 584 queryFn: async ({ pageParam }: QueryFunctionContext): Promise<FeedSkeletonPage> => { 583 585 const cursorParam = pageParam ? `&cursor=${pageParam}` : ""; 584 586 585 - if (isAuthed) { 587 + if (isAuthed && !unauthedfeedurl) { 586 588 if (!agent || !pdsUrl || !feedServiceDid) { 587 589 throw new Error("Missing required info for authenticated feed fetch."); 588 590 } ··· 597 599 if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`); 598 600 return (await res.json()) as FeedSkeletonPage; 599 601 } else { 600 - const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 602 + const url = `https://${unauthedfeedurl ? unauthedfeedurl : "discover.bsky.app"}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 601 603 const res = await fetch(url); 602 604 if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`); 603 605 return (await res.json()) as FeedSkeletonPage; ··· 612 614 isAuthed: boolean; 613 615 pdsUrl?: string; 614 616 feedServiceDid?: string; 617 + unauthedfeedurl?: string; 615 618 }) { 616 619 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 617 620 ··· 622 625 getNextPageParam: (lastPage) => lastPage.cursor as null | undefined, 623 626 staleTime: Infinity, 624 627 refetchOnWindowFocus: false, 625 - enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 628 + enabled: !!options.feedUri && (options.isAuthed ? (!!options.agent && !!options.pdsUrl || !!options.unauthedfeedurl) && !!options.feedServiceDid : true), 626 629 }), queryKey: queryKey}; 627 630 } 628 631