From f034cc4f2791ebdd25ec13dadc467dca44793e3d Mon Sep 17 00:00:00 2001 From: trisua Date: Tue, 10 Jun 2025 13:49:17 -0400 Subject: [PATCH] add: ability to mute phrases add: ability to disable gpa experiment --- crates/app/src/public/html/profile/base.lisp | 6 +- .../app/src/public/html/profile/settings.lisp | 17 ++++ crates/app/src/public/js/atto.js | 81 +++++++++++-------- crates/app/src/routes/pages/misc.rs | 50 ++++++++++-- crates/app/src/routes/pages/profile.rs | 45 +++++++++-- crates/core/src/database/posts.rs | 70 ++++++++++++---- crates/core/src/model/auth.rs | 6 ++ 7 files changed, 212 insertions(+), 63 deletions(-) diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp index 5dd0c7e..347001c 100644 --- a/crates/app/src/public/html/profile/base.lisp +++ b/crates/app/src/public/html/profile/base.lisp @@ -160,13 +160,13 @@ (text "Posts")) (span (text "{{ profile.post_count }}"))) - (text "{% if gpa and gpa > 0 -%}") + (text "{% if gpa and gpa > 0 and (not user.settings.disable_gpa_fun or is_helper) -%}") (div ("class" "w-full flex justify-between items-center") - ("title" "great post average (limited time)") + ("title" "great post average (limited time fun)") (span ("class" "notification chip") - (text "GPA 🐇")) + (text "GPA")) (span (text "{{ gpa|round(method=\"floor\", precision=2) }}"))) (text "{%- endif %}") diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index ce5b532..480a4f3 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -1204,8 +1204,19 @@ settings.warning, \"textarea\", ], + [[\"muted\", \"Muted phrases\"], settings.muted.join(\"\\n\"), \"textarea\", { + embed_html: + 'Muted phrases should all be on new lines.', + }], ], settings, + { + muted: (new_muted) => { + settings.muted = new_muted + .split(\"\\n\") + .map((t) => t.trim()); + }, + }, ); ui.generate_settings_ui( @@ -1303,6 +1314,12 @@ \"Hides dislikes on all posts. Users can still dislike your posts, you just won't be able to see it.\", \"text\", ], + [[], \"Fun\", \"title\"], + [ + [\"disable_gpa_fun\", \"Disable GPA\"], + \"{{ profile.settings.disable_gpa_fun }}\", + \"checkbox\", + ], ], settings, ); diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 3117ab1..96fdf3d 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -856,27 +856,29 @@ media_theme_pref(); } }); - self.define("render_settings_ui_field", (_, into_element, option) => { - if (option.input_element_type === "divider") { - into_element.innerHTML += `
`; - return; - } + self.define( + "render_settings_ui_field", + (_, into_element, option, id_key) => { + if (option.input_element_type === "divider") { + into_element.innerHTML += `
`; + return; + } - if (option.input_element_type === "title") { - into_element.innerHTML += `
${option.value}`; - return; - } + if (option.input_element_type === "title") { + into_element.innerHTML += `
${option.value}`; + return; + } - if (option.input_element_type === "text") { - into_element.innerHTML += `

${option.value}

`; - return; - } + if (option.input_element_type === "text") { + into_element.innerHTML += `

${option.value}

`; + return; + } - if (option.input_element_type === "checkbox") { - into_element.innerHTML += `
+ if (option.input_element_type === "checkbox") { + into_element.innerHTML += `
${option.label.replaceAll("_", " ")}
`; - return; - } + return; + } - if (option.input_element_type === "color") { - into_element.innerHTML += `
+ if (option.input_element_type === "color") { + into_element.innerHTML += `
@@ -905,7 +907,7 @@ media_theme_pref(); ${(option.attributes || { description: "" }).description}
`; - return; - } + return; + } - into_element.innerHTML += `
+ into_element.innerHTML += `
@@ -929,7 +931,7 @@ media_theme_pref();
<${option.input_element_type || "input"} type="text" - onchange="window.set_setting_field('${option.key}', event.target.value)" + onchange="window.set_setting_field${id_key}('${option.key}', event.target.value)" placeholder="${option.key}" name="${option.key}" id="${option.key}" @@ -939,26 +941,37 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} ${(option.attributes || { embed_html: "" }).embed_html}
`; - }); + }, + ); self.define( "generate_settings_ui", ({ $ }, into_element, options, settings_ref, key_map = {}) => { + const id_key = `a${crypto.randomUUID().replaceAll("-", "")}`; for (const option of options) { - $.render_settings_ui_field(into_element, { - key: Array.isArray(option[0]) ? option[0][0] : option[0], - label: Array.isArray(option[0]) ? option[0][1] : option[0], - value: option[1], - input_element_type: option[2], - attributes: option[3], - }); + $.render_settings_ui_field( + into_element, + { + key: Array.isArray(option[0]) + ? option[0][0] + : option[0], + label: Array.isArray(option[0]) + ? option[0][1] + : option[0], + value: option[1], + input_element_type: option[2], + attributes: option[3], + }, + id_key, + ); } - window.set_setting_field = (key, value) => { + window[`set_setting_field${id_key}`] = (key, value) => { if (settings_ref && !key_map[key]) { settings_ref[key] = value; } else { key_map[key](value); + console.log("custom_update", key); } console.log("update", key); diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 87bc02d..b43b9ad 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -71,7 +71,13 @@ pub async fn index_request( { Ok(l) => match data .0 - .fill_posts_with_community(l, user.id, &ignore_users, &Some(user.clone())) + .fill_posts_with_community( + data.0 + .posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + user.id, + &ignore_users, + &Some(user.clone()), + ) .await { Ok(l) => l, @@ -103,7 +109,14 @@ pub async fn popular_request( Ok(l) => match data .0 .fill_posts_with_community( - l, + data.0.posts_muted_phrase_filter( + &l, + if let Some(ref ua) = user { + Some(&ua.settings.muted) + } else { + None + }, + ), if let Some(ref ua) = user { ua.id } else { 0 }, &ignore_users, &user, @@ -149,7 +162,13 @@ pub async fn following_request( { Ok(l) => match data .0 - .fill_posts_with_community(l, user.id, &ignore_users, &Some(user.clone())) + .fill_posts_with_community( + data.0 + .posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + user.id, + &ignore_users, + &Some(user.clone()), + ) .await { Ok(l) => l, @@ -183,7 +202,14 @@ pub async fn all_request( Ok(l) => match data .0 .fill_posts_with_community( - l, + data.0.posts_muted_phrase_filter( + &l, + if let Some(ref ua) = user { + Some(&ua.settings.muted) + } else { + None + }, + ), if let Some(ref ua) = user { ua.id } else { 0 }, &ignore_users, &user, @@ -580,7 +606,13 @@ pub async fn search_request( { Ok(l) => match data .0 - .fill_posts_with_community(l, user.id, &ignore_users, &Some(user.clone())) + .fill_posts_with_community( + data.0 + .posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + user.id, + &ignore_users, + &Some(user.clone()), + ) .await { Ok(l) => l, @@ -592,7 +624,13 @@ pub async fn search_request( match data.0.get_posts_searched(12, req.page, &req.query).await { Ok(l) => match data .0 - .fill_posts_with_community(l, user.id, &ignore_users, &Some(user.clone())) + .fill_posts_with_community( + data.0 + .posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + user.id, + &ignore_users, + &Some(user.clone()), + ) .await { Ok(l) => l, diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index ef48326..86b8401 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -245,7 +245,14 @@ pub async fn posts_request( Ok(p) => match data .0 .fill_posts_with_community( - p, + data.0.posts_muted_phrase_filter( + &p, + if let Some(ref ua) = user { + Some(&ua.settings.muted) + } else { + None + }, + ), if let Some(ref ua) = user { ua.id } else { 0 }, &ignore_users, &user, @@ -266,7 +273,14 @@ pub async fn posts_request( Ok(p) => match data .0 .fill_posts_with_community( - p, + data.0.posts_muted_phrase_filter( + &p, + if let Some(ref ua) = user { + Some(&ua.settings.muted) + } else { + None + }, + ), if let Some(ref ua) = user { ua.id } else { 0 }, &ignore_users, &user, @@ -285,7 +299,14 @@ pub async fn posts_request( Ok(p) => match data .0 .fill_posts_with_community( - p, + data.0.posts_muted_phrase_filter( + &p, + if let Some(ref ua) = user { + Some(&ua.settings.muted) + } else { + None + }, + ), if let Some(ref ua) = user { ua.id } else { 0 }, &ignore_users, &user, @@ -394,7 +415,14 @@ pub async fn replies_request( Ok(p) => match data .0 .fill_posts_with_community( - p, + data.0.posts_muted_phrase_filter( + &p, + if let Some(ref ua) = user { + Some(&ua.settings.muted) + } else { + None + }, + ), if let Some(ref ua) = user { ua.id } else { 0 }, &ignore_users, &user, @@ -500,7 +528,14 @@ pub async fn media_request( Ok(p) => match data .0 .fill_posts_with_community( - p, + data.0.posts_muted_phrase_filter( + &p, + if let Some(ref ua) = user { + Some(&ua.settings.muted) + } else { + None + }, + ), if let Some(ref ua) = user { ua.id } else { 0 }, &ignore_users, &user, diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 5bda172..aa0265c 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -21,8 +21,20 @@ use oiseau::SqliteRow; #[cfg(feature = "postgres")] use oiseau::PostgresRow; +#[cfg(feature = "redis")] +use oiseau::cache::redis::Commands; + use oiseau::{execute, get, query_row, query_rows, params}; +pub type FullPost = ( + Post, + User, + Community, + Option<(User, Post)>, + Option<(Question, User)>, + Option<(Poll, bool, bool)>, +); + macro_rules! private_post_replying { ($post:ident, $replying_posts:ident, $ua1:ident, $data:ident) => { // post owner is not following us @@ -376,16 +388,7 @@ impl DataManager { user_id: usize, ignore_users: &[usize], user: &Option, - ) -> Result< - Vec<( - Post, - User, - Community, - Option<(User, Post)>, - Option<(Question, User)>, - Option<(Poll, bool, bool)>, - )>, - > { + ) -> Result> { let mut out = Vec::new(); let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new(); @@ -463,6 +466,33 @@ impl DataManager { Ok(out) } + /// Update posts which contain a muted phrase. + pub fn posts_muted_phrase_filter( + &self, + posts: &Vec, + muted: Option<&Vec>, + ) -> Vec { + let muted = match muted { + Some(m) => m, + None => return posts.to_owned(), + }; + + let mut out: Vec = Vec::new(); + + for mut post in posts.clone() { + for phrase in muted { + if post.content.contains(phrase) { + post.context.content_warning = "Contains muted phrase".to_string(); + break; + } + } + + out.push(post); + } + + out + } + /// Get all posts from the given user (from most recent). /// /// # Arguments @@ -533,7 +563,7 @@ impl DataManager { let res = query_rows!( &conn, - &format!("SELECT * FROM posts WHERE owner = $1 ORDER BY created DESC LIMIT 50"), + &format!("SELECT * FROM posts WHERE owner = $1 ORDER BY created DESC LIMIT 12"), &[&(id as i64)], |x| { Self::get_post_from_row(x) } ); @@ -581,11 +611,21 @@ impl DataManager { let gpa = (good_posts as f32 / real_posts_count as f32) * 4.0; let gpa_rounded = format!("{gpa:.2}").parse::().unwrap(); - self.0 - .1 - .set(format!("atto.user.gpa:{}", id), gpa_rounded.to_string()) - .await; + let mut redis_con = self.0.1.get_con().await; + // expires in one day + if redis_con + .set_ex::( + format!("atto.user.gpa:{}", id), + gpa_rounded.to_string(), + 86400, + ) + .is_err() + { + return 0.0; + }; + + // ... gpa_rounded } diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 425a8be..c99bdaf 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -225,6 +225,12 @@ pub struct UserSettings { /// If extra post tabs are hidden (replies, media). #[serde(default)] pub hide_extra_post_tabs: bool, + /// If the GPA experiment is disabled. + #[serde(default)] + pub disable_gpa_fun: bool, + /// A list of strings the user has muted. + #[serde(default)] + pub muted: Vec, } fn mime_avif() -> String {