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,