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