add: 8 achievements add: larger text setting fix: small infinite
timeline bugs
This commit is contained in:
parent
b860f74124
commit
5dd9fa01cb
19 changed files with 241 additions and 123 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -3231,7 +3231,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"async-stripe",
|
"async-stripe",
|
||||||
|
@ -3262,7 +3262,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
@ -3284,7 +3284,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3293,7 +3293,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
(div ("id" "toast_zone"))
|
(div ("id" "toast_zone"))
|
||||||
|
|
||||||
|
; large text
|
||||||
|
(text "{% if user and user.settings.large_text -%}")
|
||||||
|
(style
|
||||||
|
(text "button, a, p, span, b, strone, em, i, pre, code {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .icon {
|
||||||
|
font-size: 15px !important;
|
||||||
|
}"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
; templates
|
; templates
|
||||||
(template
|
(template
|
||||||
("id" "loading_skeleton")
|
("id" "loading_skeleton")
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
(text "setTimeout(async () => {
|
(text "setTimeout(async () => {
|
||||||
await trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
|
await trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
|
||||||
(await ns(\"ui\")).IO_DATA_DISABLE_RELOAD = true;
|
(await ns(\"ui\")).IO_DATA_DISABLE_RELOAD = true;
|
||||||
}, 500);"))
|
console.log(\"created profile timeline\");
|
||||||
|
}, 1000);"))
|
||||||
|
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -1407,6 +1407,22 @@
|
||||||
embed_html:
|
embed_html:
|
||||||
'<span class=\"fade\">Muted phrases should all be on new lines.</span>',
|
'<span class=\"fade\">Muted phrases should all be on new lines.</span>',
|
||||||
}],
|
}],
|
||||||
|
[[], \"Accessibility\", \"title\"],
|
||||||
|
[
|
||||||
|
[\"large_text\", \"Increase UI text size\"],
|
||||||
|
\"{{ profile.settings.large_text }}\",
|
||||||
|
\"checkbox\",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
|
||||||
|
\"{{ profile.settings.paged_timelines }}\",
|
||||||
|
\"checkbox\",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[\"auto_clear_notifs\", \"Automatically clear all notifications when you open the notifications page\"],
|
||||||
|
\"{{ profile.settings.auto_clear_notifs }}\",
|
||||||
|
\"checkbox\",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
settings,
|
settings,
|
||||||
{
|
{
|
||||||
|
@ -1531,16 +1547,6 @@
|
||||||
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
|
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
|
||||||
\"text\",
|
\"text\",
|
||||||
],
|
],
|
||||||
[
|
|
||||||
[\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
|
|
||||||
\"{{ profile.settings.paged_timelines }}\",
|
|
||||||
\"checkbox\",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[\"auto_clear_notifs\", \"Automatically clear all notifications when you open the notifications page\"],
|
|
||||||
\"{{ profile.settings.auto_clear_notifs }}\",
|
|
||||||
\"checkbox\",
|
|
||||||
],
|
|
||||||
[[], \"Fun\", \"title\"],
|
[[], \"Fun\", \"title\"],
|
||||||
[
|
[
|
||||||
[\"disable_gpa_fun\", \"Disable GPA\"],
|
[\"disable_gpa_fun\", \"Disable GPA\"],
|
||||||
|
|
|
@ -1151,82 +1151,90 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
|
||||||
});
|
});
|
||||||
|
|
||||||
// intersection observer infinite scrolling
|
// intersection observer infinite scrolling
|
||||||
const obs = (await ns("ui")).IO_DATA_OBSERVER;
|
self.IO_DATA_OBSERVER = null;
|
||||||
if (obs) {
|
|
||||||
console.log("get lost old observer");
|
|
||||||
obs.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.IO_DATA_OBSERVER = new IntersectionObserver(
|
self.define(
|
||||||
async (entries) => {
|
"io_data_load",
|
||||||
for (const entry of entries) {
|
async (_, tmpl, page, paginated_mode = false) => {
|
||||||
if (!entry.isIntersecting) {
|
// remove old
|
||||||
continue;
|
const obs = self.IO_DATA_OBSERVER;
|
||||||
}
|
if (obs) {
|
||||||
|
console.log("get lost old observer");
|
||||||
await self.io_load_data();
|
obs.disconnect();
|
||||||
break;
|
self.IO_DATA_OBSERVER = null;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
root: document.body,
|
|
||||||
rootMargin: "0px",
|
|
||||||
threshold: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
self.define("io_data_load", (_, tmpl, page, paginated_mode = false) => {
|
self.IO_DATA_OBSERVER = new IntersectionObserver(
|
||||||
self.IO_DATA_MARKER = document.querySelector(
|
async (entries) => {
|
||||||
"[ui_ident=io_data_marker]",
|
for (const entry of entries) {
|
||||||
);
|
if (!entry.isIntersecting) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
self.IO_DATA_ELEMENT = document.querySelector(
|
await self.io_load_data();
|
||||||
"[ui_ident=io_data_load]",
|
break;
|
||||||
);
|
}
|
||||||
|
},
|
||||||
self.IO_HTML_TMPL = document.getElementById("loading_skeleton");
|
{
|
||||||
|
root: document.body,
|
||||||
if (!self.IO_DATA_ELEMENT || !self.IO_DATA_MARKER) {
|
rootMargin: "0px",
|
||||||
console.warn(
|
threshold: 1,
|
||||||
"ui::io_data_load called, but required elements don't exist",
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
// ...
|
||||||
}
|
self.IO_DATA_MARKER = document.querySelector(
|
||||||
|
"[ui_ident=io_data_marker]",
|
||||||
|
);
|
||||||
|
|
||||||
self.IO_DATA_TMPL = tmpl;
|
self.IO_DATA_ELEMENT = document.querySelector(
|
||||||
self.IO_DATA_PAGE = page;
|
"[ui_ident=io_data_load]",
|
||||||
self.IO_DATA_SEEN_IDS = [];
|
);
|
||||||
self.IO_DATA_WAITING = false;
|
|
||||||
self.IO_HAS_LOADED_AT_LEAST_ONCE = false;
|
|
||||||
self.IO_DATA_DISCONNECTED = false;
|
|
||||||
self.IO_DATA_DISABLE_RELOAD = false;
|
|
||||||
|
|
||||||
if (!paginated_mode) {
|
self.IO_HTML_TMPL = document.getElementById("loading_skeleton");
|
||||||
self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER);
|
|
||||||
} else {
|
if (!self.IO_DATA_ELEMENT || !self.IO_DATA_MARKER) {
|
||||||
// immediately load first page
|
console.warn(
|
||||||
self.IO_DATA_TMPL = self.IO_DATA_TMPL.replace("&page=", "");
|
"ui::io_data_load called, but required elements don't exist",
|
||||||
self.IO_DATA_TMPL += `&paginated=true&page=`;
|
);
|
||||||
self.io_load_data();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (self.IO_DATA_DISABLE_RELOAD) {
|
|
||||||
console.log("missing data reload disabled");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!self.IO_HAS_LOADED_AT_LEAST_ONCE) {
|
self.IO_DATA_TMPL = tmpl;
|
||||||
// reload
|
self.IO_DATA_PAGE = page;
|
||||||
self.IO_DATA_OBSERVER.disconnect();
|
self.IO_DATA_SEEN_IDS = [];
|
||||||
console.log("timeline load fail :(");
|
self.IO_DATA_WAITING = false;
|
||||||
window.location.reload();
|
self.IO_HAS_LOADED_AT_LEAST_ONCE = false;
|
||||||
}
|
self.IO_DATA_DISCONNECTED = false;
|
||||||
}, 1500);
|
self.IO_DATA_DISABLE_RELOAD = false;
|
||||||
|
|
||||||
self.IO_PAGINATED = paginated_mode;
|
if (!paginated_mode) {
|
||||||
});
|
self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER);
|
||||||
|
} else {
|
||||||
|
// immediately load first page
|
||||||
|
self.IO_DATA_TMPL = self.IO_DATA_TMPL.replace("&page=", "");
|
||||||
|
self.IO_DATA_TMPL += `&paginated=true&page=`;
|
||||||
|
self.io_load_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (self.IO_DATA_DISABLE_RELOAD) {
|
||||||
|
console.log("missing data reload disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.IO_HAS_LOADED_AT_LEAST_ONCE) {
|
||||||
|
// reload
|
||||||
|
self.IO_DATA_OBSERVER.disconnect();
|
||||||
|
console.log("timeline load fail :(");
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
self.IO_PAGINATED = paginated_mode;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
self.define("io_load_data", async () => {
|
self.define("io_load_data", async () => {
|
||||||
if (self.IO_DATA_WAITING) {
|
if (self.IO_DATA_WAITING) {
|
||||||
|
|
|
@ -116,7 +116,7 @@ pub async fn update_user_settings_request(
|
||||||
Json(mut req): Json<UserSettings>,
|
Json(mut req): Json<UserSettings>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProfile) {
|
let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProfile) {
|
||||||
Some(ua) => ua,
|
Some(ua) => ua,
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
@ -153,7 +153,7 @@ pub async fn update_user_settings_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&user, AchievementName::EditSettings.into())
|
.add_achievement(&mut user, AchievementName::EditSettings.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub async fn follow_request(
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowing) {
|
let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowing) {
|
||||||
Some(ua) => ua,
|
Some(ua) => ua,
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
@ -40,7 +40,7 @@ pub async fn follow_request(
|
||||||
} else {
|
} else {
|
||||||
// create
|
// create
|
||||||
match data
|
match data
|
||||||
.create_userfollow(UserFollow::new(user.id, id), false)
|
.create_userfollow(UserFollow::new(user.id, id), &user, false)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
|
@ -59,13 +59,15 @@ pub async fn follow_request(
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&user, AchievementName::FollowUser.into())
|
.add_achievement(&mut user, AchievementName::FollowUser.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
Json(ApiReturn {
|
Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "User followed".to_string(),
|
message: "User followed".to_string(),
|
||||||
|
@ -123,7 +125,7 @@ pub async fn accept_follow_request(
|
||||||
|
|
||||||
// create follow
|
// create follow
|
||||||
match data
|
match data
|
||||||
.create_userfollow(UserFollow::new(id, user.id), true)
|
.create_userfollow(UserFollow::new(id, user.id), &user, true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|
|
@ -37,7 +37,7 @@ pub async fn create_request(
|
||||||
JsonMultipart(images, req): JsonMultipart<CreatePost>,
|
JsonMultipart(images, req): JsonMultipart<CreatePost>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreatePosts) {
|
let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreatePosts) {
|
||||||
Some(ua) => ua,
|
Some(ua) => ua,
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
@ -181,7 +181,7 @@ pub async fn create_request(
|
||||||
|
|
||||||
// achievements
|
// achievements
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&user, AchievementName::CreatePost.into())
|
.add_achievement(&mut user, AchievementName::CreatePost.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
@ -189,7 +189,7 @@ pub async fn create_request(
|
||||||
|
|
||||||
if user.post_count >= 49 {
|
if user.post_count >= 49 {
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&user, AchievementName::Create50Posts.into())
|
.add_achievement(&mut user, AchievementName::Create50Posts.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
@ -198,7 +198,7 @@ pub async fn create_request(
|
||||||
|
|
||||||
if user.post_count >= 99 {
|
if user.post_count >= 99 {
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&user, AchievementName::Create100Posts.into())
|
.add_achievement(&mut user, AchievementName::Create100Posts.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
@ -207,7 +207,7 @@ pub async fn create_request(
|
||||||
|
|
||||||
if user.post_count >= 999 {
|
if user.post_count >= 999 {
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&user, AchievementName::Create1000Posts.into())
|
.add_achievement(&mut user, AchievementName::Create1000Posts.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -52,12 +52,23 @@ pub async fn create_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Some(ref user) = user {
|
if let Some(ref user) = user {
|
||||||
|
let mut user = user.clone();
|
||||||
|
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(user, AchievementName::CreateQuestion.into())
|
.add_achievement(&mut user, AchievementName::CreateQuestion.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if drawings.len() > 0 {
|
||||||
|
if let Err(e) = data
|
||||||
|
.add_achievement(&mut user, AchievementName::CreateDrawing.into())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
|
|
@ -98,7 +98,7 @@ pub async fn create_request(
|
||||||
Json(props): Json<CreateJournal>,
|
Json(props): Json<CreateJournal>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateJournals) {
|
let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateJournals) {
|
||||||
Some(ua) => ua,
|
Some(ua) => ua,
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
@ -110,7 +110,7 @@ pub async fn create_request(
|
||||||
Ok(x) => {
|
Ok(x) => {
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&user, AchievementName::CreateJournal.into())
|
.add_achievement(&mut user, AchievementName::CreateJournal.into())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -10,7 +10,10 @@ use axum::{
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
auth::DefaultTimelineChoice, permissions::FinePermission, requests::ActionType, Error,
|
auth::{AchievementName, DefaultTimelineChoice},
|
||||||
|
permissions::FinePermission,
|
||||||
|
requests::ActionType,
|
||||||
|
Error,
|
||||||
};
|
};
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use pathbufd::PathBufD;
|
use pathbufd::PathBufD;
|
||||||
|
@ -447,7 +450,7 @@ pub async fn achievements_request(
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
let user = match get_user_from_token!(jar, data.0) {
|
let mut user = match get_user_from_token!(jar, data.0) {
|
||||||
Some(ua) => ua,
|
Some(ua) => ua,
|
||||||
None => {
|
None => {
|
||||||
return Err(Html(
|
return Err(Html(
|
||||||
|
@ -458,6 +461,15 @@ pub async fn achievements_request(
|
||||||
|
|
||||||
let achievements = data.0.fill_achievements(user.achievements.clone());
|
let achievements = data.0.fill_achievements(user.achievements.clone());
|
||||||
|
|
||||||
|
// award achievement
|
||||||
|
if let Err(e) = data
|
||||||
|
.0
|
||||||
|
.add_achievement(&mut user, AchievementName::OpenAchievements.into())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(Html(render_error(e, &jar, &data, &None).await));
|
||||||
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -712,7 +712,7 @@ impl DataManager {
|
||||||
/// Add an achievement to a user.
|
/// Add an achievement to a user.
|
||||||
///
|
///
|
||||||
/// Still returns `Ok` if the user already has the achievement.
|
/// Still returns `Ok` if the user already has the achievement.
|
||||||
pub async fn add_achievement(&self, user: &User, achievement: Achievement) -> Result<()> {
|
pub async fn add_achievement(&self, user: &mut User, achievement: Achievement) -> Result<()> {
|
||||||
if user
|
if user
|
||||||
.achievements
|
.achievements
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -734,9 +734,8 @@ impl DataManager {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// add achievement
|
// add achievement
|
||||||
let mut user = user.clone();
|
|
||||||
user.achievements.push(achievement);
|
user.achievements.push(achievement);
|
||||||
self.update_user_achievements(user.id, user.achievements)
|
self.update_user_achievements(user.id, user.achievements.to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use oiseau::cache::Cache;
|
use oiseau::cache::Cache;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
Error, Result,
|
auth::{AchievementName, Notification, User},
|
||||||
auth::{Notification, User},
|
|
||||||
permissions::FinePermission,
|
permissions::FinePermission,
|
||||||
reactions::{AssetType, Reaction},
|
reactions::{AssetType, Reaction},
|
||||||
|
Error, Result,
|
||||||
};
|
};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
|
|
||||||
|
@ -148,6 +148,33 @@ impl DataManager {
|
||||||
{
|
{
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if post.likes >= 9 {
|
||||||
|
self.add_achievement(&mut owner, AchievementName::Get10Likes.into())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if post.likes >= 49 {
|
||||||
|
self.add_achievement(&mut owner, AchievementName::Get50Likes.into())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if post.likes >= 99 {
|
||||||
|
self.add_achievement(&mut owner, AchievementName::Get100Likes.into())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if post.dislikes >= 24 {
|
||||||
|
self.add_achievement(&mut owner, AchievementName::Get25Dislikes.into())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if data.asset_type == AssetType::Question {
|
} else if data.asset_type == AssetType::Question {
|
||||||
let question = self.get_question_by_id(data.asset).await?;
|
let question = self.get_question_by_id(data.asset).await?;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use oiseau::cache::Cache;
|
use oiseau::cache::Cache;
|
||||||
use crate::model::auth::FollowResult;
|
use crate::model::auth::{AchievementName, FollowResult};
|
||||||
use crate::model::requests::{ActionRequest, ActionType};
|
use crate::model::requests::{ActionRequest, ActionType};
|
||||||
use crate::model::{Error, Result, auth::User, auth::UserFollow, permissions::FinePermission};
|
use crate::model::{Error, Result, auth::User, auth::UserFollow, permissions::FinePermission};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
|
@ -238,9 +238,14 @@ impl DataManager {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `data` - a mock [`UserFollow`] object to insert
|
/// * `data` - a mock [`UserFollow`] object to insert
|
||||||
/// * `force` - if we should skip the request stage
|
/// * `force` - if we should skip the request stage
|
||||||
pub async fn create_userfollow(&self, data: UserFollow, force: bool) -> Result<FollowResult> {
|
pub async fn create_userfollow(
|
||||||
|
&self,
|
||||||
|
data: UserFollow,
|
||||||
|
initiator: &User,
|
||||||
|
force: bool,
|
||||||
|
) -> Result<FollowResult> {
|
||||||
if !force {
|
if !force {
|
||||||
let other_user = self.get_user_by_id(data.receiver).await?;
|
let mut other_user = self.get_user_by_id(data.receiver).await?;
|
||||||
|
|
||||||
if other_user.settings.private_profile {
|
if other_user.settings.private_profile {
|
||||||
// send follow request instead
|
// send follow request instead
|
||||||
|
@ -254,6 +259,12 @@ impl DataManager {
|
||||||
|
|
||||||
return Ok(FollowResult::Requested);
|
return Ok(FollowResult::Requested);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if we're staff
|
||||||
|
if initiator.permissions.check(FinePermission::STAFF_BADGE) {
|
||||||
|
self.add_achievement(&mut other_user, AchievementName::FollowedByStaff.into())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
|
|
@ -258,6 +258,9 @@ pub struct UserSettings {
|
||||||
/// Automatically clear all notifications when notifications are viewed.
|
/// Automatically clear all notifications when notifications are viewed.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auto_clear_notifs: bool,
|
pub auto_clear_notifs: bool,
|
||||||
|
/// Increase the text size of buttons and paragraphs.
|
||||||
|
#[serde(default)]
|
||||||
|
pub large_text: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mime_avif() -> String {
|
fn mime_avif() -> String {
|
||||||
|
@ -475,26 +478,26 @@ pub struct ExternalConnectionData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The total number of achievements needed to 100% Tetratto!
|
/// The total number of achievements needed to 100% Tetratto!
|
||||||
pub const ACHIEVEMENTS: usize = 8;
|
pub const ACHIEVEMENTS: usize = 16;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum AchievementName {
|
pub enum AchievementName {
|
||||||
/// Create your first post.
|
|
||||||
CreatePost,
|
CreatePost,
|
||||||
/// Follow somebody.
|
|
||||||
FollowUser,
|
FollowUser,
|
||||||
/// Create your 50th post.
|
|
||||||
Create50Posts,
|
Create50Posts,
|
||||||
/// Create your 100th post.
|
|
||||||
Create100Posts,
|
Create100Posts,
|
||||||
/// Create your 1000th post.
|
|
||||||
Create1000Posts,
|
Create1000Posts,
|
||||||
/// Ask your first question.
|
|
||||||
CreateQuestion,
|
CreateQuestion,
|
||||||
/// Edit your settings.
|
|
||||||
EditSettings,
|
EditSettings,
|
||||||
/// Create your first journal.
|
|
||||||
CreateJournal,
|
CreateJournal,
|
||||||
|
FollowedByStaff,
|
||||||
|
CreateDrawing,
|
||||||
|
OpenAchievements,
|
||||||
|
Get1Like,
|
||||||
|
Get10Likes,
|
||||||
|
Get50Likes,
|
||||||
|
Get100Likes,
|
||||||
|
Get25Dislikes,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
@ -515,6 +518,14 @@ impl AchievementName {
|
||||||
Self::CreateQuestion => "Big questions...",
|
Self::CreateQuestion => "Big questions...",
|
||||||
Self::EditSettings => "Just how I like it!",
|
Self::EditSettings => "Just how I like it!",
|
||||||
Self::CreateJournal => "Dear diary...",
|
Self::CreateJournal => "Dear diary...",
|
||||||
|
Self::FollowedByStaff => "Big Shrimpin'",
|
||||||
|
Self::CreateDrawing => "Modern art",
|
||||||
|
Self::OpenAchievements => "Welcome!",
|
||||||
|
Self::Get1Like => "Baby steps!",
|
||||||
|
Self::Get10Likes => "WOW! 10 LIKES!",
|
||||||
|
Self::Get50Likes => "banger post follow for more",
|
||||||
|
Self::Get100Likes => "everyone liked that",
|
||||||
|
Self::Get25Dislikes => "Sorry...",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -528,19 +539,37 @@ impl AchievementName {
|
||||||
Self::CreateQuestion => "Ask your first question!",
|
Self::CreateQuestion => "Ask your first question!",
|
||||||
Self::EditSettings => "Edit your settings.",
|
Self::EditSettings => "Edit your settings.",
|
||||||
Self::CreateJournal => "Create your first journal.",
|
Self::CreateJournal => "Create your first journal.",
|
||||||
|
Self::FollowedByStaff => "Get followed by a staff member!",
|
||||||
|
Self::CreateDrawing => "Include a drawing in a question.",
|
||||||
|
Self::OpenAchievements => "Open the achievements page.",
|
||||||
|
Self::Get1Like => "Get 1 like on a post! Good job!",
|
||||||
|
Self::Get10Likes => "Get 10 likes on one post.",
|
||||||
|
Self::Get50Likes => "Get 50 likes on one post.",
|
||||||
|
Self::Get100Likes => "Get 100 likes on one post.",
|
||||||
|
Self::Get25Dislikes => "Get 25 dislikes on one post... :(",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rarity(&self) -> AchievementRarity {
|
pub fn rarity(&self) -> AchievementRarity {
|
||||||
|
// i don't want to write that long ass type name everywhere
|
||||||
|
use AchievementRarity::*;
|
||||||
match self {
|
match self {
|
||||||
Self::CreatePost => AchievementRarity::Common,
|
Self::CreatePost => Common,
|
||||||
Self::FollowUser => AchievementRarity::Common,
|
Self::FollowUser => Common,
|
||||||
Self::Create50Posts => AchievementRarity::Uncommon,
|
Self::Create50Posts => Uncommon,
|
||||||
Self::Create100Posts => AchievementRarity::Uncommon,
|
Self::Create100Posts => Uncommon,
|
||||||
Self::Create1000Posts => AchievementRarity::Rare,
|
Self::Create1000Posts => Rare,
|
||||||
Self::CreateQuestion => AchievementRarity::Common,
|
Self::CreateQuestion => Common,
|
||||||
Self::EditSettings => AchievementRarity::Common,
|
Self::EditSettings => Common,
|
||||||
Self::CreateJournal => AchievementRarity::Uncommon,
|
Self::CreateJournal => Uncommon,
|
||||||
|
Self::FollowedByStaff => Rare,
|
||||||
|
Self::CreateDrawing => Common,
|
||||||
|
Self::OpenAchievements => Common,
|
||||||
|
Self::Get1Like => Common,
|
||||||
|
Self::Get10Likes => Common,
|
||||||
|
Self::Get50Likes => Uncommon,
|
||||||
|
Self::Get100Likes => Rare,
|
||||||
|
Self::Get25Dislikes => Uncommon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "9.0.0"
|
version = "10.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue