From 2c83ed3d9d2a794e32fc73ef7161866fad067a29 Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 13 Jul 2025 18:42:08 -0400 Subject: [PATCH] add: "ask about this" from neospring --- crates/app/src/langs/en-US.toml | 1 + crates/app/src/public/html/components.lisp | 43 ++++++++++++++----- crates/app/src/public/html/misc/requests.lisp | 2 +- .../routes/api/v1/communities/questions.rs | 7 +++ crates/app/src/routes/api/v1/mod.rs | 2 + crates/app/src/routes/pages/communities.rs | 20 +++++++-- crates/core/src/database/posts.rs | 39 +++++++++++++---- crates/core/src/database/questions.rs | 36 +++++++++++++--- crates/core/src/model/communities.rs | 3 ++ 9 files changed, 122 insertions(+), 31 deletions(-) diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index abc24e1..788ca48 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -131,6 +131,7 @@ version = "1.0.0" "communities:label.edit_content" = "Edit content" "communities:label.repost" = "Repost" "communities:label.quote_post" = "Quote post" +"communities:label.ask_about_this" = "Ask about this" "communities:label.search_results" = "Search results" "communities:label.query" = "Query" "communities:label.join_new" = "Join new" diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index bd03879..0ce8821 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -118,7 +118,7 @@ (div ("class" "card-nest post_outer:{{ post.id }} post_outer") ("is_repost" "{{ is_repost }}") - (text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}") + (text "{% if question -%} {{ self::question(question=question[0], owner=question[1], asking_about=question[2], profile=owner) }} {% else %}") (div ("class" "card small") (a @@ -321,7 +321,6 @@ ("class" "button camo small") ("target" "_blank") (text "{{ icon \"external-link\" }}")) - (text "{% if user -%}") (div ("class" "dropdown") (button @@ -335,6 +334,7 @@ (b ("class" "title") (text "{{ text \"general:label.share\" }}")) + (text "{% if user -%}") (button ("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}', true])") (text "{{ icon \"repeat-2\" }}") @@ -357,7 +357,14 @@ (span (text "BlueSky"))) (text "{%- endif %}") - (text "{% if user.id != post.owner -%}") + (a + ("class" "button") + ("href" "/@{{ owner.username }}?asking_about={{ post.id }}") + (icon (text "reply")) + (span + (str (text "communities:label.ask_about_this")))) + (text "{%- endif %}") + (text "{% if user and user.id != post.owner -%}") (b ("class" "title") (text "{{ text \"general:label.safety\" }}")) @@ -367,12 +374,12 @@ (text "{{ icon \"flag\" }}") (span (text "{{ text \"general:action.report\" }}"))) - (text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}") + (text "{%- endif %} {% if user and (user.id == post.owner) or is_helper or can_manage_post %}") (b ("class" "title") (text "{{ text \"general:action.manage\" }}")) ; forge stuff - (text "{% if community and community.is_forge -%} {% if post.is_open -%}") + (text "{% if user and community and community.is_forge -%} {% if post.is_open -%}") (button ("class" "green") ("onclick" "trigger('me::update_open', ['{{ post.id }}', false])") @@ -388,7 +395,7 @@ (text "{{ text \"forge:action.reopen\" }}"))) (text "{%- endif %} {%- endif %}") ; owner stuff - (text "{% if user.id == post.owner -%}") + (text "{% if user and user.id == post.owner -%}") (a ("href" "/post/{{ post.id }}#/edit") (text "{{ icon \"pen\" }}") @@ -420,8 +427,7 @@ (text "{{ icon \"undo\" }}") (span (text "{{ text \"general:action.restore\" }}"))) - (text "{%- endif %} {%- endif %}"))) - (text "{%- endif %}")))) + (text "{%- endif %} {%- endif %}")))))) (text "{% if community and show_community and community.id != config.town_square or question %}")) (text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0 -%}") @@ -630,7 +636,7 @@ --{{ css }}: {{ color|color }} !important; }")) -(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false, profile=false) -%}") +(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, asking_about=false, show_community=true, secondary=false, profile=false) -%}") (div ("class" "card question {% if secondary -%}secondary{%- endif %} flex gap-2") (text "{% if owner.id == 0 or question.context.mask_owner -%}") @@ -700,6 +706,10 @@ (text "{{ question.content|markdown|safe }}")) ; question drawings (text "{{ self::post_media(upload_ids=question.drawings) }}") + ; asking about + (text "{% if asking_about -%}") + (text "{{ self::post(post=asking_about[1], owner=asking_about[0], secondary=not secondary, show_community=false) }}") + (text "{%- endif %}") ; anonymous user ip thing ; this is only shown if the post author is anonymous AND we are a helper (text "{% if is_helper and (owner.id == 0 or question.context.mask_owner) -%}") @@ -736,6 +746,7 @@ ("class" "no_p_margin") (text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}"))) (form + ("id" "create_question_form") ("class" "card flex flex-col gap-2") ("onsubmit" "create_question_from_form(event)") (div @@ -822,6 +833,15 @@ (script (text "globalThis.gerald = null; + // asking about + globalThis.asking_about = new URLSearchParams(window.location.search).get(\"asking_about\"); + + if (asking_about) { + document.getElementById(\"create_question_form\").innerHTML += + `
Asking about: ${asking_about} (cancel)`; + } + + // ... async function create_question_from_form(e) { e.preventDefault(); await trigger(\"atto::debounce\", [\"questions::create\"]); @@ -843,7 +863,8 @@ receiver: \"{{ receiver }}\", community: \"{{ community }}\", is_global: \"{{ is_global }}\" == \"true\", - mask_owner: (e.target.mask_owner || { checked:false }).checked + mask_owner: (e.target.mask_owner || { checked:false }).checked, + asking_about, }), ); @@ -872,7 +893,7 @@ (text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}") (div ("class" "card-nest") - (text "{{ self::question(question=question[0], owner=question[1], show_community=show_community) }}") + (text "{{ self::question(question=question[0], owner=question[1], asking_about=false, show_community=show_community) }}") (div ("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}") (div diff --git a/crates/app/src/public/html/misc/requests.lisp b/crates/app/src/public/html/misc/requests.lisp index 9ba68d2..b9700f0 100644 --- a/crates/app/src/public/html/misc/requests.lisp +++ b/crates/app/src/public/html/misc/requests.lisp @@ -92,7 +92,7 @@ (text "{%- endif %} {% endfor %} {% for question in questions %}") (div ("class" "card-nest") - (text "{{ components::question(question=question[0], owner=question[1], profile=user) }}") + (text "{{ components::question(question=question[0], owner=question[1], asking_about=question[2], profile=user) }}") (form ("class" "card flex flex-col gap-2") ("onsubmit" "answer_question_from_form(event, '{{ question[0].id }}')") diff --git a/crates/app/src/routes/api/v1/communities/questions.rs b/crates/app/src/routes/api/v1/communities/questions.rs index 1d1a7ba..e67b91b 100644 --- a/crates/app/src/routes/api/v1/communities/questions.rs +++ b/crates/app/src/routes/api/v1/communities/questions.rs @@ -96,6 +96,13 @@ pub async fn create_request( props.context.mask_owner = true; } + if !req.asking_about.is_empty() && !req.is_global { + props.context.asking_about = match req.asking_about.parse::() { + Ok(x) => Some(x), + Err(e) => return Json(Error::MiscError(e.to_string()).into()), + } + } + match data .create_question(props, drawings.iter().map(|x| x.to_vec()).collect()) .await diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 38f915e..d4e19c1 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -865,6 +865,8 @@ pub struct CreateQuestion { pub community: String, #[serde(default)] pub mask_owner: bool, + #[serde(default)] + pub asking_about: String, } #[derive(Deserialize)] diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index 30d2ce0..59dc982 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use super::{render_error, PaginatedQuery, RepostsQuery, SearchedQuery}; use crate::{ assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, State, @@ -798,7 +800,11 @@ pub async fn post_request( let (_, reposting) = data.0.get_post_reposting(&post, &ignore_users, &user).await; // check question - let question = match data.0.get_post_question(&post, &ignore_users).await { + let question = match data + .0 + .get_post_question(&post, &ignore_users, &mut HashMap::new()) + .await + { Ok(q) => q, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }; @@ -918,7 +924,11 @@ pub async fn reposts_request( let reposting = data.0.get_post_reposting(&post, &ignore_users, &user).await; // check question - let question = match data.0.get_post_question(&post, &ignore_users).await { + let question = match data + .0 + .get_post_question(&post, &ignore_users, &mut HashMap::new()) + .await + { Ok(q) => q, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }; @@ -1069,7 +1079,11 @@ pub async fn likes_request( .await; // check question - let question = match data.0.get_post_question(&post, &ignore_users).await { + let question = match data + .0 + .get_post_question(&post, &ignore_users, &mut HashMap::new()) + .await + { Ok(q) => q, Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index f17bbea..701c053 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -22,10 +22,11 @@ pub type FullPost = ( User, Community, Option<(User, Post)>, - Option<(Question, User)>, + Option<(Question, User, Option<(User, Post)>)>, Option<(Poll, bool, bool)>, Option, ); +pub type FullQuestion = (Question, User, Option<(User, Post)>); macro_rules! private_post_replying { ($post:ident, $replying_posts:ident, $ua1:ident, $data:ident) => { @@ -224,8 +225,14 @@ impl DataManager { &self, post: &Post, ignore_users: &[usize], - ) -> Result> { + seen_questions: &mut HashMap, + ) -> Result> { if post.context.answering != 0 { + if let Some(q) = seen_questions.get(&post.context.answering) { + return Ok(Some(q.to_owned())); + } + + // ... let question = self.get_question_by_id(post.context.answering).await?; if ignore_users.contains(&question.owner) { @@ -238,7 +245,11 @@ impl DataManager { self.get_user_by_id_with_void(question.owner).await? }; - Ok(Some((question, user))) + let asking_about = self.get_question_asking_about(&question).await?; + let full_question = (question, user, asking_about); + + seen_questions.insert(post.context.answering, full_question.to_owned()); + Ok(Some(full_question)) } else { Ok(None) } @@ -322,7 +333,7 @@ impl DataManager { Post, User, Option<(User, Post)>, - Option<(Question, User)>, + Option<(Question, User, Option<(User, Post)>)>, Option<(Poll, bool, bool)>, Option, )>, @@ -332,6 +343,7 @@ impl DataManager { let mut users: HashMap = HashMap::new(); let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new(); let mut seen_stacks: HashMap = HashMap::new(); + let mut seen_questions: HashMap = HashMap::new(); let mut replying_posts: HashMap = HashMap::new(); for post in posts { @@ -373,7 +385,8 @@ impl DataManager { post.clone(), ua.clone(), reposting, - self.get_post_question(&post, ignore_users).await?, + self.get_post_question(&post, ignore_users, &mut seen_questions) + .await?, self.get_post_poll(&post, user).await?, stack, )); @@ -454,7 +467,8 @@ impl DataManager { post.clone(), ua, reposting, - self.get_post_question(&post, ignore_users).await?, + self.get_post_question(&post, ignore_users, &mut seen_questions) + .await?, self.get_post_poll(&post, user).await?, stack, )); @@ -477,6 +491,7 @@ impl DataManager { let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new(); let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new(); let mut seen_stacks: HashMap = HashMap::new(); + let mut seen_questions: HashMap = HashMap::new(); let mut replying_posts: HashMap = HashMap::new(); let mut memberships: HashMap = HashMap::new(); @@ -544,7 +559,8 @@ impl DataManager { ua.clone(), community.to_owned(), reposting, - self.get_post_question(&post, ignore_users).await?, + self.get_post_question(&post, ignore_users, &mut seen_questions) + .await?, self.get_post_poll(&post, user).await?, stack, )); @@ -643,7 +659,8 @@ impl DataManager { ua, community, reposting, - self.get_post_question(&post, ignore_users).await?, + self.get_post_question(&post, ignore_users, &mut seen_questions) + .await?, self.get_post_poll(&post, user).await?, stack, )); @@ -716,8 +733,12 @@ impl DataManager { } // question - if let Some((_, ref mut x)) = post.4 { + if let Some((_, ref mut x, ref mut y)) = post.4 { x.clean(); + + if y.is_some() { + y.as_mut().unwrap().0.clean(); + } } // ... diff --git a/crates/core/src/database/questions.rs b/crates/core/src/database/questions.rs index 1cee527..900d68c 100644 --- a/crates/core/src/database/questions.rs +++ b/crates/core/src/database/questions.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use oiseau::cache::Cache; use tetratto_shared::unix_epoch_timestamp; use crate::model::addr::RemoteAddr; +use crate::model::communities::Post; use crate::model::communities_permissions::CommunityPermission; use crate::model::uploads::{MediaType, MediaUpload}; use crate::model::{ @@ -38,13 +39,26 @@ impl DataManager { auto_method!(get_question_by_id()@get_question_from_row -> "SELECT * FROM questions WHERE id = $1" --name="question" --returns=Question --cache-key-tmpl="atto.question:{}"); + /// Get the post a given question is asking about. + pub async fn get_question_asking_about( + &self, + question: &Question, + ) -> Result> { + Ok(if let Some(id) = question.context.asking_about { + let post = self.get_post_by_id(id).await?; + Some((self.get_user_by_id(post.owner).await?, post)) + } else { + None + }) + } + /// Fill the given vector of questions with their owner as well. pub async fn fill_questions( &self, questions: Vec, ignore_users: &[usize], - ) -> Result> { - let mut out: Vec<(Question, User)> = Vec::new(); + ) -> Result)>> { + let mut out: Vec<(Question, User, Option<(User, Post)>)> = Vec::new(); let mut seen_users: HashMap = HashMap::new(); for question in questions { @@ -53,7 +67,8 @@ impl DataManager { } if let Some(ua) = seen_users.get(&question.owner) { - out.push((question, ua.to_owned())); + let asking_about = self.get_question_asking_about(&question).await?; + out.push((question, ua.to_owned(), asking_about)); } else { let user = if question.owner == 0 { User::anonymous() @@ -62,7 +77,9 @@ impl DataManager { }; seen_users.insert(question.owner, user.clone()); - out.push((question, user)); + + let asking_about = self.get_question_asking_about(&question).await?; + out.push((question, user, asking_about)); } } @@ -72,12 +89,17 @@ impl DataManager { /// Filter to update questions to clean their owner for public APIs. pub fn questions_owner_filter( &self, - questions: &Vec<(Question, User)>, - ) -> Vec<(Question, User)> { - let mut out: Vec<(Question, User)> = Vec::new(); + questions: &Vec<(Question, User, Option<(User, Post)>)>, + ) -> Vec<(Question, User, Option<(User, Post)>)> { + let mut out: Vec<(Question, User, Option<(User, Post)>)> = Vec::new(); for mut question in questions.clone() { question.1.clean(); + + if question.2.is_some() { + question.2.as_mut().unwrap().0.clean(); + } + out.push(question); } diff --git a/crates/core/src/model/communities.rs b/crates/core/src/model/communities.rs index 8a4ab9a..14f640f 100644 --- a/crates/core/src/model/communities.rs +++ b/crates/core/src/model/communities.rs @@ -387,6 +387,9 @@ pub struct QuestionContext { /// If the owner is shown as anonymous in the UI. #[serde(default)] pub mask_owner: bool, + /// The POST this question is asking about. + #[serde(default)] + pub asking_about: Option, } #[derive(Clone, Debug, Serialize, Deserialize)]