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