+49
-4
src/components/BskyPost.svelte
+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
+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
+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);