diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp
index 5fe4010..6d08053 100644
--- a/crates/app/src/public/html/profile/settings.lisp
+++ b/crates/app/src/public/html/profile/settings.lisp
@@ -143,8 +143,7 @@
(text "{% endfor %}"))
(span
("class" "fade")
- (text "This represents the timeline the home button takes you
- to."))))
+ (text "This represents the timeline the home button takes you to."))))
(div
("class" "card-nest desktop")
("ui_ident" "notifications")
@@ -1540,6 +1539,14 @@
\"{{ profile.settings.hide_associated_blocked_users }}\",
\"checkbox\",
],
+ [
+ [
+ \"hide_from_social_lists\",
+ \"Hide my profile from social lists (followers/following)\",
+ ],
+ \"{{ profile.settings.hide_from_social_lists }}\",
+ \"checkbox\",
+ ],
[[], \"Questions\", \"title\"],
[
[
diff --git a/crates/app/src/routes/api/v1/auth/social.rs b/crates/app/src/routes/api/v1/auth/social.rs
index b80bd14..88a78b5 100644
--- a/crates/app/src/routes/api/v1/auth/social.rs
+++ b/crates/app/src/routes/api/v1/auth/social.rs
@@ -278,7 +278,10 @@ pub async fn followers_request(
Ok(f) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
- payload: match data.fill_userfollows_with_initiator(f).await {
+ payload: match data
+ .fill_userfollows_with_initiator(f, &Some(user.clone()), id == user.id)
+ .await
+ {
Ok(f) => Some(data.userfollows_user_filter(&f)),
Err(e) => return Json(e.into()),
},
@@ -310,7 +313,10 @@ pub async fn following_request(
Ok(f) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
- payload: match data.fill_userfollows_with_receiver(f).await {
+ payload: match data
+ .fill_userfollows_with_receiver(f, &Some(user.clone()), id == user.id)
+ .await
+ {
Ok(f) => Some(data.userfollows_user_filter(&f)),
Err(e) => return Json(e.into()),
},
diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs
index 186d291..4d12556 100644
--- a/crates/app/src/routes/pages/profile.rs
+++ b/crates/app/src/routes/pages/profile.rs
@@ -70,6 +70,8 @@ pub async fn settings_request(
.get_userfollows_by_initiator_all(profile.id)
.await
.unwrap_or(Vec::new()),
+ &None,
+ false,
)
.await
{
@@ -718,7 +720,7 @@ pub async fn following_request(
.get_userfollows_by_initiator(other_user.id, 12, props.page)
.await
{
- Ok(l) => match data.0.fill_userfollows_with_receiver(l).await {
+ Ok(l) => match data.0.fill_userfollows_with_receiver(l, &user, true).await {
Ok(l) => l,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
@@ -813,7 +815,7 @@ pub async fn followers_request(
.get_userfollows_by_receiver(other_user.id, 12, props.page)
.await
{
- Ok(l) => match data.0.fill_userfollows_with_initiator(l).await {
+ Ok(l) => match data.0.fill_userfollows_with_initiator(l, &user, true).await {
Ok(l) => l,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
diff --git a/crates/core/src/database/userfollows.rs b/crates/core/src/database/userfollows.rs
index 5428f67..63fcfbe 100644
--- a/crates/core/src/database/userfollows.rs
+++ b/crates/core/src/database/userfollows.rs
@@ -195,18 +195,29 @@ impl DataManager {
pub async fn fill_userfollows_with_receiver(
&self,
userfollows: Vec,
+ as_user: &Option,
+ do_check: bool,
) -> Result> {
let mut out: Vec<(UserFollow, User)> = Vec::new();
for userfollow in userfollows {
let receiver = userfollow.receiver;
- out.push((
- userfollow,
- match self.get_user_by_id(receiver).await {
- Ok(u) => u,
- Err(_) => continue,
- },
- ));
+ let user = match self.get_user_by_id(receiver).await {
+ Ok(u) => u,
+ Err(_) => continue,
+ };
+
+ if user.settings.hide_from_social_lists && do_check {
+ if let Some(ua) = as_user {
+ if !ua.permissions.check(FinePermission::MANAGE_USERS) {
+ continue;
+ }
+ } else {
+ continue;
+ }
+ }
+
+ out.push((userfollow, user));
}
Ok(out)
@@ -216,18 +227,29 @@ impl DataManager {
pub async fn fill_userfollows_with_initiator(
&self,
userfollows: Vec,
+ as_user: &Option,
+ do_check: bool,
) -> Result> {
let mut out: Vec<(UserFollow, User)> = Vec::new();
for userfollow in userfollows {
let initiator = userfollow.initiator;
- out.push((
- userfollow,
- match self.get_user_by_id(initiator).await {
- Ok(u) => u,
- Err(_) => continue,
- },
- ));
+ let user = match self.get_user_by_id(initiator).await {
+ Ok(u) => u,
+ Err(_) => continue,
+ };
+
+ if user.settings.hide_from_social_lists && do_check {
+ if let Some(ua) = as_user {
+ if !ua.permissions.check(FinePermission::MANAGE_USERS) {
+ continue;
+ }
+ } else {
+ continue;
+ }
+ }
+
+ out.push((userfollow, user));
}
Ok(out)
diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs
index 4303640..3b14d41 100644
--- a/crates/core/src/model/auth.rs
+++ b/crates/core/src/model/auth.rs
@@ -302,6 +302,12 @@ pub struct UserSettings {
/// Which tab is shown by default on the user's profile.
#[serde(default)]
pub default_profile_tab: DefaultProfileTabChoice,
+ /// If the user is hidden from followers/following tabs.
+ ///
+ /// The user will still impact the followers/following numbers, but will not
+ /// be shown in the UI (or API).
+ #[serde(default)]
+ pub hide_from_social_lists: bool,
}
fn mime_avif() -> String {
@@ -521,7 +527,7 @@ pub struct ExternalConnectionData {
}
/// The total number of achievements needed to 100% Tetratto!
-pub const ACHIEVEMENTS: usize = 34;
+pub const ACHIEVEMENTS: usize = 36;
/// "self-serve" achievements can be granted by the user through the API.
pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[
AchievementName::OpenReference,