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

follows and follower routes

rimar1337 74d406fb 2f1eae19

+42
src/routeTree.gen.ts
··· 18 18 import { Route as CallbackIndexRouteImport } from './routes/callback/index' 19 19 import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout' 20 20 import { Route as ProfileDidIndexRouteImport } from './routes/profile.$did/index' 21 + import { Route as ProfileDidFollowsRouteImport } from './routes/profile.$did/follows' 22 + import { Route as ProfileDidFollowersRouteImport } from './routes/profile.$did/followers' 21 23 import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' 22 24 import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' 23 25 import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey' ··· 71 73 path: '/profile/$did/', 72 74 getParentRoute: () => rootRouteImport, 73 75 } as any) 76 + const ProfileDidFollowsRoute = ProfileDidFollowsRouteImport.update({ 77 + id: '/profile/$did/follows', 78 + path: '/profile/$did/follows', 79 + getParentRoute: () => rootRouteImport, 80 + } as any) 81 + const ProfileDidFollowersRoute = ProfileDidFollowersRouteImport.update({ 82 + id: '/profile/$did/followers', 83 + path: '/profile/$did/followers', 84 + getParentRoute: () => rootRouteImport, 85 + } as any) 74 86 const PathlessLayoutNestedLayoutRouteBRoute = 75 87 PathlessLayoutNestedLayoutRouteBRouteImport.update({ 76 88 id: '/route-b', ··· 127 139 '/callback': typeof CallbackIndexRoute 128 140 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 129 141 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 142 + '/profile/$did/followers': typeof ProfileDidFollowersRoute 143 + '/profile/$did/follows': typeof ProfileDidFollowsRoute 130 144 '/profile/$did': typeof ProfileDidIndexRoute 131 145 '/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute 132 146 '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren ··· 144 158 '/callback': typeof CallbackIndexRoute 145 159 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 146 160 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 161 + '/profile/$did/followers': typeof ProfileDidFollowersRoute 162 + '/profile/$did/follows': typeof ProfileDidFollowsRoute 147 163 '/profile/$did': typeof ProfileDidIndexRoute 148 164 '/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute 149 165 '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren ··· 164 180 '/callback/': typeof CallbackIndexRoute 165 181 '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 166 182 '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 183 + '/profile/$did/followers': typeof ProfileDidFollowersRoute 184 + '/profile/$did/follows': typeof ProfileDidFollowsRoute 167 185 '/profile/$did/': typeof ProfileDidIndexRoute 168 186 '/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute 169 187 '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren ··· 183 201 | '/callback' 184 202 | '/route-a' 185 203 | '/route-b' 204 + | '/profile/$did/followers' 205 + | '/profile/$did/follows' 186 206 | '/profile/$did' 187 207 | '/profile/$did/feed/$rkey' 188 208 | '/profile/$did/post/$rkey' ··· 200 220 | '/callback' 201 221 | '/route-a' 202 222 | '/route-b' 223 + | '/profile/$did/followers' 224 + | '/profile/$did/follows' 203 225 | '/profile/$did' 204 226 | '/profile/$did/feed/$rkey' 205 227 | '/profile/$did/post/$rkey' ··· 219 241 | '/callback/' 220 242 | '/_pathlessLayout/_nested-layout/route-a' 221 243 | '/_pathlessLayout/_nested-layout/route-b' 244 + | '/profile/$did/followers' 245 + | '/profile/$did/follows' 222 246 | '/profile/$did/' 223 247 | '/profile/$did/feed/$rkey' 224 248 | '/profile/$did/post/$rkey' ··· 236 260 SearchRoute: typeof SearchRoute 237 261 SettingsRoute: typeof SettingsRoute 238 262 CallbackIndexRoute: typeof CallbackIndexRoute 263 + ProfileDidFollowersRoute: typeof ProfileDidFollowersRoute 264 + ProfileDidFollowsRoute: typeof ProfileDidFollowsRoute 239 265 ProfileDidIndexRoute: typeof ProfileDidIndexRoute 240 266 ProfileDidFeedRkeyRoute: typeof ProfileDidFeedRkeyRoute 241 267 ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren ··· 306 332 preLoaderRoute: typeof ProfileDidIndexRouteImport 307 333 parentRoute: typeof rootRouteImport 308 334 } 335 + '/profile/$did/follows': { 336 + id: '/profile/$did/follows' 337 + path: '/profile/$did/follows' 338 + fullPath: '/profile/$did/follows' 339 + preLoaderRoute: typeof ProfileDidFollowsRouteImport 340 + parentRoute: typeof rootRouteImport 341 + } 342 + '/profile/$did/followers': { 343 + id: '/profile/$did/followers' 344 + path: '/profile/$did/followers' 345 + fullPath: '/profile/$did/followers' 346 + preLoaderRoute: typeof ProfileDidFollowersRouteImport 347 + parentRoute: typeof rootRouteImport 348 + } 309 349 '/_pathlessLayout/_nested-layout/route-b': { 310 350 id: '/_pathlessLayout/_nested-layout/route-b' 311 351 path: '/route-b' ··· 420 460 SearchRoute: SearchRoute, 421 461 SettingsRoute: SettingsRoute, 422 462 CallbackIndexRoute: CallbackIndexRoute, 463 + ProfileDidFollowersRoute: ProfileDidFollowersRoute, 464 + ProfileDidFollowsRoute: ProfileDidFollowsRoute, 423 465 ProfileDidIndexRoute: ProfileDidIndexRoute, 424 466 ProfileDidFeedRkeyRoute: ProfileDidFeedRkeyRoute, 425 467 ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren,
+7 -3
src/routes/notifications.tsx
··· 132 132 ); 133 133 } 134 134 135 - function FollowsTab() { 135 + export function FollowsTab({did}:{did?:string}) { 136 136 const { agent } = useAuth(); 137 + const userdidunsafe = did ?? agent?.did; 138 + const { data: identity} = useQueryIdentity(userdidunsafe); 139 + const userdid = identity?.did; 140 + 137 141 const [constellationurl] = useAtom(constellationURLAtom); 138 142 const infinitequeryresults = useInfiniteQuery({ 139 143 ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 140 144 { 141 145 constellation: constellationurl, 142 146 method: "/links", 143 - target: agent?.did, 147 + target: userdid, 144 148 collection: "app.bsky.graph.follow", 145 149 path: ".subject", 146 150 } 147 151 ), 148 - enabled: !!agent?.did, 152 + enabled: !!userdid, 149 153 }); 150 154 151 155 const {
+1
src/routes/profile.$did/feed.$rkey.tsx
··· 13 13 component: FeedRoute, 14 14 }); 15 15 16 + // todo: scroll restoration 16 17 function FeedRoute() { 17 18 const { did, rkey } = Route.useParams(); 18 19 const { agent, status } = useAuth();
+30
src/routes/profile.$did/followers.tsx
··· 1 + import { createFileRoute } from "@tanstack/react-router"; 2 + 3 + import { Header } from "~/components/Header"; 4 + 5 + import { FollowsTab } from "../notifications"; 6 + 7 + export const Route = createFileRoute("/profile/$did/followers")({ 8 + component: RouteComponent, 9 + }); 10 + 11 + // todo: scroll restoration 12 + function RouteComponent() { 13 + const params = Route.useParams(); 14 + 15 + return ( 16 + <div> 17 + <Header 18 + title={"Followers"} 19 + backButtonCallback={() => { 20 + if (window.history.length > 1) { 21 + window.history.back(); 22 + } else { 23 + window.location.assign("/"); 24 + } 25 + }} 26 + /> 27 + <FollowsTab did={params.did} /> 28 + </div> 29 + ); 30 + }
+79
src/routes/profile.$did/follows.tsx
··· 1 + import * as ATPAPI from "@atproto/api" 2 + import { createFileRoute } from '@tanstack/react-router' 3 + import React from 'react'; 4 + 5 + import { Header } from '~/components/Header'; 6 + import { useReusableTabScrollRestore } from '~/components/ReusableTabRoute'; 7 + import { useInfiniteQueryAuthorFeed, useQueryIdentity } from '~/utils/useQuery'; 8 + 9 + import { EmptyState, ErrorState, LoadingState, NotificationItem } from '../notifications'; 10 + 11 + export const Route = createFileRoute('/profile/$did/follows')({ 12 + component: RouteComponent, 13 + }) 14 + 15 + // todo: scroll restoration 16 + function RouteComponent() { 17 + const params = Route.useParams(); 18 + return ( 19 + <div> 20 + <Header 21 + title={"Follows"} 22 + backButtonCallback={() => { 23 + if (window.history.length > 1) { 24 + window.history.back(); 25 + } else { 26 + window.location.assign("/"); 27 + } 28 + }} 29 + /> 30 + <Follows did={params.did}/> 31 + </div> 32 + ); 33 + } 34 + 35 + function Follows({did}:{did:string}) { 36 + const {data: identity} = useQueryIdentity(did); 37 + const infinitequeryresults = useInfiniteQueryAuthorFeed(identity?.did, identity?.pds, "app.bsky.graph.follow"); 38 + 39 + const { 40 + data: infiniteFollowsData, 41 + fetchNextPage, 42 + hasNextPage, 43 + isFetchingNextPage, 44 + isLoading, 45 + isError, 46 + error, 47 + } = infinitequeryresults; 48 + 49 + const followsAturis = React.useMemo( 50 + () => infiniteFollowsData?.pages.flatMap((page) => page.records) ?? [], 51 + [infiniteFollowsData] 52 + ); 53 + 54 + useReusableTabScrollRestore("Notifications"); 55 + 56 + if (isLoading) return <LoadingState text="Loading follows..." />; 57 + if (isError) return <ErrorState error={error} />; 58 + 59 + if (!followsAturis?.length) return <EmptyState text="No follows yet." />; 60 + 61 + return ( 62 + <> 63 + {followsAturis.map((m) => { 64 + const record = m.value as unknown as ATPAPI.AppBskyGraphFollow.Record; 65 + return <NotificationItem key={record.subject} notification={record.subject} /> 66 + })} 67 + 68 + {hasNextPage && ( 69 + <button 70 + onClick={() => fetchNextPage()} 71 + disabled={isFetchingNextPage} 72 + className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50" 73 + > 74 + {isFetchingNextPage ? "Loading..." : "Load More"} 75 + </button> 76 + )} 77 + </> 78 + ); 79 + }
+15
src/routes/profile.$did/index.tsx
··· 27 27 useInfiniteQueryAuthorFeed, 28 28 useQueryArbitrary, 29 29 useQueryConstellation, 30 + useQueryConstellationLinksCountDistinctDids, 30 31 useQueryIdentity, 31 32 useQueryProfile, 32 33 } from "~/utils/useQuery"; ··· 76 77 const description = profile?.description || ""; 77 78 78 79 const isReady = !!resolvedDid && !isIdentityLoading && !!profileRecord; 80 + 81 + const resultwhateversure = useQueryConstellationLinksCountDistinctDids(resolvedDid ? { 82 + method: "/links/count/distinct-dids", 83 + collection: "app.bsky.graph.follow", 84 + target: resolvedDid, 85 + path: ".subject" 86 + } : undefined) 87 + 88 + const followercount = resultwhateversure?.data?.total; 79 89 80 90 return ( 81 91 <div className=""> ··· 149 159 <div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1"> 150 160 <Mutual targetdidorhandle={did} /> 151 161 {handle} 162 + </div> 163 + <div className="flex flex-row gap-2 text-md text-gray-500 dark:text-gray-400 mb-2"> 164 + <Link to="/profile/$did/followers" params={{did: did}}>{followercount && (<span className="mr-1 text-gray-900 dark:text-gray-200 font-medium">{followercount}</span>)}Followers</Link> 165 + - 166 + <Link to="/profile/$did/follows" params={{did: did}}>Follows</Link> 152 167 </div> 153 168 {description && ( 154 169 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
+19
src/utils/useQuery.ts
··· 284 284 gcTime: /*0//*/5 * 60 * 1000, 285 285 }); 286 286 } 287 + // todo do more of these instead of overloads since overloads sucks so much apparently 288 + export function useQueryConstellationLinksCountDistinctDids(query?: { 289 + method: "/links/count/distinct-dids"; 290 + target: string; 291 + collection: string; 292 + path: string; 293 + cursor?: string; 294 + }): UseQueryResult<linksCountResponse, Error> | undefined { 295 + //if (!query) return; 296 + const [constellationurl] = useAtom(constellationURLAtom) 297 + const queryres = useQuery( 298 + constructConstellationQuery(query && {constellation: constellationurl, ...query}) 299 + ) as unknown as UseQueryResult<linksCountResponse, Error>; 300 + if (!query) { 301 + return undefined as undefined; 302 + } 303 + return queryres as UseQueryResult<linksCountResponse, Error>; 304 + } 305 + 287 306 export function useQueryConstellation(query: { 288 307 method: "/links"; 289 308 target: string;