diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 9b5f567..2f4f004 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -1084,10 +1084,12 @@ ("href" "/journals/0/0") (icon (text "notebook")) (str (text "general:link.journals"))) + (text "{% if not user.settings.disable_achievements -%}") (a ("href" "/achievements") (icon (text "award")) (str (text "general:link.achievements"))) + (text "{%- endif %}") (a ("href" "/settings") (text "{{ icon \"settings\" }}") diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index f2280e6..4ac8b4c 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -1553,6 +1553,11 @@ \"{{ profile.settings.disable_gpa_fun }}\", \"checkbox\", ], + [ + [\"disable_achievements\", \"Disable achievements\"], + \"{{ profile.settings.disable_achievements }}\", + \"checkbox\", + ], ], settings, ); diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js index 31290c9..15bf7a2 100644 --- a/crates/app/src/public/js/me.js +++ b/crates/app/src/public/js/me.js @@ -979,7 +979,13 @@ self.define( "timestamp", - ({ $ }, updated_, progress_ms_, duration_ms_, display = "full") => { + async ( + { $ }, + updated_, + progress_ms_, + duration_ms_, + display = "full", + ) => { if (duration_ms_ === "0") { return; } @@ -1003,7 +1009,7 @@ } if (display === "full") { - return `${$.ms_time_text(progress_ms)}/${$.ms_time_text(duration_ms)} (${Math.floor((progress_ms / duration_ms) * 100)}%)`; + return `${await $.ms_time_text(progress_ms)}/${await $.ms_time_text(duration_ms)} (${Math.floor((progress_ms / duration_ms) * 100)}%)`; } if (display === "left") { diff --git a/crates/app/src/routes/api/v1/communities/communities.rs b/crates/app/src/routes/api/v1/communities/communities.rs index e2c72f1..539cc08 100644 --- a/crates/app/src/routes/api/v1/communities/communities.rs +++ b/crates/app/src/routes/api/v1/communities/communities.rs @@ -292,11 +292,10 @@ pub async fn create_membership( }; match data - .create_membership(CommunityMembership::new( - user.id, - id, - CommunityPermission::default(), - )) + .create_membership( + CommunityMembership::new(user.id, id, CommunityPermission::default()), + &user, + ) .await { Ok(m) => Json(ApiReturn { diff --git a/crates/app/src/routes/api/v1/communities/drafts.rs b/crates/app/src/routes/api/v1/communities/drafts.rs index a6de4c9..346a253 100644 --- a/crates/app/src/routes/api/v1/communities/drafts.rs +++ b/crates/app/src/routes/api/v1/communities/drafts.rs @@ -4,7 +4,7 @@ use axum::{ Extension, Json, }; use axum_extra::extract::CookieJar; -use tetratto_core::model::{communities::PostDraft, oauth, ApiReturn, Error}; +use tetratto_core::model::{auth::AchievementName, communities::PostDraft, oauth, ApiReturn, Error}; use crate::{ get_user_from_token, routes::{ @@ -20,11 +20,20 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateDrafts) { + let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateDrafts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; + // award achievement + if let Err(e) = data + .add_achievement(&mut user, AchievementName::CreateDraft.into()) + .await + { + return Json(e.into()); + } + + // ... match data .create_draft(PostDraft::new(req.content, user.id)) .await diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index fee1ba8..1982d6e 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -341,11 +341,20 @@ pub async fn update_content_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserEditPosts) { + let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserEditPosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; + // award achievement + if let Err(e) = data + .add_achievement(&mut user, AchievementName::EditPost.into()) + .await + { + return Json(e.into()); + } + + // ... match data.update_post_content(id, user, req.content).await { Ok(_) => Json(ApiReturn { ok: true, diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index a34b634..f6fb848 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -713,6 +713,10 @@ impl DataManager { /// /// Still returns `Ok` if the user already has the achievement. pub async fn add_achievement(&self, user: &mut User, achievement: Achievement) -> Result<()> { + if user.settings.disable_achievements { + return Ok(()); + } + if user .achievements .iter() diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs index 2642f37..8237b7e 100644 --- a/crates/core/src/database/communities.rs +++ b/crates/core/src/database/communities.rs @@ -299,11 +299,10 @@ impl DataManager { } // add community owner as admin - self.create_membership(CommunityMembership::new( - data.owner, - data.id, - CommunityPermission::ADMINISTRATOR, - )) + self.create_membership( + CommunityMembership::new(data.owner, data.id, CommunityPermission::ADMINISTRATOR), + &owner, + ) .await .unwrap(); diff --git a/crates/core/src/database/memberships.rs b/crates/core/src/database/memberships.rs index 4ae7094..01f286b 100644 --- a/crates/core/src/database/memberships.rs +++ b/crates/core/src/database/memberships.rs @@ -1,4 +1,5 @@ use oiseau::cache::Cache; +use crate::model::auth::AchievementName; use crate::model::communities::Community; use crate::model::requests::{ActionRequest, ActionType}; use crate::model::{ @@ -169,7 +170,11 @@ impl DataManager { /// # Arguments /// * `data` - a mock [`CommunityMembership`] object to insert #[async_recursion::async_recursion] - pub async fn create_membership(&self, data: CommunityMembership) -> Result { + pub async fn create_membership( + &self, + data: CommunityMembership, + user: &User, + ) -> Result { // make sure membership doesn't already exist if self .get_membership_by_owner_community_no_void(data.owner, data.community) @@ -199,7 +204,7 @@ impl DataManager { .await?; // ... - return self.create_membership(data).await; + return self.create_membership(data, user).await; } } _ => (), @@ -237,6 +242,9 @@ 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?; + "Community joined".to_string() }) } diff --git a/crates/core/src/database/userfollows.rs b/crates/core/src/database/userfollows.rs index 3409443..ffcd891 100644 --- a/crates/core/src/database/userfollows.rs +++ b/crates/core/src/database/userfollows.rs @@ -265,6 +265,33 @@ impl DataManager { self.add_achievement(&mut other_user, AchievementName::FollowedByStaff.into()) .await?; } + + // other achivements + self.add_achievement(&mut other_user, AchievementName::Get1Follower.into()) + .await?; + + if other_user.follower_count >= 9 { + self.add_achievement(&mut other_user, AchievementName::Get10Followers.into()) + .await?; + } + + if other_user.follower_count >= 49 { + self.add_achievement(&mut other_user, AchievementName::Get50Followers.into()) + .await?; + } + + if other_user.follower_count >= 99 { + self.add_achievement(&mut other_user, AchievementName::Get100Followers.into()) + .await?; + } + + if initiator.following_count >= 9 { + self.add_achievement( + &mut initiator.clone(), + AchievementName::Follow10Users.into(), + ) + .await?; + } } // ... diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 11f015f..054d449 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -261,6 +261,9 @@ pub struct UserSettings { /// Increase the text size of buttons and paragraphs. #[serde(default)] pub large_text: bool, + /// Disable achievements. + #[serde(default)] + pub disable_achievements: bool, } fn mime_avif() -> String { @@ -478,7 +481,7 @@ pub struct ExternalConnectionData { } /// The total number of achievements needed to 100% Tetratto! -pub const ACHIEVEMENTS: usize = 16; +pub const ACHIEVEMENTS: usize = 24; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum AchievementName { @@ -498,6 +501,14 @@ pub enum AchievementName { Get50Likes, Get100Likes, Get25Dislikes, + Get1Follower, + Get10Followers, + Get50Followers, + Get100Followers, + Follow10Users, + JoinCommunity, + CreateDraft, + EditPost, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -526,6 +537,14 @@ impl AchievementName { Self::Get50Likes => "banger post follow for more", Self::Get100Likes => "everyone liked that", Self::Get25Dislikes => "Sorry...", + Self::Get1Follower => "Friends?", + Self::Get10Followers => "Friends!", + Self::Get50Followers => "50 WHOLE FOLLOWERS??", + Self::Get100Followers => "Everyone is my friend!", + Self::Follow10Users => "Big fan", + Self::JoinCommunity => "A sense of community...", + Self::CreateDraft => "Maybe later!", + Self::EditPost => "Grammar police?", } } @@ -547,6 +566,14 @@ impl AchievementName { Self::Get50Likes => "Get 50 likes on one post.", Self::Get100Likes => "Get 100 likes on one post.", Self::Get25Dislikes => "Get 25 dislikes on one post... :(", + Self::Get1Follower => "Get 1 follow. Cool!", + Self::Get10Followers => "Get 10 followers. You're getting popular!", + Self::Get50Followers => "Get 50 followers. Okay, you're fairly popular!", + Self::Get100Followers => "Get 100 followers. You might be famous..?", + Self::Follow10Users => "Follow 10 other users. I'm sure people appreciate it!", + Self::JoinCommunity => "Join a community. Welcome!", + Self::CreateDraft => "Save a post as a draft.", + Self::EditPost => "Edit a post.", } } @@ -570,6 +597,14 @@ impl AchievementName { Self::Get50Likes => Uncommon, Self::Get100Likes => Rare, Self::Get25Dislikes => Uncommon, + Self::Get1Follower => Common, + Self::Get10Followers => Common, + Self::Get50Followers => Uncommon, + Self::Get100Followers => Rare, + Self::Follow10Users => Common, + Self::JoinCommunity => Common, + Self::CreateDraft => Common, + Self::EditPost => Common, } } }