add: ability to mute phrases

add: ability to disable gpa experiment
This commit is contained in:
trisua 2025-06-10 13:49:17 -04:00
parent 9d839a1a63
commit f034cc4f27
7 changed files with 212 additions and 63 deletions

View file

@ -160,13 +160,13 @@
(text "Posts")) (text "Posts"))
(span (span
(text "{{ profile.post_count }}"))) (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 (div
("class" "w-full flex justify-between items-center") ("class" "w-full flex justify-between items-center")
("title" "great post average (limited time)") ("title" "great post average (limited time fun)")
(span (span
("class" "notification chip") ("class" "notification chip")
(text "GPA 🐇")) (text "GPA"))
(span (span
(text "{{ gpa|round(method=\"floor\", precision=2) }}"))) (text "{{ gpa|round(method=\"floor\", precision=2) }}")))
(text "{%- endif %}") (text "{%- endif %}")

View file

@ -1204,8 +1204,19 @@
settings.warning, settings.warning,
\"textarea\", \"textarea\",
], ],
[[\"muted\", \"Muted phrases\"], settings.muted.join(\"\\n\"), \"textarea\", {
embed_html:
'<span class=\"fade\">Muted phrases should all be on new lines.</span>',
}],
], ],
settings, settings,
{
muted: (new_muted) => {
settings.muted = new_muted
.split(\"\\n\")
.map((t) => t.trim());
},
},
); );
ui.generate_settings_ui( 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.\", \"Hides dislikes on all posts. Users can still dislike your posts, you just won't be able to see it.\",
\"text\", \"text\",
], ],
[[], \"Fun\", \"title\"],
[
[\"disable_gpa_fun\", \"Disable GPA\"],
\"{{ profile.settings.disable_gpa_fun }}\",
\"checkbox\",
],
], ],
settings, settings,
); );

View file

@ -856,27 +856,29 @@ media_theme_pref();
} }
}); });
self.define("render_settings_ui_field", (_, into_element, option) => { self.define(
if (option.input_element_type === "divider") { "render_settings_ui_field",
into_element.innerHTML += `<hr class="margin" />`; (_, into_element, option, id_key) => {
return; if (option.input_element_type === "divider") {
} into_element.innerHTML += `<hr class="margin" />`;
return;
}
if (option.input_element_type === "title") { if (option.input_element_type === "title") {
into_element.innerHTML += `<hr class="margin" /><b>${option.value}</b>`; into_element.innerHTML += `<hr class="margin" /><b>${option.value}</b>`;
return; return;
} }
if (option.input_element_type === "text") { if (option.input_element_type === "text") {
into_element.innerHTML += `<p>${option.value}</p>`; into_element.innerHTML += `<p>${option.value}</p>`;
return; return;
} }
if (option.input_element_type === "checkbox") { if (option.input_element_type === "checkbox") {
into_element.innerHTML += `<div class="card flex gap-2"> into_element.innerHTML += `<div class="card flex gap-2">
<input <input
type="checkbox" type="checkbox"
onchange="window.set_setting_field('${option.key}', event.target.checked)" onchange="window.set_setting_field${id_key}('${option.key}', event.target.checked)"
placeholder="${option.key}" placeholder="${option.key}"
name="${option.key}" name="${option.key}"
id="${option.key}" id="${option.key}"
@ -887,11 +889,11 @@ media_theme_pref();
<label for="${option.key}"><b>${option.label.replaceAll("_", " ")}</b></label> <label for="${option.key}"><b>${option.label.replaceAll("_", " ")}</b></label>
</div>`; </div>`;
return; return;
} }
if (option.input_element_type === "color") { if (option.input_element_type === "color") {
into_element.innerHTML += `<div class="flex flex-col gap-2"> into_element.innerHTML += `<div class="flex flex-col gap-2">
<label for="${option.key}"><b>${option.label.replaceAll("_", " ")}</b></label> <label for="${option.key}"><b>${option.label.replaceAll("_", " ")}</b></label>
<div class="card flex flex-row gap-2"> <div class="card flex flex-row gap-2">
@ -905,7 +907,7 @@ media_theme_pref();
<input <input
type="text" type="text"
onchange="window.set_setting_field('${option.key}', event.target.value); window.update_color_field('${option.key}', event.target.value)" onchange="window.set_setting_field${id_key}('${option.key}', event.target.value); window.update_color_field('${option.key}', event.target.value)"
placeholder="${option.key}" placeholder="${option.key}"
name="${option.key}" name="${option.key}"
id="${option.key}" id="${option.key}"
@ -918,10 +920,10 @@ media_theme_pref();
<span class="fade">${(option.attributes || { description: "" }).description}</span> <span class="fade">${(option.attributes || { description: "" }).description}</span>
</div>`; </div>`;
return; return;
} }
into_element.innerHTML += `<div class="card-nest"> into_element.innerHTML += `<div class="card-nest">
<div class="card small"> <div class="card small">
<label for="${option.key}"><b>${option.label.replaceAll("_", " ")}</b></label> <label for="${option.key}"><b>${option.label.replaceAll("_", " ")}</b></label>
</div> </div>
@ -929,7 +931,7 @@ media_theme_pref();
<div class="card flex flex-col gap-2"> <div class="card flex flex-col gap-2">
<${option.input_element_type || "input"} <${option.input_element_type || "input"}
type="text" 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}" placeholder="${option.key}"
name="${option.key}" name="${option.key}"
id="${option.key}" id="${option.key}"
@ -939,26 +941,37 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
${(option.attributes || { embed_html: "" }).embed_html} ${(option.attributes || { embed_html: "" }).embed_html}
</div> </div>
</div>`; </div>`;
}); },
);
self.define( self.define(
"generate_settings_ui", "generate_settings_ui",
({ $ }, into_element, options, settings_ref, key_map = {}) => { ({ $ }, into_element, options, settings_ref, key_map = {}) => {
const id_key = `a${crypto.randomUUID().replaceAll("-", "")}`;
for (const option of options) { for (const option of options) {
$.render_settings_ui_field(into_element, { $.render_settings_ui_field(
key: Array.isArray(option[0]) ? option[0][0] : option[0], into_element,
label: Array.isArray(option[0]) ? option[0][1] : option[0], {
value: option[1], key: Array.isArray(option[0])
input_element_type: option[2], ? option[0][0]
attributes: option[3], : 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]) { if (settings_ref && !key_map[key]) {
settings_ref[key] = value; settings_ref[key] = value;
} else { } else {
key_map[key](value); key_map[key](value);
console.log("custom_update", key);
} }
console.log("update", key); console.log("update", key);

View file

@ -71,7 +71,13 @@ pub async fn index_request(
{ {
Ok(l) => match data Ok(l) => match data
.0 .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 .await
{ {
Ok(l) => l, Ok(l) => l,
@ -103,7 +109,14 @@ pub async fn popular_request(
Ok(l) => match data Ok(l) => match data
.0 .0
.fill_posts_with_community( .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 }, if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users, &ignore_users,
&user, &user,
@ -149,7 +162,13 @@ pub async fn following_request(
{ {
Ok(l) => match data Ok(l) => match data
.0 .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 .await
{ {
Ok(l) => l, Ok(l) => l,
@ -183,7 +202,14 @@ pub async fn all_request(
Ok(l) => match data Ok(l) => match data
.0 .0
.fill_posts_with_community( .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 }, if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users, &ignore_users,
&user, &user,
@ -580,7 +606,13 @@ pub async fn search_request(
{ {
Ok(l) => match data Ok(l) => match data
.0 .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 .await
{ {
Ok(l) => l, Ok(l) => l,
@ -592,7 +624,13 @@ pub async fn search_request(
match data.0.get_posts_searched(12, req.page, &req.query).await { match data.0.get_posts_searched(12, req.page, &req.query).await {
Ok(l) => match data Ok(l) => match data
.0 .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 .await
{ {
Ok(l) => l, Ok(l) => l,

View file

@ -245,7 +245,14 @@ pub async fn posts_request(
Ok(p) => match data Ok(p) => match data
.0 .0
.fill_posts_with_community( .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 }, if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users, &ignore_users,
&user, &user,
@ -266,7 +273,14 @@ pub async fn posts_request(
Ok(p) => match data Ok(p) => match data
.0 .0
.fill_posts_with_community( .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 }, if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users, &ignore_users,
&user, &user,
@ -285,7 +299,14 @@ pub async fn posts_request(
Ok(p) => match data Ok(p) => match data
.0 .0
.fill_posts_with_community( .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 }, if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users, &ignore_users,
&user, &user,
@ -394,7 +415,14 @@ pub async fn replies_request(
Ok(p) => match data Ok(p) => match data
.0 .0
.fill_posts_with_community( .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 }, if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users, &ignore_users,
&user, &user,
@ -500,7 +528,14 @@ pub async fn media_request(
Ok(p) => match data Ok(p) => match data
.0 .0
.fill_posts_with_community( .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 }, if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users, &ignore_users,
&user, &user,

View file

@ -21,8 +21,20 @@ use oiseau::SqliteRow;
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
use oiseau::PostgresRow; use oiseau::PostgresRow;
#[cfg(feature = "redis")]
use oiseau::cache::redis::Commands;
use oiseau::{execute, get, query_row, query_rows, params}; 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 { macro_rules! private_post_replying {
($post:ident, $replying_posts:ident, $ua1:ident, $data:ident) => { ($post:ident, $replying_posts:ident, $ua1:ident, $data:ident) => {
// post owner is not following us // post owner is not following us
@ -376,16 +388,7 @@ impl DataManager {
user_id: usize, user_id: usize,
ignore_users: &[usize], ignore_users: &[usize],
user: &Option<User>, user: &Option<User>,
) -> Result< ) -> Result<Vec<FullPost>> {
Vec<(
Post,
User,
Community,
Option<(User, Post)>,
Option<(Question, User)>,
Option<(Poll, bool, bool)>,
)>,
> {
let mut out = Vec::new(); let mut out = Vec::new();
let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new(); let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new();
@ -463,6 +466,33 @@ impl DataManager {
Ok(out) Ok(out)
} }
/// Update posts which contain a muted phrase.
pub fn posts_muted_phrase_filter(
&self,
posts: &Vec<Post>,
muted: Option<&Vec<String>>,
) -> Vec<Post> {
let muted = match muted {
Some(m) => m,
None => return posts.to_owned(),
};
let mut out: Vec<Post> = 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). /// Get all posts from the given user (from most recent).
/// ///
/// # Arguments /// # Arguments
@ -533,7 +563,7 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &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)], &[&(id as i64)],
|x| { Self::get_post_from_row(x) } |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 = (good_posts as f32 / real_posts_count as f32) * 4.0;
let gpa_rounded = format!("{gpa:.2}").parse::<f32>().unwrap(); let gpa_rounded = format!("{gpa:.2}").parse::<f32>().unwrap();
self.0 let mut redis_con = self.0.1.get_con().await;
.1
.set(format!("atto.user.gpa:{}", id), gpa_rounded.to_string())
.await;
// expires in one day
if redis_con
.set_ex::<String, String, usize>(
format!("atto.user.gpa:{}", id),
gpa_rounded.to_string(),
86400,
)
.is_err()
{
return 0.0;
};
// ...
gpa_rounded gpa_rounded
} }

View file

@ -225,6 +225,12 @@ pub struct UserSettings {
/// If extra post tabs are hidden (replies, media). /// If extra post tabs are hidden (replies, media).
#[serde(default)] #[serde(default)]
pub hide_extra_post_tabs: bool, 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<String>,
} }
fn mime_avif() -> String { fn mime_avif() -> String {