appview-less bluesky client
1<script lang="ts">
2 import MutedAccountItem from './MutedAccountItem.svelte';
3 import VirtualList from '@tutorlatin/svelte-tiny-virtual-list';
4 import type { ActorIdentifier, Did } from '@atcute/lexicons';
5 import { allBacklinks, createBlock, deleteBlock, clients } from '$lib/state.svelte';
6 import { blockSource } from '$lib';
7 import { isActorIdentifier } from '@atcute/lexicons/syntax';
8 import { resolveHandle } from '$lib/at/client.svelte';
9
10 interface Props {
11 mutes: Did[];
12 onAddMute: (did: Did) => void;
13 onRemoveMute: (did: Did) => void;
14 selectedAccount: Did | null;
15 }
16
17 let { mutes, onAddMute, onRemoveMute, selectedAccount }: Props = $props();
18
19 let newMuteInput = $state('');
20 let newBlockInput = $state('');
21
22 const handleAddMute = async () => {
23 if (!newMuteInput.trim()) return;
24 const did = await resolveHandle(newMuteInput.trim() as ActorIdentifier);
25 if (!did.ok) return;
26 onAddMute(did.value);
27 newMuteInput = '';
28 };
29
30 const blocks = $derived.by(() => {
31 if (!selectedAccount) return [];
32 const blockMap = allBacklinks.get(blockSource);
33 if (!blockMap) return [];
34 const blockedDids: Did[] = [];
35 for (const [subjectUri, didMap] of blockMap) {
36 if (didMap.has(selectedAccount)) {
37 const did = subjectUri.replace('at://', '') as Did;
38 blockedDids.push(did);
39 }
40 }
41 return blockedDids;
42 });
43
44 const handleAddBlock = async () => {
45 if (!newBlockInput.trim() || !selectedAccount) return;
46 const client = clients.get(selectedAccount);
47 if (!client) return;
48 const did = await resolveHandle(newBlockInput.trim() as ActorIdentifier);
49 if (!did.ok) return;
50 await createBlock(client, did.value);
51 newBlockInput = '';
52 };
53
54 const handleRemoveBlock = async (did: Did) => {
55 if (!selectedAccount) return;
56 const client = clients.get(selectedAccount);
57 if (!client) return;
58 await deleteBlock(client, did);
59 };
60</script>
61
62<div class="space-y-4 p-4">
63 <div>
64 <h3 class="settings-header">muted accounts</h3>
65 <div class="settings-box space-y-2">
66 <div class="flex gap-2">
67 <input
68 type="text"
69 bind:value={newMuteInput}
70 placeholder="enter identifier"
71 class="single-line-input flex-1"
72 />
73 <button
74 disabled={!isActorIdentifier(newMuteInput)}
75 onclick={handleAddMute}
76 class="action-button">add</button
77 >
78 </div>
79 {#if mutes.length > 0}
80 <div class="h-fit">
81 <VirtualList
82 height={Math.min(mutes.length, 6) * 44}
83 itemCount={mutes.length}
84 itemSize={44}
85 >
86 {#snippet item({ index, style }: { index: number; style: string })}
87 <MutedAccountItem
88 {style}
89 did={mutes[index]}
90 onRemove={() => onRemoveMute(mutes[index])}
91 />
92 {/snippet}
93 </VirtualList>
94 </div>
95 {:else}
96 <p class="py-2 text-center text-sm opacity-50">no muted accounts</p>
97 {/if}
98 </div>
99 </div>
100
101 <div>
102 <h3 class="settings-header">blocked accounts</h3>
103 <div class="settings-box space-y-2">
104 <div class="flex gap-2">
105 <input
106 type="text"
107 bind:value={newBlockInput}
108 placeholder="enter identifier"
109 class="single-line-input flex-1"
110 />
111 <button
112 disabled={!isActorIdentifier(newBlockInput)}
113 onclick={handleAddBlock}
114 class="action-button">add</button
115 >
116 </div>
117 {#if blocks.length > 0}
118 <div class="h-fit">
119 <VirtualList
120 height={Math.min(blocks.length, 6) * 44}
121 itemCount={blocks.length}
122 itemSize={44}
123 >
124 {#snippet item({ index, style }: { index: number; style: string })}
125 <MutedAccountItem
126 {style}
127 did={blocks[index]}
128 onRemove={() => handleRemoveBlock(blocks[index])}
129 />
130 {/snippet}
131 </VirtualList>
132 </div>
133 {:else}
134 <p class="py-2 text-center text-sm opacity-50">no blocked accounts</p>
135 {/if}
136 </div>
137 </div>
138</div>