From f8ce709d94c66c9a0a509d957efb5adde95dc2d6 Mon Sep 17 00:00:00 2001 From: trisua Date: Wed, 2 Oct 2024 20:59:01 -0400 Subject: [PATCH] add: threads add: response replies add: question context fix: moderator view for questions add: response thread UI --- Cargo.lock | 2 +- crates/authbeam/src/lib.rs | 2 +- crates/databeam/src/lib.rs | 2 +- crates/rainbeam/Cargo.toml | 2 +- crates/rainbeam/sql/add_context_col.sql | 1 + crates/rainbeam/sql/add_reply_col.sql | 1 + crates/rainbeam/src/database.rs | 198 +++++++++------- crates/rainbeam/src/model.rs | 38 ++- crates/rainbeam/src/routing/api/reactions.rs | 2 +- crates/rainbeam/src/routing/api/responses.rs | 6 +- crates/rainbeam/src/routing/pages/circles.rs | 10 +- crates/rainbeam/src/routing/pages/mod.rs | 25 +- crates/rainbeam/src/routing/pages/profile.rs | 14 +- crates/rainbeam/src/routing/pages/search.rs | 6 +- crates/rainbeam/static/js/app.js | 8 + crates/rainbeam/static/js/questions.js | 58 ++--- crates/rainbeam/static/js/responses.js | 3 +- crates/rainbeam/static/style.css | 3 +- crates/rainbeam/templates/base.html | 2 +- crates/rainbeam/templates/circle/inbox.html | 1 + .../templates/circle/settings/base.html | 22 +- crates/rainbeam/templates/comment.html | 4 +- .../components/more_response_options.html | 12 + .../templates/components/response.html | 221 ++++-------------- .../templates/components/response_inner.html | 179 ++++++++++++++ .../templates/components/response_title.html | 39 +++- crates/rainbeam/templates/compose.html | 2 + crates/rainbeam/templates/inbox.html | 36 ++- crates/rainbeam/templates/profile/base.html | 13 ++ crates/rainbeam/templates/question.html | 76 +++++- crates/rainbeam/templates/response.html | 4 +- crates/shared/src/lib.rs | 2 +- 32 files changed, 660 insertions(+), 334 deletions(-) create mode 100644 crates/rainbeam/sql/add_context_col.sql create mode 100644 crates/rainbeam/sql/add_reply_col.sql create mode 100644 crates/rainbeam/templates/components/response_inner.html diff --git a/Cargo.lock b/Cargo.lock index 183787f..6e1516e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1905,7 +1905,7 @@ dependencies = [ [[package]] name = "rainbeam" -version = "1.6.1" +version = "1.7.0" dependencies = [ "ammonia", "askama", diff --git a/crates/authbeam/src/lib.rs b/crates/authbeam/src/lib.rs index ef4d62a..8a741ef 100644 --- a/crates/authbeam/src/lib.rs +++ b/crates/authbeam/src/lib.rs @@ -1,6 +1,6 @@ //! Authentication manager with user accounts and simple group-based permissions. #![doc = include_str!("../README.md")] -#![doc(issue_tracker_base_url = "https://github.com/hkauso/xsu/issues/")] +#![doc(issue_tracker_base_url = "https://github.com/swmff/rainbeam/issues/")] pub mod api; pub mod database; pub mod model; diff --git a/crates/databeam/src/lib.rs b/crates/databeam/src/lib.rs index b320788..0d02327 100644 --- a/crates/databeam/src/lib.rs +++ b/crates/databeam/src/lib.rs @@ -1,5 +1,5 @@ #![doc = include_str!("../README.md")] -#![doc(issue_tracker_base_url = "https://github.com/hkauso/xsu/issues/")] +#![doc(issue_tracker_base_url = "https://github.com/swmff/rainbeam/issues/")] pub mod cachedb; pub mod config; pub mod database; diff --git a/crates/rainbeam/Cargo.toml b/crates/rainbeam/Cargo.toml index 51c0298..1b5cccd 100644 --- a/crates/rainbeam/Cargo.toml +++ b/crates/rainbeam/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rainbeam" -version = "1.6.1" +version = "1.7.0" edition = "2021" authors = ["trisuaso", "swmff"] description = "Ask, share, socialize!" diff --git a/crates/rainbeam/sql/add_context_col.sql b/crates/rainbeam/sql/add_context_col.sql new file mode 100644 index 0000000..14362d0 --- /dev/null +++ b/crates/rainbeam/sql/add_context_col.sql @@ -0,0 +1 @@ +ALTER TABLE "xquestions" ADD COLUMN "context" TEXT DEFAULT '{}'; diff --git a/crates/rainbeam/sql/add_reply_col.sql b/crates/rainbeam/sql/add_reply_col.sql new file mode 100644 index 0000000..3bac56b --- /dev/null +++ b/crates/rainbeam/sql/add_reply_col.sql @@ -0,0 +1 @@ +ALTER TABLE "xresponses" ADD COLUMN "reply" TEXT DEFAULT ''; diff --git a/crates/rainbeam/src/database.rs b/crates/rainbeam/src/database.rs index 617346b..9368317 100644 --- a/crates/rainbeam/src/database.rs +++ b/crates/rainbeam/src/database.rs @@ -4,9 +4,9 @@ use std::collections::HashMap; use crate::config::Config; use crate::model::{ anonymous_profile, Chat, ChatAdd, ChatContext, ChatNameEdit, Circle, CircleCreate, - CircleMetadata, CommentCreate, DataExport, MembershipStatus, Message, MessageContext, - MessageCreate, QuestionCreate, QuestionResponse, Reaction, RefQuestion, ResponseComment, - ResponseContext, ResponseCreate, + CircleMetadata, CommentCreate, DataExport, FullResponse, MembershipStatus, Message, + MessageContext, MessageCreate, QuestionContext, QuestionCreate, QuestionResponse, Reaction, + RefQuestion, ResponseComment, ResponseContext, ResponseCreate, }; use crate::model::{DatabaseError, Question}; @@ -50,7 +50,8 @@ impl Database { content TEXT, id TEXT, timestamp TEXT, - ip TEXT + ip TEXT, + context TEXT )", ) .execute(c) @@ -65,7 +66,8 @@ impl Database { id TEXT, timestamp TEXT, tags TEXT, - context TEXT + context TEXT, + reply TEXT )", ) .execute(c) @@ -333,6 +335,7 @@ impl Database { .trim_matches(|c| c == '"') .parse::() .unwrap(), + context: QuestionContext::default(), }); } @@ -366,6 +369,7 @@ impl Database { id: q.id, ip: q.ip, timestamp: q.timestamp, + context: q.context, }) } Err(_) => { @@ -418,6 +422,7 @@ impl Database { id: res.get("id").unwrap().to_string(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }; // store in cache @@ -480,6 +485,7 @@ impl Database { id: res.get("id").unwrap().to_string(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }); } @@ -540,6 +546,7 @@ impl Database { id: res.get("id").unwrap().to_string(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }); } @@ -601,6 +608,7 @@ impl Database { id: id.clone(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }, // get the number of responses the question has self.get_response_count_by_question(id.clone()).await, @@ -668,6 +676,7 @@ impl Database { id: id.clone(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }, // get the number of responses the question has self.get_response_count_by_question(id.clone()).await, @@ -738,6 +747,7 @@ impl Database { id: id.clone(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }, // get the number of responses the question has self.get_response_count_by_question(id.clone()).await, @@ -805,6 +815,7 @@ impl Database { id: id.clone(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }, // get the number of responses the question has self.get_response_count_by_question(id.clone()).await, @@ -899,6 +910,7 @@ impl Database { id: id.clone(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }, // get the number of responses the question has self.get_response_count_by_question(id.clone()).await, @@ -961,6 +973,7 @@ impl Database { id: id.clone(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }, // get the number of responses the question has self.get_response_count_by_question(id.clone()).await, @@ -1060,6 +1073,7 @@ impl Database { id: id.clone(), ip: res.get("ip").unwrap().to_string(), timestamp: res.get("timestamp").unwrap().parse::().unwrap(), + context: serde_json::from_str(res.get("context").unwrap()).unwrap(), }, // get the number of responses the question has self.get_response_count_by_question(id.clone()).await, @@ -1262,6 +1276,16 @@ impl Database { return Err(DatabaseError::ContentTooShort); } + // check reply_intent + if !props.reply_intent.is_empty() { + if let Err(e) = self + .get_response(props.reply_intent.trim().to_string(), false) + .await + { + return Err(e); + } + } + // ... let question = Question { author: match self.get_profile(author).await { @@ -1276,14 +1300,17 @@ impl Database { id: utility::random_id(), timestamp: utility::unix_epoch_timestamp(), ip: ip.clone(), + context: QuestionContext { + reply_intent: props.reply_intent, + }, }; // create question let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { - "INSERT INTO \"xquestions\" VALUES (?, ?, ?, ?, ?, ?)" + "INSERT INTO \"xquestions\" VALUES (?, ?, ?, ?, ?, ?, ?)" } else { - "INSERT INTO \"xquestions\" VALEUS ($1, $2, $3, $4, $5, $6)" + "INSERT INTO \"xquestions\" VALEUS ($1, $2, $3, $4, $5, $6, $7)" } .to_string(); @@ -1295,6 +1322,7 @@ impl Database { .bind::<&String>(&question.id) .bind::<&String>(&question.timestamp.to_string()) .bind::<&String>(&ip) + .bind::<&String>(&serde_json::to_string(&question.context).unwrap()) .execute(c) .await { @@ -1435,10 +1463,12 @@ impl Database { pub async fn gimme_response( &self, res: HashMap, - ) -> Result<(Question, QuestionResponse, usize, usize)> { + recurse: bool, + ) -> Result { let question = res.get("question").unwrap().to_string(); let id = res.get("id").unwrap().to_string(); let author = res.get("author").unwrap().to_string(); + let reply = res.get("reply").unwrap_or(&String::new()).to_string(); let ctx: ResponseContext = match serde_json::from_str(res.get("context").unwrap_or(&"{}".to_string())) { Ok(t) => t, @@ -1480,9 +1510,18 @@ impl Database { Err(_) => return Err(DatabaseError::ValueError), }, context: ctx, + reply: reply.clone(), }, self.get_comment_count_by_response(id.clone()).await, self.get_reaction_count_by_asset(id).await, + if reply.is_empty() { + None + } else { + match Box::pin(self.get_response(reply, recurse)).await { + Ok(r) => Some(Box::new((r.0, r.1, r.2, r.3))), + Err(_) => None, + } + }, )) } @@ -1490,10 +1529,9 @@ impl Database { /// /// # Arguments /// * `id` - pub async fn get_response( - &self, - id: String, - ) -> Result<(Question, QuestionResponse, usize, usize)> { + /// * `recurse` + #[async_recursion] + pub async fn get_response(&self, id: String, recurse: bool) -> Result { // check in cache match self .base @@ -1504,7 +1542,7 @@ impl Database { Some(c) => { match serde_json::from_str::>(c.as_str()) { Ok(res) => { - return Ok(match self.gimme_response(res).await { + return Ok(match self.gimme_response(res, recurse).await { Ok(r) => r, Err(e) => return Err(e), }) @@ -1541,7 +1579,7 @@ impl Database { }; // return - let response = match self.gimme_response(res).await { + let response = match self.gimme_response(res, recurse).await { Ok(r) => r, Err(e) => return Err(e), }; @@ -1570,7 +1608,7 @@ impl Database { &self, question: String, author: String, - ) -> Result<(Question, QuestionResponse, usize, usize)> { + ) -> Result { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1592,7 +1630,7 @@ impl Database { }; // return - Ok(match self.gimme_response(res).await { + Ok(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }) @@ -1602,10 +1640,7 @@ impl Database { /// /// # Arguments /// * `page` - pub async fn get_posts_paginated( - &self, - page: i32, - ) -> Result> { + pub async fn get_posts_paginated(&self, page: i32) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1617,11 +1652,11 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).fetch_all(c).await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -1645,7 +1680,7 @@ impl Database { &self, page: i32, user: String, - ) -> Result> { + ) -> Result> { // get following let following = match self.auth.get_following(user.clone()).await { Ok(f) => f, @@ -1683,11 +1718,11 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).bind(&user.id).fetch_all(c).await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -1711,7 +1746,7 @@ impl Database { &self, page: i32, search: String, - ) -> Result> { + ) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1727,11 +1762,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -1750,10 +1785,7 @@ impl Database { /// /// # Arguments /// * `author` - pub async fn get_responses_by_author( - &self, - author: String, - ) -> Result> { + pub async fn get_responses_by_author(&self, author: String) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1770,11 +1802,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -1797,7 +1829,7 @@ impl Database { &self, author: String, page: i32, - ) -> Result> { + ) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1813,11 +1845,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -1841,7 +1873,7 @@ impl Database { author: String, search: String, page: i32, - ) -> Result> { + ) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1858,11 +1890,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -1887,7 +1919,7 @@ impl Database { author: String, tag: String, page: i32, - ) -> Result> { + ) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1904,11 +1936,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -1932,7 +1964,7 @@ impl Database { &self, tag: String, page: i32, - ) -> Result> { + ) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -1948,11 +1980,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -2009,7 +2041,7 @@ impl Database { &self, page: i32, search: String, - ) -> Result> { + ) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -2025,11 +2057,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -2048,10 +2080,7 @@ impl Database { /// /// # Arguments /// * `user` - pub async fn get_responses_by_following( - &self, - user: String, - ) -> Result> { + pub async fn get_responses_by_following(&self, user: String) -> Result> { // get following let following = match self.auth.get_following(user.clone()).await { Ok(f) => f, @@ -2094,11 +2123,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -2117,10 +2146,7 @@ impl Database { /// /// # Arguments /// * `id` - pub async fn get_responses_by_question( - &self, - id: String, - ) -> Result> { + pub async fn get_responses_by_question(&self, id: String) -> Result> { // pull from database let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { @@ -2133,11 +2159,11 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).bind::<&String>(&id).fetch_all(c).await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -2259,6 +2285,16 @@ impl Database { return Err(DatabaseError::ContentTooShort); } + // check reply + if !props.reply.is_empty() { + if let Err(e) = self + .get_response(props.reply.trim().to_string(), false) + .await + { + return Err(e); + } + } + // ... let response = QuestionResponse { author, @@ -2271,14 +2307,22 @@ impl Database { warning: props.warning, }, question: question.id, + reply: props.reply.trim().to_string(), }; + // make sure reply exists + if !response.reply.is_empty() { + if let Err(e) = self.get_response(response.reply.clone(), false).await { + return Err(e); + } + } + // create response let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { - "INSERT INTO \"xresponses\" VALUES (?, ?, ?, ?, ?, ?, ?)" + "INSERT INTO \"xresponses\" VALUES (?, ?, ?, ?, ?, ?, ?, ?)" } else { - "INSERT INTO \"xresponses\" VALEUS ($1, $2, $3, $4, $5, $6, $7)" + "INSERT INTO \"xresponses\" VALEUS ($1, $2, $3, $4, $5, $6, $7, $8)" } .to_string(); @@ -2294,6 +2338,7 @@ impl Database { Ok(s) => s, Err(_) => return Err(DatabaseError::ValueError), }) + .bind::<&String>(&response.reply) .execute(c) .await { @@ -2421,7 +2466,7 @@ impl Database { user: Profile, ) -> Result<()> { // make sure the response exists - let response = match self.get_response(id.clone()).await { + let response = match self.get_response(id.clone(), false).await { Ok(q) => q.1, Err(e) => return Err(e), }; @@ -2520,7 +2565,7 @@ impl Database { user: Profile, ) -> Result<()> { // make sure the response exists - let response = match self.get_response(id.clone()).await { + let response = match self.get_response(id.clone(), false).await { Ok(q) => q.1, Err(e) => return Err(e), }; @@ -2600,7 +2645,7 @@ impl Database { save_question: bool, ) -> Result<()> { // make sure response exists - let response = match self.get_response(id.clone()).await { + let response = match self.get_response(id.clone(), false).await { Ok(q) => q, Err(e) => return Err(e), }; @@ -2699,7 +2744,7 @@ impl Database { /// * `user` - the user doing this pub async fn unsend_response(&self, id: String, user: Profile) -> Result<()> { // make sure the response exists - let res = match self.get_response(id.clone()).await { + let res = match self.get_response(id.clone(), false).await { Ok(q) => q, Err(e) => return Err(e), }; @@ -3277,7 +3322,7 @@ impl Database { /// * `author` - the ID of the user creating the comment pub async fn create_comment(&self, props: CommentCreate, author: String) -> Result<()> { // make sure the response exists - let response = match self.get_response(props.response.clone()).await { + let response = match self.get_response(props.response.clone(), false).await { Ok(q) => q.1, Err(e) => return Err(e), }; @@ -3449,7 +3494,7 @@ impl Database { Err(e) => return Err(e), }; - let res = match self.get_response(comment.response.clone()).await { + let res = match self.get_response(comment.response.clone(), false).await { Ok(q) => q.1, Err(e) => return Err(e), }; @@ -4567,10 +4612,7 @@ impl Database { /// /// ## Arguments: /// * `circle` - pub async fn get_responses_by_circle( - &self, - circle: String, - ) -> Result> { + pub async fn get_responses_by_circle(&self, circle: String) -> Result> { // get circle let circle = match self.get_circle(circle.clone()).await { Ok(c) => c, @@ -4604,11 +4646,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); @@ -4631,7 +4673,7 @@ impl Database { &self, circle: String, page: i32, - ) -> Result> { + ) -> Result> { // get circle let circle = match self.get_circle(circle.clone()).await { Ok(c) => c, @@ -4664,11 +4706,11 @@ impl Database { .await { Ok(p) => { - let mut out: Vec<(Question, QuestionResponse, usize, usize)> = Vec::new(); + let mut out: Vec = Vec::new(); for row in p { let res = self.base.textify_row(row, Vec::new()).0; - out.push(match self.gimme_response(res).await { + out.push(match self.gimme_response(res, false).await { Ok(r) => r, Err(e) => return Err(e), }); diff --git a/crates/rainbeam/src/model.rs b/crates/rainbeam/src/model.rs index b04b997..5c83b50 100644 --- a/crates/rainbeam/src/model.rs +++ b/crates/rainbeam/src/model.rs @@ -32,6 +32,9 @@ pub struct Question { pub ip: String, /// The time this question was asked pub timestamp: u128, + /// Additional information about the question + #[serde(default)] + pub context: QuestionContext, } impl Question { @@ -43,6 +46,7 @@ impl Question { id: String::new(), ip: String::new(), timestamp, + context: QuestionContext::default(), } } @@ -54,6 +58,7 @@ impl Question { id: "0".to_string(), ip: String::new(), timestamp: 0, + context: QuestionContext::default(), } } @@ -67,6 +72,24 @@ impl Question { } } +/// Basic information which changes the way the response is deserialized +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct QuestionContext { + /// The ID of the response in which this question is replying to + /// + /// Will fill into the "reply" field of the response that is posted to this question + #[serde(default)] + pub reply_intent: String, +} + +impl Default for QuestionContext { + fn default() -> Self { + Self { + reply_intent: String::new(), + } + } +} + /// A question structure with ID references to profiles instead of the profiles #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RefQuestion { @@ -82,6 +105,9 @@ pub struct RefQuestion { pub ip: String, /// The time this question was asked pub timestamp: u128, + /// Additional information about the question + #[serde(default)] + pub context: QuestionContext, } impl From for RefQuestion { @@ -93,6 +119,7 @@ impl From for RefQuestion { id: value.id, ip: value.ip, timestamp: value.timestamp, + context: value.context, } } } @@ -114,8 +141,13 @@ pub struct QuestionResponse { pub tags: Vec, /// Response context pub context: ResponseContext, + /// The ID of the response this response is replying to + pub reply: String, } +pub type ResponseReply = Option>; +pub type FullResponse = (Question, QuestionResponse, usize, usize, ResponseReply); + /// Basic information which changes the way the response is deserialized #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ResponseContext { @@ -278,7 +310,7 @@ pub struct DataExport { /// All of the user's [`Question`]s pub questions: Vec<(Question, usize, usize)>, /// All of the user's [`QuestionResponse`]s - pub responses: Vec<(Question, QuestionResponse, usize, usize)>, + pub responses: Vec, /// All of the user's [`ResponseComment`]s pub comments: Vec<(ResponseComment, usize, usize)>, } @@ -338,6 +370,8 @@ pub struct QuestionCreate { pub recipient: String, pub content: String, pub anonymous: bool, + #[serde(default)] + pub reply_intent: String, } #[derive(Serialize, Deserialize, Debug)] @@ -348,6 +382,8 @@ pub struct ResponseCreate { pub tags: Vec, #[serde(default)] pub warning: String, + #[serde(default)] + pub reply: String, } #[derive(Serialize, Deserialize, Debug)] diff --git a/crates/rainbeam/src/routing/api/reactions.rs b/crates/rainbeam/src/routing/api/reactions.rs index 43fa437..f74e0c1 100644 --- a/crates/rainbeam/src/routing/api/reactions.rs +++ b/crates/rainbeam/src/routing/api/reactions.rs @@ -79,7 +79,7 @@ pub async fn create_request( } } AssetType::Response => { - let asset = match database.get_response(id.clone()).await { + let asset = match database.get_response(id.clone(), false).await { Ok(r) => r.1, Err(e) => { return Json(DefaultReturn { diff --git a/crates/rainbeam/src/routing/api/responses.rs b/crates/rainbeam/src/routing/api/responses.rs index 596aab8..230758e 100644 --- a/crates/rainbeam/src/routing/api/responses.rs +++ b/crates/rainbeam/src/routing/api/responses.rs @@ -64,7 +64,7 @@ pub async fn get_request( Path(id): Path, State(database): State, ) -> impl IntoResponse { - Json(match database.get_response(id).await { + Json(match database.get_response(id, false).await { Ok(mut r) => DefaultReturn { success: true, message: String::new(), @@ -106,7 +106,7 @@ pub async fn expand_request( Path(id): Path, State(database): State, ) -> impl IntoResponse { - match database.get_response(id).await { + match database.get_response(id, false).await { Ok(r) => Redirect::to(&format!("/response/{}", r.1.id)), Err(_) => Redirect::to("/"), } @@ -276,7 +276,7 @@ pub async fn report_request( } // get response - if let Err(_) = database.get_response(id.clone()).await { + if let Err(_) = database.get_response(id.clone(), false).await { return Json(DefaultReturn { success: false, message: DatabaseError::NotFound.to_string(), diff --git a/crates/rainbeam/src/routing/pages/circles.rs b/crates/rainbeam/src/routing/pages/circles.rs index db532e4..77c9e6e 100644 --- a/crates/rainbeam/src/routing/pages/circles.rs +++ b/crates/rainbeam/src/routing/pages/circles.rs @@ -9,9 +9,7 @@ use authbeam::model::{Permission, Profile}; use crate::config::Config; use crate::database::Database; -use crate::model::{ - Circle, CircleMetadata, DatabaseError, MembershipStatus, Question, QuestionResponse, -}; +use crate::model::{Circle, CircleMetadata, DatabaseError, FullResponse, MembershipStatus, Question}; use super::PaginatedQuery; @@ -146,11 +144,11 @@ struct ProfileTemplate { inbox_count: usize, notifs: usize, circle: Circle, - responses: Vec<(Question, QuestionResponse, usize, usize)>, + responses: Vec, response_count: usize, member_count: usize, metadata: String, - pinned: Option>, + pinned: Option>, page: i32, // ... layout: String, @@ -229,7 +227,7 @@ pub async fn profile_request( let mut out = Vec::new(); for id in pinned.split(",") { - match database.get_response(id.to_string()).await { + match database.get_response(id.to_string(), false).await { Ok(response) => { // TODO: check author circle membership status // remove from responses diff --git a/crates/rainbeam/src/routing/pages/mod.rs b/crates/rainbeam/src/routing/pages/mod.rs index cdf5b42..58326de 100644 --- a/crates/rainbeam/src/routing/pages/mod.rs +++ b/crates/rainbeam/src/routing/pages/mod.rs @@ -15,7 +15,10 @@ use authbeam::model::{ use crate::config::Config; use crate::database::Database; -use crate::model::{DatabaseError, Question, QuestionResponse, Reaction, ResponseComment}; +use crate::model::{ + DatabaseError, FullResponse, Question, QuestionResponse, Reaction, ResponseComment, + ResponseReply, +}; use super::api; @@ -54,7 +57,7 @@ struct TimelineTemplate { profile: Option, unread: usize, notifs: usize, - responses: Vec<(Question, QuestionResponse, usize, usize)>, + responses: Vec, is_powerful: bool, is_helper: bool, } @@ -300,6 +303,7 @@ pub fn remove_tags(input: &str) -> String { .replace("<", "<") .replace(">", ">") .replace("&", "&") + .replace("", ", + responses: Vec, reactions: Vec, already_responded: bool, is_powerful: bool, @@ -430,7 +434,7 @@ struct PublicPostsTemplate { unread: usize, notifs: usize, page: i32, - responses: Vec<(Question, QuestionResponse, usize, usize)>, + responses: Vec, is_powerful: bool, is_helper: bool, } @@ -539,7 +543,7 @@ struct FollowingPostsTemplate { unread: usize, notifs: usize, page: i32, - responses: Vec<(Question, QuestionResponse, usize, usize)>, + responses: Vec, is_powerful: bool, is_helper: bool, } @@ -623,6 +627,7 @@ struct ResponseTemplate { question: Question, response: QuestionResponse, comments: Vec<(ResponseComment, usize, usize)>, + reply: ResponseReply, reactions: Vec, tags: String, page: i32, @@ -669,7 +674,7 @@ pub async fn response_request( 0 }; - let response = match database.get_response(id.clone()).await { + let response = match database.get_response(id.clone(), false).await { Ok(r) => r, Err(e) => return Html(e.to_html(database)), }; @@ -709,6 +714,7 @@ pub async fn response_request( question: response.0, tags: serde_json::to_string(&response.1.tags).unwrap(), response: response.1, + reply: response.4, comments, reactions, page: query.page, @@ -735,6 +741,7 @@ struct CommentTemplate { page: i32, question: Question, response: QuestionResponse, + reply: ResponseReply, reaction_count: usize, anonymous_username: Option, anonymous_avatar: Option, @@ -784,7 +791,10 @@ pub async fn comment_request( Err(e) => return Html(e.to_html(database)), }; - let response = match database.get_response(comment.0.response.clone()).await { + let response = match database + .get_response(comment.0.response.clone(), false) + .await + { Ok(r) => r, Err(e) => return Html(e.to_html(database)), }; @@ -828,6 +838,7 @@ pub async fn comment_request( question: response.0, response: response.1, reaction_count: response.3, + reply: response.4, anonymous_username: Some("anonymous".to_string()), // TODO: fetch recipient setting anonymous_avatar: None, is_powerful, diff --git a/crates/rainbeam/src/routing/pages/profile.rs b/crates/rainbeam/src/routing/pages/profile.rs index a022922..4cff9b9 100644 --- a/crates/rainbeam/src/routing/pages/profile.rs +++ b/crates/rainbeam/src/routing/pages/profile.rs @@ -8,7 +8,7 @@ use authbeam::model::{Permission, Profile, UserFollow, Warning}; use crate::config::Config; use crate::database::Database; -use crate::model::{Chat, DatabaseError, Question, QuestionResponse, RelationshipStatus}; +use crate::model::{Chat, DatabaseError, FullResponse, Question, RelationshipStatus}; use super::{clean_metadata, PaginatedQuery, ProfileQuery}; @@ -20,7 +20,7 @@ struct ProfileTemplate { unread: usize, notifs: usize, other: Profile, - responses: Vec<(Question, QuestionResponse, usize, usize)>, + responses: Vec, response_count: usize, questions_count: usize, followers_count: usize, @@ -29,7 +29,7 @@ struct ProfileTemplate { is_following: bool, is_following_you: bool, metadata: String, - pinned: Option>, + pinned: Option>, page: i32, tag: String, query: String, @@ -163,7 +163,7 @@ pub async fn profile_request( let mut out = Vec::new(); for id in pinned.split(",") { - match database.get_response(id.to_string()).await { + match database.get_response(id.to_string(), false).await { Ok(response) => { if response.1.author.id != other.id { // don't allow us to pin responses from other users @@ -300,8 +300,8 @@ struct ProfileEmbedTemplate { config: Config, profile: Option, other: Profile, - responses: Vec<(Question, QuestionResponse, usize, usize)>, - pinned: Option>, + responses: Vec, + pinned: Option>, is_powerful: bool, is_helper: bool, lock_profile: bool, @@ -351,7 +351,7 @@ pub async fn profile_embed_request( let mut out = Vec::new(); for id in pinned.split(",") { - match database.get_response(id.to_string()).await { + match database.get_response(id.to_string(), false).await { Ok(response) => { if response.1.author.id != other.id { // don't allow us to pin responses from other users diff --git a/crates/rainbeam/src/routing/pages/search.rs b/crates/rainbeam/src/routing/pages/search.rs index a6b0bb2..0ab7690 100644 --- a/crates/rainbeam/src/routing/pages/search.rs +++ b/crates/rainbeam/src/routing/pages/search.rs @@ -9,7 +9,7 @@ use authbeam::model::{Permission, Profile}; use super::{SearchHomeQuery, SearchQuery}; use crate::config::Config; use crate::database::Database; -use crate::model::{DatabaseError, Question, QuestionResponse}; +use crate::model::{DatabaseError, FullResponse, Question}; #[derive(Template)] #[template(path = "search/homepage.html")] @@ -83,7 +83,7 @@ struct ResponsesTemplate { page: i32, driver: i8, // search-specific - results: Vec<(Question, QuestionResponse, usize, usize)>, + results: Vec, is_powerful: bool, // at least "manager" is_helper: bool, // at least "helper" } @@ -188,7 +188,7 @@ struct PostsTemplate { page: i32, driver: i8, // search-specific - results: Vec<(Question, QuestionResponse, usize, usize)>, + results: Vec, is_powerful: bool, // at least "manager" is_helper: bool, // at least "helper" } diff --git a/crates/rainbeam/static/js/app.js b/crates/rainbeam/static/js/app.js index 2ff6339..99b4191 100644 --- a/crates/rainbeam/static/js/app.js +++ b/crates/rainbeam/static/js/app.js @@ -96,6 +96,14 @@ $.toast("success", "Copied!"); }); + app.define("intent_twitter", function ({ $ }, text) { + window.open( + `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`, + ); + + $.toast("success", "Opened intent!"); + }); + app.define("smooth_remove", function (_, element, ms) { // run animation element.style.animation = `fadeout ease-in-out 1 ${ms}ms forwards running`; diff --git a/crates/rainbeam/static/js/questions.js b/crates/rainbeam/static/js/questions.js index 396c23c..c3b4fc0 100644 --- a/crates/rainbeam/static/js/questions.js +++ b/crates/rainbeam/static/js/questions.js @@ -1,34 +1,38 @@ (() => { const self = reg_ns("questions", ["app"]); - self.define("create", function ({ $, app }, recipient, content, anonymous) { - return new Promise((resolve, reject) => { - fetch("/api/v1/questions", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - recipient, - content, - anonymous, - }), - }) - .then((res) => res.json()) - .then((res) => { - app.toast( - res.success ? "success" : "error", - res.success ? "Question asked!" : res.message, - ); + self.define( + "create", + function ({ $, app }, recipient, content, anonymous, reply_intent) { + return new Promise((resolve, reject) => { + fetch("/api/v1/questions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + recipient, + content, + anonymous, + reply_intent: reply_intent || "", + }), + }) + .then((res) => res.json()) + .then((res) => { + app.toast( + res.success ? "success" : "error", + res.success ? "Question asked!" : res.message, + ); - if (res.success === true) { - return resolve(res); - } else { - return reject(res); - } - }); - }); - }); + if (res.success === true) { + return resolve(res); + } else { + return reject(res); + } + }); + }); + }, + ); self.define("delete", function ({ $, app }, id) { if (!confirm("Are you sure you want to do this?")) { diff --git a/crates/rainbeam/static/js/responses.js b/crates/rainbeam/static/js/responses.js index d7a4c9f..3d5c21f 100644 --- a/crates/rainbeam/static/js/responses.js +++ b/crates/rainbeam/static/js/responses.js @@ -3,7 +3,7 @@ self.define( "create", - function ({ $, app }, question, content, tags, warning) { + function ({ $, app }, question, content, tags, warning, reply) { if (!tags) { tags = ""; } @@ -22,6 +22,7 @@ ? [] : tags.split(",").map((t) => t.trim()), warning: warning || "", + reply: reply || "", }), }) .then((res) => res.json()) diff --git a/crates/rainbeam/static/style.css b/crates/rainbeam/static/style.css index 50636b1..1e903a5 100644 --- a/crates/rainbeam/static/style.css +++ b/crates/rainbeam/static/style.css @@ -980,7 +980,8 @@ dialog::backdrop { .thread_line { display: block; border-radius: var(--radius); - background: var(--color-super-lowered); + /* background: var(--color-super-lowered); */ + background: var(--color-primary); /* height: 100%; */ width: 5px; } diff --git a/crates/rainbeam/templates/base.html b/crates/rainbeam/templates/base.html index fc6ccd2..9fbaa11 100644 --- a/crates/rainbeam/templates/base.html +++ b/crates/rainbeam/templates/base.html @@ -41,7 +41,7 @@ @@ -254,6 +287,7 @@ e.target.content.value, e.target.tags.value, e.target.warning.value, + e.target.reply.value, ]).then((_) => { // reset if successful e.target.reset(); diff --git a/crates/rainbeam/templates/profile/base.html b/crates/rainbeam/templates/profile/base.html index ef1ff4b..6ce910d 100644 --- a/crates/rainbeam/templates/profile/base.html +++ b/crates/rainbeam/templates/profile/base.html @@ -705,6 +705,18 @@

{{ othe {% endif %} {% endif %} {% endif %}