+10
__generated__/index.ts
+10
__generated__/index.ts
···
63
63
export class AppBskyNS {
64
64
_server: Server
65
65
embed: AppBskyEmbedNS
66
+
graph: AppBskyGraphNS
66
67
feed: AppBskyFeedNS
67
68
richtext: AppBskyRichtextNS
68
69
actor: AppBskyActorNS
···
70
71
constructor(server: Server) {
71
72
this._server = server
72
73
this.embed = new AppBskyEmbedNS(server)
74
+
this.graph = new AppBskyGraphNS(server)
73
75
this.feed = new AppBskyFeedNS(server)
74
76
this.richtext = new AppBskyRichtextNS(server)
75
77
this.actor = new AppBskyActorNS(server)
···
77
79
}
78
80
79
81
export class AppBskyEmbedNS {
82
+
_server: Server
83
+
84
+
constructor(server: Server) {
85
+
this._server = server
86
+
}
87
+
}
88
+
89
+
export class AppBskyGraphNS {
80
90
_server: Server
81
91
82
92
constructor(server: Server) {
+27
__generated__/lexicons.ts
+27
__generated__/lexicons.ts
···
447
447
},
448
448
},
449
449
},
450
+
AppBskyGraphFollow: {
451
+
lexicon: 1,
452
+
id: 'app.bsky.graph.follow',
453
+
defs: {
454
+
main: {
455
+
key: 'tid',
456
+
type: 'record',
457
+
record: {
458
+
type: 'object',
459
+
required: ['subject', 'createdAt'],
460
+
properties: {
461
+
subject: {
462
+
type: 'string',
463
+
format: 'did',
464
+
},
465
+
createdAt: {
466
+
type: 'string',
467
+
format: 'datetime',
468
+
},
469
+
},
470
+
},
471
+
description:
472
+
"Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.",
473
+
},
474
+
},
475
+
},
450
476
AppBskyGraphDefs: {
451
477
lexicon: 1,
452
478
id: 'app.bsky.graph.defs',
···
2800
2826
AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia',
2801
2827
AppBskyEmbedVideo: 'app.bsky.embed.video',
2802
2828
AppBskyEmbedExternal: 'app.bsky.embed.external',
2829
+
AppBskyGraphFollow: 'app.bsky.graph.follow',
2803
2830
AppBskyGraphDefs: 'app.bsky.graph.defs',
2804
2831
AppBskyFeedDefs: 'app.bsky.feed.defs',
2805
2832
AppBskyFeedPostgate: 'app.bsky.feed.postgate',
+32
__generated__/types/app/bsky/graph/follow.ts
+32
__generated__/types/app/bsky/graph/follow.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { type ValidationResult, BlobRef } from "npm:@atproto/lexicon"
5
+
import { CID } from "npm:multiformats/cid"
6
+
import { validate as _validate } from '../../../../lexicons.ts'
7
+
import {
8
+
type $Typed,
9
+
is$typed as _is$typed,
10
+
type OmitKey,
11
+
} from '../../../../util.ts'
12
+
13
+
const is$typed = _is$typed,
14
+
validate = _validate
15
+
const id = 'app.bsky.graph.follow'
16
+
17
+
export interface Record {
18
+
$type: 'app.bsky.graph.follow'
19
+
subject: string
20
+
createdAt: string
21
+
[k: string]: unknown
22
+
}
23
+
24
+
const hashRecord = 'main'
25
+
26
+
export function isRecord<V>(v: V) {
27
+
return is$typed(v, id, hashRecord)
28
+
}
29
+
30
+
export function validateRecord<V>(v: V) {
31
+
return validate<Record & V>(v, id, hashRecord, true)
32
+
}
+2
-1
lexicons.json
+2
-1
lexicons.json
+28
lexicons/app/bsky/graph/follow.json
+28
lexicons/app/bsky/graph/follow.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.follow",
4
+
"defs": {
5
+
"main": {
6
+
"key": "tid",
7
+
"type": "record",
8
+
"record": {
9
+
"type": "object",
10
+
"required": [
11
+
"subject",
12
+
"createdAt"
13
+
],
14
+
"properties": {
15
+
"subject": {
16
+
"type": "string",
17
+
"format": "did"
18
+
},
19
+
"createdAt": {
20
+
"type": "string",
21
+
"format": "datetime"
22
+
}
23
+
}
24
+
},
25
+
"description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView."
26
+
}
27
+
}
28
+
}
+108
-2
main.tsx
+108
-2
main.tsx
···
1
1
import { lexicons } from "$lexicon/lexicons.ts";
2
2
import { Record as BskyProfile } from "$lexicon/types/app/bsky/actor/profile.ts";
3
+
import { Record as BskyFollow } from "$lexicon/types/app/bsky/graph/follow.ts";
3
4
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
4
5
import { Record as Profile } from "$lexicon/types/social/grain/actor/profile.ts";
5
6
import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts";
···
131
132
if (!actor) return ctx.next();
132
133
const profile = getActorProfile(actor.did, ctx);
133
134
if (!profile) return ctx.next();
135
+
let follow: WithBffMeta<BskyFollow> | undefined;
136
+
if (ctx.currentUser) {
137
+
follow = getFollow(
138
+
profile.did,
139
+
ctx.currentUser.did,
140
+
ctx,
141
+
);
142
+
}
134
143
ctx.state.meta = [
135
144
{
136
145
title: profile.displayName
···
142
151
if (tab) {
143
152
return ctx.html(
144
153
<ProfilePage
154
+
followUri={follow?.uri}
145
155
loggedInUserDid={ctx.currentUser?.did}
146
156
timelineItems={timelineItems}
147
157
profile={profile}
···
152
162
}
153
163
return ctx.render(
154
164
<ProfilePage
165
+
followUri={follow?.uri}
155
166
loggedInUserDid={ctx.currentUser?.did}
156
167
timelineItems={timelineItems}
157
168
profile={profile}
···
190
201
? galleryLink(ctx.currentUser.handle, galleryRkey)
191
202
: undefined}
192
203
/>,
204
+
);
205
+
}),
206
+
route("/follow/:did", ["POST"], async (_req, params, ctx) => {
207
+
requireAuth(ctx);
208
+
const did = params.did;
209
+
if (!did) return ctx.next();
210
+
const followUri = await ctx.createRecord<BskyFollow>(
211
+
"app.bsky.graph.follow",
212
+
{
213
+
subject: did,
214
+
createdAt: new Date().toISOString(),
215
+
},
216
+
);
217
+
return ctx.html(
218
+
<FollowButton followeeDid={did} followUri={followUri} />,
219
+
);
220
+
}),
221
+
route("/follow/:did/:rkey", ["DELETE"], async (_req, params, ctx) => {
222
+
requireAuth(ctx);
223
+
const did = params.did;
224
+
const rkey = params.rkey;
225
+
if (!did) return ctx.next();
226
+
await ctx.deleteRecord(
227
+
`at://${ctx.currentUser.did}/app.bsky.graph.follow/${rkey}`,
228
+
);
229
+
return ctx.html(
230
+
<FollowButton followeeDid={did} followUri={undefined} />,
193
231
);
194
232
}),
195
233
route("/dialogs/gallery/new", (_req, _params, ctx) => {
···
618
656
actorDid?: string;
619
657
};
620
658
659
+
function getFollow(followeeDid: string, followerDid: string, ctx: BffContext) {
660
+
const { items: [follow] } = ctx.indexService.getRecords<
661
+
WithBffMeta<BskyFollow>
662
+
>(
663
+
"app.bsky.graph.follow",
664
+
{
665
+
where: [
666
+
{
667
+
field: "did",
668
+
equals: followerDid,
669
+
},
670
+
{
671
+
field: "subject",
672
+
equals: followeeDid,
673
+
},
674
+
],
675
+
},
676
+
);
677
+
return follow;
678
+
}
679
+
621
680
function getGalleryItemsAndPhotos(
622
681
ctx: BffContext,
623
682
galleries: WithBffMeta<Gallery>[],
···
1150
1209
);
1151
1210
}
1152
1211
1212
+
function FollowButton({
1213
+
followeeDid,
1214
+
followUri,
1215
+
}: Readonly<{ followeeDid: string; load?: boolean; followUri?: string }>) {
1216
+
const isFollowing = followUri;
1217
+
return (
1218
+
<Button
1219
+
variant="primary"
1220
+
class={cn(
1221
+
"w-full sm:w-fit",
1222
+
isFollowing &&
1223
+
"bg-zinc-200 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-800",
1224
+
)}
1225
+
{...(isFollowing
1226
+
? {
1227
+
children: "Following",
1228
+
"hx-delete": `/follow/${followeeDid}/${new AtUri(followUri).rkey}`,
1229
+
}
1230
+
: {
1231
+
children: (
1232
+
<>
1233
+
<i class="fa-solid fa-plus mr-2" />Follow
1234
+
</>
1235
+
),
1236
+
"hx-post": `/follow/${followeeDid}`,
1237
+
})}
1238
+
hx-trigger="click"
1239
+
hx-target="this"
1240
+
hx-swap="outerHTML"
1241
+
/>
1242
+
);
1243
+
}
1244
+
1153
1245
function ProfilePage({
1246
+
followUri,
1154
1247
loggedInUserDid,
1155
1248
timelineItems,
1156
1249
profile,
1157
1250
selectedTab,
1158
1251
galleries,
1159
1252
}: Readonly<{
1253
+
followUri?: string;
1160
1254
loggedInUserDid?: string;
1161
1255
timelineItems: TimelineItem[];
1162
1256
profile: Un$Typed<ProfileView>;
1163
1257
selectedTab?: string;
1164
1258
galleries?: GalleryView[];
1165
1259
}>) {
1260
+
const isCreator = loggedInUserDid === profile.did;
1166
1261
return (
1167
1262
<div class="px-4 mb-4" id="profile-page">
1168
1263
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4">
···
1172
1267
<p class="text-zinc-600 dark:text-zinc-500">@{profile.handle}</p>
1173
1268
<p class="my-2">{profile.description}</p>
1174
1269
</div>
1175
-
{loggedInUserDid === profile.did
1270
+
{!isCreator && loggedInUserDid
1271
+
? (
1272
+
<div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row">
1273
+
<FollowButton followeeDid={profile.did} followUri={followUri} />
1274
+
</div>
1275
+
)
1276
+
: null}
1277
+
{isCreator
1176
1278
? (
1177
1279
<div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row">
1178
1280
<Button variant="primary" class="w-full sm:w-fit" asChild>
···
2039
2141
async function onSignedIn({ actor, ctx }: onSignedInArgs) {
2040
2142
await ctx.backfillCollections(
2041
2143
[actor.did],
2042
-
[...ctx.cfg.collections!, "app.bsky.actor.profile"],
2144
+
[
2145
+
...ctx.cfg.collections!,
2146
+
"app.bsky.actor.profile",
2147
+
"app.bsky.graph.follow",
2148
+
],
2043
2149
);
2044
2150
2045
2151
const profileResults = ctx.indexService.getRecords<Profile>(