1<script lang="ts">
2 import { AtpClient, resolveDidDoc } from '$lib/at/client.svelte';
3 import type { Did, Handle } from '@atcute/lexicons/syntax';
4 import type { AppBskyActorProfile } from '@atcute/bluesky';
5 import ProfilePicture from './ProfilePicture.svelte';
6 import RichText from './RichText.svelte';
7 import { onMount } from 'svelte';
8 import { getBlockRelationship, handles, profiles } from '$lib/state.svelte';
9 import BlockedUserIndicator from './BlockedUserIndicator.svelte';
10
11 interface Props {
12 client: AtpClient;
13 did: Did;
14 handle?: Handle;
15 profile?: AppBskyActorProfile.Main | null;
16 }
17
18 let {
19 client,
20 did,
21 handle = handles.get(did),
22 profile = $bindable(profiles.get(did) ?? null)
23 }: Props = $props();
24
25 const userDid = $derived(client.user?.did);
26 const blockRel = $derived(
27 userDid ? getBlockRelationship(userDid, did) : { userBlocked: false, blockedByTarget: false }
28 );
29 const isBlocked = $derived(blockRel.userBlocked || blockRel.blockedByTarget);
30
31 onMount(async () => {
32 // don't load profile info if blocked
33 if (isBlocked) return;
34
35 await Promise.all([
36 (async () => {
37 if (profile) return;
38 const res = await client.getProfile(did);
39 if (!res.ok) return;
40 profile = res.value;
41 profiles.set(did, res.value);
42 })(),
43 (async () => {
44 if (handle) return;
45 const res = await resolveDidDoc(did);
46 if (!res.ok) return;
47 handle = res.value.handle;
48 handles.set(did, res.value.handle);
49 })()
50 ]);
51 });
52
53 let displayHandle = $derived(handle ?? 'handle.invalid');
54 let profileDesc = $derived(profile?.description?.trim() ?? '');
55 let profileDisplayName = $derived(profile?.displayName ?? '');
56 let showDid = $state(false);
57</script>
58
59{#if isBlocked}
60 <BlockedUserIndicator
61 {client}
62 {did}
63 reason={blockRel.userBlocked ? 'blocked' : 'blocks-you'}
64 size="normal"
65 />
66{:else}
67 <div class="flex flex-col gap-2">
68 <div class="flex items-center gap-2">
69 <ProfilePicture {client} {did} size={20} />
70
71 <div class="flex min-w-0 flex-col items-start overflow-hidden overflow-ellipsis">
72 <span class="mb-1.5 min-w-0 overflow-hidden text-2xl text-nowrap overflow-ellipsis">
73 {profileDisplayName.length > 0 ? profileDisplayName : displayHandle}
74 {#if profile?.pronouns}
75 <span class="shrink-0 text-sm text-nowrap opacity-60">({profile.pronouns})</span>
76 {/if}
77 </span>
78 <button
79 oncontextmenu={(e) => {
80 e.stopPropagation();
81 const node = e.target as Node;
82 const selection = window.getSelection() ?? new Selection();
83 const range = document.createRange();
84 range.selectNodeContents(node);
85 selection.removeAllRanges();
86 selection.addRange(range);
87 }}
88 onmousedown={(e) => {
89 // disable double clicks to disable "double click to select text"
90 // since it doesnt work with us toggling did vs handle
91 if (e.detail > 1) e.preventDefault();
92 }}
93 onclick={() => (showDid = !showDid)}
94 class="mb-0.5 text-nowrap opacity-85 select-text hover:underline"
95 >
96 {showDid ? did : `@${displayHandle}`}
97 </button>
98 {#if profile?.website}
99 <!-- eslint-disable svelte/no-navigation-without-resolve -->
100 <a
101 target="_blank"
102 rel="noopener noreferrer"
103 href={profile.website}
104 class="text-sm text-nowrap opacity-60 hover:underline">{profile.website}</a
105 >
106 {/if}
107 </div>
108 </div>
109
110 {#if profileDesc.length > 0}
111 <div class="rounded-sm bg-black/25 p-1.5 text-wrap wrap-break-word">
112 <RichText text={profileDesc} />
113 </div>
114 {/if}
115 </div>
116{/if}