+3
-2
netlify/functions/batch-follow-users.ts
+3
-2
netlify/functions/batch-follow-users.ts
···
18
18
// Parse request body
19
19
const body = JSON.parse(event.body || "{}");
20
20
const dids: string[] = body.dids || [];
21
+
const followLexicon: string = body.followLexicon || "app.bsky.graph.follow";
21
22
22
23
if (!Array.isArray(dids) || dids.length === 0) {
23
24
return {
···
65
66
try {
66
67
await agent.api.com.atproto.repo.createRecord({
67
68
repo: userDid,
68
-
collection: "app.bsky.graph.follow",
69
+
collection: followLexicon,
69
70
record: {
70
-
$type: "app.bsky.graph.follow",
71
+
$type: followLexicon,
71
72
subject: did,
72
73
createdAt: new Date().toISOString(),
73
74
},
+6
src/App.tsx
+6
src/App.tsx
···
64
64
totalFound,
65
65
} = useSearch(session);
66
66
67
+
const currentDestinationAppId =
68
+
userSettings.platformDestinations[
69
+
currentPlatform as keyof UserSettings["platformDestinations"]
70
+
];
71
+
67
72
// Follow hook
68
73
const { isFollowing, followSelectedUsers } = useFollow(
69
74
session,
70
75
searchResults,
71
76
setSearchResults,
77
+
currentDestinationAppId,
72
78
);
73
79
74
80
// File upload hook
+6
-2
src/constants/atprotoApps.ts
+6
-2
src/constants/atprotoApps.ts
···
8
8
link: "https://bsky.app/",
9
9
icon: "🦋",
10
10
action: "Follow",
11
+
followLexicon: "app.bsky.graph.follow",
11
12
enabled: true,
12
13
},
13
14
tangled: {
···
17
18
link: "https://tangled.org/",
18
19
icon: "🐑",
19
20
action: "Follow",
20
-
enabled: false, // Not yet integrated
21
+
followLexicon: "sh.tangled.graph.follow",
22
+
enabled: true,
21
23
},
22
24
spark: {
23
25
id: "spark",
···
26
28
link: "https://sprk.so/",
27
29
icon: "✨",
28
30
action: "Follow",
29
-
enabled: false, // Not yet integrated
31
+
followLexicon: "so.sprk.graph.follow",
32
+
enabled: true,
30
33
},
31
34
lists: {
32
35
id: "bsky list",
···
35
38
link: "https://bsky.app/",
36
39
icon: "📃",
37
40
action: "Add to",
41
+
followLexicon: "app.bsky.graph.follow",
38
42
enabled: false, // Not yet implemented
39
43
},
40
44
};
+15
-4
src/constants/platforms.ts
+15
-4
src/constants/platforms.ts
···
1
1
import {
2
2
Twitter,
3
3
Instagram,
4
-
Video,
4
+
Github,
5
+
Youtube,
5
6
Hash,
6
-
Gamepad2,
7
+
Twitch,
8
+
Video,
7
9
LucideIcon,
8
10
} from "lucide-react";
9
11
···
56
58
},
57
59
twitch: {
58
60
name: "Twitch",
59
-
icon: Gamepad2,
61
+
icon: Twitch,
60
62
color: "from-purple-600 to-purple-800",
61
63
accentBg: "bg-purple-600",
62
64
fileHint: "following.json or data export",
···
65
67
},
66
68
youtube: {
67
69
name: "YouTube",
68
-
icon: Video,
70
+
icon: Youtube,
69
71
color: "from-red-600 to-red-700",
70
72
accentBg: "bg-red-600",
71
73
fileHint: "subscriptions.csv or Takeout ZIP",
72
74
enabled: false,
73
75
defaultApp: "bluesky",
76
+
},
77
+
github: {
78
+
name: "Github",
79
+
icon: Github,
80
+
color: "from-red-600 to-red-700",
81
+
accentBg: "bg-red-600",
82
+
fileHint: "subscriptions.csv or Takeout ZIP",
83
+
enabled: false,
84
+
defaultApp: "tangled",
74
85
},
75
86
};
76
87
+20
-3
src/hooks/useFollows.ts
+20
-3
src/hooks/useFollows.ts
···
1
1
import { useState } from "react";
2
2
import { apiClient } from "../lib/apiClient";
3
3
import { FOLLOW_CONFIG } from "../constants/platforms";
4
-
import type { SearchResult, AtprotoSession } from "../types";
4
+
import { ATPROTO_APPS } from "../constants/atprotoApps";
5
+
import type { SearchResult, AtprotoSession, AtprotoAppId } from "../types";
5
6
6
7
export function useFollow(
7
8
session: AtprotoSession | null,
···
9
10
setSearchResults: (
10
11
results: SearchResult[] | ((prev: SearchResult[]) => SearchResult[]),
11
12
) => void,
13
+
destinationAppId: AtprotoAppId,
12
14
) {
13
15
const [isFollowing, setIsFollowing] = useState(false);
14
16
···
17
19
): Promise<void> {
18
20
if (!session || isFollowing) return;
19
21
22
+
// Determine source platform for results
23
+
const followLexicon = ATPROTO_APPS[destinationAppId]?.followLexicon;
24
+
const destinationName =
25
+
ATPROTO_APPS[destinationAppId]?.name || "Undefined App";
26
+
27
+
if (!followLexicon) {
28
+
onUpdate(
29
+
`Error: Invalid destination app or lexicon for ${destinationAppId}`,
30
+
);
31
+
return;
32
+
}
33
+
34
+
// Follow users
20
35
const selectedUsers = searchResults.flatMap((result, resultIndex) =>
21
36
result.atprotoMatches
22
37
.filter((match) => result.selectedMatches?.has(match.did))
···
31
46
}
32
47
33
48
setIsFollowing(true);
34
-
onUpdate(`Following ${selectedUsers.length} users...`);
49
+
onUpdate(
50
+
`Following ${selectedUsers.length} users on ${destinationName}...`,
51
+
);
35
52
let totalFollowed = 0;
36
53
let totalFailed = 0;
37
54
···
43
60
const dids = batch.map((user) => user.did);
44
61
45
62
try {
46
-
const data = await apiClient.batchFollowUsers(dids);
63
+
const data = await apiClient.batchFollowUsers(dids, followLexicon);
47
64
totalFollowed += data.succeeded;
48
65
totalFailed += data.failed;
49
66
+5
-2
src/lib/apiClient/realApiClient.ts
+5
-2
src/lib/apiClient/realApiClient.ts
···
283
283
},
284
284
285
285
// Follow Operations
286
-
async batchFollowUsers(dids: string[]): Promise<{
286
+
async batchFollowUsers(
287
+
dids: string[],
288
+
followLexicon: string,
289
+
): Promise<{
287
290
success: boolean;
288
291
total: number;
289
292
succeeded: number;
···
294
297
method: "POST",
295
298
credentials: "include",
296
299
headers: { "Content-Type": "application/json" },
297
-
body: JSON.stringify({ dids }),
300
+
body: JSON.stringify({ dids, followLexicon }),
298
301
});
299
302
300
303
if (!res.ok) {