Side A restores a real feature (persistent view counts) with new state (ViewStore), middleware wiring, canonical URL normalization, and a concrete integration test verifying counts and canonicalization—substantive new functionality. Side B is a clean but purely internal refactor unifying duplicate new-thread form markup/ids across public/room scopes, valuable for maintainability but lower-impact than adding a working feature with tests.
constitution · epochs · watch · epoch 3
c_55d1158891d1 (tommy-mor) vs c_01304e7d0f46 (tommy-mor)
download prompt · raw event · cmp_2468422174e759
council reasoning
A restores a full product capability: ViewStore on AppState, GET middleware with path filters, query canonicalization, view counts wired through garden/forum/search/try layouts, and real integration assertions—lasting infrastructure and UX. B is a worthwhile but narrower maintainability win that unifies public/room new-thread DOM ids and HtmlUiAction variants without adding new user-facing behavior.
Side A restores a substantive feature by introducing a persistent ViewStore into AppState, adding middleware to count GET page views with canonicalized URLs, wiring view counts into page layouts, and replacing a stub test with integration tests that verify counting and query-order canonicalization. Side B is primarily a UI refactor that unifies public and room new-thread forms, selectors, and actions under shared IDs, improving maintainability but with less direct long-term functional impact than reintroducing durable view counting.
sides
A — c_55d1158891d1 (tommy-mor)
message
[d6da7856] Resurrect page view counts (ViewStore + middleware + layout) (#138) * Wire ViewStore through AppState, view-count middleware, and HTML. - Add views module to lib, ViewStore on AppState (create_app_state and tests). - Implement canonical_view_url and GET view_count_middleware with path filters. - Layer middleware before with_state; add url crate for query canonicalization. - Pass canonical-key view counts into garden, forum, search, and try layouts. - Replace stub integration test with real view counter assertions. Co-authored-by: tommy <thmorriss@gmail.com> * Test vote/compare query canonicalization instead of search. Vote compare uses chromeless layout without view badge; assert the shared ViewStore count for permuted left/right query order via AppState. Co-authored-by: tommy <thmorriss@gmail.com> * Browser test: stop waiting for removed vote-compare shell. Assert vote compare via body.view-vote-compare and compare heading instead of .vote-compare-shell, which was intentionally removed from the HTML. Co-authored-by: tommy <thmorriss@gmail.com> * Drop vote-compare preview morph and browser assertion. vote_compare_post_success_js now only refreshes #vote-edge-history-region. Remove preview wrap markup, browser test wait on #vote-compare-preview, and unused forum re-export of ingest_entry_markup. Co-authored-by: tommy <thmorriss@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
diff preview
diff --git a/Cargo.lock b/Cargo.lock
index ad7e4fe6d4ba2f2b033916194c1ef1ed873f1d46..bf8153d9c723af97122c9ffdd4a7cfe82e853bb6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1805,6 +1805,7 @@ dependencies = [
"tower-http 0.5.2",
"tracing",
"tracing-subscriber",
+ "url",
"urlencoding",
"uuid",
]
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6d85b8cce5701482367d4be43287394e26f5896f..0d9cdcd21d94751babdf769e118d2c89d6a009d8 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -28,6 +28,7 @@ slug-types = { path = "../types" }
async-trait = "0.1"
base64 = "0.22"
postcard = { version = "1", features = ["use-std"] }
+url = "2"
urlencoding = "2"
[dev-dependencies]
diff --git a/server/src/html/editor.rs b/server/src/html/editor.rs
index a24e638802c8bb38699b39fdb0c53c22183a4a49..8ff58326eb75806eeec3937f088fa977c7f3886e 100644
--- a/server/src/html/editor.rs
+++ b/server/src/html/editor.rs
@@ -9,8 +9,9 @@ use maud::{html, Markup};
use serde::Deserialize;
use crate::{
- api::{validate_ingest_document, resolve_item},
+ api::{resolve_item, validate_ingest_document},
html::JsBuilder,
+ middleware::canonical_view_url,
reducer::ScopeId,
state::AppState,
};
@@ -25,7 +26,10 @@ fn bc_try() -> Markup {
}
/// The interactive editor page — `/try`.
-pub async fn editor_page(jar: CookieJar, uri: Uri) -> impl IntoResponse {
+pub async fn editor_page(State(state): State<AppState>, jar: CookieJar, uri: Uri) -> impl IntoResponse {
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let page = layout(
"try — slug.social",
"view-thread",
@@ -41,7 +45,7 @@ pub async fn editor_page(jar: CookieJar, uri: Uri) -> impl IntoResponse {
div id="editor-results" {}
}
},
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
None,
diff --git a/server/src/html/forum/feed.rs b/server/src/html/forum/feed.rs
index 63f196e0df251678bd649da97107e35a741f97fa..70321dd2a75e563282f7612f2b2fa6d57b9175d9 100644
--- a/server/src/html/forum/feed.rs
+++ b/server/src/html/forum/feed.rs
@@ -8,6 +8,7 @@ use maud::{html, Markup};
use crate::api::optional_principal;
use crate::canonical_path::canonicalize_tag;
+use crate::middleware::canonical_view_url;
use crate::reducer::{ReducerState, ScopeId};
use crate::state::AppState;
use crate::timeago;
@@ -218,6 +219,9 @@ pub async fn home(
let strip = auth_strip(&headers, &jar, &reduced_read);
drop(reduced_read);
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let page = layout(
"slug.social",
"view-thread",
@@ -251,7 +255,7 @@ pub async fn home(
(render_thread_feed(Some(&nav), "thread-feed", &public_rows, now))
(cli_panel(&["npx slugsocial public forum list"]))
},
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
None,
diff --git a/server/src/html/forum/mod.rs b/server/src/html/forum/mod.rs
index 53809e94e3b107a1ddb937c2e45325fc6b8706c6..bd60703924eee5b0e5a7f69c0a2dc526442512a2 100644
--- a/server/src/html/forum/mod.rs
+++ b/server/src/html/forum/mod.rs
@@ -20,7 +20,6 @@ pub use profile::user_profile_page;
pub use views::{room_page, room_thread_view, thread_view};
pub(crate) use access::{user_can_post_room, user_can_view_room};
-pub(crate) use ingest::ingest_entry_markup;
pub(crate) use new_thread::{fragment_new_thread_slot, login_to_post_hint_markup};
pub(crate) use room_members::room_members_section_markup;
pub(crate) use thread_morph::{
diff --git a/server/src/html/forum/post_single.rs b/server/src/html/forum/post_single.rs
index 6e36e80d076ddd8ca521c1450cbf8cb7ba6a28fe..91866b2cc3dea98d0c3847f54379245eca67894c 100644
--- a/server/src/html/forum/post_single.rs
+++ b/server/src/html/forum/post_single.rs
@@ -8,6 +8,7 @@ use maud::html;
use crate::api::optional_principal;
use crate::canonical_path::canonicalize_tag;
+use crate::middleware::canonical_view_url;
use crate::reducer::ScopeId;
use crate::state::AppState;
@@ -70,6 +71,9 @@ async fn thread_post_view_inner(
}
};
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let page = layout(
&format!("#{tag} / post #{index}"),
"view-thread",
@@ -87,7 +91,7 @@ async fn thread_post_view_inner(
p class="muted" { "post not found" }
}
},
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
None,
diff --git a/server/src/html/forum/profile.rs b/server/src/html/forum/profile.rs
index ebdc41fdd44e97a10a49ecd1ac5150ae390ec319..94701f8e9e293b64b889291c2145f97c1078c1c4 100644
--- a/server/src/html/forum/profile.rs
+++ b/server/src/html/forum/profile.rs
@@ -9,6 +9,7 @@ use maud::html;
use crate::api::optional_principal;
use crate::canonical_path::canonicalize_tag;
use crate::identity::parse_username;
+use crate::middleware::canonical_view_url;
use crate::state::AppState;
use super::ingest::{thread_nav_for_ingest, thread_post_index_in_scope};
@@ -74,6 +75,9 @@ pub async fn user_profile_page(
};
let now = now_ms();
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let page = layout(
&format!("@{canon}"),
"view-thread",
@@ -108,7 +112,7 @@ pub async fn user_profile_page(
}
(cli_panel(&[format!("npx slugsocial public forum list")]))
},
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
None,
diff --git a/server/src/html/forum/views.rs b/server/src/html/forum/views.rs
index 34c47a2a6838a0ec99415e326cbd78f2cc988bd7..b363f0d7c8af3d941e4f402c1f1b87100bb0d394 100644
--- a/server/src/html/forum/views.rs
+++ b/server/src/html/forum/views.rs
@@ -11,6 +11,7 @@ use serde_json::json;
use crate::api::optional_principal;
use crate::canonical_path::canonicalize_tag;
use crate::form_template::template_json_compact;
+use crate::middleware::canonical_view_url;
use crate::reducer::ScopeId;
use crate::state::AppState;
@@ -141,6 +142,9 @@ async fn thread_view_inner(
ScopeId::Room(r) => format!("npx slugsocial private {r} forum show {tag}"),
};
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let body = html! {
(strip)
nav class="breadcrumb" { (bc) }
@@ -166,7 +170,7 @@ async fn thread_view_inner(
&format!("#{tag}"),
"view-thread",
body,
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
None,
@@ -276,6 +280,9 @@ pub async fn room_page(
let audit_cli = format!("npx slugsocial private {room_id} audit");
drop(reduced);
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let slug_display = room_id
.split_once('/')
.map(|(_, slug)| slug)
@@ -296,7 +303,7 @@ pub async fn room_page(
}
(cli_panel(&[forum_cli, garden_cli, audit_cli]))
},
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
None,
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 2d5fcd903fe8f56800ad2c5564b704bd25f9b18d..9e08307e636ce7984766168db1187f84b8635201 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -14,11 +14,12 @@ 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},
+ middleware::canonical_view_url,
events::ThreadCapability,
form_template::template_json_compact,
html::{JsBuilder, ui_action::UI_RPC_FIELD, user_can_post_room},
path_types::ItemId,
- reducer::{ContentState, ReducerState, ScopeId, scope_from_room_wire},
+ reducer::{ContentState, ReducerState, ScopeId},
scope_rank::{ChildrenRankings, build_children_rankings},
state::AppState,
timeago,
@@ -28,7 +29,7 @@ use super::{
bc_path, bc_path_external, bc_segment,
breadcrumb_path::{ExternalOntologyPath, OntologyPath},
cli_panel,
- forum::{ThreadNav, ingest_entry_markup},
+ forum::ThreadNav,
layout, layout_full_bleed_chromeless, now_ms, ratio_pct, render_linkified_with_embeds_in_scope,
theme_from_jar, theme_next_from_uri,
};
@@ -224,39 +225,24 @@ fn vote_edge_history_markup(content: &ContentState, left: &ItemId, right: &ItemI
}
}
-/// After a successful vote post: morph the new card into `#vote-compare-preview` and refresh edge history.
+/// After a successful vote post: refresh edge history (no in-page preview card).
pub(crate) async fn vote_compare_post_success_js(
state: &AppState,
nav: &ThreadNav,
- room_wire: &str,
- thread_tag: &str,
+ _room_wire: &str,
+ _thread_tag: &str,
left: &ItemId,
right: &ItemId,
- post_id: &str,
- post_idx: Option<usize>,
+ _post_id: &str,
+ _post_idx: Option<usize>,
) -> String {
let reduced = state.reduced.read().await;
- let scope = scope_from_room_wire(room_wire);
- let Some(ing) = reduced.ingests_by_id.get(post_id).cloned() else {
- drop(reduced);
- return "console.warn('vote compare: new post not found');".to_string();
- };
- let idx = match post_idx {
- Some(i) => i,
- None => reduced
- .try_thread_post_index_chronological(&scope, thread_tag, post_id)
- .unwrap_or(0),
- };
- let viewer = None::<&str>;
- let now = now_ms();
let content = content_for_garden_view(&reduced, &nav.scope());
let edge_history = vote_edge_history_markup(content, left, right);
- let card = ingest_entry_markup(nav, thread_tag, idx, &ing, viewer, now, &reduced);
drop(reduced);
- let mut b = JsBuilder::new();
- b = b.morph_inner_selector("#vote-compare-preview", card);
- b = b.morph_inner_selector("#vote-edge-history-region", edge_history);
- b.build()
+ JsBuilder::new()
+ .morph_inner_selector("#vote-edge-history-region", edge_history)
+ .build()
}
fn item_display_path(item: &str) -> String {
@@ -515,6 +501,9 @@ pub async fn garden_index(
build_children_rankings(reduced.public(), &ItemId::ontology_root())
};
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let page = layout(
"~/",
"view-ontology view-ontology-light",
@@ -556,7 +545,7 @@ pub async fn garden_index(
}
(cli_panel(&["npx slugsocial garden tree"]))
},
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
Some("public"),
@@ -597,6 +586,9 @@ pub async fn external_garden_index(
build_children_rankings(reduced.public(), &parent)
};
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let page = layout(
"-/",
"view-ontology view-ontology-light",
@@ -637,7 +629,7 @@ pub async fn external_garden_index(
}
}
},
- None,
+ Some(view_count),
theme_from_jar(&jar),
&theme_next_from_uri(&uri),
Some("public"),
@@ -727,6 +719,9 @@ pub async fn room_external_garden_index(
build_children_rankings(content_for_garden_view(&reduced, &nav.scope()), &parent);
drop(reduced);
+ let url_key = canonical_view_url(&uri);
+ let view_count = state.views.get_views(&url_key);
+
let page = layout(
"-/",
… preview truncated; 10,577 characters omittedB — c_01304e7d0f46 (tommy-mor)
message
[d88719a1] refactor(html): unify public and room new-thread forms
- Single DOM contract: #new-thread-ui-slot, #new-thread-compose, #new-thread-form,
#new-thread-tag; shared check_ingest + post_ingest error wiring for all scopes.
- Replace ExpandPublic/ExpandRoom/SetRoomNewThreadComposeExpanded with
ExpandNewThreadForm { room_wire } and SetNewThreadComposeExpanded { room_wire, expanded };
room_wire "public" covers the home toolbar flow.
- SSE refresh resets #new-thread-compose form for any room_key.
- Browser test selectors updated for the unified ids.
Made-with: Cursordiff preview
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 03f4f7efe08df3ec160e50d08652bde7d6a9c4d2..422b520f527f64571f86f0ee3cb57edcbc21c0c3 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -63,11 +63,7 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str
crate::html::thread_feed_region_markup(state, Some(room_key), thread_id, None).await;
let builder = JsBuilder::new().morph_selector(&format!("#{feed_id}"), feed_markup);
- let builder = if room_key == "public" {
- builder.qs("#public-new-thread-compose form").reset()
- } else {
- builder.qs("#room-new-thread-compose form").reset()
- };
+ let builder = builder.qs("#new-thread-compose form").reset();
let builder = builder.if_current_path_matches(&thread_url, |builder| {
builder.morph_selector("#thread-feed-region", thread_feed_markup)
});
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 6ff9f1319e91f61bf8546eb65637c20f3a58483c..e979053ff1ba8c0e2bf55add0a32b7de11cf1e56 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -19,7 +19,7 @@ use crate::{
},
canonical_path::canonicalize_tag,
html::{
- fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,
+ fragment_new_thread_slot, login_to_post_hint_markup,
parse_html_ui_from_form, room_members_section_markup, thread_feed_html,
thread_feed_html_for_room, thread_feed_region_markup, thread_ui_collapse_redacted_post,
thread_ui_expand_post_full, thread_ui_expand_redacted_post, ui_js_warn, user_can_post_room,
@@ -139,24 +139,24 @@ async fn dispatch_ui_action(
}
}
}
- HtmlUiAction::ExpandPublicNewThreadForm => {
- let reduced = state.reduced.read().await;
- let user = session.map(|s| s.username.as_str());
- drop(reduced);
- let markup = if user.is_some() {
- fragment_public_new_thread_form(true)
- } else {
- login_to_post_hint_markup()
- };
- JsBuilder::new()
- .morph_inner_selector("#public-new-thread-ui-slot", markup)
- .into_response()
- }
- HtmlUiAction::ExpandRoomNewThreadForm { room_wire } => {
+ HtmlUiAction::ExpandNewThreadForm { room_wire } => {
let room_wire = room_wire.trim().to_string();
if room_wire.is_empty() {
return ui_js_warn("missing room").into_response();
}
+ if room_wire == "public" {
+ let reduced = state.reduced.read().await;
+ let user = session.map(|s| s.username.as_str());
+ drop(reduced);
+ let markup = if user.is_some() {
+ fragment_new_thread_slot(&ThreadNav::public(), true, false)
+ } else {
+ login_to_post_hint_markup()
+ };
+ return JsBuilder::new()
+ .morph_inner_selector("#new-thread-ui-slot", markup)
+ .into_response();
+ }
let reduced = state.reduced.read().await;
let user = session.map(|s| s.username.as_str());
if !reduced.rooms.contains(&room_wire) {
@@ -176,12 +176,12 @@ async fn dispatch_ui_action(
return ui_js_warn("bad room").into_response();
};
let markup = if can_post {
- fragment_room_new_thread_form(&nav, true, false)
+ fragment_new_thread_slot(&nav, true, false)
} else {
login_to_post_hint_markup()
};
JsBuilder::new()
- .morph_inner_selector("#room-new-thread-ui-slot", markup)
+ .morph_inner_selector("#new-thread-ui-slot", markup)
.into_response()
}
HtmlUiAction::SetRoomMembersExpanded { room_wire, expanded } => {
@@ -205,11 +205,28 @@ async fn dispatch_ui_action(
.morph_selector("#room-members-section", markup)
.into_response()
}
- HtmlUiAction::SetRoomNewThreadComposeExpanded { room_wire, expanded } => {
+ HtmlUiAction::SetNewThreadComposeExpanded { room_wire, expanded } => {
let room_wire = room_wire.trim().to_string();
if room_wire.is_empty() {
return ui_js_warn("missing room").into_response();
}
+ if room_wire == "public" {
+ let reduced = state.reduced.read().await;
+ let _user = session.map(|s| s.username.as_str());
+ drop(reduced);
+ let can_post = session.is_some();
+ let nav = ThreadNav::public();
+ let markup = if can_post {
+ fragment_new_thread_slot(&nav, true, expanded)
+ } else {
+ login_to_post_hint_markup()
+ };
+ let mut b = JsBuilder::new().morph_inner_selector("#new-thread-ui-slot", markup);
+ if expanded && can_post {
+ b = b.focus_selector("#new-thread-tag");
+ }
+ return b.into_response();
+ }
let reduced = state.reduced.read().await;
let user = session.map(|s| s.username.as_str());
if !reduced.rooms.contains(&room_wire) {
@@ -229,13 +246,13 @@ async fn dispatch_ui_action(
return ui_js_warn("bad room").into_response();
};
let markup = if can_post {
- fragment_room_new_thread_form(&nav, true, expanded)
+ fragment_new_thread_slot(&nav, true, expanded)
} else {
login_to_post_hint_markup()
};
- let mut b = JsBuilder::new().morph_inner_selector("#room-new-thread-ui-slot", markup);
+ let mut b = JsBuilder::new().morph_inner_selector("#new-thread-ui-slot", markup);
if expanded && can_post {
- b = b.focus_selector("#room-new-tag");
+ b = b.focus_selector("#new-thread-tag");
}
b.into_response()
}
diff --git a/server/src/html/forum/feed.rs b/server/src/html/forum/feed.rs
index 30b95ff6e85245a82199c96e41484d03c24989dd..1b4ae7baa3ad4757b74172d7b67f3f2b33d1075d 100644
--- a/server/src/html/forum/feed.rs
+++ b/server/src/html/forum/feed.rs
@@ -245,11 +245,13 @@ pub async fn home(
p class="muted" { "dark = time-ordered · light = vote-ranked" }
div class="thread-feed-toolbar" {
form method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandPublicNewThreadForm).expect("static json"));
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandNewThreadForm {
+ room_wire: "public".into(),
+ }).expect("static json"));
button type="submit" class="section-add-btn" { "+" }
}
}
- div id="public-new-thread-ui-slot" {}
+ div id="new-thread-ui-slot" {}
(render_thread_feed(Some(&nav), "thread-feed", &public_rows, now))
(cli_panel(&["npx slugsocial public forum list"]))
},
diff --git a/server/src/html/forum/mod.rs b/server/src/html/forum/mod.rs
index 8fbf3b71f41fafb474999ebb3d622e6bd5b5f313..bd60703924eee5b0e5a7f69c0a2dc526442512a2 100644
--- a/server/src/html/forum/mod.rs
+++ b/server/src/html/forum/mod.rs
@@ -20,9 +20,7 @@ pub use profile::user_profile_page;
pub use views::{room_page, room_thread_view, thread_view};
pub(crate) use access::{user_can_post_room, user_can_view_room};
-pub(crate) use new_thread::{
- fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,
-};
+pub(crate) use new_thread::{fragment_new_thread_slot, login_to_post_hint_markup};
pub(crate) use room_members::room_members_section_markup;
pub(crate) use thread_morph::{
thread_ui_collapse_redacted_post, thread_ui_expand_post_full, thread_ui_expand_redacted_post,
diff --git a/server/src/html/forum/new_thread.rs b/server/src/html/forum/new_thread.rs
index b2d24b8ab3ec7eee0edd16bed8ee6605e49bf408..4dd2654f953a7db6ee9b25c348f99906da93a94a 100644
--- a/server/src/html/forum/new_thread.rs
+++ b/server/src/html/forum/new_thread.rs
@@ -5,120 +5,48 @@ use serde_json::json;
use super::nav::ThreadNav;
-struct NewThreadIds {
- compose_section_id: &'static str,
- errors_id: &'static str,
- form_id: &'static str,
- tag_input_id: &'static str,
- text_input_id: Option<&'static str>,
-}
-
-const PUBLIC_IDS: NewThreadIds = NewThreadIds {
- compose_section_id: "public-new-thread-compose",
- errors_id: "public-new-thread-errors",
- form_id: "public-new-thread-form",
- tag_input_id: "new-thread-tag",
- text_input_id: Some("new-thread-text"),
-};
-
-const ROOM_IDS: NewThreadIds = NewThreadIds {
- compose_section_id: "room-new-thread-compose",
- errors_id: "room-new-thread-errors",
- form_id: "room-new-thread-form",
- tag_input_id: "room-new-tag",
- text_input_id: None,
-};
-
-#[derive(Clone, Copy)]
-enum NewThreadComposeKind {
- /// Home page: no client-side check RPC; post template omits `error_target` / `form_id`.
- Public,
- /// Room page: `check_ingest` + error targets on post (matches thread compose).
- Room,
-}
-
-/// Shared `<section class="compose">` for creating a thread + first post.
-fn new_thread_compose_section(room_wire: &str, ids: &NewThreadIds, kind: NewThreadComposeKind) -> Markup {
- let client_check = matches!(kind, NewThreadComposeKind::Room);
- let (tag_placeholder, text_placeholder, submit_label) = match kind {
- NewThreadComposeKind::Public => (
- "thread-title-slug-here",
- "Hello threadgoers!! Behold my new thread!",
- "create thread / make first post",
- ),
- NewThreadComposeKind::Room => (
- "thread-topic-slug-here",
- "First post body…",
- "post",
- ),
- };
+/// Stable ids shared by public home and private room “new thread” UI (`#new-thread-ui-slot`).
+const COMPOSE_SECTION_ID: &str = "new-thread-compose";
+const ERRORS_ID: &str = "new-thread-errors";
+const FORM_ID: &str = "new-thread-form";
+const TAG_INPUT_ID: &str = "new-thread-tag";
+/// Shared `<section class="compose">`: check + post with `error_target` / `form_id` (same as thread reply compose).
+fn new_thread_compose_section(room_wire: &str) -> Markup {
html! {
- section class="compose" id=(ids.compose_section_id) {
- div id=(ids.errors_id) {}
- @if client_check {
- form id=(ids.form_id) method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
- "action": "check_ingest",
+ section class="compose" id=(COMPOSE_SECTION_ID) {
+ div id=(ERRORS_ID) {}
+ form id=(FORM_ID) method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
+ "action": "check_ingest",
+ "room": room_wire,
+ "thread_tag": {"$form": "thread_tag"},
+ "text": {"$form": "text"},
+ "error_target": ERRORS_ID,
+ "form_id": FORM_ID,
+ })).unwrap()) {
+ input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&json!({
+ "action": "post_ingest",
"room": room_wire,
"thread_tag": {"$form": "thread_tag"},
"text": {"$form": "text"},
- "error_target": ids.errors_id,
-
… preview truncated; 13,556 characters omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.