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

Configure Feed

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

follows and follower routes

rimar1337 74d406fb 2f1eae19

+193 -3
+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;