WIP PWA for Grain
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add getFollowing API method

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+94
+94
src/services/grain-api.js
··· 275 275 }; 276 276 } 277 277 278 + async getFollowing(handle, { first = 20, after = null } = {}) { 279 + // First get the user's DID 280 + const profileQuery = ` 281 + query GetDid($handle: String!) { 282 + socialGrainActorProfile(first: 1, where: { actorHandle: { eq: $handle } }) { 283 + edges { 284 + node { did } 285 + } 286 + } 287 + } 288 + `; 289 + const profileResponse = await this.#execute(profileQuery, { handle }); 290 + const did = profileResponse.data?.socialGrainActorProfile?.edges?.[0]?.node?.did; 291 + 292 + if (!did) { 293 + return { profiles: [], pageInfo: { hasNextPage: false, endCursor: null }, totalCount: 0 }; 294 + } 295 + 296 + // Query following (people this user follows) 297 + const followQuery = ` 298 + query GetFollowing($did: String!, $first: Int, $after: String) { 299 + socialGrainGraphFollow( 300 + first: $first 301 + after: $after 302 + where: { did: { eq: $did } } 303 + sortBy: [{ field: createdAt, direction: DESC }] 304 + ) { 305 + edges { 306 + node { 307 + subject 308 + } 309 + } 310 + pageInfo { 311 + hasNextPage 312 + endCursor 313 + } 314 + totalCount 315 + } 316 + } 317 + `; 318 + 319 + const followResponse = await this.#execute(followQuery, { did, first, after }); 320 + const connection = followResponse.data?.socialGrainGraphFollow; 321 + const subjectDids = connection?.edges?.map(e => e.node.subject).filter(Boolean) || []; 322 + 323 + if (subjectDids.length === 0) { 324 + return { 325 + profiles: [], 326 + pageInfo: connection?.pageInfo || { hasNextPage: false, endCursor: null }, 327 + totalCount: connection?.totalCount || 0 328 + }; 329 + } 330 + 331 + // Fetch profiles for the subject DIDs 332 + const profilesQuery = ` 333 + query GetProfiles($dids: [String!]!) { 334 + appBskyActorProfile(where: { did: { in: $dids } }) { 335 + edges { 336 + node { 337 + did 338 + actorHandle 339 + displayName 340 + description 341 + avatar { url } 342 + } 343 + } 344 + } 345 + } 346 + `; 347 + 348 + const profilesResponse = await this.#execute(profilesQuery, { dids: subjectDids }); 349 + const profilesMap = new Map(); 350 + profilesResponse.data?.appBskyActorProfile?.edges?.forEach(edge => { 351 + const node = edge.node; 352 + profilesMap.set(node.did, { 353 + handle: node.actorHandle, 354 + displayName: node.displayName || '', 355 + description: node.description || '', 356 + avatarUrl: node.avatar?.url || '' 357 + }); 358 + }); 359 + 360 + // Return profiles in order, with fallback for missing profiles 361 + const profiles = subjectDids.map(did => 362 + profilesMap.get(did) || { handle: did, displayName: '', description: '', avatarUrl: '' } 363 + ); 364 + 365 + return { 366 + profiles, 367 + pageInfo: connection?.pageInfo || { hasNextPage: false, endCursor: null }, 368 + totalCount: connection?.totalCount || 0 369 + }; 370 + } 371 + 278 372 async getGalleryDetail(handle, rkey) { 279 373 const query = ` 280 374 query GetGalleryDetail($handle: String!, $rkey: String!) {