add: hide_social_follows setting
This commit is contained in:
parent
e78c43ab62
commit
a337e0c7c1
11 changed files with 179 additions and 96 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -3320,7 +3320,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "12.0.1"
|
version = "12.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
@ -3353,7 +3353,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "12.0.5"
|
version = "12.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -4,15 +4,11 @@ use nanoneo::{
|
||||||
};
|
};
|
||||||
use pathbufd::PathBufD;
|
use pathbufd::PathBufD;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::{
|
use std::{collections::HashMap, fs::read_to_string, sync::LazyLock, time::SystemTime};
|
||||||
collections::HashMap,
|
|
||||||
fs::{exists, read_to_string, write},
|
|
||||||
sync::LazyLock,
|
|
||||||
time::SystemTime,
|
|
||||||
};
|
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
use tetratto_core::{
|
use tetratto_core::{
|
||||||
config::Config,
|
config::Config,
|
||||||
|
html::{pull_icons, ICONS},
|
||||||
model::{
|
model::{
|
||||||
auth::{DefaultTimelineChoice, User},
|
auth::{DefaultTimelineChoice, User},
|
||||||
permissions::{FinePermission, SecondaryPermission},
|
permissions::{FinePermission, SecondaryPermission},
|
||||||
|
@ -157,38 +153,6 @@ pub const TETRATTO_BUNNY: &[u8] = include_bytes!("./public/images/tetratto_bunny
|
||||||
pub(crate) static HTML_FOOTER: LazyLock<RwLock<String>> =
|
pub(crate) static HTML_FOOTER: LazyLock<RwLock<String>> =
|
||||||
LazyLock::new(|| RwLock::new(String::new()));
|
LazyLock::new(|| RwLock::new(String::new()));
|
||||||
|
|
||||||
/// A container for all loaded icons.
|
|
||||||
pub(crate) static ICONS: LazyLock<RwLock<HashMap<String, String>>> =
|
|
||||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
|
||||||
|
|
||||||
/// Pull an icon given its name and insert it into [`ICONS`].
|
|
||||||
pub(crate) async fn pull_icon(icon: &str, icons_dir: &str) {
|
|
||||||
let writer = &mut ICONS.write().await;
|
|
||||||
|
|
||||||
let icon_url = format!(
|
|
||||||
"https://raw.githubusercontent.com/lucide-icons/lucide/refs/heads/main/icons/{icon}.svg"
|
|
||||||
);
|
|
||||||
|
|
||||||
let file_path = PathBufD::current().extend(&[icons_dir, &format!("{icon}.svg")]);
|
|
||||||
|
|
||||||
if exists(&file_path).unwrap() {
|
|
||||||
writer.insert(icon.to_string(), read_to_string(&file_path).unwrap());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("download icon: {icon}");
|
|
||||||
let svg = reqwest::get(icon_url)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.replace("\n", "");
|
|
||||||
|
|
||||||
write(&file_path, &svg).unwrap();
|
|
||||||
writer.insert(icon.to_string(), svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! vendor_icon {
|
macro_rules! vendor_icon {
|
||||||
($name:literal, $icon:ident, $icons_dir:expr) => {{
|
($name:literal, $icon:ident, $icons_dir:expr) => {{
|
||||||
let writer = &mut ICONS.write().await;
|
let writer = &mut ICONS.write().await;
|
||||||
|
@ -261,56 +225,8 @@ pub(crate) async fn replace_in_html(
|
||||||
input = input.replace(cap.get(0).unwrap().as_str(), &replace_with);
|
input = input.replace(cap.get(0).unwrap().as_str(), &replace_with);
|
||||||
}
|
}
|
||||||
|
|
||||||
// icon (with class)
|
// icons
|
||||||
let icon_with_class =
|
input = pull_icons(input, &config.dirs.icons).await;
|
||||||
Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*c\\((.*?)\\)\\s*(\\}\\})").unwrap();
|
|
||||||
|
|
||||||
for cap in icon_with_class.captures_iter(&input.clone()) {
|
|
||||||
let cap_str = &cap.get(3).unwrap().as_str().replace("\"", "");
|
|
||||||
let icon = &(if cap_str.contains(" }}") {
|
|
||||||
cap_str.split(" }}").next().unwrap().to_string()
|
|
||||||
} else {
|
|
||||||
cap_str.to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
pull_icon(icon, &config.dirs.icons).await;
|
|
||||||
|
|
||||||
let reader = ICONS.read().await;
|
|
||||||
let icon_text = reader.get(icon).unwrap().replace(
|
|
||||||
"<svg",
|
|
||||||
&format!("<svg class=\"icon {}\"", cap.get(4).unwrap().as_str()),
|
|
||||||
);
|
|
||||||
|
|
||||||
input = input.replace(
|
|
||||||
&format!(
|
|
||||||
"{{{{ icon \"{cap_str}\" c({}) }}}}",
|
|
||||||
cap.get(4).unwrap().as_str()
|
|
||||||
),
|
|
||||||
&icon_text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// icon (without class)
|
|
||||||
let icon_without_class = Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*(\\}\\})").unwrap();
|
|
||||||
|
|
||||||
for cap in icon_without_class.captures_iter(&input.clone()) {
|
|
||||||
let cap_str = &cap.get(3).unwrap().as_str().replace("\"", "");
|
|
||||||
let icon = &(if cap_str.contains(" }}") {
|
|
||||||
cap_str.split(" }}").next().unwrap().to_string()
|
|
||||||
} else {
|
|
||||||
cap_str.to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
pull_icon(icon, &config.dirs.icons).await;
|
|
||||||
|
|
||||||
let reader = ICONS.read().await;
|
|
||||||
let icon_text = reader
|
|
||||||
.get(icon)
|
|
||||||
.unwrap()
|
|
||||||
.replace("<svg", "<svg class=\"icon\"");
|
|
||||||
|
|
||||||
input = input.replace(&format!("{{{{ icon \"{cap_str}\" }}}}",), &icon_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return
|
// return
|
||||||
input = input.replacen("<!-- html_footer_goes_here -->", &format!("{reader}"), 1);
|
input = input.replacen("<!-- html_footer_goes_here -->", &format!("{reader}"), 1);
|
||||||
|
|
|
@ -107,6 +107,7 @@
|
||||||
(p
|
(p
|
||||||
(text "{{ profile.settings.status }}"))
|
(text "{{ profile.settings.status }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
(text "{% if not profile.settings.hide_social_follows or (user and user.id == profile.id) -%}")
|
||||||
(div
|
(div
|
||||||
("class" "w-full flex")
|
("class" "w-full flex")
|
||||||
(a
|
(a
|
||||||
|
@ -123,6 +124,7 @@
|
||||||
(text "{{ profile.following_count }}"))
|
(text "{{ profile.following_count }}"))
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"auth:label.following\" }}"))))
|
(text "{{ text \"auth:label.following\" }}"))))
|
||||||
|
(text "{%- endif %}")
|
||||||
(text "{% if is_following_you -%}")
|
(text "{% if is_following_you -%}")
|
||||||
(b
|
(b
|
||||||
("class" "notification chip w-content flex items-center gap-2")
|
("class" "notification chip w-content flex items-center gap-2")
|
||||||
|
|
|
@ -1889,6 +1889,14 @@
|
||||||
\"{{ profile.settings.hide_from_social_lists }}\",
|
\"{{ profile.settings.hide_from_social_lists }}\",
|
||||||
\"checkbox\",
|
\"checkbox\",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
\"hide_social_follows\",
|
||||||
|
\"Hide followers/following links on my profile\",
|
||||||
|
],
|
||||||
|
\"{{ profile.settings.hide_social_follows }}\",
|
||||||
|
\"checkbox\",
|
||||||
|
],
|
||||||
[[], \"Questions\", \"title\"],
|
[[], \"Questions\", \"title\"],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
|
|
@ -731,6 +731,21 @@ pub async fn following_request(
|
||||||
|
|
||||||
check_user_blocked_or_private!(user, other_user, data, jar);
|
check_user_blocked_or_private!(user, other_user, data, jar);
|
||||||
|
|
||||||
|
// check hide_social_follows
|
||||||
|
if other_user.settings.hide_social_follows {
|
||||||
|
if let Some(ref ua) = user {
|
||||||
|
if ua.id != other_user.id {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fetch data
|
// fetch data
|
||||||
let list = match data
|
let list = match data
|
||||||
.0
|
.0
|
||||||
|
@ -826,6 +841,21 @@ pub async fn followers_request(
|
||||||
|
|
||||||
check_user_blocked_or_private!(user, other_user, data, jar);
|
check_user_blocked_or_private!(user, other_user, data, jar);
|
||||||
|
|
||||||
|
// check hide_social_follows
|
||||||
|
if other_user.settings.hide_social_follows {
|
||||||
|
if let Some(ref ua) = user {
|
||||||
|
if ua.id != other_user.id {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fetch data
|
// fetch data
|
||||||
let list = match data
|
let list = match data
|
||||||
.0
|
.0
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
description = "The core behind Tetratto"
|
description = "The core behind Tetratto"
|
||||||
version = "12.0.1"
|
version = "12.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
@ -18,7 +18,7 @@ default = ["database", "types", "sdk"]
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
toml = "0.9.2"
|
toml = "0.9.2"
|
||||||
tetratto-shared = { version = "12.0.0", path = "../shared" }
|
tetratto-shared = { version = "12.0.6", path = "../shared" }
|
||||||
tetratto-l10n = { version = "12.0.0", path = "../l10n" }
|
tetratto-l10n = { version = "12.0.0", path = "../l10n" }
|
||||||
serde_json = "1.0.141"
|
serde_json = "1.0.141"
|
||||||
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"], optional = true }
|
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"], optional = true }
|
||||||
|
@ -35,6 +35,4 @@ oiseau = { version = "0.1.2", default-features = false, features = [
|
||||||
"redis",
|
"redis",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
paste = { version = "1.0.15", optional = true }
|
paste = { version = "1.0.15", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
97
crates/core/src/html.rs
Normal file
97
crates/core/src/html.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::{exists, read_to_string, write},
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use pathbufd::PathBufD;
|
||||||
|
|
||||||
|
/// A container for all loaded icons.
|
||||||
|
pub static ICONS: LazyLock<RwLock<HashMap<String, String>>> =
|
||||||
|
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
/// Pull an icon given its name and insert it into [`ICONS`].
|
||||||
|
pub async fn pull_icon(icon: &str, icons_dir: &str) {
|
||||||
|
let writer = &mut ICONS.write().await;
|
||||||
|
|
||||||
|
let icon_url = format!(
|
||||||
|
"https://raw.githubusercontent.com/lucide-icons/lucide/refs/heads/main/icons/{icon}.svg"
|
||||||
|
);
|
||||||
|
|
||||||
|
let file_path = PathBufD::current().extend(&[icons_dir, &format!("{icon}.svg")]);
|
||||||
|
|
||||||
|
if exists(&file_path).unwrap_or(false) {
|
||||||
|
writer.insert(icon.to_string(), read_to_string(&file_path).unwrap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("download icon: {icon}");
|
||||||
|
let svg = reqwest::get(icon_url)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.replace("\n", "");
|
||||||
|
|
||||||
|
write(&file_path, &svg).unwrap();
|
||||||
|
writer.insert(icon.to_string(), svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a string and pull all icons found within it.
|
||||||
|
pub async fn pull_icons(mut input: String, icon_dir: &str) -> String {
|
||||||
|
// icon (with class)
|
||||||
|
let icon_with_class =
|
||||||
|
regex::Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*c\\((.*?)\\)\\s*(\\}\\})").unwrap();
|
||||||
|
|
||||||
|
for cap in icon_with_class.captures_iter(&input.clone()) {
|
||||||
|
let cap_str = &cap.get(3).unwrap().as_str().replace("\"", "");
|
||||||
|
let icon = &(if cap_str.contains(" }}") {
|
||||||
|
cap_str.split(" }}").next().unwrap().to_string()
|
||||||
|
} else {
|
||||||
|
cap_str.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
pull_icon(icon, icon_dir).await;
|
||||||
|
|
||||||
|
let reader = ICONS.read().await;
|
||||||
|
let icon_text = reader.get(icon).unwrap().replace(
|
||||||
|
"<svg",
|
||||||
|
&format!("<svg class=\"icon {}\"", cap.get(4).unwrap().as_str()),
|
||||||
|
);
|
||||||
|
|
||||||
|
input = input.replace(
|
||||||
|
&format!(
|
||||||
|
"{{{{ icon \"{cap_str}\" c({}) }}}}",
|
||||||
|
cap.get(4).unwrap().as_str()
|
||||||
|
),
|
||||||
|
&icon_text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon (without class)
|
||||||
|
let icon_without_class = regex::Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*(\\}\\})").unwrap();
|
||||||
|
|
||||||
|
for cap in icon_without_class.captures_iter(&input.clone()) {
|
||||||
|
let cap_str = &cap.get(3).unwrap().as_str().replace("\"", "");
|
||||||
|
let icon = &(if cap_str.contains(" }}") {
|
||||||
|
cap_str.split(" }}").next().unwrap().to_string()
|
||||||
|
} else {
|
||||||
|
cap_str.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
pull_icon(icon, icon_dir).await;
|
||||||
|
|
||||||
|
let reader = ICONS.read().await;
|
||||||
|
let icon_text = reader
|
||||||
|
.get(icon)
|
||||||
|
.unwrap()
|
||||||
|
.replace("<svg", "<svg class=\"icon\"");
|
||||||
|
|
||||||
|
input = input.replace(&format!("{{{{ icon \"{cap_str}\" }}}}",), &icon_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
input
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ pub mod config;
|
||||||
#[cfg(feature = "database")]
|
#[cfg(feature = "database")]
|
||||||
pub mod database;
|
pub mod database;
|
||||||
#[cfg(feature = "types")]
|
#[cfg(feature = "types")]
|
||||||
|
pub mod html;
|
||||||
|
#[cfg(feature = "types")]
|
||||||
pub mod model;
|
pub mod model;
|
||||||
#[cfg(feature = "sdk")]
|
#[cfg(feature = "sdk")]
|
||||||
pub mod sdk;
|
pub mod sdk;
|
||||||
|
|
|
@ -337,6 +337,10 @@ pub struct UserSettings {
|
||||||
/// Biography shown on `profile/private.lisp` page.
|
/// Biography shown on `profile/private.lisp` page.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub private_biography: String,
|
pub private_biography: String,
|
||||||
|
/// If the followers/following links are hidden from the user's profile.
|
||||||
|
/// Will also revoke access to their respective pages.
|
||||||
|
#[serde(default)]
|
||||||
|
pub hide_social_follows: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
description = "Shared stuff for Tetratto"
|
description = "Shared stuff for Tetratto"
|
||||||
version = "12.0.5"
|
version = "12.0.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -3,7 +3,7 @@ use pulldown_cmark::{Parser, Options, html::push_html};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
pub fn render_markdown_dirty(input: &str) -> String {
|
pub fn render_markdown_dirty(input: &str) -> String {
|
||||||
let input = &autolinks(&parse_alignment(input));
|
let input = &autolinks(&parse_alignment(&parse_backslash_breaks(input)));
|
||||||
|
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||||
|
@ -180,6 +180,32 @@ pub fn parse_alignment(input: &str) -> String {
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_backslash_breaks(input: &str) -> String {
|
||||||
|
let mut in_pre_block = false;
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
for line in input.split("\n") {
|
||||||
|
if line.starts_with("```") {
|
||||||
|
in_pre_block = !in_pre_block;
|
||||||
|
output.push_str(&format!("{line}\n"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_pre_block {
|
||||||
|
output.push_str(&format!("{line}\n"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.trim_end().ends_with("\\") {
|
||||||
|
output.push_str(&format!("{line}<br />\n"));
|
||||||
|
} else {
|
||||||
|
output.push_str(&format!("{line}\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
/// Adapted from <https://git.cypr.io/oz/autolink-rust>.
|
/// Adapted from <https://git.cypr.io/oz/autolink-rust>.
|
||||||
///
|
///
|
||||||
/// The only real change here is that autolinks require a whitespace OR end the
|
/// The only real change here is that autolinks require a whitespace OR end the
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue