replies timeline only, appview-less bluesky client
at main 4.7 kB view raw
1<script lang="ts"> 2 import { AtpClient, resolveDidDoc } from '$lib/at/client.svelte'; 3 import { isDid, isHandle, type ActorIdentifier, type Did } from '@atcute/lexicons/syntax'; 4 import TimelineView from './TimelineView.svelte'; 5 import ProfileInfo from './ProfileInfo.svelte'; 6 import type { State as PostComposerState } from './PostComposer.svelte'; 7 import Icon from '@iconify/svelte'; 8 import { accounts, generateColorForDid } from '$lib/accounts'; 9 import { img } from '$lib/cdn'; 10 import { isBlob } from '@atcute/lexicons/interfaces'; 11 import { 12 handles, 13 profiles, 14 getBlockRelationship, 15 fetchBlocked, 16 blockFlags 17 } from '$lib/state.svelte'; 18 import BlockedUserIndicator from './BlockedUserIndicator.svelte'; 19 import ProfileActions from './ProfileActions.svelte'; 20 21 interface Props { 22 client: AtpClient; 23 actor: string; 24 onBack: () => void; 25 postComposerState: PostComposerState; 26 } 27 28 let { client, actor, onBack, postComposerState = $bindable() }: Props = $props(); 29 30 const profile = $derived(profiles.get(actor as Did)); 31 const displayName = $derived(profile?.displayName ?? ''); 32 const handle = $derived(isHandle(actor) ? actor : handles.get(actor as Did)); 33 let loading = $state(true); 34 let error = $state<string | null>(null); 35 let did = $state(isDid(actor) ? actor : null); 36 37 let userBlocked = $state(false); 38 let blockedByTarget = $state(false); 39 40 const loadProfile = async (identifier: ActorIdentifier) => { 41 loading = true; 42 error = null; 43 44 const docRes = await resolveDidDoc(identifier); 45 if (docRes.ok) { 46 did = docRes.value.did; 47 handles.set(did, docRes.value.handle); 48 } else { 49 error = docRes.error; 50 return; 51 } 52 53 // check block relationship 54 if (client.user?.did) { 55 let blockRel = getBlockRelationship(client.user.did, did); 56 blockRel = blockFlags.get(client.user.did)?.has(did) 57 ? blockRel 58 : await (async () => { 59 const [userBlocked, blockedByTarget] = await Promise.all([ 60 await fetchBlocked(client, did, client.user!.did), 61 await fetchBlocked(client, client.user!.did, did) 62 ]); 63 return { userBlocked, blockedByTarget }; 64 })(); 65 userBlocked = blockRel.userBlocked; 66 blockedByTarget = blockRel.blockedByTarget; 67 } 68 69 // don't load profile if blocked 70 if (userBlocked || blockedByTarget) { 71 loading = false; 72 return; 73 } 74 75 const res = await client.getProfile(did, true); 76 if (res.ok) profiles.set(did, res.value); 77 else error = res.error; 78 79 loading = false; 80 }; 81 82 $effect(() => { 83 // if we have accounts, wait until we are logged in to load the profile 84 if (!($accounts.length > 0 && !client.user?.did)) loadProfile(actor as ActorIdentifier); 85 }); 86 87 const color = $derived(did ? generateColorForDid(did) : 'var(--nucleus-accent)'); 88 const bannerUrl = $derived( 89 did && profile && isBlob(profile.banner) 90 ? img('feed_fullsize', did, profile.banner.ref.$link) 91 : null 92 ); 93</script> 94 95<div class="flex min-h-dvh flex-col"> 96 <!-- header --> 97 <div 98 class="sticky top-0 z-20 flex items-center gap-4 border-b-2 bg-(--nucleus-bg)/80 p-2 backdrop-blur-md" 99 style="border-color: {color};" 100 > 101 <button 102 onclick={onBack} 103 class="rounded-sm p-1 text-(--nucleus-fg) transition-all hover:bg-(--nucleus-fg)/10" 104 > 105 <Icon icon="heroicons:arrow-left-20-solid" width={24} /> 106 </button> 107 <h2 class="text-xl font-bold"> 108 {displayName.length > 0 ? displayName : loading ? 'loading...' : (handle ?? 'handle.invalid')} 109 </h2> 110 <div class="grow"></div> 111 {#if did && client.user && client.user.did !== did} 112 <ProfileActions {client} targetDid={did} bind:userBlocked {blockedByTarget} /> 113 {/if} 114 </div> 115 116 {#if !loading} 117 {#if error} 118 <div class="p-8 text-center text-red-500"> 119 <p>failed to load profile: {error}</p> 120 </div> 121 {:else if userBlocked || blockedByTarget} 122 <div class="p-8"> 123 <BlockedUserIndicator 124 {client} 125 did={did!} 126 reason={userBlocked ? 'blocked' : 'blocks-you'} 127 size="large" 128 /> 129 </div> 130 {:else} 131 <!-- banner --> 132 {#if bannerUrl} 133 <div class="relative h-32 w-full overflow-hidden bg-(--nucleus-fg)/5 md:h-48"> 134 <img src={bannerUrl} alt="banner" class="h-full w-full object-cover" /> 135 <div 136 class="absolute inset-0 bg-linear-to-b from-transparent to-(--nucleus-bg)" 137 style="opacity: 0.8;" 138 ></div> 139 </div> 140 {/if} 141 142 {#if did} 143 <div class="px-4 pb-4"> 144 <div class="relative z-10 {bannerUrl ? '-mt-12' : 'mt-4'} mb-4"> 145 <ProfileInfo {client} {did} {profile} /> 146 </div> 147 148 <TimelineView 149 showReplies={false} 150 {client} 151 targetDid={did} 152 bind:postComposerState 153 class="min-h-[50vh]" 154 /> 155 </div> 156 {/if} 157 {/if} 158 {/if} 159</div>