an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
93
fork

Configure Feed

Select the types of activity you want to include in your feed.

feed page with bad hack

rimar1337 ca2ab70a 0883da1a

+199 -25
+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