replies timeline only, appview-less bluesky client

implement the uploading part for attachments, ui later

ptr.pet a13ea2e7 68a4da7b

verified
Changed files
+53 -10
src
-1
src/components/BskyPost.svelte
··· 27 27 import type { PostWithUri } from '$lib/at/fetch'; 28 28 import { onMount, type Snippet } from 'svelte'; 29 29 import { derived } from 'svelte/store'; 30 - import Device from 'svelte-device-info'; 31 30 import Dropdown from './Dropdown.svelte'; 32 31 import { settings } from '$lib/settings'; 33 32 import RichText from './RichText.svelte';
+2 -2
src/components/EmbedMedia.svelte
··· 1 1 <script lang="ts"> 2 - import { AppBskyEmbedExternal, AppBskyEmbedImages, AppBskyEmbedVideo } from '@atcute/bluesky'; 3 2 import { isBlob } from '@atcute/lexicons/interfaces'; 4 3 import PhotoSwipeGallery, { type GalleryItem } from './PhotoSwipeGallery.svelte'; 5 4 import { blob, img } from '$lib/cdn'; 6 5 import { type Did } from '@atcute/lexicons'; 7 6 import { resolveDidDoc } from '$lib/at/client'; 7 + import type { AppBskyEmbedMedia } from '$lib/at/types'; 8 8 9 9 interface Props { 10 10 did: Did; 11 - embed: AppBskyEmbedImages.Main | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main; 11 + embed: AppBskyEmbedMedia; 12 12 } 13 13 14 14 let { did, embed }: Props = $props();
+46 -7
src/components/PostComposer.svelte
··· 1 1 <script lang="ts"> 2 2 import type { AtpClient } from '$lib/at/client'; 3 3 import { ok, err, type Result, expect } from '$lib/result'; 4 - import type { AppBskyFeedPost } from '@atcute/bluesky'; 4 + import type { AppBskyEmbedRecordWithMedia, AppBskyFeedPost } from '@atcute/bluesky'; 5 5 import { generateColorForDid } from '$lib/accounts'; 6 6 import type { PostWithUri } from '$lib/at/fetch'; 7 7 import BskyPost from './BskyPost.svelte'; 8 - import { parseCanonicalResourceUri } from '@atcute/lexicons'; 8 + import { parseCanonicalResourceUri, type Blob as AtpBlob } from '@atcute/lexicons'; 9 9 import type { ComAtprotoRepoStrongRef } from '@atcute/atproto'; 10 10 import { parseToRichText } from '$lib/richtext'; 11 11 import { tokenize } from '$lib/richtext/parser'; 12 12 import Icon from '@iconify/svelte'; 13 13 import ProfilePicture from './ProfilePicture.svelte'; 14 + import type { AppBskyEmbedMedia } from '$lib/at/types'; 14 15 15 16 export type FocusState = 'null' | 'focused'; 16 17 export type State = { ··· 18 19 text: string; 19 20 quoting?: PostWithUri; 20 21 replying?: PostWithUri; 22 + attachedMedia?: AppBskyEmbedMedia; 21 23 }; 22 24 23 25 interface Props { ··· 43 45 44 46 const rt = await parseToRichText(text); 45 47 48 + let media: AppBskyEmbedMedia | undefined = _state.attachedMedia; 49 + if (_state.attachedMedia?.$type === 'app.bsky.embed.images') { 50 + const images = _state.attachedMedia.images; 51 + let uploadedImages: typeof images = []; 52 + for (const image of images) { 53 + const blobUrl = (image.image as AtpBlob<string>).ref.$link; 54 + const blob = await (await fetch(blobUrl)).blob(); 55 + const result = await client.uploadBlob(blob); 56 + if (!result.ok) return result; 57 + uploadedImages.push({ 58 + ...image, 59 + image: result.value 60 + }); 61 + } 62 + media = { 63 + ..._state.attachedMedia, 64 + $type: 'app.bsky.embed.images', 65 + images: uploadedImages 66 + }; 67 + } else if (_state.attachedMedia?.$type === 'app.bsky.embed.video') { 68 + const blobUrl = (_state.attachedMedia.video as AtpBlob<string>).ref.$link; 69 + const blob = await (await fetch(blobUrl)).blob(); 70 + const result = await client.uploadVideo(blob); 71 + if (!result.ok) return result; 72 + media = { 73 + ..._state.attachedMedia, 74 + $type: 'app.bsky.embed.video', 75 + video: result.value 76 + }; 77 + } 78 + 46 79 const record: AppBskyFeedPost.Main = { 47 80 $type: 'app.bsky.feed.post', 48 81 text: rt.text, ··· 56 89 : undefined, 57 90 embed: 58 91 _state.focus === 'focused' && _state.quoting 59 - ? { 60 - $type: 'app.bsky.embed.record', 61 - record: strongRef(_state.quoting) 62 - } 63 - : undefined, 92 + ? media 93 + ? { 94 + $type: 'app.bsky.embed.recordWithMedia', 95 + record: { record: strongRef(_state.quoting) }, 96 + media: media as AppBskyEmbedRecordWithMedia.Main['media'] 97 + } 98 + : { 99 + $type: 'app.bsky.embed.record', 100 + record: strongRef(_state.quoting) 101 + } 102 + : (media as AppBskyFeedPost.Main['embed']), 64 103 createdAt: new Date().toISOString() 65 104 }; 66 105
+5
src/lib/at/types.ts
··· 12 12 | AppBskyEmbedRecord.Main 13 13 | AppBskyEmbedRecordWithMedia.Main 14 14 | AppBskyEmbedVideo.Main; 15 + 16 + export type AppBskyEmbedMedia = 17 + | AppBskyEmbedImages.Main 18 + | AppBskyEmbedVideo.Main 19 + | AppBskyEmbedExternal.Main;