your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import type { Item } from '$lib/types';
4 import type { SettingsComponentProps } from '../types';
5 import type { AppBskyActorDefs } from '@atcute/bluesky';
6 import type { Did } from '@atcute/lexicons';
7 import type { FriendsProfile } from '.';
8 import { getBlentoOrBskyProfile } from '$lib/atproto/methods';
9 import HandleInput from '$lib/atproto/UI/HandleInput.svelte';
10 import { Avatar, Button } from '@foxui/core';
11
12 let { item = $bindable<Item>() }: SettingsComponentProps = $props();
13
14 let handleValue = $state('');
15 let inputRef: HTMLInputElement | null = $state(null);
16 let profiles: FriendsProfile[] = $state([]);
17
18 let dids: string[] = $derived(item.cardData.friends ?? []);
19
20 onMount(() => {
21 loadProfiles();
22 });
23
24 async function loadProfiles() {
25 const results = await Promise.all(
26 dids.map((did) => getBlentoOrBskyProfile({ did: did as Did }).catch(() => undefined))
27 );
28 profiles = results.filter((p): p is FriendsProfile => !!p && p.handle !== 'handle.invalid');
29 }
30
31 function addFriend(actor: AppBskyActorDefs.ProfileViewBasic) {
32 if (!item.cardData.friends) item.cardData.friends = [];
33 if (item.cardData.friends.includes(actor.did)) return;
34 item.cardData.friends = [...item.cardData.friends, actor.did];
35 profiles = [
36 ...profiles,
37 {
38 did: actor.did,
39 handle: actor.handle,
40 displayName: actor.displayName || actor.handle,
41 avatar: actor.avatar,
42 hasBlento: false
43 } as FriendsProfile
44 ];
45 requestAnimationFrame(() => {
46 handleValue = '';
47 if (inputRef) inputRef.value = '';
48 });
49 }
50
51 function removeFriend(did: string) {
52 item.cardData.friends = item.cardData.friends.filter((d: string) => d !== did);
53 profiles = profiles.filter((p) => p.did !== did);
54 }
55
56 function getProfile(did: string): FriendsProfile | undefined {
57 return profiles.find((p) => p.did === did);
58 }
59</script>
60
61<div class="flex flex-col gap-3">
62 <HandleInput bind:value={handleValue} onselected={addFriend} bind:ref={inputRef} />
63
64 {#if dids.length > 0}
65 <div class="flex flex-col gap-1.5">
66 {#each dids as did (did)}
67 {@const profile = getProfile(did)}
68 <div class="flex items-center gap-2">
69 <Avatar src={profile?.avatar} alt={profile?.handle ?? did} class="size-6 rounded-full" />
70 <span class="min-w-0 flex-1 truncate text-sm">
71 {profile?.handle ?? did}
72 </span>
73 <Button
74 variant="ghost"
75 size="icon"
76 class="size-6 min-w-6"
77 onclick={() => removeFriend(did)}
78 >
79 <svg
80 xmlns="http://www.w3.org/2000/svg"
81 fill="none"
82 viewBox="0 0 24 24"
83 stroke-width="2"
84 stroke="currentColor"
85 class="size-3.5"
86 >
87 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
88 </svg>
89 </Button>
90 </div>
91 {/each}
92 </div>
93 {/if}
94</div>