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: resolve handles from _repos in all remaining endpoints

Apply lookupHandles fallback to galleries hydration, stories hydration,
getGalleryThread, getStory, getStoryAuthors, and getActorProfile so
users without a grain profile show their handle instead of a raw DID.

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

+38 -22
+5 -3
server/hydrate/galleries.ts
··· 3 3 import type { PhotoView, GalleryView, ExifView } from "$hatk"; 4 4 import type { BaseContext, Row } from "$hatk"; 5 5 import { countComments } from "./comments.ts"; 6 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 6 7 7 8 const SCALE = 1_000_000; 8 9 ··· 100 101 for (const row of favRows) viewerFavs.set(row.subject, row.uri); 101 102 } 102 103 103 - const [profiles, favCounts, commentCounts, labelsByUri, galleryItemRows, crossPosts] = 104 + const [profiles, handleMap, favCounts, commentCounts, labelsByUri, galleryItemRows, crossPosts] = 104 105 await Promise.all([ 105 106 ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids), 107 + lookupHandles(ctx.db, dids), 106 108 galleryUris.length > 0 107 109 ? ( 108 110 ctx.db.query( ··· 219 221 ? views.grainActorDefsProfileView({ 220 222 cid: author.cid, 221 223 did: author.did, 222 - handle: author.handle ?? author.did, 224 + handle: author.handle ?? handleMap.get(author.did) ?? author.did, 223 225 displayName: author.value.displayName, 224 226 avatar: ctx.blobUrl(author.did, author.value.avatar) ?? undefined, 225 227 }) 226 228 : views.grainActorDefsProfileView({ 227 229 cid: item.cid, 228 230 did: item.did, 229 - handle: item.handle ?? item.did, 231 + handle: handleMap.get(item.did) ?? item.did, 230 232 }), 231 233 items: photoViews, 232 234 ...(item.value.location
+6 -4
server/hydrate/stories.ts
··· 2 2 import type { GrainActorProfile, Story, Label, Row, BaseContext } from "$hatk"; 3 3 import { HIDE_LABELS } from "../labels/_hidden.ts"; 4 4 import { lookupCrossPosts } from "./galleries.ts"; 5 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 5 6 6 7 export type StoryRow = { 7 8 uri: string; ··· 20 21 */ 21 22 export async function hydrateStories(ctx: BaseContext, actor: string, rows: StoryRow[]) { 22 23 // Resolve author profile 23 - const profiles = await ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", [ 24 - actor, 24 + const [profiles, handleMap] = await Promise.all([ 25 + ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", [actor]), 26 + lookupHandles(ctx.db, [actor]), 25 27 ]); 26 28 const author = profiles.get(actor); 27 29 const profileView = author 28 30 ? views.grainActorDefsProfileView({ 29 31 cid: author.cid, 30 32 did: author.did, 31 - handle: author.handle ?? author.did, 33 + handle: author.handle ?? handleMap.get(author.did) ?? author.did, 32 34 displayName: author.value.displayName, 33 35 avatar: ctx.blobUrl(author.did, author.value.avatar) ?? undefined, 34 36 }) 35 37 : views.grainActorDefsProfileView({ 36 38 cid: "", 37 39 did: actor, 38 - handle: actor, 40 + handle: handleMap.get(actor) ?? actor, 39 41 }); 40 42 41 43 // Hydrate external labels
+7 -5
server/xrpc/getActorProfile.ts
··· 95 95 const followersCount = followerCounts.get(actor) || 0; 96 96 const followsCount = followsCounts.get(actor) || 0; 97 97 98 + const repoRows = (await ctx.db.query("SELECT handle FROM _repos WHERE did = $1", [actor])) as { 99 + handle: string; 100 + }[]; 101 + const repoHandle = repoRows[0]?.handle; 102 + 98 103 if (!profile) { 99 - const repos = (await ctx.db.query("SELECT handle FROM _repos WHERE did = $1", [actor])) as { 100 - handle: string; 101 - }[]; 102 104 return ok({ 103 105 cid: "", 104 106 did: actor, 105 - handle: repos[0]?.handle || actor, 107 + handle: repoHandle || actor, 106 108 galleryCount, 107 109 followersCount, 108 110 followsCount, ··· 113 115 return ok({ 114 116 cid: profile.cid, 115 117 did: profile.did, 116 - handle: profile.handle ?? profile.did, 118 + handle: profile.handle ?? repoHandle ?? profile.did, 117 119 displayName: profile.value.displayName, 118 120 description: profile.value.description, 119 121 avatar: blobUrl(profile.did, profile.value.avatar, "avatar"),
+7 -3
server/xrpc/getGalleryThread.ts
··· 3 3 import { views } from "$hatk"; 4 4 import { NOT_ORPHANED } from "../hydrate/comments.ts"; 5 5 import { blockFilter } from "../filters/blockMute.ts"; 6 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 6 7 7 8 export default defineQuery("social.grain.unspecced.getGalleryThread", async (ctx) => { 8 9 const { ok, params, db, lookup, blobUrl, getRecords, viewer } = ctx; ··· 62 63 63 64 // Hydrate author profiles 64 65 const dids = [...new Set(items.map((r) => r.did))]; 65 - const profiles = await lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids); 66 + const [profiles, handleMap] = await Promise.all([ 67 + lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids), 68 + lookupHandles(db, dids), 69 + ]); 66 70 67 71 // Check which comment authors the viewer has muted 68 72 let mutedDids = new Set<string>(); ··· 97 101 ? views.grainActorDefsProfileView({ 98 102 cid: author.cid, 99 103 did: author.did, 100 - handle: author.handle ?? author.did, 104 + handle: author.handle ?? handleMap.get(author.did) ?? author.did, 101 105 displayName: author.value.displayName, 102 106 avatar: blobUrl(author.did, author.value.avatar) ?? undefined, 103 107 }) 104 108 : views.grainActorDefsProfileView({ 105 109 cid: row.cid, 106 110 did: row.did, 107 - handle: row.did, 111 + handle: handleMap.get(row.did) ?? row.did, 108 112 }), 109 113 ...(focusPhoto 110 114 ? {
+6 -4
server/xrpc/getStory.ts
··· 2 2 import { views } from "$hatk"; 3 3 import type { GrainActorProfile, Story, Label } from "$hatk"; 4 4 import { lookupCrossPosts } from "../hydrate/galleries.ts"; 5 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 5 6 6 7 export default defineQuery("social.grain.unspecced.getStory", async (ctx) => { 7 8 const { db, ok } = ctx; ··· 28 29 if (!row) return ok({}); 29 30 30 31 // Resolve author profile 31 - const profiles = await ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", [ 32 - row.did, 32 + const [profiles, handleMap] = await Promise.all([ 33 + ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", [row.did]), 34 + lookupHandles(db, [row.did]), 33 35 ]); 34 36 const author = profiles.get(row.did); 35 37 const profileView = author 36 38 ? views.grainActorDefsProfileView({ 37 39 cid: author.cid, 38 40 did: author.did, 39 - handle: author.handle ?? author.did, 41 + handle: author.handle ?? handleMap.get(author.did) ?? author.did, 40 42 displayName: author.value.displayName, 41 43 avatar: ctx.blobUrl(author.did, author.value.avatar) ?? undefined, 42 44 }) 43 45 : views.grainActorDefsProfileView({ 44 46 cid: "", 45 47 did: row.did, 46 - handle: row.did, 48 + handle: handleMap.get(row.did) ?? row.did, 47 49 }); 48 50 49 51 let blobRef: any;
+7 -3
server/xrpc/getStoryAuthors.ts
··· 3 3 import type { GrainActorProfile } from "$hatk"; 4 4 import { hideLabelsFilter } from "../labels/_hidden.ts"; 5 5 import { blockMuteFilter } from "../filters/blockMute.ts"; 6 + import { lookupHandles } from "../helpers/lookupHandles.ts"; 6 7 7 8 const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; 8 9 ··· 29 30 )) as { did: string; story_count: number; latest_at: string }[]; 30 31 31 32 const dids = rows.map((r) => r.did); 32 - const profiles = await ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids); 33 + const [profiles, handleMap] = await Promise.all([ 34 + ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids), 35 + lookupHandles(db, dids), 36 + ]); 33 37 34 38 const authors = rows.map((row) => { 35 39 const author = profiles.get(row.did); ··· 38 42 ? views.grainActorDefsProfileView({ 39 43 cid: author.cid, 40 44 did: author.did, 41 - handle: author.handle ?? author.did, 45 + handle: author.handle ?? handleMap.get(author.did) ?? author.did, 42 46 displayName: author.value.displayName, 43 47 avatar: ctx.blobUrl(author.did, author.value.avatar) ?? undefined, 44 48 }) 45 49 : views.grainActorDefsProfileView({ 46 50 cid: "", 47 51 did: row.did, 48 - handle: row.did, 52 + handle: handleMap.get(row.did) ?? row.did, 49 53 }), 50 54 storyCount: row.story_count, 51 55 latestAt: row.latest_at,