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 // 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
··· 64 totalFound, 65 } = useSearch(session); 66 67 // Follow hook 68 const { isFollowing, followSelectedUsers } = useFollow( 69 session, 70 searchResults, 71 setSearchResults, 72 ); 73 74 // File upload hook
··· 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
··· 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
··· 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
··· 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
··· 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) {
+1
src/types/settings.ts
··· 6 description: string; 7 link: string; 8 icon: string; 9 action: string; 10 enabled: boolean; 11 }
··· 6 description: string; 7 link: string; 8 icon: string; 9 + followLexicon: string; 10 action: string; 11 enabled: boolean; 12 }