tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
follows and follower routes
rimar1337
4 months ago
74d406fb
2f1eae19
+193
-3
7 changed files
expand all
collapse all
unified
split
src
routeTree.gen.ts
routes
notifications.tsx
profile.$did
feed.$rkey.tsx
followers.tsx
follows.tsx
index.tsx
utils
useQuery.ts
+42
src/routeTree.gen.ts
···
18
import { Route as CallbackIndexRouteImport } from './routes/callback/index'
19
import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
20
import { Route as ProfileDidIndexRouteImport } from './routes/profile.$did/index'
0
0
21
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
22
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
23
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
···
71
path: '/profile/$did/',
72
getParentRoute: () => rootRouteImport,
73
} as any)
0
0
0
0
0
0
0
0
0
0
74
const PathlessLayoutNestedLayoutRouteBRoute =
75
PathlessLayoutNestedLayoutRouteBRouteImport.update({
76
id: '/route-b',
···
127
'/callback': typeof CallbackIndexRoute
128
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
129
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
0
0
130
'/profile/$did': typeof ProfileDidIndexRoute
131
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
132
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
···
144
'/callback': typeof CallbackIndexRoute
145
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
146
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
0
0
147
'/profile/$did': typeof ProfileDidIndexRoute
148
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
149
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
···
164
'/callback/': typeof CallbackIndexRoute
165
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
166
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
0
0
167
'/profile/$did/': typeof ProfileDidIndexRoute
168
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
169
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
···
183
| '/callback'
184
| '/route-a'
185
| '/route-b'
0
0
186
| '/profile/$did'
187
| '/profile/$did/feed/$rkey'
188
| '/profile/$did/post/$rkey'
···
200
| '/callback'
201
| '/route-a'
202
| '/route-b'
0
0
203
| '/profile/$did'
204
| '/profile/$did/feed/$rkey'
205
| '/profile/$did/post/$rkey'
···
219
| '/callback/'
220
| '/_pathlessLayout/_nested-layout/route-a'
221
| '/_pathlessLayout/_nested-layout/route-b'
0
0
222
| '/profile/$did/'
223
| '/profile/$did/feed/$rkey'
224
| '/profile/$did/post/$rkey'
···
236
SearchRoute: typeof SearchRoute
237
SettingsRoute: typeof SettingsRoute
238
CallbackIndexRoute: typeof CallbackIndexRoute
0
0
239
ProfileDidIndexRoute: typeof ProfileDidIndexRoute
240
ProfileDidFeedRkeyRoute: typeof ProfileDidFeedRkeyRoute
241
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren
···
306
preLoaderRoute: typeof ProfileDidIndexRouteImport
307
parentRoute: typeof rootRouteImport
308
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
309
'/_pathlessLayout/_nested-layout/route-b': {
310
id: '/_pathlessLayout/_nested-layout/route-b'
311
path: '/route-b'
···
420
SearchRoute: SearchRoute,
421
SettingsRoute: SettingsRoute,
422
CallbackIndexRoute: CallbackIndexRoute,
0
0
423
ProfileDidIndexRoute: ProfileDidIndexRoute,
424
ProfileDidFeedRkeyRoute: ProfileDidFeedRkeyRoute,
425
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren,
···
18
import { Route as CallbackIndexRouteImport } from './routes/callback/index'
19
import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
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'
23
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
24
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
25
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
···
73
path: '/profile/$did/',
74
getParentRoute: () => rootRouteImport,
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)
86
const PathlessLayoutNestedLayoutRouteBRoute =
87
PathlessLayoutNestedLayoutRouteBRouteImport.update({
88
id: '/route-b',
···
139
'/callback': typeof CallbackIndexRoute
140
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
141
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
142
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
143
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
144
'/profile/$did': typeof ProfileDidIndexRoute
145
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
146
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
···
158
'/callback': typeof CallbackIndexRoute
159
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
160
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
161
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
162
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
163
'/profile/$did': typeof ProfileDidIndexRoute
164
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
165
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
···
180
'/callback/': typeof CallbackIndexRoute
181
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
182
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
183
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
184
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
185
'/profile/$did/': typeof ProfileDidIndexRoute
186
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
187
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
···
201
| '/callback'
202
| '/route-a'
203
| '/route-b'
204
+
| '/profile/$did/followers'
205
+
| '/profile/$did/follows'
206
| '/profile/$did'
207
| '/profile/$did/feed/$rkey'
208
| '/profile/$did/post/$rkey'
···
220
| '/callback'
221
| '/route-a'
222
| '/route-b'
223
+
| '/profile/$did/followers'
224
+
| '/profile/$did/follows'
225
| '/profile/$did'
226
| '/profile/$did/feed/$rkey'
227
| '/profile/$did/post/$rkey'
···
241
| '/callback/'
242
| '/_pathlessLayout/_nested-layout/route-a'
243
| '/_pathlessLayout/_nested-layout/route-b'
244
+
| '/profile/$did/followers'
245
+
| '/profile/$did/follows'
246
| '/profile/$did/'
247
| '/profile/$did/feed/$rkey'
248
| '/profile/$did/post/$rkey'
···
260
SearchRoute: typeof SearchRoute
261
SettingsRoute: typeof SettingsRoute
262
CallbackIndexRoute: typeof CallbackIndexRoute
263
+
ProfileDidFollowersRoute: typeof ProfileDidFollowersRoute
264
+
ProfileDidFollowsRoute: typeof ProfileDidFollowsRoute
265
ProfileDidIndexRoute: typeof ProfileDidIndexRoute
266
ProfileDidFeedRkeyRoute: typeof ProfileDidFeedRkeyRoute
267
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren
···
332
preLoaderRoute: typeof ProfileDidIndexRouteImport
333
parentRoute: typeof rootRouteImport
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
+
}
349
'/_pathlessLayout/_nested-layout/route-b': {
350
id: '/_pathlessLayout/_nested-layout/route-b'
351
path: '/route-b'
···
460
SearchRoute: SearchRoute,
461
SettingsRoute: SettingsRoute,
462
CallbackIndexRoute: CallbackIndexRoute,
463
+
ProfileDidFollowersRoute: ProfileDidFollowersRoute,
464
+
ProfileDidFollowsRoute: ProfileDidFollowsRoute,
465
ProfileDidIndexRoute: ProfileDidIndexRoute,
466
ProfileDidFeedRkeyRoute: ProfileDidFeedRkeyRoute,
467
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren,
+7
-3
src/routes/notifications.tsx
···
132
);
133
}
134
135
-
function FollowsTab() {
136
const { agent } = useAuth();
0
0
0
0
137
const [constellationurl] = useAtom(constellationURLAtom);
138
const infinitequeryresults = useInfiniteQuery({
139
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
140
{
141
constellation: constellationurl,
142
method: "/links",
143
-
target: agent?.did,
144
collection: "app.bsky.graph.follow",
145
path: ".subject",
146
}
147
),
148
-
enabled: !!agent?.did,
149
});
150
151
const {
···
132
);
133
}
134
135
+
export function FollowsTab({did}:{did?:string}) {
136
const { agent } = useAuth();
137
+
const userdidunsafe = did ?? agent?.did;
138
+
const { data: identity} = useQueryIdentity(userdidunsafe);
139
+
const userdid = identity?.did;
140
+
141
const [constellationurl] = useAtom(constellationURLAtom);
142
const infinitequeryresults = useInfiniteQuery({
143
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
144
{
145
constellation: constellationurl,
146
method: "/links",
147
+
target: userdid,
148
collection: "app.bsky.graph.follow",
149
path: ".subject",
150
}
151
),
152
+
enabled: !!userdid,
153
});
154
155
const {
+1
src/routes/profile.$did/feed.$rkey.tsx
···
13
component: FeedRoute,
14
});
15
0
16
function FeedRoute() {
17
const { did, rkey } = Route.useParams();
18
const { agent, status } = useAuth();
···
13
component: FeedRoute,
14
});
15
16
+
// todo: scroll restoration
17
function FeedRoute() {
18
const { did, rkey } = Route.useParams();
19
const { agent, status } = useAuth();
+30
src/routes/profile.$did/followers.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
useInfiniteQueryAuthorFeed,
28
useQueryArbitrary,
29
useQueryConstellation,
0
30
useQueryIdentity,
31
useQueryProfile,
32
} from "~/utils/useQuery";
···
76
const description = profile?.description || "";
77
78
const isReady = !!resolvedDid && !isIdentityLoading && !!profileRecord;
0
0
0
0
0
0
0
0
0
79
80
return (
81
<div className="">
···
149
<div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1">
150
<Mutual targetdidorhandle={did} />
151
{handle}
0
0
0
0
0
152
</div>
153
{description && (
154
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
···
27
useInfiniteQueryAuthorFeed,
28
useQueryArbitrary,
29
useQueryConstellation,
30
+
useQueryConstellationLinksCountDistinctDids,
31
useQueryIdentity,
32
useQueryProfile,
33
} from "~/utils/useQuery";
···
77
const description = profile?.description || "";
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;
89
90
return (
91
<div className="">
···
159
<div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1">
160
<Mutual targetdidorhandle={did} />
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>
167
</div>
168
{description && (
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
gcTime: /*0//*/5 * 60 * 1000,
285
});
286
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
287
export function useQueryConstellation(query: {
288
method: "/links";
289
target: string;
···
284
gcTime: /*0//*/5 * 60 * 1000,
285
});
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
+
306
export function useQueryConstellation(query: {
307
method: "/links";
308
target: string;