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 in actor feed and 404 unknown profiles

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

+41 -22
+26 -20
app/routes/profile/[did]/+page.svelte
··· 98 98 </div> 99 99 {/if} 100 100 101 - {#if lightboxSrc} 102 - <AvatarLightbox src={lightboxSrc} onclose={() => (lightboxSrc = null)} /> 103 - {/if} 101 + {#if profile.isError} 102 + <DetailHeader label="Not Found" /> 103 + <div class="not-found">This profile doesn't exist.</div> 104 + {:else} 105 + {#if lightboxSrc} 106 + <AvatarLightbox src={lightboxSrc} onclose={() => (lightboxSrc = null)} /> 107 + {/if} 104 108 105 - <div class="view-toggle"> 106 - <button class="toggle-btn" class:active={viewMode === 'grid'} onclick={() => (viewMode = 'grid')} aria-label="Grid view"> 107 - <Grid3x3 size={18} /> 108 - </button> 109 - <button class="toggle-btn" class:active={viewMode === 'list'} onclick={() => (viewMode = 'list')} aria-label="List view"> 110 - <List size={18} /> 111 - </button> 112 - </div> 109 + <div class="view-toggle"> 110 + <button class="toggle-btn" class:active={viewMode === 'grid'} onclick={() => (viewMode = 'grid')} aria-label="Grid view"> 111 + <Grid3x3 size={18} /> 112 + </button> 113 + <button class="toggle-btn" class:active={viewMode === 'list'} onclick={() => (viewMode = 'list')} aria-label="List view"> 114 + <List size={18} /> 115 + </button> 116 + </div> 113 117 114 - {#if viewMode === 'grid'} 115 - <GalleryGrid items={feed.data?.items ?? []} loading={feed.isLoading} /> 116 - {:else} 117 - {#if feed.isLoading} 118 - <FeedList feed="actor" params={{ actor: did }} skeleton /> 118 + {#if viewMode === 'grid'} 119 + <GalleryGrid items={feed.data?.items ?? []} loading={feed.isLoading} /> 119 120 {:else} 120 - <FeedList feed="actor" params={{ actor: did }} initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 121 + {#if feed.isLoading} 122 + <FeedList feed="actor" params={{ actor: did }} skeleton /> 123 + {:else} 124 + <FeedList feed="actor" params={{ actor: did }} initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 125 + {/if} 121 126 {/if} 122 - {/if} 123 127 124 - {#if showStoryViewer} 125 - <StoryViewer initialDid={did} onclose={() => (showStoryViewer = false)} /> 128 + {#if showStoryViewer} 129 + <StoryViewer initialDid={did} onclose={() => (showStoryViewer = false)} /> 130 + {/if} 126 131 {/if} 127 132 128 133 <style> ··· 197 202 } 198 203 .toggle-btn:hover { color: var(--text-secondary); } 199 204 .toggle-btn.active { color: var(--text-primary); border-bottom-color: var(--text-primary); } 205 + .not-found { text-align: center; color: var(--text-muted); padding: 48px 16px; font-size: 14px; } 200 206 </style>
+12 -1
server/feeds/actor.ts
··· 10 10 async generate(ctx) { 11 11 const { params, ok, isTakendown } = ctx; 12 12 13 - const actor = params.actor; 13 + let actor = params.actor; 14 14 if (!actor) { 15 15 return ok({ uris: [], cursor: undefined }); 16 + } 17 + 18 + // Resolve handle to DID if needed 19 + if (!actor.startsWith("did:")) { 20 + const rows = (await ctx.db.query( 21 + `SELECT did FROM _repos WHERE handle = $1`, 22 + [actor], 23 + )) as { did: string }[]; 24 + if (rows[0]?.did) { 25 + actor = rows[0].did; 26 + } 16 27 } 17 28 18 29 if (await isTakendown(actor)) {
+3 -1
server/xrpc/getActorProfile.ts
··· 1 - import { defineQuery } from "$hatk"; 1 + import { defineQuery, InvalidRequestError } from "$hatk"; 2 2 import type { GrainActorProfile } from "$hatk"; 3 3 4 4 export default defineQuery("social.grain.unspecced.getActorProfile", async (ctx) => { ··· 13 13 }[]; 14 14 if (rows[0]?.did) { 15 15 actor = rows[0].did; 16 + } else { 17 + throw new InvalidRequestError("Actor not found"); 16 18 } 17 19 } 18 20