feat(tauri-app): X-style redesign — action bar, compose, tabs, sidebar, reply-mode
A two-pass rewrite of the home / compose / search / profile flows
to follow the X (Twitter) layout conventions while staying in our
monospace / orange-on-black terminal aesthetic. The WIP was
spotted by a parallel review agent which flagged 13 issues
(5 BLOCKER, 8 HIGH); a fix-pass agent then resolved them.
What changed
------------
**PostCard.svelte** — X-style action bar. Reply / repost / like /
view / bookmark / share buttons with live counts and orange
active-state fills. Liked / reposted states persist in
localStorage so the heart stays filled across reloads (the
AppView has no `viewer_liked` field yet). Hover shows the
action affordance. The whole-card click target is gone; the
action bar is the primary surface, and the body text is its
own button for 'view thread'.
**ComposeBox.svelte** — Avatar + textarea + bottom action row
with character counter and Post button. Counter uses
`Intl.Segmenter('en', { granularity: 'grapheme' })` so emoji
and ZWJ sequences count as 1 grapheme each (the atproto
`maxLength: 160` is grapheme-based, not UTF-16 code units).
`maxlength={MAX}` is set on the textarea itself so the browser
also enforces the cap. Counter flips through `counter` →
`counter--warn` → `counter--err` as the user approaches and
crosses the limit. Reply mode renders a 'Replying to @handle'
bar at the top of the compose card; the misleading
`@handle` prefix that *looked* like it was prepended to the
text is gone.
**Sidebar.svelte** (new) — 280px right-rail on the home view.
Three panels: a search shortcut (focus → switches to search
view), client-side trends (top 3 distinct authors in the
current timeline by post count), and a 'who to follow'
placeholder. Hidden below 900px viewport.
**App.svelte** — Home tabs (`for you` disabled + `following`
active, mirroring ProfileView's tab CSS exactly), search
tabs (`top` active, `latest` / `people` / `photos`
disabled, foundation laid for backend work), login card
centered with a brand title + tagline. New `replyTo` state
plumbs the reply click chain end-to-end.
End-to-end reply chain
---------------------
User clicks reply on a PostCard →
`PostCard.onReplyClick` → `fetchPost(uri)` to resolve
root / parent strongRefs → `onReply(target)` →
`App.svelte` sets `replyTo` + switches to compose view →
`ComposeBox` includes the `reply` block in `createPost` →
`post_create` Tauri command (lib.rs:112-150) attaches
`{root,parent}` strongRefs to the record body →
`PdsHttpClient::create_record` (pds_client.rs:214) writes the
post to the PDS with the reply block. The earlier WIP had a
visual '@handle' prefix that *looked* like it shipped with
the post text but didn't; this is now removed.
Review findings addressed
-------------------------
BLOCKER 1: Reply mode end-to-end. (B2-B5, H6-H13 trivial;
implementation agent handled all 13 in one pass.)
`Intl.Segmenter` counts emoji correctly (`🇯🇵` = 1 grapheme,
not 4 code units). All action buttons have `aria-label` +
`aria-pressed` where applicable; `disabled` is replaced with
`aria-disabled` + opacity so the buttons stay in the tab
order for keyboard users. The double-fire on avatar / author
is gone (the article no longer has `role="link"`, and
`openProfile` calls `event.stopPropagation()`). The
quoted-post null-guard crash is fixed with optional chaining.
Like / repost counts are derived from `post.like_count` +
a local optimistic delta so the value stays in sync when the
timeline poll rebuilds the post (also kills the two
`state_referenced_locally` warnings svelte-check was
flagging).
Verification
------------
* `cargo check --manifest-path crates/tauri-app/src-tauri/Cargo.toml` → 0 errors (2 pre-existing warnings).
* `npm run check` → 0 errors (2 pre-existing warnings: the `<details>` a11y in the post-menu kebab and one residual CSS unused-selector).
* `npm run test` → 20/20 passing (localStorage, client, NavRail).
* Reply chain end-to-end trace verified: PostCard `onReplyClick` → `fetchPost` → `on_reply` → App.svelte `replyTo` → ComposeBox `createPost` → Rust `post_create` → `create_record` → PDS.
This commit is contained in:
@@ -113,6 +113,7 @@ async fn post_create(
|
||||
state: tauri::State<'_, AppState>,
|
||||
text: String,
|
||||
embed: Option<serde_json::Value>,
|
||||
reply: Option<pds_client::ReplyRef>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let sess = state
|
||||
.store
|
||||
@@ -130,6 +131,16 @@ async fn post_create(
|
||||
record["embed"] = emb;
|
||||
}
|
||||
}
|
||||
// The `reply` field on a post record (see
|
||||
// `app.bsky.feed.post`) is `{root, parent}` strongRefs. We only
|
||||
// attach it when the caller passes a non-null object; missing
|
||||
// means "top-level post" which is the default.
|
||||
if let Some(rp) = reply {
|
||||
record["reply"] = serde_json::json!({
|
||||
"root": { "uri": rp.root.uri, "cid": rp.root.cid },
|
||||
"parent": { "uri": rp.parent.uri, "cid": rp.parent.cid },
|
||||
});
|
||||
}
|
||||
let resp = state
|
||||
.pds
|
||||
.create_record(&sess.did, "app.twi.post", record, &sess.access_jwt)
|
||||
|
||||
@@ -43,6 +43,26 @@ pub struct CreateRecordReq {
|
||||
pub record: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Strong reference as defined by
|
||||
/// `com.atproto.repo.strongRef` — `{uri, cid}`. Used inside
|
||||
/// `app.bsky.feed.post#reply` (root + parent) and inside
|
||||
/// `app.bsky.embed.record` (the quoted post).
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct StrongRef {
|
||||
pub uri: String,
|
||||
pub cid: String,
|
||||
}
|
||||
|
||||
/// `app.bsky.feed.post#reply` — the `reply` field on a post
|
||||
/// record. `root` is the topmost ancestor of the thread,
|
||||
/// `parent` is the post being directly replied to. For a
|
||||
/// top-level reply they point at the same `strongRef`.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ReplyRef {
|
||||
pub root: StrongRef,
|
||||
pub parent: StrongRef,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateRecordResp {
|
||||
pub uri: String,
|
||||
|
||||
Reference in New Issue
Block a user