appview-less bluesky client
27
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve post loading errors and info messages

dusk 1da41596 4d35388f

+61 -38
+1 -1
src/components/BskyPost.svelte
··· 97 97 {#if post.ok} 98 98 {@const record = post.value} 99 99 <span style="color: {color};">@{handle}</span>: {@render embedBadge(record)} 100 - {record.text} 100 + <span title={record.text}>{record.text}</span> 101 101 {:else} 102 102 {post.error} 103 103 {/if}
+60 -37
src/routes/+page.svelte
··· 29 29 30 30 onMount(async () => { 31 31 if ($accounts.length > 0) { 32 + loaderState.status = 'LOADING'; 32 33 selectedDid = $accounts[0].did; 33 34 Promise.all($accounts.map(loginAccount)).then(() => { 34 - loaderState.isFirstLoad = true; 35 35 loadMore(); 36 36 }); 37 37 } ··· 66 66 }; 67 67 68 68 let posts = new SvelteMap<Did, SvelteMap<ResourceUri, AppBskyFeedPost.Main>>(); 69 - let cursors = new SvelteMap<Did, string | undefined>(); 69 + let cursors = new SvelteMap<Did, { value?: string; end: boolean }>(); 70 70 71 71 const fetchTimeline = async (account: Account) => { 72 72 const client = clients.get(account.did); 73 73 if (!client) return; 74 74 75 75 const cursor = cursors.get(account.did); 76 - const accPosts = await fetchPostsWithBacklinks(client, account.did, cursor, 6); 76 + if (cursor && cursor.end) return; 77 + 78 + const accPosts = await fetchPostsWithBacklinks(client, account.did, cursor?.value, 6); 77 79 if (!accPosts.ok) { 78 80 throw `failed to fetch posts for account ${account.handle}: ${accPosts.error}`; 79 81 } 80 82 81 - // Update cursor for next fetch 82 - cursors.set(account.did, accPosts.value.cursor); 83 + // if the cursor is undefined, we've reached the end of the timeline 84 + if (!accPosts.value.cursor) { 85 + cursors.set(account.did, { ...cursor, end: true }); 86 + return; 87 + } 83 88 89 + cursors.set(account.did, { value: accPosts.value.cursor, end: false }); 84 90 const accTimeline = await hydratePosts(client, accPosts.value.posts); 85 91 if (!posts.has(account.did)) { 86 92 posts.set(account.did, new SvelteMap(accTimeline)); ··· 106 112 loaderState.error(); 107 113 } finally { 108 114 loading = false; 115 + if (cursors.values().every((cursor) => cursor.end)) loaderState.complete(); 109 116 } 110 117 }; 111 118 ··· 176 183 177 184 post.depth = depth; 178 185 179 - if (!childrenMap.has(post.parentUri)) { 180 - childrenMap.set(post.parentUri, []); 181 - } 186 + if (!childrenMap.has(post.parentUri)) childrenMap.set(post.parentUri, []); 182 187 childrenMap.get(post.parentUri)!.push(post); 183 188 } 184 189 185 190 // Sort children by time (newest first) 186 - for (const children of childrenMap.values()) { 187 - children.sort((a, b) => b.newestTime - a.newestTime); 188 - } 191 + childrenMap 192 + .values() 193 + .forEach((children) => children.sort((a, b) => b.newestTime - a.newestTime)); 189 194 190 195 // Helper to create a thread from posts 191 196 const createThread = ( ··· 207 212 const addWithChildren = (post: ThreadPost) => { 208 213 result.push(post); 209 214 const children = childrenMap.get(post.uri) || []; 210 - for (const child of children) { 211 - addWithChildren(child); 212 - } 215 + children.forEach(addWithChildren); 213 216 }; 214 217 addWithChildren(startPost); 215 218 return result; ··· 334 337 </div> 335 338 336 339 <div class="mt-4 overflow-y-scroll [scrollbar-width:none]" bind:this={scrollContainer}> 337 - <InfiniteLoader 338 - {loaderState} 339 - triggerLoad={loadMore} 340 - intersectionOptions={{ root: scrollContainer }} 341 - > 342 - {@render threadsView()} 343 - {#snippet loading()} 344 - <div class="flex justify-center py-4"> 345 - <div 346 - class="h-8 w-8 animate-spin rounded-full border-4 border-t-transparent" 347 - style="border-color: {theme.accent} {theme.accent} {theme.accent} transparent;" 348 - ></div> 349 - </div> 350 - {/snippet} 351 - {#snippet error()} 352 - <div class="flex justify-center py-4"> 353 - <p class="text-sm opacity-80" style="color: {theme.fg};"> 354 - an error occurred while loading posts: {loadError} 355 - </p> 356 - </div> 357 - {/snippet} 358 - </InfiniteLoader> 340 + {#if $accounts.length > 0} 341 + {@render renderThreads()} 342 + {:else} 343 + <div class="flex justify-center py-4"> 344 + <p class="text-xl opacity-80" style="color: {theme.fg};"> 345 + <span class="text-4xl">x_x</span> <br /> no accounts are logged in! 346 + </p> 347 + </div> 348 + {/if} 359 349 </div> 360 350 </div> 361 351 352 + {#snippet renderThreads()} 353 + <InfiniteLoader 354 + {loaderState} 355 + triggerLoad={loadMore} 356 + loopDetectionTimeout={0} 357 + intersectionOptions={{ root: scrollContainer }} 358 + > 359 + {@render threadsView()} 360 + {#snippet noData()} 361 + <div class="flex justify-center py-4"> 362 + <p class="text-xl opacity-80" style="color: {theme.fg};"> 363 + all posts seen! <span class="text-2xl">:o</span> 364 + </p> 365 + </div> 366 + {/snippet} 367 + {#snippet loading()} 368 + <div class="flex justify-center"> 369 + <div 370 + class="h-12 w-12 animate-spin rounded-full border-4 border-t-transparent" 371 + style="border-color: {theme.accent} {theme.accent} {theme.accent} transparent;" 372 + ></div> 373 + </div> 374 + {/snippet} 375 + {#snippet error()} 376 + <div class="flex justify-center py-4"> 377 + <p class="text-xl opacity-80" style="color: {theme.fg};"> 378 + <span class="text-4xl">:(</span> <br /> an error occurred while loading posts: {loadError} 379 + </p> 380 + </div> 381 + {/snippet} 382 + </InfiniteLoader> 383 + {/snippet} 384 + 362 385 {#snippet threadsView()} 363 386 {#each threads as thread (thread.rootUri)} 364 387 <div class="flex {reverseChronological ? 'flex-col' : 'flex-col-reverse'} mb-6.5"> 365 388 {#if thread.branchParentPost} 366 389 {@const post = thread.branchParentPost} 367 390 <div class="mb-1.5 flex items-center gap-1.5"> 368 - <span class="text-sm opacity-60" style="color: {theme.fg};" 391 + <span class="text-sm text-nowrap opacity-60" style="color: {theme.fg};" 369 392 >{reverseChronological ? '↱' : '↳'}</span 370 393 > 371 394 <BskyPost