1<script lang="ts">
2 import ProfilePicture from './ProfilePicture.svelte';
3 import BlockedUserIndicator from './BlockedUserIndicator.svelte';
4 import { getRelativeTime } from '$lib/date';
5 import { generateColorForDid } from '$lib/accounts';
6 import type { Did } from '@atcute/lexicons';
7 import type { calculateFollowedUserStats, Sort } from '$lib/following';
8 import { resolveDidDoc, type AtpClient } from '$lib/at/client.svelte';
9 import { router, getBlockRelationship, profiles, handles } from '$lib/state.svelte';
10 import { map } from '$lib/result';
11
12 interface Props {
13 style: string;
14 did: Did;
15 stats: NonNullable<ReturnType<typeof calculateFollowedUserStats>>;
16 client: AtpClient;
17 sort: Sort;
18 currentTime: Date;
19 }
20
21 let { style, did, stats, client, sort, currentTime }: Props = $props();
22
23 const userDid = $derived(client.user?.did);
24 const blockRel = $derived(
25 userDid ? getBlockRelationship(userDid, did) : { userBlocked: false, blockedByTarget: false }
26 );
27 const isBlocked = $derived(blockRel.userBlocked || blockRel.blockedByTarget);
28
29 const displayName = $derived(profiles.get(did)?.displayName);
30 const handle = $derived(handles.get(did) ?? 'loading...');
31
32 let error = $state('');
33
34 const loadProfile = async (targetDid: Did) => {
35 if (profiles.has(targetDid) && handles.has(targetDid)) return;
36
37 try {
38 const [profileRes, handleRes] = await Promise.all([
39 client.getProfile(targetDid),
40 resolveDidDoc(targetDid).then((r) => map(r, (doc) => doc.handle))
41 ]);
42 if (did !== targetDid) return;
43
44 if (profileRes.ok) profiles.set(targetDid, profileRes.value);
45 if (handleRes.ok) handles.set(targetDid, handleRes.value);
46 else handles.set(targetDid, 'handle.invalid');
47 } catch (e) {
48 if (did !== targetDid) return;
49 console.error(`failed to load profile for ${targetDid}`, e);
50 error = String(e);
51 }
52 };
53
54 $effect(() => {
55 loadProfile(did);
56 });
57
58 const lastPostAt = $derived(stats?.lastPostAt ?? new Date(0));
59 const relTime = $derived(getRelativeTime(lastPostAt, currentTime));
60 const color = $derived(generateColorForDid(did));
61
62 const goToProfile = () => router.navigate(`/profile/${did}`);
63</script>
64
65<div {style} class="box-border w-full pb-2">
66 {#if isBlocked}
67 <!-- svelte-ignore a11y_click_events_have_key_events -->
68 <!-- svelte-ignore a11y_no_static_element_interactions -->
69 <div onclick={goToProfile} class="cursor-pointer">
70 <BlockedUserIndicator
71 {client}
72 {did}
73 reason={blockRel.userBlocked ? 'blocked' : 'blocks-you'}
74 size="small"
75 />
76 </div>
77 {:else}
78 <!-- svelte-ignore a11y_click_events_have_key_events -->
79 <!-- svelte-ignore a11y_no_static_element_interactions -->
80 <div
81 onclick={goToProfile}
82 class="group flex cursor-pointer items-center gap-2 rounded-sm bg-(--nucleus-accent)/7 p-3 transition-colors hover:bg-(--post-color)/20"
83 style={`--post-color: ${color};`}
84 >
85 <ProfilePicture {client} {did} size={10} />
86 <div class="min-w-0 flex-1 space-y-1">
87 {#if error.length === 0}
88 <div
89 class="flex items-baseline gap-2 truncate font-bold transition-colors group-hover:text-(--post-color)"
90 style={`--post-color: ${color};`}
91 >
92 <span class="truncate">{displayName || handle}</span>
93 <span class="truncate text-sm opacity-60">@{handle}</span>
94 </div>
95 {:else}
96 <div class="flex items-baseline truncate text-sm text-red-500">
97 error: {error}
98 </div>
99 {/if}
100 <div class="flex gap-2 text-xs opacity-70">
101 <span
102 class={Date.now() - lastPostAt.getTime() < 1000 * 60 * 60 * 2
103 ? 'text-(--nucleus-accent)'
104 : ''}
105 >
106 posted {relTime}
107 {relTime !== 'now' ? 'ago' : ''}
108 </span>
109 {#if stats?.recentPostCount && stats.recentPostCount > 0}
110 <span class="text-(--nucleus-accent2)">
111 {stats.recentPostCount} posts / 6h
112 </span>
113 {/if}
114 {#if sort === 'conversational' && stats?.conversationalScore && stats.conversationalScore > 0}
115 <span class="ml-auto font-bold text-(--nucleus-accent)">
116 ★ {stats.conversationalScore.toFixed(1)}
117 </span>
118 {/if}
119 </div>
120 </div>
121 </div>
122 {/if}
123</div>