<script lang="ts">
import { AtpClient, resolveDidDoc } from '$lib/at/client.svelte';
import { isDid, isHandle, type ActorIdentifier, type Did } from '@atcute/lexicons/syntax';
import TimelineView from './TimelineView.svelte';
import ProfileInfo from './ProfileInfo.svelte';
import type { State as PostComposerState } from './PostComposer.svelte';
import Icon from '@iconify/svelte';
import { accounts, generateColorForDid } from '$lib/accounts';
import { img } from '$lib/cdn';
import { isBlob } from '@atcute/lexicons/interfaces';
import {
handles,
profiles,
getBlockRelationship,
fetchBlocked,
blockFlags
} from '$lib/state.svelte';
import BlockedUserIndicator from './BlockedUserIndicator.svelte';
import ProfileActions from './ProfileActions.svelte';
interface Props {
client: AtpClient;
actor: string;
onBack: () => void;
postComposerState: PostComposerState;
}
let { client, actor, onBack, postComposerState = $bindable() }: Props = $props();
const profile = $derived(profiles.get(actor as Did));
const displayName = $derived(profile?.displayName ?? '');
const handle = $derived(isHandle(actor) ? actor : handles.get(actor as Did));
let did = $derived(isDid(actor) ? actor : null);
let loading = $state(true);
let error = $state<string | null>(null);
let userBlocked = $state(false);
let blockedByTarget = $state(false);
const loadProfile = async (identifier: ActorIdentifier) => {
loading = true;
error = null;
const docRes = await resolveDidDoc(identifier);
if (docRes.ok) {
did = docRes.value.did;
handles.set(did, docRes.value.handle);
} else {
error = docRes.error;
return;
}
// check block relationship
if (client.user?.did) {
let blockRel = getBlockRelationship(client.user.did, did);
blockRel = blockFlags.get(client.user.did)?.has(did)
? blockRel
: await (async () => {
const [userBlocked, blockedByTarget] = await Promise.all([
await fetchBlocked(client, did, client.user!.did),
await fetchBlocked(client, client.user!.did, did)
]);
return { userBlocked, blockedByTarget };
})();
userBlocked = blockRel.userBlocked;
blockedByTarget = blockRel.blockedByTarget;
}
// don't load profile if blocked
if (userBlocked || blockedByTarget) {
loading = false;
return;
}
const res = await client.getProfile(did, true);
if (res.ok) profiles.set(did, res.value);
else error = res.error;
loading = false;
};
$effect(() => {
// if we have accounts, wait until we are logged in to load the profile
if (!($accounts.length > 0 && !client.user?.did)) loadProfile(actor as ActorIdentifier);
});
const color = $derived(did ? generateColorForDid(did) : 'var(--nucleus-accent)');
const bannerUrl = $derived(
did && profile && isBlob(profile.banner)
? img('feed_fullsize', did, profile.banner.ref.$link)
: null
);
</script>
<div class="flex min-h-dvh flex-col">
<!-- header -->
<div
class="sticky top-0 z-20 flex items-center gap-4 border-b-2 bg-(--nucleus-bg)/80 p-2 backdrop-blur-md"
style="border-color: {color};"
>
<button
onclick={onBack}
class="rounded-sm p-1 text-(--nucleus-fg) transition-all hover:bg-(--nucleus-fg)/10"
>
<Icon icon="heroicons:arrow-left-20-solid" width={24} />
</button>
<h2 class="text-xl font-bold">
{displayName.length > 0 ? displayName : loading ? 'loading...' : (handle ?? 'handle.invalid')}
</h2>
<div class="grow"></div>
{#if did && client.user && client.user.did !== did}
<ProfileActions {client} targetDid={did} bind:userBlocked {blockedByTarget} />
{/if}
</div>
{#if !loading}
{#if error}
<div class="p-8 text-center text-red-500">
<p>failed to load profile: {error}</p>
</div>
{:else if userBlocked || blockedByTarget}
<div class="p-8">
<BlockedUserIndicator
{client}
did={did!}
reason={userBlocked ? 'blocked' : 'blocks-you'}
size="large"
/>
</div>
{:else}
<!-- banner -->
{#if bannerUrl}
<div class="relative h-32 w-full overflow-hidden bg-(--nucleus-fg)/5 md:h-48">
<img src={bannerUrl} alt="banner" class="h-full w-full object-cover" />
<div
class="absolute inset-0 bg-linear-to-b from-transparent to-(--nucleus-bg)"
style="opacity: 0.8;"
></div>
</div>
{/if}
{#if did}
<div class="px-4 pb-4">
<div class="relative z-10 {bannerUrl ? '-mt-12' : 'mt-4'} mb-4">
<ProfileInfo {client} {did} {profile} />
</div>
<TimelineView
showReplies={false}
{client}
targetDid={did}
bind:postComposerState
class="min-h-[50vh]"
/>
</div>
{/if}
{/if}
{/if}
</div>