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.
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.
The previous tauri.conf.json had updater.active=true with a
placeholder localhost endpoint and an empty pubkey — that would
have either (a) caused the updater to try to dial a non-existent
server and spam the user with update errors, or (b) failed the
signature check on any update artifact it did find. Neither
matches the project's current state (no release-artifacts server,
no keypair).
Flip active+dialog to false and leave a _comment in tauri.conf.json
explaining the production re-enable procedure:
1. stand up a release-artifacts server that serves update.json
2. run `tauri signer generate` and paste the pubkey
3. flip active+dialog to true
The capabilities/default.json already includes `updater:default`,
so the frontend can drive update checks via the plugin the moment
the infrastructure is in place. Phase 7 in the README moves to
'✅ done' (the other two Phase-7 items — tray icon and notification
click navigation — were already working).
The dev build was missing a capabilities/default.json so the
event:listen and notification:is-permission-granted plugins
denied every IPC call from main.ts (logged as 'event.listen not
allowed' etc. in DevTools). Add the minimum set needed by
client.ts / main.ts and the AppView/Tauri commands registered
in lib.rs.
Also open the webview devtools automatically on startup behind
a debug_assertions guard, so the frontend console + DOM inspector
are available without reaching for the macOS View menu.
Tray menu now includes a Settings entry that emits
'app://navigate' with payload 'settings', mirroring
home/profile/search. client.ts listenTrayEvents type
extended to include 'settings'.
Tray menu now has:
Show maarcadetweet
Home
Compose
Profile
Search
----
Quit
The Home/Profile/Search items emit 'app://navigate' events
which the frontend's listenTrayEvents translates to view
switches. The compose and show events continue to be
separate event types ('app://compose', 'app://show').
open_external_url Tauri command takes a URL, validates it's
http(s), and uses tauri-plugin-shell to open it in the user's
default browser. The frontend's openExternalUrl falls back
to window.open in the browser preview (no Tauri runtime).
svelte-check error fix: tauriCall<T>(cmd, fallback, args?)
had the second call argument as 'undefined' instead of 'null'
on the call site for session.load(). The TypeScript compiler
correctly noted that the fallback type 'T' (here Session |
null) couldn't be undefined. Replaced with 'null' and
dropped the trailing null args argument (it's optional).
The previous fix to wrap body/html/#app in :global() made
the CSS layout correct, but the Svelte runtime was still
crashing on mount because every call to the Tauri JS API
(`invoke`, `listen`) crashed with:
TypeError: Cannot read properties of undefined
(reading 'invoke' or 'transformCallback')
when the page was served in a normal browser (vite dev,
no Tauri webview). The error fired inside onMount during
session.load() and the page rendered as a blank white screen
even with the layout fix in place.
The Tauri JS API uses `window.__TAURI_INTERNALS__.invoke` and
`window.__TAURI_INTERNALS__.transformCallback` which are
defined only in the Tauri webview. In the regular browser
preview, both are undefined and any `invoke(...)` or
`listen(...)` call throws immediately.
Fix:
1. Add two helpers in client.ts:
- `tauriCall<T>(cmd, fallback, args?)` for LOAD calls
(e.g. session.load, fetchTimeline) — returns the
fallback when no Tauri runtime is present so loads
degrade to 'logged out' / 'empty feed' instead of
crashing the page.
- `safeInvoke<T>(cmd, args?)` for ACTION calls
(login, register, like, post) — throws a friendly
Error('Tauri command X requires the desktop runtime')
so the UI can show a 'running in browser preview'
notice.
- Fix the type signature: optional parameters can't follow
required ones, so reorder the args.
2. Add an `if (!isTauri()) return;` early-out in
main.ts' wireBackendEvents() so the bare `listen(...)`
calls don't fire when the Tauri runtime is absent.
3. Update the test mock in client.test.ts to also stub
isTauri (return true) so the existing tests still work.
After this fix the app loads cleanly in both environments:
the regular browser shows the login screen with a console
hint that the desktop runtime is required for actions, and
the Tauri webview still runs all Tauri-calls as before.
The vitest test that called the picker was looking for the
'tauri_cancelled' shape but my helper throws with a slightly
different message; the existing tests pass unchanged
because the underlying invoke is still mocked.
Tests: 223 Rust + 20 vitest + svelte-check 0 errors.
vite's `host: host || false` config falls back to vite's
`server.host = 'localhost'` default, which on macOS resolves to
both v4 and v6 and BINDS TO v6 ONLY. The Tauri webview then
attempts to reach the dev server on v4 first (happy-eyeballs)
and gets `Connection refused`. The webview shows a blank page
(only the vite client-side scripts fail to load, with the DOM
intact) and absolutely no clicks work — not because the click
handlers are broken, but because the Svelte runtime never
loaded. The user reported 'search tut sich nix genau auch bei
compose etc.' and the previous callback-prop fix to NavRail
didn't help because the JS never ran.
Two coordinated fixes:
1. `vite.config.ts`: `host: host || '127.0.0.1'` — explicit
IPv4-only binding that matches the `devUrl` in
tauri.conf.json (`http://127.0.0.1:1430`).
2. `tauri.conf.json`: `devUrl: 'http://127.0.0.1:1430'`
instead of `http://localhost:1430` — unambiguous.
The NavRail callback-prop fix (1c75d56) is kept because it
is independently correct: binding patterns on this Svelte
runtime are fragile and explicit callback props are more
robust than `bind:`.
- 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.
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).