From d47ce48d4412ec4511b95336bdead37f68517706 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Thu, 1 Jan 2026 21:18:52 +0300 Subject: [PATCH] fix routing when opening app from not root url, many minor improvements --- src/components/BskyPost.svelte | 84 +++++++++++++-------- src/components/FollowingView.svelte | 17 +++-- src/components/NotLoggedIn.svelte | 5 ++ src/components/PostComposer.svelte | 12 ++- src/components/ProfilePicture.svelte | 10 +-- src/components/ProfileView.svelte | 28 +++++-- src/components/TimelineView.svelte | 7 +- src/lib/at/client.ts | 9 ++- src/lib/at/fetch.ts | 4 +- src/lib/index.ts | 7 +- src/lib/router.svelte.ts | 8 +- src/lib/state.svelte.ts | 13 ++-- src/routes/{ => [...catchall]}/+page.svelte | 77 ++++++++----------- src/routes/{ => [...catchall]}/+page.ts | 0 14 files changed, 156 insertions(+), 125 deletions(-) create mode 100644 src/components/NotLoggedIn.svelte rename src/routes/{ => [...catchall]}/+page.svelte (88%) rename src/routes/{ => [...catchall]}/+page.ts (100%) diff --git a/src/components/BskyPost.svelte b/src/components/BskyPost.svelte index a8b1af8..4f3a5ed 100644 --- a/src/components/BskyPost.svelte +++ b/src/components/BskyPost.svelte @@ -40,7 +40,7 @@ import { settings } from '$lib/settings'; import RichText from './RichText.svelte'; import { getRelativeTime } from '$lib/date'; - import { likeSource, repostSource } from '$lib'; + import { likeSource, repostSource, toCanonicalUri } from '$lib'; import ProfileInfo from './ProfileInfo.svelte'; interface Props { @@ -72,7 +72,7 @@ const selectedDid = $derived(client.user?.did ?? null); const isLoggedInUser = $derived($accounts.some((acc) => acc.did === did)); - const aturi = $derived(`at://${did}/app.bsky.feed.post/${rkey}` as CanonicalResourceUri); + const aturi = $derived(toCanonicalUri({ did, collection: 'app.bsky.feed.post', rkey })); const color = $derived(generateColorForDid(did)); let handle: ActorIdentifier = $state('handle.invalid'); @@ -384,50 +384,69 @@ {#snippet postControls(post: PostWithUri)} {@const myRepost = findBacklinksBy(post.uri, repostSource, selectedDid!).length > 0} {@const myLike = findBacklinksBy(post.uri, likeSource, selectedDid!).length > 0} - {#snippet control( - name: string, - icon: string, - onClick: (e: MouseEvent) => void, - isFull?: boolean, - hasSolid?: boolean - )} + {#snippet control({ + name, + icon, + onClick, + isFull, + hasSolid, + canBeDisabled = true + }: { + name: string; + icon: string; + onClick: (e: MouseEvent) => void; + isFull?: boolean; + hasSolid?: boolean; + canBeDisabled?: boolean; + })} {/snippet}
- {@render control('reply', 'heroicons:chat-bubble-left', () => onReply?.(post), false, true)} - {@render control( - 'repost', - 'heroicons:arrow-path-rounded-square-20-solid', - () => { + {@render control({ + name: 'reply', + icon: 'heroicons:chat-bubble-left', + hasSolid: true, + onClick: () => onReply?.(post) + })} + {@render control({ + name: 'repost', + icon: 'heroicons:arrow-path-rounded-square-20-solid', + onClick: () => { if (!selectedDid) return; if (myRepost) deletePostBacklink(client, post, repostSource); else createPostBacklink(client, post, repostSource); }, - myRepost - )} - {@render control('quote', 'heroicons:paper-clip-20-solid', () => onQuote?.(post), false)} - {@render control( - 'like', - 'heroicons:star', - () => { + isFull: myRepost + })} + {@render control({ + name: 'quote', + icon: 'heroicons:paper-clip-20-solid', + onClick: () => onQuote?.(post) + })} + {@render control({ + name: 'like', + icon: 'heroicons:star', + onClick: () => { if (!selectedDid) return; if (myLike) deletePostBacklink(client, post, likeSource); else createPostBacklink(client, post, likeSource); }, - myLike, - true - )} + isFull: myLike, + hasSolid: true + })}
- {@render control('actions', 'heroicons:ellipsis-horizontal-16-solid', (e) => { - e.stopPropagation(); - actionsOpen = !actionsOpen; - actionsPos = { x: 0, y: 0 }; + {@render control({ + name: 'actions', + icon: 'heroicons:ellipsis-horizontal-16-solid', + onClick: (e: MouseEvent) => { + e.stopPropagation(); + actionsOpen = !actionsOpen; + actionsPos = { x: 0, y: 0 }; + }, + canBeDisabled: false })}
{/snippet} diff --git a/src/components/FollowingView.svelte b/src/components/FollowingView.svelte index 8be3191..40a7aff 100644 --- a/src/components/FollowingView.svelte +++ b/src/components/FollowingView.svelte @@ -10,16 +10,17 @@ type Sort } from '$lib/following'; import FollowingItem from './FollowingItem.svelte'; + import NotLoggedIn from './NotLoggedIn.svelte'; interface Props { - selectedDid: Did; - selectedClient: AtpClient; + client: AtpClient | undefined; followingSort: Sort; } - let { selectedDid, selectedClient, followingSort = $bindable('active') }: Props = $props(); + let { client, followingSort = $bindable('active') }: Props = $props(); - const followsMap = $derived(follows.get(selectedDid)); + const selectedDid = $derived(client?.user?.did); + const followsMap = $derived(selectedDid ? follows.get(selectedDid) : undefined); // eslint-disable-next-line @typescript-eslint/no-explicit-any let sortedFollowing = $state<{ did: Did; data: any }[]>([]); @@ -36,7 +37,7 @@ if (calculationTimer) clearTimeout(calculationTimer); isLongCalculation = false; - if (!followsMap) { + if (!followsMap || !selectedDid) { sortedFollowing = []; return; } @@ -137,7 +138,9 @@
- {#if sortedFollowing.length === 0 || isLongCalculation} + {#if !client} + + {:else if sortedFollowing.length === 0 || isLongCalculation}
diff --git a/src/components/NotLoggedIn.svelte b/src/components/NotLoggedIn.svelte new file mode 100644 index 0000000..dd7964e --- /dev/null +++ b/src/components/NotLoggedIn.svelte @@ -0,0 +1,5 @@ +
+

+ x_x
no accounts are logged in! +

+
diff --git a/src/components/PostComposer.svelte b/src/components/PostComposer.svelte index b51fdcb..86e632d 100644 --- a/src/components/PostComposer.svelte +++ b/src/components/PostComposer.svelte @@ -67,13 +67,10 @@ } }); - if (!res) { - return err('failed to post: not logged in'); - } + if (!res) return err('failed to post: not logged in'); - if (!res.ok) { + if (!res.ok) return err(`failed to post: ${res.data.error}: ${res.data.message ?? 'no details'}`); - } return ok({ uri: res.data.uri, @@ -99,15 +96,16 @@ postText = ''; info = 'posted!'; unfocus(); - setTimeout(() => (info = ''), 1000 * 0.8); + setTimeout(() => (info = ''), 800); } else { - // todo: add a way to clear error info = res.error; + setTimeout(() => (info = ''), 3000); } }); }; $effect(() => { + if (!client.atcute) info = 'not logged in'; document.documentElement.style.setProperty('--acc-color', color); if (isFocused && textareaEl) textareaEl.focus(); }); diff --git a/src/components/ProfilePicture.svelte b/src/components/ProfilePicture.svelte index 45903eb..c17efa1 100644 --- a/src/components/ProfilePicture.svelte +++ b/src/components/ProfilePicture.svelte @@ -1,5 +1,5 @@ @@ -24,11 +24,7 @@ let avatarUrl = $state(avatarCache.get(did) ?? null); const loadProfile = async (targetDid: Did) => { - // If we already have it in cache, we might want to re-validate eventually, - // but for UI stability, using the cache is priority. - // However, we still need to handle the case where we don't have it. - if (avatarCache.has(targetDid)) avatarUrl = avatarCache.get(targetDid) ?? null; - else avatarUrl = null; + avatarUrl = avatarCache.get(targetDid) ?? null; try { const profile = await client.getProfile(targetDid); @@ -46,8 +42,6 @@ avatarCache.set(targetDid, null); } } else { - // Don't cache errors aggressively, or maybe cache 'null' to stop retrying? - // For now, just set local state. avatarUrl = null; } } catch (e) { diff --git a/src/components/ProfileView.svelte b/src/components/ProfileView.svelte index 882adb2..6ddde04 100644 --- a/src/components/ProfileView.svelte +++ b/src/components/ProfileView.svelte @@ -1,6 +1,11 @@