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

@ -71,6 +71,7 @@ pub const PROFILE_BANNED: &str = include_str!("./public/html/profile/banned.lisp
pub const PROFILE_REPLIES: &str = include_str!("./public/html/profile/replies.lisp");
pub const PROFILE_MEDIA: &str = include_str!("./public/html/profile/media.lisp");
pub const PROFILE_OUTBOX: &str = include_str!("./public/html/profile/outbox.lisp");
pub const PROFILE_RESPONSES: &str = include_str!("./public/html/profile/responses.lisp");
pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.lisp");
pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.lisp");
@ -370,6 +371,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
write_template!(html_path->"profile/replies.html"(crate::assets::PROFILE_REPLIES) --config=config --lisp plugins);
write_template!(html_path->"profile/media.html"(crate::assets::PROFILE_MEDIA) --config=config --lisp plugins);
write_template!(html_path->"profile/outbox.html"(crate::assets::PROFILE_OUTBOX) --config=config --lisp plugins);
write_template!(html_path->"profile/responses.html"(crate::assets::PROFILE_RESPONSES) --config=config --lisp plugins);
write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config --lisp plugins);
write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config --lisp plugins);

View file

@ -76,6 +76,7 @@ version = "1.0.0"
"auth:label.recent_replies" = "Recent replies"
"auth:label.recent_posts_with_media" = "Recent posts (with media)"
"auth:label.posts" = "Posts"
"auth:label.responses" = "Answers"
"auth:label.replies" = "Replies"
"auth:label.media" = "Media"
"auth:label.outbox" = "Outbox"

View file

@ -800,7 +800,7 @@
}"))
(text "{%- endif %}"))
(text "{% if not is_global and allow_anonymous -%}")
(text "{% if not is_global and allow_anonymous and not user -%}")
(div
("class" "flex gap-2 items-center")
(input
@ -1155,10 +1155,8 @@
(icon (text "code"))
(str (text "general:link.source_code")))
(a
("href" "/reference/tetratto/index.html")
("class" "button")
("data-turbo" "false")
(button
("onclick" "trigger('me::achievement_link', ['OpenReference', '/reference/tetratto/index.html'])")
(icon (text "rabbit"))
(str (text "general:link.reference")))

View file

@ -252,10 +252,17 @@
("class" "pillmenu")
(text "{% if is_self or is_helper or not profile.settings.hide_extra_post_tabs -%}")
(a
("href" "/@{{ profile.username }}")
("href" "/@{{ profile.username }}?f=true")
("class" "{% if selected == 'posts' -%}active{%- endif %}")
(str (text "auth:label.posts")))
(text "{% if profile.settings.enable_questions -%}")
(a
("href" "/@{{ profile.username }}?r=true")
("class" "{% if selected == 'responses' -%}active{%- endif %}")
(str (text "auth:label.responses")))
(text "{%- endif %}")
(a
("href" "/@{{ profile.username }}/replies")
("class" "{% if selected == 'replies' -%}active{%- endif %}")
@ -311,8 +318,9 @@
(span
(text "{{ text \"settings:tab.theme\" }}")))
(a
("href" "#")
("data-tab-button" "sessions")
("href" "#/sessions")
("onclick" "trigger('me::achievement_link', ['OpenSessionSettings', '#/sessions'])")
(text "{{ icon \"cookie\" }}")
(span
(text "{{ text \"settings:tab.sessions\" }}")))

View file

@ -0,0 +1,55 @@
(text "{% extends \"profile/base.html\" %} {% block content %} {% if profile.settings.enable_questions and (user or profile.settings.allow_anonymous_questions) %}")
(div
("style" "display: contents")
(text "{{ components::create_question_form(receiver=profile.id, header=profile.settings.motivational_header, drawing_enabled=profile.settings.enable_drawings, allow_anonymous=profile.settings.allow_anonymous_questions) }}"))
(text "{%- endif %} {% if not tag and pinned|length != 0 -%}")
(div
("class" "card-nest")
(div
("class" "card small flex gap-2 items-center")
(text "{{ icon \"pin\" }}")
(span
(text "{{ text \"communities:label.pinned\" }}")))
(div
("class" "card flex flex-col gap-4")
(text "{% for post in pinned %} {% if post[2].read_access == \"Everybody\" -%} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], can_manage_post=is_self, poll=post[5]) }} {%- endif %} {%- endif %} {% endfor %}")))
(text "{%- endif %} {{ macros::profile_nav(selected=\"responses\") }}")
(div
("class" "card-nest")
(div
("class" "card small flex gap-2 justify-between items-center")
(div
("class" "flex gap-2 items-center")
(text "{% if not tag -%} {{ icon \"clock\" }}")
(span
(text "{{ text \"auth:label.recent_posts\" }}"))
(text "{% else %} {{ icon \"tag\" }}")
(span
(text "{{ text \"auth:label.recent_with_tag\" }}: ")
(b
(text "{{ tag }}")))
(text "{%- endif %}"))
(text "{% if user -%}")
(a
("href" "/search?profile={{ profile.id }}")
("class" "button lowered small")
(text "{{ icon \"search\" }}")
(span
(text "{{ text \"general:link.search\" }}")))
(text "{%- endif %}"))
(div
("class" "card w-full flex flex-col gap-2")
("ui_ident" "io_data_load")
(div ("ui_ident" "io_data_marker"))))
(text "{% set paged = user and user.settings.paged_timelines %}")
(script
(text "setTimeout(async () => {
await trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&responses_only=true&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
(await ns(\"ui\")).IO_DATA_DISABLE_RELOAD = true;
console.log(\"created profile timeline\");
}, 1000);"))
(text "{% endblock %}")

View file

@ -757,7 +757,29 @@
(text "{{ icon \"check\" }}")))
(span
("class" "fade")
(text "Use an image of 1100x350px for the best results.")))))
(text "Use an image of 1100x350px for the best results."))))
(div
("class" "card-nest")
("ui_ident" "default_profile_page")
(div
("class" "card small")
(b
(text "Default profile tab")))
(div
("class" "card")
(select
("onchange" "window.SETTING_SET_FUNCTIONS[0]('default_profile_tab', event.target.selectedOptions[0].value)")
(option
("value" "Posts")
("selected" "{% if profile.settings.default_profile_tab == 'Posts' -%}true{% else %}false{%- endif %}")
(text "Posts"))
(option
("value" "Responses")
("selected" "{% if profile.settings.default_profile_tab == 'Responses' -%}true{% else %}false{%- endif %}")
(text "Responses")))
(span
("class" "fade")
(text "This represents the timeline that is shown on your profile by default.")))))
(button
("onclick" "save_settings()")
("id" "save_button")
@ -1387,6 +1409,7 @@
\"supporter_ad\",
\"change_avatar\",
\"change_banner\",
\"default_profile_page\",
]);
ui.refresh_container(theme_settings, [
\"supporter_ad\",

View file

@ -1363,7 +1363,8 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
JSON.stringify(accepted_warnings),
);
setTimeout(() => {
setTimeout(async () => {
await trigger("me::achievement", ["AcceptProfileWarning"]);
window.history.back();
}, 100);
});

View file

@ -43,6 +43,12 @@
};
socket.addEventListener("message", async (event) => {
const sock = await $.sock(stream);
if (!sock) {
return;
}
if (event.data === "Ping") {
return socket.send("Pong");
}
@ -54,7 +60,7 @@
return console.info(`${stream} ${data.data}`);
}
return (await $.sock(stream)).events.message(data);
return sock.events.message(data);
});
return $.STREAMS[stream];

View file

@ -154,7 +154,7 @@ pub async fn update_user_settings_request(
// award achievement
if let Err(e) = data
.add_achievement(&mut user, AchievementName::EditSettings.into())
.add_achievement(&mut user, AchievementName::EditSettings.into(), true)
.await
{
return Json(e.into());
@ -500,7 +500,7 @@ pub async fn enable_totp_request(
// award achievement
if let Err(e) = data
.add_achievement(&mut user, AchievementName::Enable2fa.into())
.add_achievement(&mut user, AchievementName::Enable2fa.into(), true)
.await
{
return Json(e.into());
@ -968,7 +968,7 @@ pub async fn self_serve_achievement_request(
}
// award achievement
match data.add_achievement(&mut user, req.name.into()).await {
match data.add_achievement(&mut user, req.name.into(), true).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Achievement granted".to_string(),

View file

@ -62,7 +62,7 @@ pub async fn follow_request(
// award achievement
if let Err(e) = data
.add_achievement(&mut user, AchievementName::FollowUser.into())
.add_achievement(&mut user, AchievementName::FollowUser.into(), true)
.await
{
return Json(e.into());

View file

@ -27,7 +27,7 @@ pub async fn create_request(
// award achievement
if let Err(e) = data
.add_achievement(&mut user, AchievementName::CreateDraft.into())
.add_achievement(&mut user, AchievementName::CreateDraft.into(), true)
.await
{
return Json(e.into());

View file

@ -181,7 +181,7 @@ pub async fn create_request(
// achievements
if let Err(e) = data
.add_achievement(&mut user, AchievementName::CreatePost.into())
.add_achievement(&mut user, AchievementName::CreatePost.into(), true)
.await
{
return Json(e.into());
@ -189,7 +189,7 @@ pub async fn create_request(
if user.post_count >= 49 {
if let Err(e) = data
.add_achievement(&mut user, AchievementName::Create50Posts.into())
.add_achievement(&mut user, AchievementName::Create50Posts.into(), true)
.await
{
return Json(e.into());
@ -198,7 +198,7 @@ pub async fn create_request(
if user.post_count >= 99 {
if let Err(e) = data
.add_achievement(&mut user, AchievementName::Create100Posts.into())
.add_achievement(&mut user, AchievementName::Create100Posts.into(), true)
.await
{
return Json(e.into());
@ -207,7 +207,7 @@ pub async fn create_request(
if user.post_count >= 999 {
if let Err(e) = data
.add_achievement(&mut user, AchievementName::Create1000Posts.into())
.add_achievement(&mut user, AchievementName::Create1000Posts.into(), true)
.await
{
return Json(e.into());
@ -348,7 +348,7 @@ pub async fn update_content_request(
// award achievement
if let Err(e) = data
.add_achievement(&mut user, AchievementName::EditPost.into())
.add_achievement(&mut user, AchievementName::EditPost.into(), true)
.await
{
return Json(e.into());

View file

@ -55,7 +55,7 @@ pub async fn create_request(
let mut user = user.clone();
if let Err(e) = data
.add_achievement(&mut user, AchievementName::CreateQuestion.into())
.add_achievement(&mut user, AchievementName::CreateQuestion.into(), true)
.await
{
return Json(e.into());
@ -63,7 +63,7 @@ pub async fn create_request(
if drawings.len() > 0 {
if let Err(e) = data
.add_achievement(&mut user, AchievementName::CreateDrawing.into())
.add_achievement(&mut user, AchievementName::CreateDrawing.into(), true)
.await
{
return Json(e.into());

View file

@ -110,7 +110,7 @@ pub async fn create_request(
Ok(x) => {
// award achievement
if let Err(e) = data
.add_achievement(&mut user, AchievementName::CreateJournal.into())
.add_achievement(&mut user, AchievementName::CreateJournal.into(), true)
.await
{
return Json(e.into());

View file

@ -198,7 +198,7 @@ pub async fn update_content_request(
// award achievement
if let Err(e) = data
.add_achievement(&mut user, AchievementName::EditNote.into())
.add_achievement(&mut user, AchievementName::EditNote.into(), true)
.await
{
return Json(e.into());

View file

@ -464,7 +464,7 @@ pub async fn achievements_request(
// award achievement
if let Err(e) = data
.0
.add_achievement(&mut user, AchievementName::OpenAchievements.into())
.add_achievement(&mut user, AchievementName::OpenAchievements.into(), true)
.await
{
return Err(Html(render_error(e, &jar, &data, &None).await));
@ -633,6 +633,8 @@ pub struct TimelineQuery {
pub paginated: bool,
#[serde(default)]
pub before: usize,
#[serde(default)]
pub responses_only: bool,
}
/// `/_swiss_army_timeline`
@ -680,12 +682,24 @@ pub async fn swiss_army_timeline_request(
check_user_blocked_or_private!(user, other_user, data, jar);
if req.tag.is_empty() {
if req.responses_only {
data.0
.get_responses_by_user(req.user_id, 12, req.page)
.await
} else {
data.0.get_posts_by_user(req.user_id, 12, req.page).await
}
} else {
if req.responses_only {
data.0
.get_responses_by_user_tag(req.user_id, &req.tag, 12, req.page)
.await
} else {
data.0
.get_posts_by_user_tag(req.user_id, &req.tag, 12, req.page)
.await
}
}
} else {
// everything else
match req.tl {

View file

@ -179,6 +179,10 @@ pub struct ProfileQuery {
pub warning: bool,
#[serde(default)]
pub tag: String,
#[serde(default, alias = "r")]
pub responses_only: bool,
#[serde(default, alias = "f")]
pub force: bool,
}
#[derive(Deserialize)]

View file

@ -11,7 +11,12 @@ use axum::{
use axum_extra::extract::CookieJar;
use serde::Deserialize;
use tera::Context;
use tetratto_core::model::{auth::User, communities::Community, permissions::FinePermission, Error};
use tetratto_core::model::{
auth::{DefaultProfileTabChoice, User},
communities::Community,
permissions::FinePermission,
Error,
};
use tetratto_shared::hash::hash;
use contrasted::{Color, MINIMUM_CONTRAST_THRESHOLD};
@ -252,6 +257,10 @@ pub async fn posts_request(
check_user_blocked_or_private!(user, other_user, data, jar);
let responses_only = props.responses_only
| (other_user.settings.default_profile_tab == DefaultProfileTabChoice::Responses
&& !props.force);
// check for warning
if props.warning {
let lang = get_lang!(jar, data.0);
@ -356,7 +365,13 @@ pub async fn posts_request(
);
// return
if responses_only {
Ok(Html(
data.1.render("profile/responses.html", &context).unwrap(),
))
} else {
Ok(Html(data.1.render("profile/posts.html", &context).unwrap()))
}
}
/// `/@{username}/replies`

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,7 +242,11 @@ impl DataManager {
Ok(if data.role.check(CommunityPermission::REQUESTED) {
"Join request sent".to_string()
} else {
self.add_achievement(&mut user.clone(), AchievementName::JoinCommunity.into())
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,7 +1731,11 @@ impl DataManager {
}
// award achievement
self.add_achievement(&mut owner, AchievementName::CreatePostWithTitle.into())
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,26 +262,42 @@ impl DataManager {
// check if we're staff
if initiator.permissions.check(FinePermission::STAFF_BADGE) {
self.add_achievement(&mut other_user, AchievementName::FollowedByStaff.into())
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())
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())
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())
self.add_achievement(
&mut other_user,
AchievementName::Get100Followers.into(),
true,
)
.await?;
}
@ -289,6 +305,7 @@ impl DataManager {
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,
}
}
}