feat(tauri-app): profile view, Avatar component, handle navigation

UI half of the profile feature. Mirrors the previous three commits
so the user can browse and edit profiles.

* `<Avatar did cid name size>` — reusable avatar component.
  Falls back to an initial-letter (or "?" when name is empty) circle
  when `cid` is null. Resolves the blob through the standard PDS
  fetch path so it works for any author whose PDS the client can
  reach.
* `<UserProfileView handle on_thread_click current_user_did>` —
  public profile page. Fetches `GET /api/profile/<handle>` on
  mount, renders the avatar / display name / bio / counts /
  posts. The "edit profile" button is gated on
  `current_user_did === profile.did` so a user browsing
  someone else's profile can't issue an unintended `setMyProfile`
  against their own DID.
* `PostCard` now renders an inline `<Avatar>` + clickable handle
  button that calls a new `on_handle_click` prop. The clickable
  area replaces the previous dead `<a href>` (Tauri webviews
  have no router).
* `App.svelte` adds an `openUserProfile(handle)` handler that
  sets `selectedHandle` + `view = "user"` and mounts
  `<UserProfileView>`.
* New Tauri commands `profile_get_record` / `profile_set` in
  `lib.rs` + matching client helpers `getMyProfile` /
  `setMyProfile` in `client.ts`. The set command sends camelCase
  field names; the PDS endpoint (previous commit) round-trips them
  through `#[serde(rename_all = "camelCase")]`.
* Empty-state UX for users with no profile yet (new account, or a
  third-party-PDS author whose profile the AppView hasn't indexed
  yet): both the current-user "profile" view and the public
  "user" view render a hint ("// no profile yet — click 'edit
  profile' to set one up." / "// no profile yet.") instead of a
  blank bio box.
* NavRail / NavRailHarness `View` union extended with "user"
  so the navigation prop type accepts the new view.
This commit is contained in:
tomdebone
2026-07-18 17:57:15 +02:00
parent 59a3cb02dd
commit ffee5c6685
9 changed files with 798 additions and 21 deletions
+35
View File
@@ -191,6 +191,9 @@ export type Post = {
embed?: Embed | null;
langs: string[];
created_at: string;
/// Resolved author-avatar CID from the AppView's `profiles`
/// cache. NULL when the user has no profile record yet.
avatar_cid?: string | null;
};
export type TimelineResponse = {
@@ -204,6 +207,11 @@ export type ProfileResponse = {
posts: Post[];
followers: number;
following: number;
display_name?: string | null;
description?: string | null;
avatar_cid?: string | null;
banner_cid?: string | null;
post_count: number;
};
export type SearchResponse = {
@@ -429,6 +437,33 @@ export async function listenTrayEvents(
/// browser preview where no Tauri runtime is present, fall back
/// to `window.open` and treat a popup-blocker denial as
/// "fine, user can copy the URL themselves".
export type ProfileRecord = {
displayName?: string;
description?: string;
avatar?: { ref: { $link: string }; mimeType?: string; size?: number };
banner?: { ref: { $link: string }; mimeType?: string; size?: number };
};
/// Read the authenticated user's `app.bsky.actor.profile` record.
/// Returns `null` if no profile record exists yet (a brand-new
/// account, or a user whose PDS hasn't pushed one).
export async function getMyProfile(): Promise<ProfileRecord | null> {
return await safeInvoke<ProfileRecord | null>("profile_get_record");
}
/// Read-modify-write the authenticated user's profile. The Rust
/// `profile_set` command fetches the existing record, overlays
/// the supplied fields, and writes a new commit. `undefined` fields
/// are preserved.
export async function setMyProfile(fields: {
displayName?: string;
description?: string;
avatarBlobCid?: string;
bannerBlobCid?: string;
}): Promise<ProfileRecord | null> {
return await safeInvoke<ProfileRecord | null>("profile_set", fields);
}
export async function openExternalUrl(url: string): Promise<void> {
try {
if (isTauri()) {