[READ-ONLY] Mirror of https://github.com/flo-bit/atmo-garden. atmo.garden
0

Configure Feed

Select the types of activity you want to include in your feed.

commit

+38 -11
+20
src/lib/atproto/server/feed.remote.ts
··· 162 162 } 163 163 ); 164 164 165 + export const getProfile = command( 166 + v.object({ 167 + actor: v.string() 168 + }), 169 + async (input) => { 170 + const { locals } = getRequestEvent(); 171 + 172 + const client = locals.client ?? new Client({ 173 + handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) 174 + }); 175 + 176 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 177 + const res = await client.get('app.bsky.actor.getProfile', { 178 + params: { actor: input.actor as any } // eslint-disable-line @typescript-eslint/no-explicit-any 179 + }); 180 + if (!res.ok) error(res.status, 'Failed to load profile'); 181 + return res.data; 182 + } 183 + ); 184 + 165 185 export const getPostThread = command( 166 186 v.object({ 167 187 uri: v.string(),
+18 -11
src/routes/profile/[handle]/+page.svelte
··· 6 6 import { Button } from '@foxui/core'; 7 7 import { Loader2, LogOut } from '@lucide/svelte'; 8 8 import { user, logout } from '$lib/atproto/auth.svelte'; 9 - import { actorToDid, getDetailedProfile } from '$lib/atproto/methods'; 9 + import { actorToDid } from '$lib/atproto/methods'; 10 10 import { getCachedProfile, cacheProfile, prefetchThread, ingestFeedPosts } from '$lib/cache.svelte'; 11 - import { getAuthorFeed, followUser, unfollowUser } from '$lib/atproto/server/feed.remote'; 12 - import { Client, simpleFetchHandler } from '@atcute/client'; 11 + import { getAuthorFeed, followUser, unfollowUser, getProfile } from '$lib/atproto/server/feed.remote'; 13 12 import type { FeedItem } from '$lib/cache.svelte'; 14 13 import ScrollablePostList, { getCachedList, setCachedList } from '$lib/components/ScrollablePostList.svelte'; 15 14 ··· 18 17 let isOwnProfile = $derived(user.did && profile?.did === user.did); 19 18 let followUri = $state<string | null>(null); 20 19 let isFollowing = $derived(followUri !== null); 20 + let followsMe = $state(false); 21 + let isMutual = $derived(isFollowing && followsMe); 21 22 let followLoading = $state(false); 22 23 23 24 let loading = $state(true); ··· 60 61 async function loadProfile(actor: string) { 61 62 error = null; 62 63 followUri = null; 64 + followsMe = false; 63 65 64 66 const key = `profile-${actor}`; 65 67 ··· 90 92 postsLoading = true; 91 93 } 92 94 93 - // Fetch fresh profile 95 + // Fetch fresh profile (authenticated if logged in, for viewer state) 94 96 try { 95 - const client = new Client({ 96 - handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) 97 - }); 98 - const fresh = await getDetailedProfile({ did, client }); 97 + const fresh = await getProfile({ actor }); 99 98 if (fresh) { 100 99 profile = fresh; 101 100 cacheProfile(fresh); 102 101 followUri = fresh.viewer?.following ?? null; 102 + followsMe = !!fresh.viewer?.followedBy; 103 103 } else if (!cached) { 104 104 error = 'Profile not found'; 105 105 } ··· 155 155 </script> 156 156 157 157 <div> 158 - <div class="mx-auto w-full max-w-xl flex-1"> 158 + <div class="mx-auto w-full max-w-lg flex-1"> 159 159 {#if loading} 160 160 <div class="flex items-center justify-center py-12"> 161 161 <Loader2 class="text-base-400 animate-spin" size={28} /> ··· 176 176 class="" 177 177 > 178 178 <div class="flex items-center justify-between"> 179 - <div class="flex gap-4 text-sm"> 179 + <div class="flex items-center gap-4 text-sm"> 180 180 <button onclick={() => {}} class="hover:underline"> 181 181 <span class="text-base-900 dark:text-base-100 font-semibold">{numberToHuman(profile.followsCount ?? 0)}</span> 182 182 <span class="text-base-500 dark:text-base-400"> following</span> ··· 185 185 <span class="text-base-900 dark:text-base-100 font-semibold">{numberToHuman(profile.followersCount ?? 0)}</span> 186 186 <span class="text-base-500 dark:text-base-400"> followers</span> 187 187 </button> 188 + {#if !isOwnProfile && user.did} 189 + {#if isMutual} 190 + <span class="bg-accent-100 text-accent-700 dark:bg-accent-900/30 dark:text-accent-400 rounded-full px-2 py-0.5 text-xs font-medium">Mutuals</span> 191 + {:else if followsMe} 192 + <span class="bg-base-200 text-base-600 dark:bg-base-800 dark:text-base-400 rounded-full px-2 py-0.5 text-xs font-medium">Follows you</span> 193 + {/if} 194 + {/if} 188 195 </div> 189 196 {#if isOwnProfile} 190 197 <Button variant="ghost" onclick={logout} class="gap-2" size="sm"> ··· 193 200 </Button> 194 201 {:else if user.did} 195 202 <Button 196 - variant={isFollowing ? 'outline' : 'default'} 203 + variant="primary" 197 204 size="sm" 198 205 onclick={toggleFollow} 199 206 disabled={followLoading}