diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs index 76ba96e45c9e16291a6ccd8096fdd01b274c1560..2d5fcd903fe8f56800ad2c5564b704bd25f9b18d 100644 --- a/server/src/html/garden.rs +++ b/server/src/html/garden.rs @@ -9,30 +9,28 @@ use serde::Deserialize; use serde_json::json; use std::collections::HashSet; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD as B64_ENGINE, Engine as _}; +use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as B64_ENGINE}; use crate::{ api::optional_principal, canonical_path::{canonicalize_item, canonicalize_tag}, - form_template::template_json_compact, - html::{ - ui_action::UI_RPC_FIELD, - user_can_post_room, - JsBuilder, - }, events::ThreadCapability, + form_template::template_json_compact, + html::{JsBuilder, ui_action::UI_RPC_FIELD, user_can_post_room}, path_types::ItemId, - reducer::{scope_from_room_wire, ContentState, ReducerState, ScopeId}, - scope_rank::{build_children_rankings, ChildrenRankings}, + reducer::{ContentState, ReducerState, ScopeId, scope_from_room_wire}, + scope_rank::{ChildrenRankings, build_children_rankings}, state::AppState, timeago, }; use super::{ - bc_path, bc_path_external, bc_segment, cli_panel, layout, layout_full_bleed_chromeless, now_ms, - ratio_pct, render_linkified_with_embeds_in_scope, theme_from_jar, theme_next_from_uri, + bc_path, bc_path_external, bc_segment, breadcrumb_path::{ExternalOntologyPath, OntologyPath}, - forum::{ingest_entry_markup, ThreadNav}, + cli_panel, + forum::{ThreadNav, ingest_entry_markup}, + layout, layout_full_bleed_chromeless, now_ms, ratio_pct, render_linkified_with_embeds_in_scope, + theme_from_jar, theme_next_from_uri, }; /// `GET /vote/compare` — pairs `left` / `right` query params with optional `thread`. @@ -90,7 +88,11 @@ fn canonical_edge_items(a: &ItemId, b: &ItemId) -> (ItemId, ItemId) { } /// All votes whose endpoints are exactly this unordered pair (unsorted). -fn edge_vote_entries_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) -> Vec { +fn edge_vote_entries_for_pair( + content: &ContentState, + a: &ItemId, + b: &ItemId, +) -> Vec { let (lo, hi) = canonical_edge_items(a, b); let lo_s = lo.as_str(); let hi_s = hi.as_str(); @@ -107,7 +109,11 @@ fn edge_vote_entries_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) -> .collect() } -fn ratios_for_compare_page(v: &crate::reducer::VoteData, page_left: &ItemId, page_right: &ItemId) -> (i32, i32) { +fn ratios_for_compare_page( + v: &crate::reducer::VoteData, + page_left: &ItemId, + page_right: &ItemId, +) -> (i32, i32) { let pl = page_left.as_str(); let pr = page_right.as_str(); match (v.a.as_str(), v.b.as_str()) { @@ -121,11 +127,7 @@ fn left_share_normalized(ratio_left: i32, ratio_right: i32) -> f64 { let l = ratio_left.max(0) as f64; let r = ratio_right.max(0) as f64; let sum = l + r; - if sum <= 0.0 { - 0.5 - } else { - l / sum - } + if sum <= 0.0 { 0.5 } else { l / sum } } /// Stronger preference for **`page_left` first**; ties **newer first**. @@ -177,11 +179,7 @@ fn vote_thread_tags_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) -> v.into_iter().map(|t| canonicalize_tag(&t)).collect() } -fn vote_edge_history_markup( - content: &ContentState, - left: &ItemId, - right: &ItemId, -) -> maud::Markup { +fn vote_edge_history_markup(content: &ContentState, left: &ItemId, right: &ItemId) -> maud::Markup { let votes = edge_vote_entries_for_pair(content, left, right); let votes = sort_votes_for_compare_display(votes, left, right); let legend_left = item_display_path(left.as_str()); @@ -276,7 +274,12 @@ fn item_code_label(item: &str) -> String { item_display_path(item) } -fn vote_compare_href(nav: &ThreadNav, left: &ItemId, right: &ItemId, thread_override: Option<&str>) -> String { +fn vote_compare_href( + nav: &ThreadNav, + left: &ItemId, + right: &ItemId, + thread_override: Option<&str>, +) -> String { let left_q = urlencoding::encode(left.as_str()); let right_q = urlencoding::encode(right.as_str()); let base = format!( @@ -299,7 +302,8 @@ fn ont_pin_vote_controls( next_path: &str, ) -> maud::Markup { let room_wire = nav.room_wire.clone(); - let current = ItemId::parse(current_storage).unwrap_or_else(|| ItemId::opaque(current_storage.to_string())); + let current = ItemId::parse(current_storage) + .unwrap_or_else(|| ItemId::opaque(current_storage.to_string())); let pin_matches_scope = pinned_room_and_item .map(|(r, _)| r == nav.room_wire.as_str()) .unwrap_or(false); @@ -307,26 +311,22 @@ fn ont_pin_vote_controls( .filter(|_| pin_matches_scope) .map(|(_, i)| i); - let pin_rpc = template_json_compact( - &json!({ - "action": "set_garden_pin", - "clear": false, - "room_wire": room_wire, - "item_storage": current.as_str(), - "next": next_path, - "form_action": "/ui", - }), - ) + let pin_rpc = template_json_compact(&json!({ + "action": "set_garden_pin", + "clear": false, + "room_wire": room_wire, + "item_storage": current.as_str(), + "next": next_path, + "form_action": "/ui", + })) .expect("pin rpc json"); - let unpin_rpc = template_json_compact( - &json!({ - "action": "set_garden_pin", - "clear": true, - "room_wire": "", - "next": next_path, - "form_action": "/ui", - }), - ) + 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! { @@ -497,9 +497,9 @@ fn room_scope_has_garden_content(reduced: &ReducerState, nav: &ThreadNav) -> boo fn content_for_garden_view<'a>(reduced: &'a ReducerState, scope: &ScopeId) -> &'a ContentState { match scope { ScopeId::Public => reduced.public(), - ScopeId::Room(_) => reduced.content_for_scope(scope).expect( - "room garden only renders after room_scope_has_garden_content returned true", - ), + ScopeId::Room(_) => reduced + .content_for_scope(scope) + .expect("room garden only renders after room_scope_has_garden_content returned true"), } } @@ -723,10 +723,8 @@ pub async fn room_external_garden_index( } let ext_path = ExternalOntologyPath::from_input(""); let parent = ItemId::parse("https://.").unwrap(); - let child_rankings = build_children_rankings( - content_for_garden_view(&reduced, &nav.scope()), - &parent, - ); + let child_rankings = + build_children_rankings(content_for_garden_view(&reduced, &nav.scope()), &parent); drop(reduced); let page = layout( @@ -953,48 +951,74 @@ fn build_rank_history( None => return vec![], Some(e) => e, }; - entries.iter().map(|e| { - // Resolve caused_by: votes from this ingest that directly touched this item. - let caused_by: Vec = reduced.ingests_by_id - .get(&e.post_id) - .and_then(|ing| crate::dsl::parse_full(&ing.raw).ok()) - .map(|doc| { - doc.statements.into_iter().filter_map(|s| { - if let crate::dsl::Stmt::Vote { item1, item2, ratio_left, ratio_right, explanation } = s { - let a_str = crate::canonical_path::canonicalize_item(&item1); - let b_str = crate::canonical_path::canonicalize_item(&item2); - if a_str == item || b_str == item { - Some(crate::reducer::VoteData { - ts: e.ts, - a: ItemId::parse(&a_str).unwrap_or_else(|| ItemId::opaque(a_str)), - b: ItemId::parse(&b_str).unwrap_or_else(|| ItemId::opaque(b_str)), - ratio_left, ratio_right, - body: explanation, - principal: reduced.ingests_by_id.get(&e.post_id) - .map(|ing| ing.principal.clone()) - .unwrap_or_default(), - delegate: reduced.ingests_by_id.get(&e.post_id).and_then(|ing| ing.delegate.clone()), - thread_tag: e.thread.clone(), - }) - } else { None } - } else { None } - }).collect() - }) - .unwrap_or_default(); - - let thread_post_index = - reduced.thread_post_index_chronological(scope, &e.thread, &e.post_id); - - RankHistoryEntryView { - ts: e.ts, - scope_rank: e.scope_rank, - scope_total: e.scope_total, - scope_rank_delta: e.scope_rank_delta, - thread: e.thread.clone(), - thread_post_index, - caused_by, - } - }).collect() + entries + .iter() + .map(|e| { + // Resolve caused_by: votes from this ingest that directly touched this item. + let caused_by: Vec = reduced + .ingests_by_id + .get(&e.post_id) + .and_then(|ing| crate::dsl::parse_full(&ing.raw).ok()) + .map(|doc| { + doc.statements + .into_iter() + .filter_map(|s| { + if let crate::dsl::Stmt::Vote { + item1, + item2, + ratio_left, + ratio_right, + explanation, + } = s + { + let a_str = crate::canonical_path::canonicalize_item(&item1); + let b_str = crate::canonical_path::canonicalize_item(&item2); + if a_str == item || b_str == item { + Some(crate::reducer::VoteData { + ts: e.ts, + a: ItemId::parse(&a_str) + .unwrap_or_else(|| ItemId::opaque(a_str)), + b: ItemId::parse(&b_str) + .unwrap_or_else(|| ItemId::opaque(b_str)), + ratio_left, + ratio_right, + body: explanation, + principal: reduced + .ingests_by_id + .get(&e.post_id) + .map(|ing| ing.principal.clone()) + .unwrap_or_default(), + delegate: reduced + .ingests_by_id + .get(&e.post_id) + .and_then(|ing| ing.delegate.clone()), + thread_tag: e.thread.clone(), + }) + } else { + None + } + } else { + None + } + }) + .collect() + }) + .unwrap_or_default(); + + let thread_post_index = + reduced.thread_post_index_chronological(scope, &e.thread, &e.post_id); + + RankHistoryEntryView { + ts: e.ts, + scope_rank: e.scope_rank, + scope_total: e.scope_total, + scope_rank_delta: e.scope_rank_delta, + thread: e.thread.clone(), + thread_post_index, + caused_by, + } + }) + .collect() } fn build_item_page_view_model( @@ -1301,23 +1325,28 @@ async fn vote_compare_inner( let edge_history = vote_edge_history_markup(content, &left, &right); drop(reduced); - let title = format!("vote — {} vs {}", item_display_path(left.as_str()), item_display_path(right.as_str())); - let next_path = uri.path_and_query().map(|pq| pq.as_str().to_string()).unwrap_or_else(|| "/vote/compare".into()); - - let rpc_json = template_json_compact( - &json!({ - "action": "vote_compare_post", - "room": nav.room_wire, - "thread_tag": {"$form": "thread_tag"}, - "left_item": left.as_str(), - "right_item": right.as_str(), - "ratio_left": {"$form": "ratio_left"}, - "ratio_right": {"$form": "ratio_right"}, - "explanation": {"$form": "explanation"}, - "next": next_path, - "form_action": "/ui", - }), - ) + let title = format!( + "vote — {} vs {}", + item_display_path(left.as_str()), + item_display_path(right.as_str()) + ); + let next_path = uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/vote/compare".into()); + + let rpc_json = template_json_compact(&json!({ + "action": "vote_compare_post", + "room": nav.room_wire, + "thread_tag": {"$form": "thread_tag"}, + "left_item": left.as_str(), + "right_item": right.as_str(), + "ratio_left": {"$form": "ratio_left"}, + "ratio_right": {"$form": "ratio_right"}, + "explanation": {"$form": "explanation"}, + "next": next_path, + "form_action": "/ui", + })) .expect("vote compare rpc json"); let body = html! { @@ -1532,11 +1561,23 @@ mod tests { .iter() .map(|r| r.item.as_str()) .collect(); - assert_eq!(names, vec!["https://slug.social/~/topic/a", "https://slug.social/~/topic/b"]); + assert_eq!( + names, + vec![ + "https://slug.social/~/topic/a", + "https://slug.social/~/topic/b" + ] + ); use crate::path_types::ItemId; assert!( - model.child_rankings.unranked_items.contains(&ItemId::parse("https://slug.social/~/topic/kid1").unwrap()) - || model.child_rankings.unranked_items.contains(&ItemId::parse("https://slug.social/~/topic/kid2").unwrap()) + model + .child_rankings + .unranked_items + .contains(&ItemId::parse("https://slug.social/~/topic/kid1").unwrap()) + || model + .child_rankings + .unranked_items + .contains(&ItemId::parse("https://slug.social/~/topic/kid2").unwrap()) ); } @@ -1609,11 +1650,8 @@ mod tests { 1, "@00000000-0000-0000-0000-000000000000:test:local/test\n~/x {x}\n", ); - let model = build_item_page_view_model( - &reduced, - &ScopeId::Public, - "https://slug.social/~/", - ); + let model = + build_item_page_view_model(&reduced, &ScopeId::Public, "https://slug.social/~/"); assert_eq!(model.child_rankings.unranked_items.len(), 1); assert_eq!( model.child_rankings.unranked_items[0].as_str(),