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,
}
}
}