replies timeline only, appview-less bluesky client

feat: post deletion dropdown item

ptr.pet c0026839 b4cfeb6f

verified
Changed files
+59 -12
src
+49 -4
src/components/BskyPost.svelte
··· 23 23 import BskyPost from './BskyPost.svelte'; 24 24 import Icon from '@iconify/svelte'; 25 25 import { type Backlink, type BacklinksSource } from '$lib/at/constellation'; 26 - import { postActions, pulsingPostId, type PostActions } from '$lib/state.svelte'; 26 + import { clients, postActions, posts, pulsingPostId, type PostActions } from '$lib/state.svelte'; 27 27 import * as TID from '@atcute/tid'; 28 28 import type { PostWithUri } from '$lib/at/fetch'; 29 29 import { onMount } from 'svelte'; ··· 61 61 }: Props = $props(); 62 62 63 63 const selectedDid = $derived(client.user?.did ?? null); 64 + const actionClient = $derived(clients.get(did as AtprotoDid)); 64 65 65 66 const aturi: CanonicalResourceUri = `at://${did}/app.bsky.feed.post/${rkey}`; 66 67 const color = generateColorForDid(did); ··· 224 225 actionsPos = { x: event.clientX, y: event.clientY }; 225 226 event.preventDefault(); 226 227 }; 228 + 229 + let deleteState: 'waiting' | 'confirm' | 'deleted' = $state('waiting'); 230 + $effect(() => { 231 + if (deleteState === 'confirm' && !actionsOpen) deleteState = 'waiting'; 232 + }); 233 + 234 + const deletePost = () => { 235 + if (deleteState === 'deleted') return; 236 + if (deleteState === 'waiting') { 237 + deleteState = 'confirm'; 238 + return; 239 + } 240 + 241 + actionClient?.atcute 242 + ?.post('com.atproto.repo.deleteRecord', { 243 + input: { 244 + collection: 'app.bsky.feed.post', 245 + repo: did, 246 + rkey 247 + } 248 + }) 249 + .then((result) => { 250 + if (!result.ok) return; 251 + posts.get(did)?.delete(aturi); 252 + deleteState = 'deleted'; 253 + }); 254 + actionsOpen = false; 255 + }; 227 256 </script> 228 257 229 258 {#snippet embedBadge(embed: AppBskyEmbeds)} ··· 492 521 {@render dropdownItem('heroicons:link-20-solid', 'copy at uri', () => 493 522 navigator.clipboard.writeText(post.uri) 494 523 )} 495 - <div class="my-0.75 h-px w-full opacity-60" style="background: {color};"></div> 496 524 {@render dropdownItem('heroicons:clipboard', 'copy post text', () => 497 525 navigator.clipboard.writeText(post.record.text) 498 526 )} 527 + {#if actionClient} 528 + <div class="my-0.75 h-px w-full opacity-60" style="background: {color};"></div> 529 + {@render dropdownItem( 530 + deleteState === 'confirm' ? 'heroicons:check-20-solid' : 'heroicons:trash-20-solid', 531 + deleteState === 'confirm' ? 'are you sure?' : 'delete post', 532 + deletePost, 533 + false, 534 + deleteState === 'confirm' ? 'text-red-500' : '' 535 + )} 536 + {/if} 499 537 500 538 {#snippet trigger()} 501 539 <div ··· 517 555 </div> 518 556 {/snippet} 519 557 520 - {#snippet dropdownItem(icon: string, label: string, onClick: () => void)} 558 + {#snippet dropdownItem( 559 + icon: string, 560 + label: string, 561 + onClick: () => void, 562 + autoClose: boolean = true, 563 + extraClass: string = '' 564 + )} 521 565 <button 522 566 class=" 523 567 flex items-center justify-between rounded-sm px-2 py-1.5 524 568 transition-all duration-100 hover:[backdrop-filter:brightness(120%)] 569 + {extraClass} 525 570 " 526 571 onclick={() => { 527 572 onClick(); 528 - actionsOpen = false; 573 + if (autoClose) actionsOpen = false; 529 574 }} 530 575 > 531 576 <span class="font-bold">{label}</span>
+9 -1
src/lib/state.svelte.ts
··· 1 1 import { writable } from 'svelte/store'; 2 - import { type NotificationsStream } from './at/client'; 2 + import { AtpClient, type NotificationsStream } from './at/client'; 3 3 import { SvelteMap } from 'svelte/reactivity'; 4 4 import type { Did, ResourceUri } from '@atcute/lexicons'; 5 5 import type { Backlink } from './at/constellation'; 6 + import type { PostWithUri } from './at/fetch'; 7 + import type { AtprotoDid } from '@atcute/lexicons/syntax'; 6 8 // import type { JetstreamSubscription } from '@atcute/jetstream'; 7 9 8 10 export const notificationStream = writable<NotificationsStream | null>(null); ··· 17 19 export const postActions = new SvelteMap<`${Did}:${ResourceUri}`, PostActions>(); 18 20 19 21 export const pulsingPostId = writable<string | null>(null); 22 + 23 + export const viewClient = new AtpClient(); 24 + export const clients = new SvelteMap<AtprotoDid, AtpClient>(); 25 + 26 + export const posts = new SvelteMap<Did, SvelteMap<ResourceUri, PostWithUri>>(); 27 + export const cursors = new SvelteMap<Did, { value?: string; end: boolean }>();
+1 -7
src/routes/+page.svelte
··· 12 12 import { AppBskyFeedPost } from '@atcute/bluesky'; 13 13 import { SvelteMap, SvelteSet } from 'svelte/reactivity'; 14 14 import { InfiniteLoader, LoaderState } from 'svelte-infinite'; 15 - import { notificationStream } from '$lib/state.svelte'; 15 + import { clients, cursors, notificationStream, posts, viewClient } from '$lib/state.svelte'; 16 16 import { get } from 'svelte/store'; 17 17 import Icon from '@iconify/svelte'; 18 18 import { sessions } from '$lib/at/oauth'; ··· 35 35 } 36 36 }); 37 37 38 - const clients = new SvelteMap<AtprotoDid, AtpClient>(); 39 38 const selectedClient = $derived(selectedDid ? clients.get(selectedDid) : null); 40 39 41 40 const loginAccount = async (account: Account) => { ··· 65 64 cursors.delete(did); 66 65 handleAccountSelected(newAccounts[0]?.did); 67 66 }; 68 - 69 - const viewClient = new AtpClient(); 70 - 71 - const posts = new SvelteMap<Did, SvelteMap<ResourceUri, PostWithUri>>(); 72 - const cursors = new SvelteMap<Did, { value?: string; end: boolean }>(); 73 67 74 68 let isSettingsOpen = $state(false); 75 69 let isNotificationsOpen = $state(false);