+42
src/routeTree.gen.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
+
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
+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
+1
src/routes/profile.$did/feed.$rkey.tsx
+30
src/routes/profile.$did/followers.tsx
+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
+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
+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
+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;