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: floating pull-to-refresh pill on all feed tabs

Use a fixed-position pill indicator instead of pushing content down.
Add pull-to-refresh to Following and For You tabs.

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

+56 -36
+24 -20
app/lib/components/molecules/PullToRefresh.svelte
··· 45 45 } 46 46 </script> 47 47 48 - <!-- svelte-ignore a11y_no_static_element_interactions --> 49 - <div 50 - ontouchstart={onTouchStart} 51 - ontouchmove={onTouchMove} 52 - ontouchend={onTouchEnd} 53 - > 54 - 55 48 {#if pullY > 0 || refreshing} 56 - <div class="pull-indicator" style:height="{pullY}px"> 57 - <svg class="pull-spinner" class:spinning={refreshing} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 49 + <div class="pull-pill" style:transform="translateY({pullY - 36}px)" style:opacity={Math.min(pullY / 40, 1)}> 50 + <svg class="pull-spinner" class:spinning={refreshing} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 58 51 <path d="M21 12a9 9 0 1 1-6.219-8.56" /> 59 52 </svg> 60 53 </div> 61 54 {/if} 62 55 63 - {@render children()} 64 - 56 + <!-- svelte-ignore a11y_no_static_element_interactions --> 57 + <div 58 + ontouchstart={onTouchStart} 59 + ontouchmove={onTouchMove} 60 + ontouchend={onTouchEnd} 61 + > 62 + {@render children()} 65 63 </div> 66 64 67 65 <style> 68 - .pull-indicator { 66 + .pull-pill { 67 + position: fixed; 68 + top: 0; 69 + left: 50%; 70 + translate: -50% 0; 71 + z-index: 100; 69 72 display: flex; 70 - align-items: flex-end; 73 + align-items: center; 71 74 justify-content: center; 72 - padding-bottom: 8px; 73 - color: var(--text-muted); 74 - overflow: hidden; 75 - transition: height 0.2s ease; 76 - } 77 - .pull-spinner { 78 - transition: transform 0.2s; 75 + width: 36px; 76 + height: 36px; 77 + border-radius: 50%; 78 + background: var(--bg-elevated); 79 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 80 + color: var(--text-secondary); 81 + pointer-events: none; 82 + transition: transform 0.2s ease, opacity 0.15s ease; 79 83 } 80 84 .pull-spinner.spinning { 81 85 animation: spin 0.8s linear infinite;
+16 -8
app/routes/feeds/following/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { createQuery } from '@tanstack/svelte-query' 2 + import { createQuery, useQueryClient } from '@tanstack/svelte-query' 3 3 import FeedList from '$lib/components/organisms/FeedList.svelte' 4 4 import FeedTabs from '$lib/components/molecules/FeedTabs.svelte' 5 + import PullToRefresh from '$lib/components/molecules/PullToRefresh.svelte' 5 6 import { followingFeedQuery } from '$lib/queries' 6 7 import { viewer } from '$lib/stores' 7 8 import OGMeta from '$lib/components/atoms/OGMeta.svelte' 8 9 10 + const queryClient = useQueryClient() 9 11 const feed = createQuery(() => followingFeedQuery($viewer?.did ?? '')) 12 + 13 + async function refresh() { 14 + await queryClient.invalidateQueries({ queryKey: ['getFeed'] }) 15 + } 10 16 </script> 11 17 12 18 <OGMeta title="Following - grain" /> 13 19 <FeedTabs /> 14 - {#if !$viewer?.did} 15 - <div class="empty">Log in to see galleries from people you follow.</div> 16 - {:else if feed.isLoading} 17 - <FeedList feed="following" params={{ actor: $viewer.did }} skeleton /> 18 - {:else} 19 - <FeedList feed="following" params={{ actor: $viewer.did }} initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 20 - {/if} 20 + <PullToRefresh onRefresh={refresh}> 21 + {#if !$viewer?.did} 22 + <div class="empty">Log in to see galleries from people you follow.</div> 23 + {:else if feed.isLoading} 24 + <FeedList feed="following" params={{ actor: $viewer.did }} skeleton /> 25 + {:else} 26 + <FeedList feed="following" params={{ actor: $viewer.did }} initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 27 + {/if} 28 + </PullToRefresh> 21 29 22 30 <style> 23 31 .empty {
+16 -8
app/routes/feeds/for-you/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { createQuery } from '@tanstack/svelte-query' 2 + import { createQuery, useQueryClient } from '@tanstack/svelte-query' 3 3 import FeedList from '$lib/components/organisms/FeedList.svelte' 4 4 import FeedTabs from '$lib/components/molecules/FeedTabs.svelte' 5 + import PullToRefresh from '$lib/components/molecules/PullToRefresh.svelte' 5 6 import { forYouFeedQuery } from '$lib/queries' 6 7 import { viewer } from '$lib/stores' 7 8 import OGMeta from '$lib/components/atoms/OGMeta.svelte' 8 9 10 + const queryClient = useQueryClient() 9 11 const feed = createQuery(() => forYouFeedQuery($viewer?.did ?? '')) 12 + 13 + async function refresh() { 14 + await queryClient.invalidateQueries({ queryKey: ['getFeed'] }) 15 + } 10 16 </script> 11 17 12 18 <OGMeta title="For You - grain" /> 13 19 <FeedTabs /> 14 - {#if !$viewer?.did} 15 - <div class="empty">Log in to get personalized gallery recommendations.</div> 16 - {:else if feed.isLoading} 17 - <FeedList feed="foryou" params={{ actor: $viewer.did }} skeleton /> 18 - {:else} 19 - <FeedList feed="foryou" params={{ actor: $viewer.did }} initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 20 - {/if} 20 + <PullToRefresh onRefresh={refresh}> 21 + {#if !$viewer?.did} 22 + <div class="empty">Log in to get personalized gallery recommendations.</div> 23 + {:else if feed.isLoading} 24 + <FeedList feed="foryou" params={{ actor: $viewer.did }} skeleton /> 25 + {:else} 26 + <FeedList feed="foryou" params={{ actor: $viewer.did }} initialItems={feed.data?.items ?? []} initialCursor={feed.data?.cursor} /> 27 + {/if} 28 + </PullToRefresh> 21 29 22 30 <style> 23 31 .empty {