add: dedicated responses tab for profiles

This commit is contained in:
trisua 2025-07-06 13:34:20 -04:00
parent 9ba6320d46
commit 07a23f505b
24 changed files with 332 additions and 55 deletions

View file

@ -1,6 +1,8 @@
use super::common::NAME_REGEX;
use oiseau::cache::Cache;
use crate::model::auth::{Achievement, AchievementRarity, Notification, UserConnections};
use crate::model::auth::{
Achievement, AchievementName, AchievementRarity, Notification, UserConnections, ACHIEVEMENTS,
};
use crate::model::moderation::AuditLogEntry;
use crate::model::oauth::AuthGrant;
use crate::model::permissions::SecondaryPermission;
@ -764,7 +766,13 @@ impl DataManager {
/// Add an achievement to a user.
///
/// Still returns `Ok` if the user already has the achievement.
pub async fn add_achievement(&self, user: &mut User, achievement: Achievement) -> Result<()> {
#[async_recursion::async_recursion]
pub async fn add_achievement(
&self,
user: &mut User,
achievement: Achievement,
check_for_final: bool,
) -> Result<()> {
if user.settings.disable_achievements {
return Ok(());
}
@ -794,6 +802,15 @@ impl DataManager {
self.update_user_achievements(user.id, user.achievements.to_owned())
.await?;
// check for final
if check_for_final {
if user.achievements.len() + 1 == ACHIEVEMENTS {
self.add_achievement(user, AchievementName::GetAllOtherAchievements.into(), false)
.await?;
}
}
// ...
Ok(())
}

View file

@ -242,8 +242,12 @@ impl DataManager {
Ok(if data.role.check(CommunityPermission::REQUESTED) {
"Join request sent".to_string()
} else {
self.add_achievement(&mut user.clone(), AchievementName::JoinCommunity.into())
.await?;
self.add_achievement(
&mut user.clone(),
AchievementName::JoinCommunity.into(),
true,
)
.await?;
"Community joined".to_string()
})

View file

@ -758,6 +758,37 @@ impl DataManager {
Ok(res.unwrap())
}
/// Get all posts (that are answering a question) from the given user (from most recent).
///
/// # Arguments
/// * `id` - the ID of the user the requested posts belong to
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
pub async fn get_responses_by_user(
&self,
id: usize,
batch: usize,
page: usize,
) -> Result<Vec<Post>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 AND NOT context::jsonb->>'answering' = '0' ORDER BY created DESC LIMIT $2 OFFSET $3",
&[&(id as i64), &(batch as i64), &((page * batch) as i64)],
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
}
Ok(res.unwrap())
}
/// Calculate the GPA (great post average) of a given user.
///
/// To be considered a "great post", a post must have a score ((likes - dislikes) / (likes + dislikes))
@ -1066,6 +1097,45 @@ impl DataManager {
Ok(res.unwrap())
}
/// Get all posts (that are answering a question) from the given user
/// with the given tag (from most recent).
///
/// # Arguments
/// * `id` - the ID of the user the requested posts belong to
/// * `tag` - the tag to filter by
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
pub async fn get_responses_by_user_tag(
&self,
id: usize,
tag: &str,
batch: usize,
page: usize,
) -> Result<Vec<Post>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM posts WHERE owner = $1 AND context::json->>'tags' LIKE $2 AND is_deleted = 0 AND NOT context::jsonb->>'answering' = '0' ORDER BY created DESC LIMIT $3 OFFSET $4",
params![
&(id as i64),
&format!("%\"{tag}\"%"),
&(batch as i64),
&((page * batch) as i64)
],
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
}
Ok(res.unwrap())
}
/// Get all posts from the given community (from most recent).
///
/// # Arguments
@ -1661,8 +1731,12 @@ impl DataManager {
}
// award achievement
self.add_achievement(&mut owner, AchievementName::CreatePostWithTitle.into())
.await?;
self.add_achievement(
&mut owner,
AchievementName::CreatePostWithTitle.into(),
true,
)
.await?;
}
}
@ -1803,7 +1877,7 @@ impl DataManager {
}
// award achievement
self.add_achievement(&mut owner, AchievementName::CreateRepost.into())
self.add_achievement(&mut owner, AchievementName::CreateRepost.into(), true)
.await?;
}

View file

@ -162,26 +162,26 @@ impl DataManager {
// achievements
if user.id != post.owner {
let mut owner = self.get_user_by_id(post.owner).await?;
self.add_achievement(&mut owner, AchievementName::Get1Like.into())
self.add_achievement(&mut owner, AchievementName::Get1Like.into(), true)
.await?;
if post.likes >= 9 {
self.add_achievement(&mut owner, AchievementName::Get10Likes.into())
self.add_achievement(&mut owner, AchievementName::Get10Likes.into(), true)
.await?;
}
if post.likes >= 49 {
self.add_achievement(&mut owner, AchievementName::Get50Likes.into())
self.add_achievement(&mut owner, AchievementName::Get50Likes.into(), true)
.await?;
}
if post.likes >= 99 {
self.add_achievement(&mut owner, AchievementName::Get100Likes.into())
self.add_achievement(&mut owner, AchievementName::Get100Likes.into(), true)
.await?;
}
if post.dislikes >= 24 {
self.add_achievement(&mut owner, AchievementName::Get25Dislikes.into())
self.add_achievement(&mut owner, AchievementName::Get25Dislikes.into(), true)
.await?;
}
}

View file

@ -262,33 +262,50 @@ impl DataManager {
// check if we're staff
if initiator.permissions.check(FinePermission::STAFF_BADGE) {
self.add_achievement(&mut other_user, AchievementName::FollowedByStaff.into())
.await?;
self.add_achievement(
&mut other_user,
AchievementName::FollowedByStaff.into(),
true,
)
.await?;
}
// other achivements
self.add_achievement(&mut other_user, AchievementName::Get1Follower.into())
self.add_achievement(&mut other_user, AchievementName::Get1Follower.into(), true)
.await?;
if other_user.follower_count >= 9 {
self.add_achievement(&mut other_user, AchievementName::Get10Followers.into())
.await?;
self.add_achievement(
&mut other_user,
AchievementName::Get10Followers.into(),
true,
)
.await?;
}
if other_user.follower_count >= 49 {
self.add_achievement(&mut other_user, AchievementName::Get50Followers.into())
.await?;
self.add_achievement(
&mut other_user,
AchievementName::Get50Followers.into(),
true,
)
.await?;
}
if other_user.follower_count >= 99 {
self.add_achievement(&mut other_user, AchievementName::Get100Followers.into())
.await?;
self.add_achievement(
&mut other_user,
AchievementName::Get100Followers.into(),
true,
)
.await?;
}
if initiator.following_count >= 9 {
self.add_achievement(
&mut initiator.clone(),
AchievementName::Follow10Users.into(),
true,
)
.await?;
}

View file

@ -124,6 +124,20 @@ impl DefaultTimelineChoice {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum DefaultProfileTabChoice {
/// General posts (in any community) from the user.
Posts,
/// Responses to questions.
Responses,
}
impl Default for DefaultProfileTabChoice {
fn default() -> Self {
Self::Posts
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct UserSettings {
#[serde(default)]
@ -285,6 +299,9 @@ pub struct UserSettings {
/// Automatically hide users that you've blocked on your other accounts from your timelines.
#[serde(default)]
pub hide_associated_blocked_users: bool,
/// Which tab is shown by default on the user's profile.
#[serde(default)]
pub default_profile_tab: DefaultProfileTabChoice,
}
fn mime_avif() -> String {
@ -504,10 +521,15 @@ pub struct ExternalConnectionData {
}
/// The total number of achievements needed to 100% Tetratto!
pub const ACHIEVEMENTS: usize = 30;
pub const ACHIEVEMENTS: usize = 34;
/// "self-serve" achievements can be granted by the user through the API.
pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] =
&[AchievementName::OpenTos, AchievementName::OpenPrivacyPolicy];
pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[
AchievementName::OpenReference,
AchievementName::OpenTos,
AchievementName::OpenPrivacyPolicy,
AchievementName::AcceptProfileWarning,
AchievementName::OpenSessionSettings,
];
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AchievementName {
@ -541,6 +563,10 @@ pub enum AchievementName {
CreateRepost,
OpenTos,
OpenPrivacyPolicy,
OpenReference,
GetAllOtherAchievements,
AcceptProfileWarning,
OpenSessionSettings,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -583,6 +609,10 @@ impl AchievementName {
Self::CreateRepost => "More than a like or comment...",
Self::OpenTos => "Well informed!",
Self::OpenPrivacyPolicy => "Privacy conscious",
Self::OpenReference => "What does this do?",
Self::GetAllOtherAchievements => "The final performance",
Self::AcceptProfileWarning => "I accept the risks!",
Self::OpenSessionSettings => "Am I alone in here?",
}
}
@ -618,6 +648,10 @@ impl AchievementName {
Self::CreateRepost => "Create a repost or quote.",
Self::OpenTos => "Open the terms of service.",
Self::OpenPrivacyPolicy => "Open the privacy policy.",
Self::OpenReference => "Open the source code reference documentation.",
Self::GetAllOtherAchievements => "Get every other achievement.",
Self::AcceptProfileWarning => "Accept a profile warning.",
Self::OpenSessionSettings => "Open your session settings.",
}
}
@ -655,6 +689,10 @@ impl AchievementName {
Self::CreateRepost => Common,
Self::OpenTos => Uncommon,
Self::OpenPrivacyPolicy => Uncommon,
Self::OpenReference => Uncommon,
Self::GetAllOtherAchievements => Rare,
Self::AcceptProfileWarning => Common,
Self::OpenSessionSettings => Common,
}
}
}