ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

you can now follow on spark and tangled !!!!!! (ps it will duplicate follows that's a different thing i've to work on ok)

Changed files
+56 -13
netlify
src
+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
··· 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
··· 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
··· 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
··· 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
··· 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) {
+1
src/types/settings.ts
··· 6 6 description: string; 7 7 link: string; 8 8 icon: string; 9 + followLexicon: string; 9 10 action: string; 10 11 enabled: boolean; 11 12 }