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: make home dynamically render first pinned feed

- `/` now renders whatever feed is first in the pinned list
- Add dedicated `/feeds/recent` route so recent is accessible when not first
- Update FeedTabs, SidebarRight, MobileDrawer to link first pin to `/`
- Migrate old `path: "/"` to `/feeds/recent` for existing users
- Redirect to custom feed path if non-core feed is pinned first

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

+112 -20
+7 -3
app/lib/components/molecules/FeedTabs.svelte
··· 11 11 12 12 <div class="center-header"> 13 13 <div class="feed-tabs"> 14 - {#each tabFeeds as feed (feed.id)} 14 + {#each tabFeeds as feed, i (feed.id)} 15 + {@const href = i === 0 ? '/' : feed.path} 16 + {@const isActive = i === 0 17 + ? page.url.pathname === '/' 18 + : page.url.pathname + page.url.search === feed.path || page.url.pathname === feed.path} 15 19 <a 16 20 class="feed-tab" 17 - class:active={page.url.pathname + page.url.search === feed.path || page.url.pathname === feed.path} 18 - href={feed.path} 21 + class:active={isActive} 22 + {href} 19 23 >{feed.label}</a> 20 24 {/each} 21 25 </div>
+2 -2
app/lib/components/organisms/MobileDrawer.svelte
··· 45 45 </div> 46 46 {/if} 47 47 48 - {#each $pinnedFeeds as feed (feed.id)} 48 + {#each $pinnedFeeds as feed, i (feed.id)} 49 49 {@const Icon = feedIcon(feed)} 50 - <button class="drawer-link" onclick={() => nav(feed.path)}> 50 + <button class="drawer-link" onclick={() => nav(i === 0 ? '/' : feed.path)}> 51 51 <span class="drawer-link-icon"><Icon size={18} /></span> {feed.type === 'hashtag' ? feed.label.replace(/^#/, '') : feed.label} 52 52 </button> 53 53 {/each}
+6 -3
app/lib/components/organisms/SidebarRight.svelte
··· 23 23 24 24 <div class="sidebar-card"> 25 25 <div class="sidebar-card-header">Feeds</div> 26 - {#each $pinnedFeeds as feed (feed.id)} 26 + {#each $pinnedFeeds as feed, i (feed.id)} 27 + {@const href = i === 0 ? '/' : feed.path} 27 28 <a 28 - href={feed.path} 29 + {href} 29 30 class="sidebar-link" 30 - class:active={page.url.pathname + page.url.search === feed.path || page.url.pathname === feed.path} 31 + class:active={i === 0 32 + ? page.url.pathname === '/' 33 + : page.url.pathname + page.url.search === feed.path || page.url.pathname === feed.path} 31 34 > 32 35 <span class="sidebar-link-icon"><svelte:component this={feedIcon(feed)} size={16} /></span> 33 36 <span class="sidebar-link-label">{feed.type === 'hashtag' ? feed.label.replace(/^#/, '') : feed.label}</span>
+4 -2
app/lib/preferences.ts
··· 10 10 } 11 11 12 12 export const DEFAULT_PINNED: PinnedFeed[] = [ 13 - { id: "recent", label: "Recent", type: "feed", path: "/" }, 13 + { id: "recent", label: "Recent", type: "feed", path: "/feeds/recent" }, 14 14 { id: "following", label: "Following", type: "feed", path: "/feeds/following" }, 15 15 { id: "foryou", label: "For You", type: "feed", path: "/feeds/for-you" }, 16 16 ]; ··· 49 49 export function loadPreferences(prefs: Record<string, unknown> | null): void { 50 50 if (!prefs) return; 51 51 if (Array.isArray(prefs.pinnedFeeds)) { 52 - const valid = prefs.pinnedFeeds.filter(isValidFeed); 52 + const valid = prefs.pinnedFeeds.filter(isValidFeed).map((f) => 53 + f.id === "recent" && f.path === "/" ? { ...f, path: "/feeds/recent" } : f 54 + ); 53 55 if (valid.length > 0) pinnedFeeds.set(valid); 54 56 } 55 57 if (typeof prefs.includeExif === "boolean") includeExif.set(prefs.includeExif);
+30 -10
app/routes/+page.svelte
··· 6 6 import PullToRefresh from '$lib/components/molecules/PullToRefresh.svelte' 7 7 import StoryViewer from '$lib/components/organisms/StoryViewer.svelte' 8 8 import StoryCreate from '$lib/components/molecules/StoryCreate.svelte' 9 - import { recentFeedQuery } from '$lib/queries' 9 + import { recentFeedQuery, followingFeedQuery, forYouFeedQuery } from '$lib/queries' 10 10 import { pinnedFeeds } from '$lib/preferences' 11 + import { viewer } from '$lib/stores' 11 12 import { goto } from '$app/navigation' 12 13 import OGMeta from '$lib/components/atoms/OGMeta.svelte' 13 14 14 - // Redirect to first pinned feed if "recent" isn't pinned 15 + const CORE_FEEDS = new Set(['recent', 'following', 'foryou']) 16 + const first = $derived($pinnedFeeds[0]) 17 + const firstFeed = $derived(first?.id ?? 'recent') 18 + const needsActor = $derived(firstFeed === 'following' || firstFeed === 'foryou') 19 + const actorDid = $derived($viewer?.did ?? '') 20 + 21 + // If first pinned feed is a custom feed (camera, location, hashtag), redirect to it 15 22 $effect(() => { 16 - const feeds = $pinnedFeeds 17 - const hasRecent = feeds.some((f) => f.id === 'recent') 18 - if (!hasRecent && feeds.length > 0) { 19 - goto(feeds[0].path, { replaceState: true }) 23 + if (first && !CORE_FEEDS.has(first.id)) { 24 + goto(first.path, { replaceState: true }) 20 25 } 21 26 }) 22 27 23 28 const queryClient = useQueryClient() 24 - const feed = createQuery(() => recentFeedQuery()) 29 + const feed = createQuery(() => { 30 + if (firstFeed === 'following') return followingFeedQuery(actorDid) 31 + if (firstFeed === 'foryou') return forYouFeedQuery(actorDid) 32 + return recentFeedQuery() 33 + }) 25 34 26 35 let showViewer = $state(false) 27 36 let viewerDid = $state('') ··· 58 67 59 68 <PullToRefresh onRefresh={refresh}> 60 69 <StoryStrip onCreateStory={openCreate} onViewStory={openViewer} /> 61 - {#if feed.isLoading} 62 - <FeedList feed="recent" skeleton /> 70 + {#if needsActor && !actorDid} 71 + <div class="empty">Log in to see this feed.</div> 72 + {:else if feed.isLoading} 73 + <FeedList feed={firstFeed} params={needsActor ? { actor: actorDid } : undefined} skeleton /> 63 74 {:else} 64 - <FeedList feed="recent" initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 75 + <FeedList feed={firstFeed} params={needsActor ? { actor: actorDid } : undefined} initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 65 76 {/if} 66 77 </PullToRefresh> 67 78 ··· 72 83 {#if showCreate} 73 84 <StoryCreate onclose={closeCreate} /> 74 85 {/if} 86 + 87 + <style> 88 + .empty { 89 + text-align: center; 90 + color: var(--text-muted); 91 + padding: 48px 16px; 92 + font-size: 14px; 93 + } 94 + </style>
+63
app/routes/feeds/recent/+page.svelte
··· 1 + <script lang="ts"> 2 + import { createQuery, useQueryClient } from '@tanstack/svelte-query' 3 + import FeedList from '$lib/components/organisms/FeedList.svelte' 4 + import FeedTabs from '$lib/components/molecules/FeedTabs.svelte' 5 + import StoryStrip from '$lib/components/molecules/StoryStrip.svelte' 6 + import PullToRefresh from '$lib/components/molecules/PullToRefresh.svelte' 7 + import StoryViewer from '$lib/components/organisms/StoryViewer.svelte' 8 + import StoryCreate from '$lib/components/molecules/StoryCreate.svelte' 9 + import { recentFeedQuery } from '$lib/queries' 10 + import OGMeta from '$lib/components/atoms/OGMeta.svelte' 11 + 12 + const queryClient = useQueryClient() 13 + const feed = createQuery(() => recentFeedQuery()) 14 + 15 + let showViewer = $state(false) 16 + let viewerDid = $state('') 17 + let showCreate = $state(false) 18 + 19 + async function refresh() { 20 + await Promise.all([ 21 + queryClient.invalidateQueries({ queryKey: ['getFeed'] }), 22 + queryClient.invalidateQueries({ queryKey: ['storyAuthors'] }), 23 + ]) 24 + } 25 + 26 + function openViewer(did: string) { 27 + viewerDid = did 28 + showViewer = true 29 + } 30 + 31 + function closeViewer() { 32 + showViewer = false 33 + } 34 + 35 + function openCreate() { 36 + showCreate = true 37 + } 38 + 39 + function closeCreate() { 40 + showCreate = false 41 + } 42 + </script> 43 + 44 + <OGMeta title="grain" /> 45 + 46 + <FeedTabs /> 47 + 48 + <PullToRefresh onRefresh={refresh}> 49 + <StoryStrip onCreateStory={openCreate} onViewStory={openViewer} /> 50 + {#if feed.isLoading} 51 + <FeedList feed="recent" skeleton /> 52 + {:else} 53 + <FeedList feed="recent" initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 54 + {/if} 55 + </PullToRefresh> 56 + 57 + {#if showViewer} 58 + <StoryViewer initialDid={viewerDid} onclose={closeViewer} /> 59 + {/if} 60 + 61 + {#if showCreate} 62 + <StoryCreate onclose={closeCreate} /> 63 + {/if}