replies timeline only, appview-less bluesky client
at main 3.6 kB view raw
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}