your personal website on atproto - mirror blento.app

add bluesky feed

+218 -13
+7 -1
src/lib/atproto/methods.ts
··· 450 client?: Client; 451 filter?: string; 452 limit?: number; 453 }) { 454 data ??= {}; 455 data.did ??= user.did; ··· 461 }); 462 463 const response = await data.client.get('app.bsky.feed.getAuthorFeed', { 464 - params: { actor: data.did, filter: data.filter ?? 'posts_with_media', limit: data.limit || 100 } 465 }); 466 467 if (!response.ok) return;
··· 450 client?: Client; 451 filter?: string; 452 limit?: number; 453 + cursor?: string; 454 }) { 455 data ??= {}; 456 data.did ??= user.did; ··· 462 }); 463 464 const response = await data.client.get('app.bsky.feed.getAuthorFeed', { 465 + params: { 466 + actor: data.did, 467 + filter: data.filter ?? 'posts_with_media', 468 + limit: data.limit || 100, 469 + cursor: data.cursor 470 + } 471 }); 472 473 if (!response.ok) return;
+96
src/lib/cards/BlueskyFeedCard/BlueskyFeedCard.svelte
···
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { BlueskyPost } from '../../components/bluesky-post'; 5 + import { getAdditionalUserData, getDidContext } from '$lib/website/context'; 6 + import { resolveHandle, getAuthorFeed } from '$lib/atproto/methods'; 7 + import type { Did, Handle } from '@atcute/lexicons'; 8 + 9 + let { item }: { item: Item } = $props(); 10 + 11 + const data = getAdditionalUserData(); 12 + const did = getDidContext(); 13 + 14 + // svelte-ignore state_referenced_locally 15 + const lookupKey = (item.cardData.did as string) || (item.cardData.handle as string) || did; 16 + // svelte-ignore state_referenced_locally 17 + const preloaded = (data[item.cardType] as Record<string, any>)?.[lookupKey]; 18 + let feed: any[] | undefined = $state(preloaded?.feed); 19 + let cursor = $state<string | undefined>(preloaded?.cursor); 20 + // svelte-ignore state_referenced_locally 21 + let targetDid = $state<Did | undefined>(item.cardData.did ? (item.cardData.did as Did) : did); 22 + let loading = $state(false); 23 + 24 + async function loadMore() { 25 + if (loading || !cursor || !targetDid) return; 26 + loading = true; 27 + try { 28 + const result = await getAuthorFeed({ 29 + did: targetDid, 30 + filter: 'posts_no_replies', 31 + limit: 20, 32 + cursor 33 + }); 34 + if (result?.feed) { 35 + feed = [...(feed ?? []), ...result.feed]; 36 + } 37 + cursor = result?.cursor; 38 + } finally { 39 + loading = false; 40 + } 41 + } 42 + 43 + function handleScroll(e: Event) { 44 + const el = e.currentTarget as HTMLElement; 45 + if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) { 46 + loadMore(); 47 + } 48 + } 49 + 50 + onMount(async () => { 51 + if (feed) return; 52 + 53 + // Resolve handle to DID if needed 54 + if (item.cardData.handle && !item.cardData.did) { 55 + try { 56 + targetDid = await resolveHandle({ handle: item.cardData.handle as Handle }); 57 + } catch { 58 + // fall back to context did 59 + } 60 + } 61 + 62 + try { 63 + const result = await getAuthorFeed({ 64 + did: targetDid, 65 + filter: 'posts_no_replies', 66 + limit: 20 67 + }); 68 + feed = result?.feed; 69 + cursor = result?.cursor; 70 + } catch { 71 + // failed to fetch feed 72 + } 73 + }); 74 + </script> 75 + 76 + <div class="flex h-full flex-col overflow-x-hidden overflow-y-auto p-3" onscroll={handleScroll}> 77 + {#if feed && feed.length > 0} 78 + <div class={[item.cardData.label ? 'pt-8' : '']}> 79 + {#each feed as feedItem, i (feedItem.post?.uri ?? i)} 80 + <BlueskyPost showAvatar compact feedViewPost={feedItem.post} /> 81 + {#if i < feed.length - 1} 82 + <div 83 + class="border-base-200 dark:border-base-800 accent:border-base-50/5 my-3 border-t" 84 + ></div> 85 + {/if} 86 + {/each} 87 + </div> 88 + {#if loading} 89 + <div class="text-base-400 py-2 text-center text-xs">Loading...</div> 90 + {/if} 91 + {:else} 92 + <div class="text-base-500 flex h-full items-center justify-center text-sm"> 93 + No posts to show 94 + </div> 95 + {/if} 96 + </div>
+99
src/lib/cards/BlueskyFeedCard/index.ts
···
··· 1 + import type { CardDefinition } from '../types'; 2 + import BlueskyFeedCard from './BlueskyFeedCard.svelte'; 3 + import { getAuthorFeed, resolveHandle } from '$lib/atproto/methods'; 4 + import type { Did, Handle } from '@atcute/lexicons'; 5 + import { isDid } from '@atcute/lexicons/syntax'; 6 + 7 + export const BlueskyFeedCardDefinition = { 8 + type: 'blueskyFeed', 9 + contentComponent: BlueskyFeedCard, 10 + createNew: (card) => { 11 + card.cardType = 'blueskyFeed'; 12 + card.w = 4; 13 + card.mobileW = 8; 14 + card.h = 6; 15 + card.mobileH = 10; 16 + }, 17 + 18 + onUrlHandler: (url, item) => { 19 + const match = url.match(/bsky\.app\/profile\/([^/]+)\/?$/); 20 + if (!match) return null; 21 + 22 + const actor = match[1]; 23 + if (isDid(actor)) { 24 + item.cardData.did = actor; 25 + } else { 26 + item.cardData.handle = actor; 27 + } 28 + 29 + item.w = 4; 30 + item.mobileW = 8; 31 + item.h = 6; 32 + item.mobileH = 10; 33 + 34 + return item; 35 + }, 36 + urlHandlerPriority: 1, 37 + 38 + loadData: async (items, { did }) => { 39 + // Map from original key (handle or did from cardData) to resolved DID 40 + const keysToDid = new Map<string, Did>(); 41 + 42 + for (const item of items) { 43 + if (item.cardData?.did) { 44 + const d = item.cardData.did as Did; 45 + keysToDid.set(d, d); 46 + } else if (item.cardData?.handle) { 47 + try { 48 + const resolved = await resolveHandle({ handle: item.cardData.handle as Handle }); 49 + keysToDid.set(item.cardData.handle as string, resolved); 50 + } catch { 51 + // skip unresolvable handles 52 + } 53 + } else { 54 + keysToDid.set(did, did); 55 + } 56 + } 57 + 58 + const result: Record<string, unknown> = {}; 59 + const fetched = new Set<string>(); 60 + 61 + await Promise.all( 62 + Array.from(keysToDid.entries()).map(async ([key, fetchDid]) => { 63 + try { 64 + let feedData; 65 + if (!fetched.has(fetchDid)) { 66 + feedData = await getAuthorFeed({ 67 + did: fetchDid, 68 + filter: 'posts_no_replies', 69 + limit: 20 70 + }); 71 + result[fetchDid] = feedData; 72 + fetched.add(fetchDid); 73 + } else { 74 + feedData = result[fetchDid]; 75 + } 76 + // Also store under original key so the component can look it up 77 + if (key !== fetchDid) { 78 + result[key] = feedData; 79 + } 80 + } catch { 81 + // skip failed fetches 82 + } 83 + }) 84 + ); 85 + 86 + return result; 87 + }, 88 + 89 + minW: 4, 90 + minH: 4, 91 + 92 + name: 'Bluesky Feed', 93 + 94 + canHaveLabel: true, 95 + 96 + keywords: ['bsky', 'atproto', 'feed', 'timeline', 'posts'], 97 + groups: ['Social'], 98 + icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M6.335 3.836a47.2 47.2 0 0 1 5.354 4.94c.088.093.165.18.232.26a18 18 0 0 1 .232-.26 47.2 47.2 0 0 1 5.355-4.94C18.882 2.687 21.46 1.37 22.553 2.483c.986 1.003.616 4.264.305 5.857-.567 2.902-2.018 4.274-3.703 4.542 2.348.386 4.678 1.96 3.13 5.602-1.97 4.636-7.065 1.763-9.795-.418a3 3 0 0 1-.18-.15 3 3 0 0 1-.18.15c-2.73 2.18-7.825 5.054-9.795.418-1.548-3.643.782-5.216 3.13-5.602C3.98 12.631 2.529 11.26 1.962 8.357c-.311-1.593-.681-4.854.305-5.857C3.361 1.37 5.94 2.687 6.335 3.836Z" /></svg>` 99 + } as CardDefinition & { type: 'blueskyFeed' };
+3 -1
src/lib/cards/BlueskyPostCard/BlueskyPostCard.svelte
··· 39 40 <div class="flex h-full flex-col justify-center-safe overflow-y-scroll p-4"> 41 {#if post} 42 - <BlueskyPost showLogo feedViewPost={post}></BlueskyPost> 43 <div class="h-4 w-full"></div> 44 {:else} 45 <p class="text-base-600 dark:text-base-400 text-center">A bluesky post will appear here</p>
··· 39 40 <div class="flex h-full flex-col justify-center-safe overflow-y-scroll p-4"> 41 {#if post} 42 + <div class={[item.cardData.label ? 'pt-8' : '']}> 43 + <BlueskyPost showLogo feedViewPost={post}></BlueskyPost> 44 + </div> 45 <div class="h-4 w-full"></div> 46 {:else} 47 <p class="text-base-600 dark:text-base-400 text-center">A bluesky post will appear here</p>
+2
src/lib/cards/BlueskyPostCard/index.ts
··· 64 minW: 4, 65 name: 'Bluesky Post', 66 67 keywords: ['skeet', 'bsky', 'atproto', 'post'], 68 groups: ['Social'], 69 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" /></svg>`
··· 64 minW: 4, 65 name: 'Bluesky Post', 66 67 + canHaveLabel: true, 68 + 69 keywords: ['skeet', 'bsky', 'atproto', 'post'], 70 groups: ['Social'], 71 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" /></svg>`
+3 -6
src/lib/cards/LatestBlueskyPostCard/LatestBlueskyPostCard.svelte
··· 29 </script> 30 31 <div class="flex h-full flex-col justify-center-safe overflow-y-scroll p-4"> 32 - <div 33 - class="accent:text-base-950 bg-base-200/50 dark:bg-base-700/30 mx-auto mb-6 w-fit rounded-xl p-1 px-2 text-2xl font-semibold" 34 - > 35 - My latest bluesky post 36 - </div> 37 {#if feed?.[0]?.post} 38 - <BlueskyPost showLogo feedViewPost={feed?.[0].post}></BlueskyPost> 39 <div class="h-4 w-full"></div> 40 {:else} 41 Your latest bluesky post will appear here.
··· 29 </script> 30 31 <div class="flex h-full flex-col justify-center-safe overflow-y-scroll p-4"> 32 {#if feed?.[0]?.post} 33 + <div class={[item.cardData.label ? "pt-8" : '']}> 34 + <BlueskyPost showLogo feedViewPost={feed?.[0].post}></BlueskyPost> 35 + </div> 36 <div class="h-4 w-full"></div> 37 {:else} 38 Your latest bluesky post will appear here.
+2
src/lib/cards/LatestBlueskyPostCard/index.ts
··· 21 22 name: 'Latest Bluesky Post', 23 24 keywords: ['bsky', 'atproto', 'recent', 'feed'], 25 groups: ['Social'], 26 icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M6.335 3.836a47.2 47.2 0 0 1 5.354 4.94c.088.093.165.18.232.26a18 18 0 0 1 .232-.26 47.2 47.2 0 0 1 5.355-4.94C18.882 2.687 21.46 1.37 22.553 2.483c.986 1.003.616 4.264.305 5.857-.567 2.902-2.018 4.274-3.703 4.542 2.348.386 4.678 1.96 3.13 5.602-1.97 4.636-7.065 1.763-9.795-.418a3 3 0 0 1-.18-.15 3 3 0 0 1-.18.15c-2.73 2.18-7.825 5.054-9.795.418-1.548-3.643.782-5.216 3.13-5.602C3.98 12.631 2.529 11.26 1.962 8.357c-.311-1.593-.681-4.854.305-5.857C3.361 1.37 5.94 2.687 6.335 3.836Z" /></svg>`
··· 21 22 name: 'Latest Bluesky Post', 23 24 + canHaveLabel: true, 25 + 26 keywords: ['bsky', 'atproto', 'recent', 'feed'], 27 groups: ['Social'], 28 icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M6.335 3.836a47.2 47.2 0 0 1 5.354 4.94c.088.093.165.18.232.26a18 18 0 0 1 .232-.26 47.2 47.2 0 0 1 5.355-4.94C18.882 2.687 21.46 1.37 22.553 2.483c.986 1.003.616 4.264.305 5.857-.567 2.902-2.018 4.274-3.703 4.542 2.348.386 4.678 1.96 3.13 5.602-1.97 4.636-7.065 1.763-9.795-.418a3 3 0 0 1-.18-.15 3 3 0 0 1-.18.15c-2.73 2.18-7.825 5.054-9.795.418-1.548-3.643.782-5.216 3.13-5.602C3.98 12.631 2.529 11.26 1.962 8.357c-.311-1.593-.681-4.854.305-5.857C3.361 1.37 5.94 2.687 6.335 3.836Z" /></svg>`
+2
src/lib/cards/index.ts
··· 3 import { BigSocialCardDefinition } from './BigSocialCard'; 4 import { BlueskyMediaCardDefinition } from './BlueskyMediaCard'; 5 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 6 import { LatestBlueskyPostCardDefinition } from './LatestBlueskyPostCard'; 7 import { DinoGameCardDefinition } from './GameCards/DinoGameCard'; 8 import { EmbedCardDefinition } from './EmbedCard'; ··· 50 YoutubeCardDefinition, 51 BlueskyPostCardDefinition, 52 LatestBlueskyPostCardDefinition, 53 LivestreamCardDefitition, 54 LivestreamEmbedCardDefitition, 55 // EmbedCardDefinition,
··· 3 import { BigSocialCardDefinition } from './BigSocialCard'; 4 import { BlueskyMediaCardDefinition } from './BlueskyMediaCard'; 5 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 6 + import { BlueskyFeedCardDefinition } from './BlueskyFeedCard'; 7 import { LatestBlueskyPostCardDefinition } from './LatestBlueskyPostCard'; 8 import { DinoGameCardDefinition } from './GameCards/DinoGameCard'; 9 import { EmbedCardDefinition } from './EmbedCard'; ··· 51 YoutubeCardDefinition, 52 BlueskyPostCardDefinition, 53 LatestBlueskyPostCardDefinition, 54 + BlueskyFeedCardDefinition, 55 LivestreamCardDefitition, 56 LivestreamEmbedCardDefitition, 57 // EmbedCardDefinition,
+2 -2
src/lib/components/post/embeds/QuotedPost.svelte
··· 9 </script> 10 11 <div 12 - class="border-base-300 dark:border-base-600/30 bg-base-950/5 dark:bg-base-950/20 overflow-hidden rounded-2xl border text-sm" 13 > 14 <div class="p-3"> 15 <div class="flex items-center gap-2"> ··· 22 {record.author.displayName} 23 </span> 24 {/if} 25 - <span class="text-base-500 dark:text-base-400 truncate"> 26 @{record.author.handle} 27 </span> 28 </div>
··· 9 </script> 10 11 <div 12 + class="border-base-300 dark:border-base-600/30 accent:border-accent-300/20 accent:bg-accent-100/10 bg-base-950/5 dark:bg-base-950/20 overflow-hidden rounded-2xl border text-sm" 13 > 14 <div class="p-3"> 15 <div class="flex items-center gap-2"> ··· 22 {record.author.displayName} 23 </span> 24 {/if} 25 + <span class="text-base-500 dark:text-base-400 accent:text-accent-950 truncate"> 26 @{record.author.handle} 27 </span> 28 </div>
+1 -1
src/lib/website/Account.svelte
··· 29 <Button 30 variant="ghost" 31 onclick={() => { 32 - goto('/' + getHandleOrDid(user.profile), {}); 33 }}>Leave edit mode</Button 34 > 35 {/if}
··· 29 <Button 30 variant="ghost" 31 onclick={() => { 32 + if (user.profile) goto('/' + getHandleOrDid(user.profile), {}); 33 }}>Leave edit mode</Button 34 > 35 {/if}
+1 -2
src/routes/(auth)/oauth/callback/+page.svelte
··· 12 goto('/' + getHandleOrDid(user.profile) + '/edit', {}); 13 } 14 15 - if(!user.isInitializing && !startedErrorTimer) { 16 startedErrorTimer = true; 17 18 setTimeout(() => { ··· 30 >There was an error signing you in, please go back to the 31 <a class="text-accent-600 dark:text-accent-400" href="/">homepage</a> 32 and try again. 33 - 34 </span> 35 </div> 36 {/if}
··· 12 goto('/' + getHandleOrDid(user.profile) + '/edit', {}); 13 } 14 15 + if (!user.isInitializing && !startedErrorTimer) { 16 startedErrorTimer = true; 17 18 setTimeout(() => { ··· 30 >There was an error signing you in, please go back to the 31 <a class="text-accent-600 dark:text-accent-400" href="/">homepage</a> 32 and try again. 33 </span> 34 </div> 35 {/if}