You are a constitutional council ranking individual git commits for ownership allocation. Compare these two commits. Decide which contributed more lasting value to the project. Judge substance, not spectacle: - Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise. - Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one. - Do not favor a side merely because its patch is longer or noisier. - Weight what the change does for the project, not the contributor's name. Return ONLY a JSON object: {"winner": "A" or "B", "ratio": "N:M", "explanation": "..."} The explanation must cite concrete differences in the patches (1-3 sentences). Side A — contributor: tommy-mor Side A — commit message: [03cd8f2e] Skip pinned Reddit posts when importing subreddit listings. Co-authored-by: Cursor Side A — unified diff (full patch): diff --git a/server/src/reddit.rs b/server/src/reddit.rs index f409764c1e1f36216f1b08107043c2eab905694c..fa5577f8ee0a8c53ff4dbec988a90a5d2fc3cdee 100644 --- a/server/src/reddit.rs +++ b/server/src/reddit.rs @@ -661,6 +661,7 @@ pub fn map_children_url(id: &ItemId, api_base: &str) -> String { /// Parse a subreddit listing payload into `(child_id, child_payload)` entries. /// Each child id is the post's permalink under `reddit.com/…`, and the payload /// is the raw `{kind, data}` listing element (persisted per child). +/// Pinned / stickied posts are skipped. fn parse_children(_parent: &ItemId, payload: &Value) -> Vec<(ItemId, Value)> { let mut out = Vec::new(); let children = match payload.pointer("/data/children").and_then(|c| c.as_array()) { @@ -668,6 +669,9 @@ fn parse_children(_parent: &ItemId, payload: &Value) -> Vec<(ItemId, Value)> { None => return out, }; for child in children { + if child_is_pinned(child) { + continue; + } let permalink = match child.pointer("/data/permalink").and_then(|p| p.as_str()) { Some(p) if !p.is_empty() => p, _ => continue, @@ -680,6 +684,15 @@ fn parse_children(_parent: &ItemId, payload: &Value) -> Vec<(ItemId, Value)> { out } +fn child_is_pinned(child: &Value) -> bool { + let data = match child.get("data") { + Some(d) => d, + None => return false, + }; + data.get("stickied").and_then(|v| v.as_bool()) == Some(true) + || data.get("pinned").and_then(|v| v.as_bool()) == Some(true) +} + fn parse_reddit_view(id: &ItemId, v: &Value) -> Option { let segments: Vec<&str> = id.as_str().split('/').collect(); @@ -853,4 +866,46 @@ mod tests { Some("http://v3.redgifs.com/watch/impossibleprestigioushedgehog") ); } + + #[test] + fn parse_children_skips_pinned_posts() { + let payload = serde_json::json!({ + "kind": "Listing", + "data": { + "children": [ + { + "kind": "t3", + "data": { + "title": "Official rules (pinned)", + "permalink": "/r/rust/comments/pin/official_rules/", + "stickied": true + } + }, + { + "kind": "t3", + "data": { + "title": "Also pinned via pinned field", + "permalink": "/r/rust/comments/pin2/also_pinned/", + "pinned": true + } + }, + { + "kind": "t3", + "data": { + "title": "Normal post", + "permalink": "/r/rust/comments/aaa/normal_post/", + "stickied": false + } + } + ] + } + }); + let parent = ItemId::from_url("https://reddit.com/r/rust").unwrap(); + let children = parse_children(&parent, &payload); + assert_eq!(children.len(), 1); + assert_eq!( + children[0].0.as_str(), + "https://reddit.com/r/rust/comments/aaa" + ); + } } Side B — contributor: tommy-mor Side B — commit message: [02cd761c] Make ranked child group pin icons clickable to unpin. Lets users clear the garden pin from the ranked child groups list via the same POST /ui set_garden_pin flow as the HUD and item header. Co-authored-by: Cursor Side B — unified diff (full patch): diff --git a/server/src/html/garden/pin.rs b/server/src/html/garden/pin.rs index 1820d7fe7ee167ecbf5ad5124f23b2ab92ffb01f..b7ea92bbe67d6d499915edb607a3a0b36178d7f6 100644 --- a/server/src/html/garden/pin.rs +++ b/server/src/html/garden/pin.rs @@ -101,6 +101,7 @@ pub(super) fn child_row_pin_or_vote( row_item: &ItemId, pinned_room_and_item: Option<&(String, ItemId)>, scope_content: &ContentState, + next_path: &str, ) -> maud::Markup { let pin_matches_scope = pinned_room_and_item .map(|(r, _)| r == nav.room_wire.as_str()) @@ -108,12 +109,25 @@ pub(super) fn child_row_pin_or_vote( let pinned_item = pinned_room_and_item .filter(|_| pin_matches_scope) .map(|(_, i)| i); + let unpin_rpc = template_json_compact(&json!({ + "action": "set_garden_pin", + "clear": true, + "room_wire": "", + "next": next_path, + "form_action": "/ui", + })) + .expect("unpin rpc json"); html! { @if let Some(pi) = pinned_item { span class="ont-garden-child-actions" data-garden-room=(nav.room_wire.as_str()) { @if pi == row_item { - span class="ont-garden-pinned-here" title="Pinned" aria-label="Pinned" { "📌" } + form method="POST" action="/ui" data-navigate="full" class="ont-garden-pin-form" { + input type="hidden" name=(UI_RPC_FIELD) value=(unpin_rpc); + button type="submit" class="ont-garden-pin-ico ont-garden-pin-ico-active" title="Unpin" aria-label="Unpin from HUD" { + span class="ont-garden-pin-glyph" aria-hidden="true" { "📌" } + } + } } @else { @let nv = edge_vote_count_for_pair(scope_content, pi, row_item); @let tip = format!( diff --git a/server/src/html/garden/render.rs b/server/src/html/garden/render.rs index bd8cbce059ee789de6e8dc9b6f69f54beee2f994..7bfaadc0786b734db8973c53b82296a188d26e22 100644 --- a/server/src/html/garden/render.rs +++ b/server/src/html/garden/render.rs @@ -216,7 +216,7 @@ pub(super) async fn render_scope_view( @let item_url = item_href(r.item.as_str(), &nav); @let score_str = format!("{:.3}", r.score); li data-garden-item=(r.item.as_str()) { - (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content)) + (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content, &next_for_pin)) a class="item-link" href=(item_url) { code { (item_display_path(r.item.as_str())) } } span class="ont-rank-score" { (score_str) } } @@ -232,7 +232,7 @@ pub(super) async fn render_scope_view( ul class="ont-group-list" { @for name in &model.child_rankings.unranked_items { li data-garden-item=(name.as_str()) { - (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content)) + (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content, &next_for_pin)) @let href = item_href(name.as_str(), &nav); a class="item-link" href=(href) { code { (item_display_path(name.as_str())) } } } diff --git a/server/static/theme_default.css b/server/static/theme_default.css index 2dabf3a7094cecd89d0c9674d27efe30cd01c010..b846ab86cb6eb5fb56be87efb054fb8b18082583 100644 --- a/server/static/theme_default.css +++ b/server/static/theme_default.css @@ -891,8 +891,7 @@ button.ont-pin-btn-active { vertical-align: middle; } button.ont-garden-pin-ico, -a.ont-garden-vote-ico, -span.ont-garden-pinned-here { +a.ont-garden-vote-ico { font-size: 13px; line-height: 1; padding: 2px 6px; @@ -910,6 +909,11 @@ span.ont-garden-pinned-here { align-items: center; justify-content: center; } +form.ont-garden-pin-form { + display: inline; + margin: 0; + padding: 0; +} a.ont-garden-vote-ico { gap: 4px; } @@ -925,10 +929,14 @@ a.ont-garden-vote-ico:hover { span.ont-garden-vote-glyph { line-height: 1; } -span.ont-garden-pinned-here { +button.ont-garden-pin-ico-active { border-color: var(--link); color: var(--signal); - cursor: default; + background: color-mix(in srgb, var(--link) 12%, var(--g3)); +} +button.ont-garden-pin-ico-active:hover { + color: var(--signal); + background: color-mix(in srgb, var(--link) 18%, var(--g4)); } button.ont-garden-pin-ico:focus-visible { outline: 2px solid var(--link); diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css index bdd9031b82154a5019782e6dcb8bf7589fafce09..5d4954812ad43f2ee7e63c71c7f0ae0b3e3fc95b 100644 --- a/server/static/theme_retro_craft.css +++ b/server/static/theme_retro_craft.css @@ -711,8 +711,7 @@ body.view-ontology span.ont-garden-vote-glyph { line-height: 1; } body.view-ontology button.ont-garden-pin-ico, -body.view-ontology a.ont-garden-vote-ico, -body.view-ontology span.ont-garden-pinned-here { +body.view-ontology a.ont-garden-vote-ico { font-size: 0.95rem; line-height: 1; padding: 0.1rem 0.35rem; @@ -732,10 +731,14 @@ body.view-ontology a.ont-garden-vote-ico:hover { border-color: #a68e6b; background: #ebe6dc; } -body.view-ontology span.ont-garden-pinned-here { +body.view-ontology button.ont-garden-pin-ico-active { border-color: #1a4a8c; background: color-mix(in srgb, #1a4a8c 10%, #f7f3eb); - cursor: default; + color: #0d2d5c; +} +body.view-ontology button.ont-garden-pin-ico-active:hover { + border-color: #a68e6b; + background: color-mix(in srgb, #1a4a8c 14%, #ebe6dc); } body.view-ontology form.ont-garden-pin-form { display: inline; diff --git a/test/browser_garden_pin.clj b/test/browser_garden_pin.clj index d4fa9ed60b45e872ec60e29307cfa61bcd9dd6b2..579976f4c2493a149f9eba081862ca07919f4fe9 100644 --- a/test/browser_garden_pin.clj +++ b/test/browser_garden_pin.clj @@ -58,10 +58,11 @@ (let [alice-token (oauth/fetch-bearer-token! base-url :username "alice") thread-tag "browser-garden-pin" raw (str "# " thread-tag "\n\n" - "~/gp-pin-a {alpha}\n" - "~/gp-pin-b {beta}\n" + "~/gp-parent {parent}\n" + "~/gp-parent/pin-a {alpha}\n" + "~/gp-parent/pin-b {beta}\n" "{pin test vote}\n" - "~/gp-pin-a 2:1 ~/gp-pin-b\n") + "~/gp-parent/pin-a 2:1 ~/gp-parent/pin-b\n") post-resp (oauth/http-post-json (str base-url "/api/v0/rpc") [{"Post" {"room" "public" @@ -77,23 +78,43 @@ (core/with-page [pg (core/new-page-from-context ctx)] (page/navigate pg (str base-url "/login")) (is (wait-for-text pg "body" "@alice" 15000) "alice session after login") - (page/navigate pg (str base-url "/~/gp-pin-a")) - (is (wait-for-text pg ".ont-item-shell" "~/gp-pin-a" 15000) "on item a page") + (page/navigate pg (str base-url "/~/gp-parent/pin-a")) + (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-a" 15000) "on item a page") ;; Native form POST /ui (data-navigate=full) — not fetch/eval (locator/click (page/locator pg ".ont-item-pin-zone form.ont-pin-form button[type=submit]")) - (is (wait-for-text pg "#slug-pin-hud" "gp-pin-a" 15000) + (is (wait-for-text pg "#slug-pin-hud" "gp-parent/pin-a" 15000) "HUD shows pinned item label after redirect") - (page/navigate pg (str base-url "/~/gp-pin-b")) - (is (wait-for-text pg ".ont-item-shell" "~/gp-pin-b" 15000) "on item b page") + (page/navigate pg (str base-url "/~/gp-parent/pin-b")) + (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-b" 15000) "on item b page") (is (wait-for-text pg "a.ont-vote-compare-btn" "vote" 10000) "vote link visible vs pinned item") - ;; HUD clears pin (POST set_garden_pin clear), not navigate to item + ;; Unpin from ranked child groups on parent page + (page/navigate pg (str base-url "/~/gp-parent")) + (is (wait-for-text pg ".ont-tab-panel-children" "ranked child groups" 15000) + "parent page shows ranked child groups") + (is (wait-for-text pg ".ont-ranking-list button.ont-garden-pin-ico-active" "📌" 10000) + "pinned row shows active pin in ranked list") + (locator/click (page/locator pg ".ont-ranking-list button.ont-garden-pin-ico-active")) + (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-parent/pin-a" 15000) + "HUD clears after unpin from ranked child list") + (is (wait-for-absence-substr pg ".ont-ranking-list" "ont-garden-vote-ico" 10000) + "vote icons removed from ranked list after unpin") + (page/navigate pg (str base-url "/~/gp-parent/pin-b")) + (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-b" 15000) "on item b page again") + (is (wait-for-absence-substr pg "body" "ont-vote-compare-btn" 15000) + "compare vote CTA removed after ranked-list unpin") + ;; HUD unpin still works from item page context + (page/navigate pg (str base-url "/~/gp-parent/pin-a")) + (locator/click (page/locator pg ".ont-item-pin-zone form.ont-pin-form button[type=submit]")) + (is (wait-for-text pg "#slug-pin-hud" "gp-parent/pin-a" 15000) + "re-pin from item page for HUD unpin test") + (page/navigate pg (str base-url "/~/gp-parent/pin-b")) (locator/click (page/locator pg "#slug-pin-hud button.slug-pin-hud-unpin-btn")) - (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-pin-a" 15000) + (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-parent/pin-a" 15000) "HUD clears after unpin from HUD button") (is (wait-for-absence-substr pg "body" "ont-vote-compare-btn" 15000) "compare vote CTA removed after HUD unpin on same reload") - (is (str/includes? (or (page/url pg) "") "/~/gp-pin-b") + (is (str/includes? (or (page/url pg) "") "/~/gp-parent/pin-b") "still on item b after HUD unpin")))))) (finally