diff --git a/crates/tauri-app/src-tauri/src/pds_client.rs b/crates/tauri-app/src-tauri/src/pds_client.rs index 47689f0..afe7c54 100644 --- a/crates/tauri-app/src-tauri/src/pds_client.rs +++ b/crates/tauri-app/src-tauri/src/pds_client.rs @@ -385,6 +385,8 @@ pub struct UploadedBlobRef { #[serde(rename = "$link")] pub link: String, } + +impl PdsHttpClient { /// `POST /xrpc/com.atproto.repo.getRecord?repo=&collection=app.bsky.actor.profile&rkey=self` /// Returns the record's CBOR-decoded value as JSON, or `None` if no /// record exists for that path. The server replies with a @@ -446,3 +448,4 @@ pub struct UploadedBlobRef { let v: serde_json::Value = r.json().await?; Ok(v.get("profile").cloned().unwrap_or(serde_json::Value::Null)) } +} diff --git a/crates/tauri-app/src/App.svelte b/crates/tauri-app/src/App.svelte index e76fa39..6cbaa0b 100644 --- a/crates/tauri-app/src/App.svelte +++ b/crates/tauri-app/src/App.svelte @@ -4,23 +4,18 @@ session, pdsStatus, fetchTimeline, - fetchProfile, fetchSearch, fetchPost, openExternalUrl, showError, - pickAndUploadImage, - setMyProfile, type Session, type Post, - type ProfileResponse, } from "./lib/api/client"; import NavRail from "./lib/components/NavRail.svelte"; import StatusBar from "./lib/components/StatusBar.svelte"; import PostCard from "./lib/components/PostCard.svelte"; import ComposeBox from "./lib/components/ComposeBox.svelte"; - import UserProfileView from "./lib/components/UserProfileView.svelte"; - import Avatar from "./lib/components/Avatar.svelte"; + import ProfileView from "./lib/components/ProfileView.svelte"; import LoginScreen from "./lib/components/LoginScreen.svelte"; import Terminal from "./lib/components/Terminal.svelte"; import Skeleton from "./lib/components/Skeleton.svelte"; @@ -44,50 +39,6 @@ let seenUris: Set = new Set(); let _statusTimer: number | undefined; - // Profile state. - let profile: ProfileResponse | null = $state(null); - let profileLoading: boolean = $state(false); - let profileError: string | null = $state(null); - - // Edit-profile state. - let editingProfile: boolean = $state(false); - let editProfileName: string = $state(""); - let editProfileDesc: string = $state(""); - let editProfileAvatarCid: string | null = $state(null); - let savingProfile: boolean = $state(false); - - async function pickAndUploadAvatar() { - try { - const r = await pickAndUploadImage(); - if (r) editProfileAvatarCid = r.cid; - } catch (e) { - console.warn("avatar upload failed", e); - } - } - - async function saveProfile() { - if (!currentUser) return; - savingProfile = true; - try { - const updated = await setMyProfile({ - displayName: editProfileName || undefined, - description: editProfileDesc || undefined, - avatarBlobCid: editProfileAvatarCid || undefined, - }); - // Refresh the cached profile from the response (or re-fetch). - if (updated) { - profile = { ...profile, ...updated } as ProfileResponse | null; - } else { - await refreshProfile(currentUser.handle); - } - editingProfile = false; - } catch (e) { - console.warn("profile save failed", e); - } finally { - savingProfile = false; - } - } - // Search state. let searchQuery: string = $state(""); let searchResults: Post[] = $state([]); @@ -133,7 +84,7 @@ /// Navigate to the "user" profile view for `handle`. Called from /// `` and the avatar/handle buttons in /// the post header. The actual profile fetch happens inside - /// `` on mount. + /// `` on mount. function openUserProfile(handle: string) { selectedHandle = handle; view = "user"; @@ -306,8 +257,8 @@ void refreshTimeline(true); } if (next === "profile") { - const handle = currentUser.handle; - void refreshProfile(handle); + // ProfileView fetches its own data on mount; nothing to + // preload here. } if (next === "search" && searchQuery.trim().length > 0) { scheduleSearch(); @@ -376,19 +327,6 @@ } } - async function refreshProfile(handle: string) { - profileLoading = true; - profileError = null; - try { - profile = await fetchProfile(handle); - } catch (e) { - profileError = String(e); - profile = null; - } finally { - profileLoading = false; - } - } - function scheduleSearch() { if (_searchDebounce) clearTimeout(_searchDebounce); _searchDebounce = window.setTimeout(() => { @@ -439,7 +377,6 @@ try { await session.logout(); setView("home"); - profile = null; searchResults = []; threadRoot = null; threadParent = null; @@ -451,13 +388,6 @@ // Derive a display handle. The session already gives us the user's // real handle (e.g. "alice.bsky.social"). When the AppView decorates // posts that have empty handles it falls back to a synthetic - // "@did:plc:abcd…" form, so the fallback here matches that. - function displayHandle(h: string | null | undefined): string { - if (!h) return "@unknown"; - if (h.startsWith("@")) return h; - return `@${h}`; - } - // Mirror the URLs the Rust shell reads from MAARCADETWEET_PDS_URL / // MAARCADETWEET_APPVIEW_URL (see `crates/tauri-app/src-tauri/src/lib.rs`). // Used in the Settings view to show which backends the client is @@ -552,156 +482,23 @@ // profile — @{selectedHandle} - {:else if view === "profile"} -
- $ - // profile — - @{currentUser.handle} -
- {#if profileLoading && !profile} - - {:else if profileError} -
err: {profileError}
- {:else if profile} -
-
-
{displayHandle(profile.handle)}
-
{profile.did}
- {#if profile.avatar_cid} - - {/if} -
- - {#if profile.description} -
{profile.description}
- {:else} -
- // no profile yet — click "edit profile" to set one up. -
- {/if} - -
- -
- {#if editingProfile} -
- - -
- {#if editProfileAvatarCid} - cid: {editProfileAvatarCid.slice(0, 10)}… - - {:else} - no avatar - {/if} - -
-
- -
-
- {/if} - -
- {profile.post_count} posts - {profile.followers} followers - {profile.following} following -
- -
- - - - -
- -
-
-
followers
-
{profile.followers}
-
-
-
following
-
{profile.following}
-
-
-
posts
-
{profile.posts.length}
-
-
- - {#if profile.posts.length === 0} -
// no posts yet — compose your first one
- {:else} -

// recent posts

- {#each profile.posts as p (p.uri)} - - {/each} - {/if} -
+ {#if currentUser} +
+ $ + // profile — + @{currentUser.handle} +
+ {/if} {:else if view === "settings"}
@@ -895,11 +692,6 @@ padding: var(--s-3); } - .profile__actions { - display: flex; - gap: var(--s-2); - margin: var(--s-3) 0; - } .head { font-family: var(--font-mono); font-size: var(--fs-50); @@ -972,61 +764,6 @@ .btn--ghost:hover:not(:disabled) { color: var(--orange); border-color: var(--orange); } .btn:disabled { opacity: 0.4; cursor: not-allowed; } - .profile { - padding: 0 var(--s-3); - } - .profile__head { - display: flex; - flex-direction: column; - gap: var(--s-1); - padding: var(--s-3) 0 var(--s-4); - border-bottom: 1px solid var(--line); - margin-bottom: var(--s-3); - } - .profile__handle { - font-family: var(--font-mono); - font-weight: 700; - font-size: var(--fs-200); - color: var(--orange); - } - .profile__did { - font-family: var(--font-mono); - font-size: var(--fs-50); - color: var(--text-dim); - word-break: break-all; - } - .profile__bio { - font-family: var(--font-mono); - font-size: var(--fs-100); - color: var(--text); - padding: var(--s-2) 0; - } - .profile__bio--empty { - color: var(--text-dim); - font-style: italic; - } - .counts { - display: flex; - gap: var(--s-6); - padding: var(--s-2) var(--s-4); - margin: 0 0 var(--s-4); - font-family: var(--font-mono); - font-size: var(--fs-50); - } - .counts > div { - display: flex; - flex-direction: column; - gap: 2px; - } - .counts dt { color: var(--text-dim); letter-spacing: 0.04em; } - .counts dd { - margin: 0; - color: var(--text); - font-weight: 700; - font-size: var(--fs-200); - font-variant-numeric: tabular-nums; - } - .toasts { position: fixed; right: var(--s-4); @@ -1066,7 +803,6 @@ border-color: var(--red); } - .profile__h3, .settings__h3 { font-family: var(--font-mono); font-size: var(--fs-50); diff --git a/crates/tauri-app/src/lib/components/ProfileView.svelte b/crates/tauri-app/src/lib/components/ProfileView.svelte new file mode 100644 index 0000000..fa5db70 --- /dev/null +++ b/crates/tauri-app/src/lib/components/ProfileView.svelte @@ -0,0 +1,652 @@ + + +
+ +
+ {#if !bannerUrl} + + + {/if} +
+ + +
+
+ {#if viewModel.kind === "ready"} + + {:else} + + {/if} +
+
+ {#if viewModel.kind === "ready"} + {#if isOwn} + {#if editing} + + {:else} + + {/if} + {:else} + + + {/if} + {/if} +
+
+ + + {#if viewModel.kind === "ready"} +
+

+ {viewModel.data.display_name ?? viewModel.data.handle} +

+
@{viewModel.data.handle}
+
+ {:else if viewModel.kind === "loading"} +
+

+
@{handle}
+
+ {:else} +
+ err: {viewModel.message} +
+ {/if} + + + {#if viewModel.kind === "ready"} + {#if viewModel.data.description} +

{viewModel.data.description}

+ {:else if isEmptyProfile} +

+ {#if isOwn} + // no profile yet — click "edit profile" to set one up. + {:else} + // no profile yet. + {/if} +

+ {/if} + {/if} + + + {#if viewModel.kind === "ready"} +
+ + did: {shortDid(viewModel.data.did)} + +
+ {/if} + + + {#if viewModel.kind === "ready"} +
+
+
posts
+
{viewModel.data.post_count}
+
+
+
followers
+
{viewModel.data.followers}
+
+
+
following
+
{viewModel.data.following}
+
+
+ {/if} + + + + + +
+ {#if viewModel.kind === "ready"} + {#each viewModel.data.posts as p (p.uri)} + + {/each} + {#if viewModel.data.posts.length === 0} +
// no posts yet.
+ {/if} + {/if} +
+ + + {#if editing && viewModel.kind === "ready"} +
+

// edit profile

+ + +
+ avatar +
+ {#if editAvatarCid} + cid: {editAvatarCid.slice(0, 10)}… + + {:else} + none + {/if} + +
+
+
+ +
+
+ {/if} +
+ + + + diff --git a/crates/tauri-app/src/lib/components/UserProfileView.svelte b/crates/tauri-app/src/lib/components/UserProfileView.svelte deleted file mode 100644 index 33a52f4..0000000 --- a/crates/tauri-app/src/lib/components/UserProfileView.svelte +++ /dev/null @@ -1,334 +0,0 @@ - - - - -