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
···
import BskyPost from './BskyPost.svelte';
import Icon from '@iconify/svelte';
import { type Backlink, type BacklinksSource } from '$lib/at/constellation';
-
import { postActions, pulsingPostId, type PostActions } from '$lib/state.svelte';
+
import { clients, postActions, posts, pulsingPostId, type PostActions } from '$lib/state.svelte';
import * as TID from '@atcute/tid';
import type { PostWithUri } from '$lib/at/fetch';
import { onMount } from 'svelte';
···
}: Props = $props();
const selectedDid = $derived(client.user?.did ?? null);
+
const actionClient = $derived(clients.get(did as AtprotoDid));
const aturi: CanonicalResourceUri = `at://${did}/app.bsky.feed.post/${rkey}`;
const color = generateColorForDid(did);
···
actionsPos = { x: event.clientX, y: event.clientY };
event.preventDefault();
};
+
+
let deleteState: 'waiting' | 'confirm' | 'deleted' = $state('waiting');
+
$effect(() => {
+
if (deleteState === 'confirm' && !actionsOpen) deleteState = 'waiting';
+
});
+
+
const deletePost = () => {
+
if (deleteState === 'deleted') return;
+
if (deleteState === 'waiting') {
+
deleteState = 'confirm';
+
return;
+
}
+
+
actionClient?.atcute
+
?.post('com.atproto.repo.deleteRecord', {
+
input: {
+
collection: 'app.bsky.feed.post',
+
repo: did,
+
rkey
+
}
+
})
+
.then((result) => {
+
if (!result.ok) return;
+
posts.get(did)?.delete(aturi);
+
deleteState = 'deleted';
+
});
+
actionsOpen = false;
+
};
</script>
{#snippet embedBadge(embed: AppBskyEmbeds)}
···
{@render dropdownItem('heroicons:link-20-solid', 'copy at uri', () =>
navigator.clipboard.writeText(post.uri)
)}
-
<div class="my-0.75 h-px w-full opacity-60" style="background: {color};"></div>
{@render dropdownItem('heroicons:clipboard', 'copy post text', () =>
navigator.clipboard.writeText(post.record.text)
)}
+
{#if actionClient}
+
<div class="my-0.75 h-px w-full opacity-60" style="background: {color};"></div>
+
{@render dropdownItem(
+
deleteState === 'confirm' ? 'heroicons:check-20-solid' : 'heroicons:trash-20-solid',
+
deleteState === 'confirm' ? 'are you sure?' : 'delete post',
+
deletePost,
+
false,
+
deleteState === 'confirm' ? 'text-red-500' : ''
+
)}
+
{/if}
{#snippet trigger()}
<div
···
</div>
{/snippet}
-
{#snippet dropdownItem(icon: string, label: string, onClick: () => void)}
+
{#snippet dropdownItem(
+
icon: string,
+
label: string,
+
onClick: () => void,
+
autoClose: boolean = true,
+
extraClass: string = ''
+
)}
<button
class="
flex items-center justify-between rounded-sm px-2 py-1.5
transition-all duration-100 hover:[backdrop-filter:brightness(120%)]
+
{extraClass}
"
onclick={() => {
onClick();
-
actionsOpen = false;
+
if (autoClose) actionsOpen = false;
}}
>
<span class="font-bold">{label}</span>
+9 -1
src/lib/state.svelte.ts
···
import { writable } from 'svelte/store';
-
import { type NotificationsStream } from './at/client';
+
import { AtpClient, type NotificationsStream } from './at/client';
import { SvelteMap } from 'svelte/reactivity';
import type { Did, ResourceUri } from '@atcute/lexicons';
import type { Backlink } from './at/constellation';
+
import type { PostWithUri } from './at/fetch';
+
import type { AtprotoDid } from '@atcute/lexicons/syntax';
// import type { JetstreamSubscription } from '@atcute/jetstream';
export const notificationStream = writable<NotificationsStream | null>(null);
···
export const postActions = new SvelteMap<`${Did}:${ResourceUri}`, PostActions>();
export const pulsingPostId = writable<string | null>(null);
+
+
export const viewClient = new AtpClient();
+
export const clients = new SvelteMap<AtprotoDid, AtpClient>();
+
+
export const posts = new SvelteMap<Did, SvelteMap<ResourceUri, PostWithUri>>();
+
export const cursors = new SvelteMap<Did, { value?: string; end: boolean }>();
+1 -7
src/routes/+page.svelte
···
import { AppBskyFeedPost } from '@atcute/bluesky';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { InfiniteLoader, LoaderState } from 'svelte-infinite';
-
import { notificationStream } from '$lib/state.svelte';
+
import { clients, cursors, notificationStream, posts, viewClient } from '$lib/state.svelte';
import { get } from 'svelte/store';
import Icon from '@iconify/svelte';
import { sessions } from '$lib/at/oauth';
···
}
});
-
const clients = new SvelteMap<AtprotoDid, AtpClient>();
const selectedClient = $derived(selectedDid ? clients.get(selectedDid) : null);
const loginAccount = async (account: Account) => {
···
cursors.delete(did);
handleAccountSelected(newAccounts[0]?.did);
};
-
-
const viewClient = new AtpClient();
-
-
const posts = new SvelteMap<Did, SvelteMap<ResourceUri, PostWithUri>>();
-
const cursors = new SvelteMap<Did, { value?: string; end: boolean }>();
let isSettingsOpen = $state(false);
let isNotificationsOpen = $state(false);