grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

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

fix: show handle instead of DID in followers/following/suggestions

Extract lookupHandles helper to resolve handles from _repos for users
without a grain profile. Applied to getFollowers, getFollowing,
getKnownFollowers, getSuggestedFollows, and DRYed up getNotifications.

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

+35 -13
+17
server/helpers/lookupHandles.ts
··· 1 + /** 2 + * Look up handles from _repos for a list of DIDs. 3 + * Used as a fallback when users don't have a grain profile. 4 + */ 5 + export async function lookupHandles( 6 + db: { query: (sql: string, params?: unknown[]) => Promise<unknown[]> }, 7 + dids: string[], 8 + ): Promise<Map<string, string>> { 9 + const map = new Map<string, string>(); 10 + if (dids.length === 0) return map; 11 + const rows = (await db.query( 12 + `SELECT did, handle FROM _repos WHERE did IN (${dids.map((_, i) => `$${i + 1}`).join(",")})`, 13 + dids, 14 + )) as { did: string; handle: string }[]; 15 + for (const row of rows) map.set(row.did, row.handle); 16 + return map; 17 + }
+4 -1
server/xrpc/getFollowers.ts
··· 1 1 import { defineQuery, type GrainActorProfile } from "$hatk"; 2 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 2 3 3 4 export default defineQuery("social.grain.unspecced.getFollowers", async (ctx) => { 4 5 const { ok, params, lookup, blobUrl, packCursor, unpackCursor } = ctx; ··· 33 34 34 35 const viewerFollowMap = new Map(viewerFollows.map((r) => [r.subject, r.uri])); 35 36 37 + const handleMap = await lookupHandles(ctx.db, dids); 38 + 36 39 const items = dids.map((did) => { 37 40 const p = profiles.get(did); 38 41 return { 39 42 did, 40 - handle: p?.handle ?? did, 43 + handle: p?.handle ?? handleMap.get(did) ?? did, 41 44 displayName: p?.value.displayName, 42 45 description: p?.value.description, 43 46 avatar: p ? blobUrl(did, p.value.avatar, "avatar") : undefined,
+4 -1
server/xrpc/getFollowing.ts
··· 1 1 import { defineQuery, type GrainActorProfile } from "$hatk"; 2 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 2 3 3 4 export default defineQuery("social.grain.unspecced.getFollowing", async (ctx) => { 4 5 const { ok, params, lookup, blobUrl, packCursor, unpackCursor } = ctx; ··· 33 34 34 35 const viewerFollowMap = new Map(viewerFollows.map((r) => [r.subject, r.uri])); 35 36 37 + const handleMap = await lookupHandles(ctx.db, dids); 38 + 36 39 const items = dids.map((did) => { 37 40 const p = profiles.get(did); 38 41 return { 39 42 did, 40 - handle: p?.handle ?? did, 43 + handle: p?.handle ?? handleMap.get(did) ?? did, 41 44 displayName: p?.value.displayName, 42 45 description: p?.value.description, 43 46 avatar: p ? blobUrl(did, p.value.avatar, "avatar") : undefined,
+4 -1
server/xrpc/getKnownFollowers.ts
··· 2 2 // GET /xrpc/social.grain.unspecced.getKnownFollowers?actor=did:...&viewer=did:... 3 3 4 4 import { defineQuery, type GrainActorProfile } from "$hatk"; 5 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 5 6 6 7 export default defineQuery("social.grain.unspecced.getKnownFollowers", async (ctx) => { 7 8 const { ok, params, lookup, blobUrl } = ctx; ··· 24 25 25 26 const profiles = await lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids); 26 27 28 + const handleMap = await lookupHandles(ctx.db, dids); 29 + 27 30 const items = dids.map((did) => { 28 31 const p = profiles.get(did); 29 32 return { 30 33 did, 31 - handle: p?.handle ?? did, 34 + handle: p?.handle ?? handleMap.get(did) ?? did, 32 35 displayName: p?.value.displayName, 33 36 description: p?.value.description, 34 37 avatar: p ? blobUrl(did, p.value.avatar, "avatar") : undefined,
+2 -9
server/xrpc/getNotifications.ts
··· 1 1 import { defineQuery, InvalidRequestError } from "$hatk"; 2 2 import type { GrainActorProfile, Photo, Gallery } from "$hatk"; 3 3 import { views } from "$hatk"; 4 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 4 5 5 6 function blockMuteNotifFilter(didCol = "did"): string { 6 7 return ` ··· 163 164 const dids = [...new Set(items.map((r) => r.did))]; 164 165 const profiles = await lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids); 165 166 166 - // Look up handles from _repos for all notification authors 167 - const handleMap = new Map<string, string>(); 168 - if (dids.length > 0) { 169 - const handleRows = (await db.query( 170 - `SELECT did, handle FROM _repos WHERE did IN (${dids.map((_, i) => `$${i + 1}`).join(",")})`, 171 - dids, 172 - )) as { did: string; handle: string }[]; 173 - for (const row of handleRows) handleMap.set(row.did, row.handle); 174 - } 167 + const handleMap = await lookupHandles(db, dids); 175 168 176 169 // Hydrate galleries for thumbnails 177 170 const galleryUris = [...new Set(items.map((r) => r.gallery_uri).filter(Boolean))] as string[];
+4 -1
server/xrpc/getSuggestedFollows.ts
··· 1 1 import { defineQuery, type GrainActorProfile } from "$hatk"; 2 2 import { blockMuteFilter } from "../filters/blockMute.ts"; 3 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 3 4 4 5 export default defineQuery("social.grain.unspecced.getSuggestedFollows", async (ctx) => { 5 6 const { ok, params, lookup, blobUrl, db } = ctx; ··· 70 71 } 71 72 } 72 73 74 + const handleMap = await lookupHandles(db, dids); 75 + 73 76 const items = dids.map((did) => { 74 77 const p = profiles.get(did); 75 78 return { 76 79 did, 77 - handle: p?.handle ?? did, 80 + handle: p?.handle ?? handleMap.get(did) ?? did, 78 81 displayName: p?.value.displayName, 79 82 description: p?.value.description, 80 83 avatar: p ? blobUrl(did, p.value.avatar, "avatar") : undefined,