replies timeline only, appview-less bluesky client

fix image uploads

ptr.pet 1d09f64b 33070e17

verified
Changed files
+26 -27
src
+7 -14
src/components/FollowingItem.svelte
··· 1 - <script lang="ts" module> 2 - const profileCache = new SvelteMap<string, { displayName?: string; handle: string }>(); 3 - </script> 4 - 5 1 <script lang="ts"> 6 2 import ProfilePicture from './ProfilePicture.svelte'; 7 3 import BlockedUserIndicator from './BlockedUserIndicator.svelte'; ··· 10 6 import type { Did } from '@atcute/lexicons'; 11 7 import type { calculateFollowedUserStats, Sort } from '$lib/following'; 12 8 import { resolveDidDoc, type AtpClient } from '$lib/at/client.svelte'; 13 - import { SvelteMap } from 'svelte/reactivity'; 14 - import { router, getBlockRelationship } from '$lib/state.svelte'; 9 + import { router, getBlockRelationship, profiles, handles } from '$lib/state.svelte'; 15 10 import { map } from '$lib/result'; 16 11 17 12 interface Props { ··· 31 26 ); 32 27 const isBlocked = $derived(blockRel.userBlocked || blockRel.blockedByTarget); 33 28 34 - const cached = $derived(profileCache.get(did)); 35 - const displayName = $derived(cached?.displayName); 36 - const handle = $derived(cached?.handle ?? 'handle.invalid'); 29 + const displayName = $derived(profiles.get(did)?.displayName); 30 + const handle = $derived(handles.get(did) ?? 'loading...'); 37 31 38 32 let error = $state(''); 39 33 40 34 const loadProfile = async (targetDid: Did) => { 41 - if (profileCache.has(targetDid)) return; 35 + if (profiles.has(targetDid) && handles.has(targetDid)) return; 42 36 43 37 try { 44 38 const [profileRes, handleRes] = await Promise.all([ ··· 47 41 ]); 48 42 if (did !== targetDid) return; 49 43 50 - profileCache.set(targetDid, { 51 - handle: handleRes.ok ? handleRes.value : handle, 52 - displayName: profileRes.ok ? profileRes.value.displayName : displayName 53 - }); 44 + if (profileRes.ok) profiles.set(targetDid, profileRes.value); 45 + if (handleRes.ok) handles.set(targetDid, handleRes.value); 46 + else handles.set(targetDid, 'handle.invalid'); 54 47 } catch (e) { 55 48 if (did !== targetDid) return; 56 49 console.error(`failed to load profile for ${targetDid}`, e);
+18 -12
src/components/PostComposer.svelte
··· 44 44 ); 45 45 46 46 const uploadVideo = async (blobUrl: string, mimeType: string) => { 47 - const blob = await (await fetch(blobUrl)).blob(); 48 - return await client.uploadVideo(blob, mimeType, (status) => { 47 + const file = await (await fetch(blobUrl)).blob(); 48 + return await client.uploadVideo(file, mimeType, (status) => { 49 49 if (status.stage === 'uploading' && status.progress !== undefined) { 50 50 _state.blobsState.set(blobUrl, { state: 'uploading', progress: status.progress * 0.5 }); 51 51 } else if (status.stage === 'processing' && status.progress !== undefined) { ··· 57 57 }); 58 58 }; 59 59 const uploadImage = async (blobUrl: string) => { 60 - const blob = await (await fetch(blobUrl)).blob(); 61 - return await client.uploadBlob(blob, (progress) => { 60 + const file = await (await fetch(blobUrl)).blob(); 61 + return await client.uploadBlob(file, (progress) => { 62 62 _state.blobsState.set(blobUrl, { state: 'uploading', progress }); 63 63 }); 64 64 }; ··· 101 101 video: upload.blob 102 102 }; 103 103 } 104 - console.log('media', media); 104 + // console.log('media', media); 105 105 106 106 const record: AppBskyFeedPost.Main = { 107 107 $type: 'app.bsky.feed.post', ··· 156 156 let fileInputEl: HTMLInputElement | undefined = $state(); 157 157 let selectingFile = $state(false); 158 158 159 + const canUpload = $derived( 160 + !( 161 + _state.attachedMedia?.$type === 'app.bsky.embed.video' || 162 + (_state.attachedMedia?.$type === 'app.bsky.embed.images' && 163 + _state.attachedMedia.images.length >= 4) 164 + ) 165 + ); 166 + 159 167 const unfocus = () => (_state.focus = 'null'); 160 168 161 169 const handleFiles = (files: File[]) => { 162 - if (!files || files.length === 0) return; 170 + if (!canUpload || !files || files.length === 0) return; 163 171 164 172 const existingImages = 165 173 _state.attachedMedia?.$type === 'app.bsky.embed.images' ? _state.attachedMedia.images : []; ··· 220 228 }; 221 229 } 222 230 223 - const handleUpload = (blobUrl: string, blob: Result<AtpBlob<string>, string>) => { 224 - if (blob.ok) _state.blobsState.set(blobUrl, { state: 'uploaded', blob: blob.value }); 225 - else _state.blobsState.set(blobUrl, { state: 'error', message: blob.error }); 231 + const handleUpload = (blobUrl: string, res: Result<AtpBlob<string>, string>) => { 232 + if (res.ok) _state.blobsState.set(blobUrl, { state: 'uploaded', blob: res.value }); 233 + else _state.blobsState.set(blobUrl, { state: 'error', message: res.error }); 226 234 }; 227 235 228 236 const media = _state.attachedMedia; ··· 459 467 fileInputEl?.click(); 460 468 }} 461 469 onmousedown={(e) => e.preventDefault()} 462 - disabled={_state.attachedMedia?.$type === 'app.bsky.embed.video' || 463 - (_state.attachedMedia?.$type === 'app.bsky.embed.images' && 464 - _state.attachedMedia.images.length >= 4)} 470 + disabled={!canUpload} 465 471 class="rounded-sm p-1.5 transition-all duration-150 enabled:hover:scale-110 disabled:cursor-not-allowed disabled:opacity-50" 466 472 style="background: color-mix(in srgb, {color} 15%, transparent); color: {color};" 467 473 title="attach media"
+1 -1
src/lib/at/client.svelte.ts
··· 327 327 (uploaded, total) => onProgress?.(uploaded / total) 328 328 ); 329 329 if (!result.ok) return err(`upload failed: ${result.error.message}`); 330 - return ok(result.value); 330 + return ok(result.value.blob); 331 331 } 332 332 333 333 async uploadVideo(