···
162
162
}
163
163
);
164
164
165
165
+
export const getProfile = command(
166
166
+
v.object({
167
167
+
actor: v.string()
168
168
+
}),
169
169
+
async (input) => {
170
170
+
const { locals } = getRequestEvent();
171
171
+
172
172
+
const client = locals.client ?? new Client({
173
173
+
handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
174
174
+
});
175
175
+
176
176
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
177
177
+
const res = await client.get('app.bsky.actor.getProfile', {
178
178
+
params: { actor: input.actor as any } // eslint-disable-line @typescript-eslint/no-explicit-any
179
179
+
});
180
180
+
if (!res.ok) error(res.status, 'Failed to load profile');
181
181
+
return res.data;
182
182
+
}
183
183
+
);
184
184
+
165
185
export const getPostThread = command(
166
186
v.object({
167
187
uri: v.string(),
···
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
9
-
import { actorToDid, getDetailedProfile } from '$lib/atproto/methods';
9
9
+
import { actorToDid } from '$lib/atproto/methods';
10
10
import { getCachedProfile, cacheProfile, prefetchThread, ingestFeedPosts } from '$lib/cache.svelte';
11
11
-
import { getAuthorFeed, followUser, unfollowUser } from '$lib/atproto/server/feed.remote';
12
12
-
import { Client, simpleFetchHandler } from '@atcute/client';
11
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
20
+
let followsMe = $state(false);
21
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
64
+
followsMe = false;
63
65
64
66
const key = `profile-${actor}`;
65
67
···
90
92
postsLoading = true;
91
93
}
92
94
93
93
-
// Fetch fresh profile
95
95
+
// Fetch fresh profile (authenticated if logged in, for viewer state)
94
96
try {
95
95
-
const client = new Client({
96
96
-
handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
97
97
-
});
98
98
-
const fresh = await getDetailedProfile({ did, client });
97
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
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
158
-
<div class="mx-auto w-full max-w-xl flex-1">
158
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
179
-
<div class="flex gap-4 text-sm">
179
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
188
+
{#if !isOwnProfile && user.did}
189
189
+
{#if isMutual}
190
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
191
+
{:else if followsMe}
192
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
193
+
{/if}
194
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
196
-
variant={isFollowing ? 'outline' : 'default'}
203
203
+
variant="primary"
197
204
size="sm"
198
205
onclick={toggleFollow}
199
206
disabled={followLoading}