replies timeline only, appview-less bluesky client

feat: implement most embeds

Changed files
+62 -10
src
components
lib
-1
src/app.css
··· 52 width: 100%; 53 height: 100%; 54 pointer-events: none; 55 - z-index: 1; 56 } 57 58 .color-picker {
··· 52 width: 100%; 53 height: 100%; 54 pointer-events: none; 55 } 56 57 .color-picker {
+59 -9
src/components/BskyPost.svelte
··· 1 <script lang="ts"> 2 import type { AtpClient } from '$lib/at/client'; 3 import { AppBskyFeedPost } from '@atcute/bluesky'; 4 - import type { ActorIdentifier, Did, RecordKey } from '@atcute/lexicons'; 5 - import { map, ok } from '$lib/result'; 6 import { generateColorForDid } from '$lib/accounts'; 7 import ProfilePicture from './ProfilePicture.svelte'; 8 9 interface Props { 10 client: AtpClient; ··· 20 const color = generateColorForDid(did); 21 22 let handle: ActorIdentifier = $state(did); 23 - client 24 - .resolveDidDoc(did) 25 - .then((res) => map(res, (data) => data.handle)) 26 - .then((res) => { 27 - if (res.ok) handle = res.value; 28 - }); 29 const post = record 30 ? Promise.resolve(ok(record)) 31 : client.getRecord(AppBskyFeedPost.mainSchema, did, rkey); ··· 170 </div> 171 <p class="leading-relaxed text-wrap"> 172 {record.text} 173 - {@render embedBadge(record)} 174 </p> 175 </div> 176 {:else} 177 <div class="rounded-xl border-2 p-4" style="background: #ef444422; border-color: #ef4444;">
··· 1 <script lang="ts"> 2 import type { AtpClient } from '$lib/at/client'; 3 import { AppBskyFeedPost } from '@atcute/bluesky'; 4 + import { 5 + parseCanonicalResourceUri, 6 + type ActorIdentifier, 7 + type Did, 8 + type RecordKey, 9 + type ResourceUri 10 + } from '@atcute/lexicons'; 11 + import { expect, ok } from '$lib/result'; 12 import { generateColorForDid } from '$lib/accounts'; 13 import ProfilePicture from './ProfilePicture.svelte'; 14 + import { isBlob } from '@atcute/lexicons/interfaces'; 15 + import { blob, img } from '$lib/cdn'; 16 + import BskyPost from './BskyPost.svelte'; 17 18 interface Props { 19 client: AtpClient; ··· 29 const color = generateColorForDid(did); 30 31 let handle: ActorIdentifier = $state(did); 32 + const didDoc = client.resolveDidDoc(did).then((res) => { 33 + if (res.ok) handle = res.value.handle; 34 + return res; 35 + }); 36 const post = record 37 ? Promise.resolve(ok(record)) 38 : client.getRecord(AppBskyFeedPost.mainSchema, did, rkey); ··· 177 </div> 178 <p class="leading-relaxed text-wrap"> 179 {record.text} 180 </p> 181 + {#if record.embed} 182 + {@const embed = record.embed} 183 + <div class="mt-2"> 184 + {#snippet embedPost(uri: ResourceUri)} 185 + {@const parsedUri = expect(parseCanonicalResourceUri(uri))} 186 + <!-- reject recursive quotes --> 187 + {#if !(did === parsedUri.repo && rkey === parsedUri.rkey)} 188 + <BskyPost {client} did={parsedUri.repo} rkey={parsedUri.rkey} /> 189 + {:else} 190 + <span>you think you're funny with that recursive quote but i'm onto you</span> 191 + {/if} 192 + {/snippet} 193 + {#if embed.$type === 'app.bsky.embed.images'} 194 + <!-- todo: improve how images are displayed, and pop out on click --> 195 + {#each embed.images as image (image.image)} 196 + {#if isBlob(image.image)} 197 + <img 198 + class="rounded-sm" 199 + src={img('feed_thumbnail', did, image.image.ref.$link)} 200 + alt={image.alt} 201 + /> 202 + {/if} 203 + {/each} 204 + {:else if embed.$type === 'app.bsky.embed.video'} 205 + {#if isBlob(embed.video)} 206 + {#await didDoc then didDoc} 207 + {#if didDoc.ok} 208 + <!-- svelte-ignore a11y_media_has_caption --> 209 + <video 210 + class="rounded-sm" 211 + src={blob(didDoc.value.pds, did, embed.video.ref.$link)} 212 + controls 213 + ></video> 214 + {/if} 215 + {/await} 216 + {/if} 217 + {:else if embed.$type === 'app.bsky.embed.record'} 218 + {@render embedPost(embed.record.uri)} 219 + {:else if embed.$type === 'app.bsky.embed.recordWithMedia'} 220 + {@render embedPost(embed.record.record.uri)} 221 + {/if} 222 + <!-- todo: implement external link embeds --> 223 + </div> 224 + {/if} 225 </div> 226 {:else} 227 <div class="rounded-xl border-2 p-4" style="background: #ef444422; border-color: #ef4444;">
+3
src/lib/cdn.ts
··· 7 8 export const img = (kind: ImageKind, did: Did, blob: string, format: ImageFormat = 'webp') => 9 `${cdn}/img/${kind}/plain/${did}/${blob}@${format}`;
··· 7 8 export const img = (kind: ImageKind, did: Did, blob: string, format: ImageFormat = 'webp') => 9 `${cdn}/img/${kind}/plain/${did}/${blob}@${format}`; 10 + 11 + export const blob = (pds: string, did: Did, cid: string) => 12 + `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`;