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
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
21
+
import { Route as ProfileDidFollowsRouteImport } from './routes/profile.$did/follows'
22
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
76
+
const ProfileDidFollowsRoute = ProfileDidFollowsRouteImport.update({
77
77
+
id: '/profile/$did/follows',
78
78
+
path: '/profile/$did/follows',
79
79
+
getParentRoute: () => rootRouteImport,
80
80
+
} as any)
81
81
+
const ProfileDidFollowersRoute = ProfileDidFollowersRouteImport.update({
82
82
+
id: '/profile/$did/followers',
83
83
+
path: '/profile/$did/followers',
84
84
+
getParentRoute: () => rootRouteImport,
85
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
142
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
143
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
161
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
162
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
183
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
184
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
204
+
| '/profile/$did/followers'
205
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
223
+
| '/profile/$did/followers'
224
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
244
+
| '/profile/$did/followers'
245
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
263
+
ProfileDidFollowersRoute: typeof ProfileDidFollowersRoute
264
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
335
+
'/profile/$did/follows': {
336
336
+
id: '/profile/$did/follows'
337
337
+
path: '/profile/$did/follows'
338
338
+
fullPath: '/profile/$did/follows'
339
339
+
preLoaderRoute: typeof ProfileDidFollowsRouteImport
340
340
+
parentRoute: typeof rootRouteImport
341
341
+
}
342
342
+
'/profile/$did/followers': {
343
343
+
id: '/profile/$did/followers'
344
344
+
path: '/profile/$did/followers'
345
345
+
fullPath: '/profile/$did/followers'
346
346
+
preLoaderRoute: typeof ProfileDidFollowersRouteImport
347
347
+
parentRoute: typeof rootRouteImport
348
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
463
+
ProfileDidFollowersRoute: ProfileDidFollowersRoute,
464
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
135
-
function FollowsTab() {
135
135
+
export function FollowsTab({did}:{did?:string}) {
136
136
const { agent } = useAuth();
137
137
+
const userdidunsafe = did ?? agent?.did;
138
138
+
const { data: identity} = useQueryIdentity(userdidunsafe);
139
139
+
const userdid = identity?.did;
140
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
143
-
target: agent?.did,
147
147
+
target: userdid,
144
148
collection: "app.bsky.graph.follow",
145
149
path: ".subject",
146
150
}
147
151
),
148
148
-
enabled: !!agent?.did,
152
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
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
1
+
import { createFileRoute } from "@tanstack/react-router";
2
2
+
3
3
+
import { Header } from "~/components/Header";
4
4
+
5
5
+
import { FollowsTab } from "../notifications";
6
6
+
7
7
+
export const Route = createFileRoute("/profile/$did/followers")({
8
8
+
component: RouteComponent,
9
9
+
});
10
10
+
11
11
+
// todo: scroll restoration
12
12
+
function RouteComponent() {
13
13
+
const params = Route.useParams();
14
14
+
15
15
+
return (
16
16
+
<div>
17
17
+
<Header
18
18
+
title={"Followers"}
19
19
+
backButtonCallback={() => {
20
20
+
if (window.history.length > 1) {
21
21
+
window.history.back();
22
22
+
} else {
23
23
+
window.location.assign("/");
24
24
+
}
25
25
+
}}
26
26
+
/>
27
27
+
<FollowsTab did={params.did} />
28
28
+
</div>
29
29
+
);
30
30
+
}
+79
src/routes/profile.$did/follows.tsx
···
1
1
+
import * as ATPAPI from "@atproto/api"
2
2
+
import { createFileRoute } from '@tanstack/react-router'
3
3
+
import React from 'react';
4
4
+
5
5
+
import { Header } from '~/components/Header';
6
6
+
import { useReusableTabScrollRestore } from '~/components/ReusableTabRoute';
7
7
+
import { useInfiniteQueryAuthorFeed, useQueryIdentity } from '~/utils/useQuery';
8
8
+
9
9
+
import { EmptyState, ErrorState, LoadingState, NotificationItem } from '../notifications';
10
10
+
11
11
+
export const Route = createFileRoute('/profile/$did/follows')({
12
12
+
component: RouteComponent,
13
13
+
})
14
14
+
15
15
+
// todo: scroll restoration
16
16
+
function RouteComponent() {
17
17
+
const params = Route.useParams();
18
18
+
return (
19
19
+
<div>
20
20
+
<Header
21
21
+
title={"Follows"}
22
22
+
backButtonCallback={() => {
23
23
+
if (window.history.length > 1) {
24
24
+
window.history.back();
25
25
+
} else {
26
26
+
window.location.assign("/");
27
27
+
}
28
28
+
}}
29
29
+
/>
30
30
+
<Follows did={params.did}/>
31
31
+
</div>
32
32
+
);
33
33
+
}
34
34
+
35
35
+
function Follows({did}:{did:string}) {
36
36
+
const {data: identity} = useQueryIdentity(did);
37
37
+
const infinitequeryresults = useInfiniteQueryAuthorFeed(identity?.did, identity?.pds, "app.bsky.graph.follow");
38
38
+
39
39
+
const {
40
40
+
data: infiniteFollowsData,
41
41
+
fetchNextPage,
42
42
+
hasNextPage,
43
43
+
isFetchingNextPage,
44
44
+
isLoading,
45
45
+
isError,
46
46
+
error,
47
47
+
} = infinitequeryresults;
48
48
+
49
49
+
const followsAturis = React.useMemo(
50
50
+
() => infiniteFollowsData?.pages.flatMap((page) => page.records) ?? [],
51
51
+
[infiniteFollowsData]
52
52
+
);
53
53
+
54
54
+
useReusableTabScrollRestore("Notifications");
55
55
+
56
56
+
if (isLoading) return <LoadingState text="Loading follows..." />;
57
57
+
if (isError) return <ErrorState error={error} />;
58
58
+
59
59
+
if (!followsAturis?.length) return <EmptyState text="No follows yet." />;
60
60
+
61
61
+
return (
62
62
+
<>
63
63
+
{followsAturis.map((m) => {
64
64
+
const record = m.value as unknown as ATPAPI.AppBskyGraphFollow.Record;
65
65
+
return <NotificationItem key={record.subject} notification={record.subject} />
66
66
+
})}
67
67
+
68
68
+
{hasNextPage && (
69
69
+
<button
70
70
+
onClick={() => fetchNextPage()}
71
71
+
disabled={isFetchingNextPage}
72
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
73
+
>
74
74
+
{isFetchingNextPage ? "Loading..." : "Load More"}
75
75
+
</button>
76
76
+
)}
77
77
+
</>
78
78
+
);
79
79
+
}
+15
src/routes/profile.$did/index.tsx
···
27
27
useInfiniteQueryAuthorFeed,
28
28
useQueryArbitrary,
29
29
useQueryConstellation,
30
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
80
+
81
81
+
const resultwhateversure = useQueryConstellationLinksCountDistinctDids(resolvedDid ? {
82
82
+
method: "/links/count/distinct-dids",
83
83
+
collection: "app.bsky.graph.follow",
84
84
+
target: resolvedDid,
85
85
+
path: ".subject"
86
86
+
} : undefined)
87
87
+
88
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
162
+
</div>
163
163
+
<div className="flex flex-row gap-2 text-md text-gray-500 dark:text-gray-400 mb-2">
164
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
165
+
-
166
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
287
+
// todo do more of these instead of overloads since overloads sucks so much apparently
288
288
+
export function useQueryConstellationLinksCountDistinctDids(query?: {
289
289
+
method: "/links/count/distinct-dids";
290
290
+
target: string;
291
291
+
collection: string;
292
292
+
path: string;
293
293
+
cursor?: string;
294
294
+
}): UseQueryResult<linksCountResponse, Error> | undefined {
295
295
+
//if (!query) return;
296
296
+
const [constellationurl] = useAtom(constellationURLAtom)
297
297
+
const queryres = useQuery(
298
298
+
constructConstellationQuery(query && {constellation: constellationurl, ...query})
299
299
+
) as unknown as UseQueryResult<linksCountResponse, Error>;
300
300
+
if (!query) {
301
301
+
return undefined as undefined;
302
302
+
}
303
303
+
return queryres as UseQueryResult<linksCountResponse, Error>;
304
304
+
}
305
305
+
287
306
export function useQueryConstellation(query: {
288
307
method: "/links";
289
308
target: string;