···6767 }
6868 });
69697070- if (!res) {
7171- return err('failed to post: not logged in');
7272- }
7070+ if (!res) return err('failed to post: not logged in');
73717474- if (!res.ok) {
7272+ if (!res.ok)
7573 return err(`failed to post: ${res.data.error}: ${res.data.message ?? 'no details'}`);
7676- }
77747875 return ok({
7976 uri: res.data.uri,
···9996 postText = '';
10097 info = 'posted!';
10198 unfocus();
102102- setTimeout(() => (info = ''), 1000 * 0.8);
9999+ setTimeout(() => (info = ''), 800);
103100 } else {
104104- // todo: add a way to clear error
105101 info = res.error;
102102+ setTimeout(() => (info = ''), 3000);
106103 }
107104 });
108105 };
109106110107 $effect(() => {
108108+ if (!client.atcute) info = 'not logged in';
111109 document.documentElement.style.setProperty('--acc-color', color);
112110 if (isFocused && textareaEl) textareaEl.focus();
113111 });
+2-8
src/components/ProfilePicture.svelte
···11<script lang="ts" module>
22- // Module-level cache for synchronous access during component recycling
22+ // we have this to prevent avatars from "flickering"
33 const avatarCache = new SvelteMap<string, string | null>();
44</script>
55···2424 let avatarUrl = $state<string | null>(avatarCache.get(did) ?? null);
25252626 const loadProfile = async (targetDid: Did) => {
2727- // If we already have it in cache, we might want to re-validate eventually,
2828- // but for UI stability, using the cache is priority.
2929- // However, we still need to handle the case where we don't have it.
3030- if (avatarCache.has(targetDid)) avatarUrl = avatarCache.get(targetDid) ?? null;
3131- else avatarUrl = null;
2727+ avatarUrl = avatarCache.get(targetDid) ?? null;
32283329 try {
3430 const profile = await client.getProfile(targetDid);
···4642 avatarCache.set(targetDid, null);
4743 }
4844 } else {
4949- // Don't cache errors aggressively, or maybe cache 'null' to stop retrying?
5050- // For now, just set local state.
5145 avatarUrl = null;
5246 }
5347 } catch (e) {
+23-5
src/components/ProfileView.svelte
···11<script lang="ts">
22- import { AtpClient, resolveHandle } from '$lib/at/client';
33- import type { ActorIdentifier, AtprotoDid } from '@atcute/lexicons/syntax';
22+ import { AtpClient, resolveDidDoc, resolveHandle } from '$lib/at/client';
33+ import {
44+ isHandle,
55+ type ActorIdentifier,
66+ type AtprotoDid,
77+ type Handle
88+ } from '@atcute/lexicons/syntax';
49 import TimelineView from './TimelineView.svelte';
510 import ProfileInfo from './ProfileInfo.svelte';
611 import type { State as PostComposerState } from './PostComposer.svelte';
···914 import { img } from '$lib/cdn';
1015 import { isBlob } from '@atcute/lexicons/interfaces';
1116 import type { AppBskyActorProfile } from '@atcute/bluesky';
1717+ import { onMount } from 'svelte';
12181319 interface Props {
1420 client: AtpClient;
···2026 let { client, actor, onBack, postComposerState = $bindable({ type: 'null' }) }: Props = $props();
21272228 let profile = $state<AppBskyActorProfile.Main | null>(null);
2929+ const displayName = $derived(profile?.displayName ?? '');
2330 let loading = $state(true);
2431 let error = $state<string | null>(null);
2532 let did = $state<AtprotoDid | null>(null);
3333+ let handle = $state<Handle | null>(null);
26342735 const loadProfile = async (identifier: ActorIdentifier) => {
2836 loading = true;
2937 error = null;
3038 profile = null;
3939+ handle = isHandle(identifier) ? identifier : null;
31403241 const resDid = await resolveHandle(identifier);
3342 if (resDid.ok) did = resDid.value;
···3746 return;
3847 }
39484949+ if (!handle) {
5050+ const resHandle = await resolveDidDoc(did);
5151+ if (resHandle.ok) handle = resHandle.value.handle;
5252+ }
5353+4054 const res = await client.getProfile(did);
4155 if (res.ok) profile = res.value;
4256 else error = res.error;
···4458 loading = false;
4559 };
46604747- $effect(() => {
4848- loadProfile(actor as ActorIdentifier);
6161+ onMount(async () => {
6262+ await loadProfile(actor as ActorIdentifier);
4963 });
50645165 const color = $derived(did ? generateColorForDid(did) : 'var(--nucleus-fg)');
···6983 <Icon icon="heroicons:arrow-left-20-solid" width={24} />
7084 </button>
7185 <h2 class="text-xl font-bold">
7272- {profile?.displayName ?? (loading ? 'loading...' : actor || 'profile')}
8686+ {displayName.length > 0
8787+ ? displayName
8888+ : loading
8989+ ? 'loading...'
9090+ : (handle ?? actor ?? 'profile')}
7391 </h2>
7492 </div>
7593
+2-5
src/components/TimelineView.svelte
···1616 import Icon from '@iconify/svelte';
1717 import { buildThreads, filterThreads, type ThreadPost } from '$lib/thread';
1818 import type { AtprotoDid } from '@atcute/lexicons/syntax';
1919+ import NotLoggedIn from './NotLoggedIn.svelte';
19202021 interface Props {
2122 client?: AtpClient | null;
···191192 {/snippet}
192193 </InfiniteLoader>
193194 {:else}
194194- <div class="flex justify-center py-4">
195195- <p class="text-xl opacity-80">
196196- <span class="text-4xl">x_x</span> <br /> no accounts are logged in!
197197- </p>
198198- </div>
195195+ <NotLoggedIn />
199196 {/if}
200197</div>
···99import type { Backlinks } from './constellation';
1010import { AppBskyFeedPost } from '@atcute/bluesky';
1111import type { AtprotoDid, Did, RecordKey } from '@atcute/lexicons/syntax';
1212-import { replySource } from '$lib';
1212+import { replySource, toCanonicalUri } from '$lib';
13131414export type PostWithUri = { uri: ResourceUri; cid: Cid | undefined; record: AppBskyFeedPost.Main };
1515export type PostWithBacklinks = PostWithUri & {
···127127 for (const reply of backlinks.value.records) {
128128 if (reply.did !== postRepo) continue;
129129 // if we already have this reply, then we already fetched this chain / are fetching it
130130- if (posts.has(`at://${reply.did}/${reply.collection}/${reply.rkey}`)) continue;
130130+ if (posts.has(toCanonicalUri(reply))) continue;
131131 const record =
132132 cacheFn(reply.did, reply.rkey) ??
133133 (await client.getRecord(AppBskyFeedPost.mainSchema, reply.did, reply.rkey));
+5-2
src/lib/index.ts
···55 ParsedResourceUri,
66 ResourceUri
77} from '@atcute/lexicons';
88-import type { BacklinksSource } from './at/constellation';
88+import type { Backlink, BacklinksSource } from './at/constellation';
99import { parse as parseTid } from '@atcute/tid';
10101111export const toResourceUri = (parsed: ParsedResourceUri): ResourceUri => {
1212 return `at://${parsed.repo}${parsed.collection ? `/${parsed.collection}${parsed.rkey ? `/${parsed.rkey}` : ''}` : ''}`;
1313};
1414-export const toCanonicalUri = (parsed: ParsedCanonicalResourceUri): CanonicalResourceUri => {
1414+export const toCanonicalUri = (
1515+ parsed: ParsedCanonicalResourceUri | Backlink
1616+): CanonicalResourceUri => {
1717+ if ('did' in parsed) return `at://${parsed.did}/${parsed.collection}/${parsed.rkey}`;
1518 return `at://${parsed.repo}/${parsed.collection}/${parsed.rkey}${parsed.fragment ? `#${parsed.fragment}` : ''}`;
1619};
1720