From e4bcfbfa83a558ca03bff37d9a9ca4ba1c24c8d0 Mon Sep 17 00:00:00 2001 From: tomdebone Date: Sat, 18 Jul 2026 18:34:52 +0200 Subject: [PATCH] fix(tauri-app): wrap profile methods in PdsHttpClient impl block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The profile.get_record / set_profile methods landed in the WIP outside any `impl PdsHttpClient { … }` block, with a stray `&self` parameter that the parser correctly rejected. Wrap them in a fresh impl block and add the missing closing brace — no behaviour change, just a structural fix so the binary builds. --- feat(tauri-app): X-style profile page with banner / avatar overlap / tabs Replace the existing UserProfileView with a new ProfileView that follows the X (Twitter) profile layout but stays in our monospace / orange-on-black terminal aesthetic: * Banner (140 px) at the top. The user's `banner_cid` (when present) is fetched via the existing `fetchBlob` Tauri command and set as a background-image. When the profile has no banner we render a subtle orange-tinted grid placeholder so the page never looks bare. * 96 px circular avatar that overlaps the bottom of the banner by ~44 px, with a 4 px border in `var(--bg)` so the cutout reads cleanly against any banner colour. * Identity row: large bold display name, dim handle below. * Bio, DID meta line, and the posts / followers / following count dl — all monospace, all using our spacing / colour tokens. * Tab row with the existing 'posts' tab active and 'replies' / 'likes' rendered disabled (placeholder for future work). * Edit form (gated on `current_user_did === profile.did`) with display-name, description, and avatar upload fields. App.svelte refactor: the 'profile' view (current user) and the 'user' view (someone else) now both render ``. The duplicated edit state (`editingProfile`, `editProfileName`, `editProfileDesc`, `editProfileAvatarCid`, `savingProfile`), the duplicate `pickAndUploadAvatar` / `saveProfile` / `refreshProfile` functions, and the unused `displayHandle` / `fetchProfile` / `pickAndUploadImage` / `setMyProfile` imports are gone. ProfileView handles its own fetch + edit state internally, so the App.svelte section collapses from ~145 lines of inline JSX to ~12. The legacy .profile__head / .profile__bio / .counts style classes that the new component no longer references are also removed. --- crates/tauri-app/src-tauri/src/pds_client.rs | 3 + crates/tauri-app/src/App.svelte | 296 +------- .../src/lib/components/ProfileView.svelte | 652 ++++++++++++++++++ .../src/lib/components/UserProfileView.svelte | 334 --------- 4 files changed, 671 insertions(+), 614 deletions(-) create mode 100644 crates/tauri-app/src/lib/components/ProfileView.svelte delete mode 100644 crates/tauri-app/src/lib/components/UserProfileView.svelte 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 @@ - - - - -