JavaScript-optional public web frontend for Bluesky anartia.kelinci.net
sveltekit atcute bluesky typescript svelte

refactor: fetch all quotes properly

mary.my.id ce9e58c9 a764917d

verified
Changed files
+112 -5
src
lib
components
queries
routes
(app)
[actor=did]
[rkey=tid]
all-quotes
quotes
+19 -1
src/lib/components/page/page-header.svelte
··· 1 1 <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + 2 4 interface Props { 3 5 title: string; 6 + children?: Snippet; 4 7 } 5 8 6 - const { title }: Props = $props(); 9 + const { title, children }: Props = $props(); 7 10 </script> 8 11 9 12 <div class="page-header"> 10 13 <h2 class="title">{title}</h2> 14 + 15 + {#if children} 16 + <div class="actions"> 17 + {@render children()} 18 + </div> 19 + {/if} 11 20 </div> 12 21 13 22 <style> 14 23 .page-header { 24 + display: flex; 15 25 position: sticky; 16 26 top: 0; 27 + justify-content: space-between; 28 + align-items: center; 17 29 z-index: 1; 18 30 border-bottom: 1px solid var(--divider-sm); 19 31 background: var(--bg-primary); ··· 24 36 font-weight: 600; 25 37 font-size: 1rem; 26 38 line-height: 1.5rem; 39 + } 40 + 41 + .actions { 42 + display: flex; 43 + align-items: center; 44 + gap: 4px; 27 45 } 28 46 </style>
+75
src/lib/queries/constellation.ts
··· 63 63 64 64 return json as LinkResponse<K>; 65 65 }; 66 + 67 + // due to the way Bluesky has designed its embeds, quotes can be in two 68 + // different paths, `.embed.record.uri` and `.embed.record.record.uri`. 69 + // Since Constellation can only support one path at a time, here's a function 70 + // that will make this happen really nicely 71 + const MP_CURSOR_RE = /^mp:(\d+)(?::(.+))?$/; 72 + 73 + export const getLinksMultiPath = async <K extends keyof Records>({ 74 + uri, 75 + collection, 76 + paths, 77 + limit = 10, 78 + cursor = null, 79 + }: { 80 + uri: string; 81 + collection: K; 82 + paths: [string, string, ...string[]]; 83 + limit?: number; 84 + cursor?: string | null; 85 + }): Promise<LinkResponse<K>> => { 86 + let index = 0; 87 + let curs: string | null = null; 88 + 89 + const result: LinkResponse<K> = { 90 + // this will never be anything other than 0 unfortunately, 91 + // can't make it work across different paths 92 + total: 0, 93 + cursor: null, 94 + linking_records: [], 95 + }; 96 + 97 + if (cursor !== null) { 98 + const match = MP_CURSOR_RE.exec(cursor); 99 + if (match === null) { 100 + return result; 101 + } 102 + 103 + index = parseInt(match[1], 10); 104 + curs = match[2] ?? null; 105 + 106 + if (index >= paths.length) { 107 + return result; 108 + } 109 + } 110 + 111 + while (index < paths.length) { 112 + const data = await getLinks({ 113 + uri: uri, 114 + collection: collection, 115 + path: paths[index], 116 + limit: limit - result.linking_records.length, 117 + cursor: curs, 118 + }); 119 + 120 + result.linking_records = [...result.linking_records, ...data.linking_records]; 121 + 122 + // response returned a cursor, so we're breaking early 123 + if (data.cursor !== null) { 124 + result.cursor = `mp:${index}:${data.cursor}`; 125 + break; 126 + } 127 + 128 + // we've reached the limit 129 + if (result.linking_records.length >= limit) { 130 + break; 131 + } 132 + 133 + // move to the next path 134 + index++; 135 + curs = null; 136 + result.cursor = index < paths.length ? `mp:${index}` : null; 137 + } 138 + 139 + return result; 140 + };
+3 -3
src/routes/(app)/[actor=did]/[rkey=tid]/all-quotes/+page.ts
··· 3 3 import { PUBLIC_APPVIEW_URL } from '$env/static/public'; 4 4 import type { PageLoad } from './$types'; 5 5 6 + import { getLinksMultiPath } from '$lib/queries/constellation'; 6 7 import { makeAtUri } from '$lib/types/at-uri'; 7 - import { getLinks } from '$lib/queries/constellation'; 8 8 9 9 export const load: PageLoad = async ({ url, params, fetch }) => { 10 10 const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) }); 11 11 12 12 const parentUri = makeAtUri(params.actor, 'app.bsky.feed.post', params.rkey); 13 13 14 - const { cursor, linking_records } = await getLinks({ 14 + const { cursor, linking_records } = await getLinksMultiPath({ 15 15 uri: parentUri, 16 16 collection: 'app.bsky.feed.post', 17 - path: '.embed.record.uri', 17 + paths: ['.embed.record.uri', '.embed.record.record.uri'], 18 18 cursor: url.searchParams.get('cursor'), 19 19 limit: 25, 20 20 });
+15 -1
src/routes/(app)/[actor=did]/[rkey=tid]/quotes/+page.svelte
··· 1 1 <script lang="ts"> 2 + import { base } from '$app/paths'; 2 3 import { page } from '$app/state'; 3 4 import { PUBLIC_APP_NAME } from '$env/static/public'; 4 5 import type { PageProps } from './$types'; 5 6 6 7 import { paginate } from '$lib/utils/pagination'; 7 8 9 + import OverflowMenu from '$lib/components/overflow-menu.svelte'; 8 10 import PageContainer from '$lib/components/page/page-container.svelte'; 9 11 import PageHeader from '$lib/components/page/page-header.svelte'; 10 12 import PageListing from '$lib/components/page/page-listing.svelte'; 11 13 import PostFeedItem from '$lib/components/timeline/post-feed-item.svelte'; 14 + 15 + import ThreadOutlined from '$lib/components/central-icons/thread-outlined.svelte'; 12 16 13 17 const { data }: PageProps = $props(); 14 18 ··· 20 24 </svelte:head> 21 25 22 26 <PageContainer> 23 - <PageHeader title="Quotes" /> 27 + <PageHeader title="Quotes"> 28 + <OverflowMenu 29 + items={[ 30 + { 31 + label: `Show all quotes`, 32 + href: `${base}/${page.params.actor}/${page.params.rkey}/all-quotes`, 33 + icon: ThreadOutlined, 34 + }, 35 + ]} 36 + /> 37 + </PageHeader> 24 38 25 39 <PageListing subject="posts" {rootUrl} {nextUrl}> 26 40 {#each data.quotes.items as post (post.uri)}