Commit Graph
8 Commits
Author SHA1 Message Date
tomdebone e4bcfbfa83 fix(tauri-app): wrap profile methods in PdsHttpClient impl block
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 `<ProfileView>`. 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.
2026-07-18 18:35:35 +02:00
tomdebone ffee5c6685 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.
2026-07-18 17:57:15 +02:00
tomdebone abea4d2a8a fix(tauri-app): kill effect_update_depth_exceeded via untrack + setView
Two independent Svelte 5 effect-loop bugs that triggered the
same 'effect_update_depth_exceeded' guard:

1. PostCard.svelte: the embed-quote-fetch $effect and the
   likedBox $effect.pre read a state variable (quotedLoading /
   likedBox) and then synchronously wrote to it in the same
   effect run. Svelte 5's effect tracker schedules
   possible_effect_self_invalidation on the touched state, the
   effect re-fires immediately, and the cycle trips the
   'flush_count > 1000' guard. With ~30 PostCards mounting on
   login the per-card loop compounds into the depth exceeded
   error. Wrap the read+write blocks in untrack() so the
   hydration flags don't contribute to the effect's dep set;
   the outer 'post.embed.uri / post.did / post.rkey' reads
   remain tracked so navigation between cards still triggers a
   fresh hydrate.

2. App.svelte: the four $effect blocks (home-refresh,
   home-poll, profile-refresh, search-debounce) lived and died
   together. Even after splitting, Svelte 5 still flagged the
   call chain into refreshTimeline / refreshProfile because
   their sync prelude writes 'timelineLoading = true' /
   'profileLoading = true' while the effect already tracks the
   same downstream state via the proxy. Drive everything
   imperatively through a single setView(v) function and move
   the 5s poll into the session.subscribe callback, which fires
   only on actual login/logout transitions. setView is the
   single point that flips view AND triggers the right refresh
   per destination — NavRail on_select, LoginScreen onLogin,
   handleLogout, the 'back to timeline' button, and the tray
   navigate-event bridge all route through it now.

Also: NavRail and StatusBar self-style their grid-area in
their own component styles ('grid-area: rail' / 'status') so
App.svelte doesn't need the fragile '$state.s-XXX > nav.rail'
cross-component selector that Svelte 5 was failing to match
in the Tauri webview, leaving rail buttons invisible to clicks.
2026-07-07 20:54:35 +02:00
tomdebone 43fc889d47 tauri-app: settings view + profile polish
- App.svelte: new 'settings' view with account/actions/about sections,
  sign-out button, pdsBase()/appviewBase() helpers, showError wired
  into logout, profile gains open-in-browser + sign-out + recent-posts
  heading
- NavRail: add settings item + gear icon, View union extended to 5
- tests: NavRail.test.ts now expects 5 buttons + covers settings click,
  NavRailHarness View type extended
2026-07-07 18:52:10 +02:00
tomdebone 1c75d56134 fix(tauri-app): NavRail click → view switch via callback prop
The `bind:current={view}` pattern in NavRail did not propagate
clicks to the parent's $state. Svelte 5's $bindable on this
runtime is flakey and on this particular build (Tauri 2.11 +
svelte 5.x) the setter was never invoked when a button was
clicked, so the view state stayed 'home' no matter which rail
button the user pressed. The user reported 'search tut sich nix
genau auch bei compose etc.'

Replace with explicit callback prop:
  let { view = 'home', on_select } = $props();
  onclick={() => on_select?.(item.id)}
The parent then mutates its own `view` rune directly via the
arrow function — Svelte tracks this unconditionally regardless of
runtime-specific bindable semantics.

Includes a vitest regression test that mounts a real Svelte
component harness (jsdom) and asserts that click events on each
rail button flip the parent's `view` and update the .active
class — so any future regression is caught in CI rather than at
the Tauri app window.
2026-07-06 22:59:29 +02:00
tomdebone b912132a05 tauri-app: 8a review fixes
- fetchBlob cache keyed by (did, cid), not just cid.
  Security: future per-DID access control on getBlob would
  otherwise leak the first responder's bytes to subsequent
  viewers.
- EmbedImage: pass did to releaseBlob, release previous cid
  on cid change (no leaked URLs).
- ComposeBox: releaseBlob called with both did and cid.
- pds-server: rename test
  get_blob_after_upload_with_different_did ->
  get_blob_returns_404_for_cross_did_cid_lookup. The
  docstring was misleading — the test only verifies the
  (did,cid) PK on the PDS row, not auth. The renamed name
  matches what the test actually checks.
- vitest: update releaseBlob call sites to the new
  (did, cid) signature.
2026-07-06 18:53:09 +02:00
tomdebone 226cfdac5c tauri-app: switch to native title bar (fixes drag + clicks)
The previous 'custom' title bar used tauri.conf.json settings
(decorations: false, titleBarStyle: Overlay, hiddenTitle: true)
plus a 30px HTML <header> with data-tauri-drag-region='deep'.

Two problems made the app unusable:
1. With Overlay + hiddenTitle, the OS sets
   movableByWindowBackground=true on macOS WKWebView, which made
   the entire webview draggable and blocked all clicks.
2. Tauri 2's WKWebView integration has a known issue
   (tao#N) where the drag.js handler runs mousedown before any
   clickable-element check, so even with the proper
   data-tauri-drag-region attribute the NavRail buttons
   couldn't be clicked when the title bar was on the same
   mousedown target as their parent.

Fix: revert to native macOS title bar:
- decorations: true
- titleBarStyle: Visible
- hiddenTitle: false

The user gets the standard macOS chrome (traffic lights, drag
handle, 'maarcadetweet' title) but everything just works.

Custom title-bar work deferred until Tauri 3.0 (which fixes the
WKWebView + movableByWindowBackground interaction).

Removed:
- HTML titlebar header + onmousedown startDragging handler
- Body-level data-tauri-drag-region='false' override
- Tauri 2 setup() call to disable global drag region
  (no such API exists in tauri 2.11.5)

Also clean up $bindable<View> → $bindable() in NavRail — the
generic form was the wrong syntax on the tauri runtime's
Svelte 5 version.

All 240 tests still pass (231 Rust + 9 vitest).
2026-07-06 16:29:39 +02:00
tomdebone c586fd39c9 maarcadetweet: initial commit
AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit.

- PDS (Rust + axum + sqlx)
  - Auth: createAccount, createSession, refreshSession
  - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE)
  - Feed: feed.like.create, feed.repost.create
  - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos
  - Identity: resolveHandle
  - MST: spec-conformant (at-mst crate, 27 tests)
  - Repo: signed commits, TID counter (monotonic, 4096 wrap safe)

- AppView (Rust + axum + sqlx)
  - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed)
  - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration)
  - Handle-sync worker (did:plc + did:web)
  - JSONB embed storage + thread columns (migration 0003)
  - Like/repost counter cache (migration 0004)

- Tauri 2 + Svelte 5 Desktop Client
  - System tray (Show/Compose/Quit menu)
  - OS notifications (tauri-plugin-notification)
  - Auto-update (tauri-plugin-updater, placeholder endpoint)
  - Window-state (tauri-plugin-window-state)
  - 160-char compose with live counter
  - Image/Link embed rendering
  - LocalStorage-persisted like state
  - Timeline with poll (prepend new posts)
  - Custom TitleBar (transparent, no decorations)
  - Orange/IBM Plex Mono maarcade design

Tests: 231 Rust + 9 vitest = 240 passed.
2026-07-05 20:01:31 +02:00