add: 8 achievements add: larger text setting fix: small infinite

timeline bugs
This commit is contained in:
trisua 2025-06-27 13:10:04 -04:00
parent b860f74124
commit 5dd9fa01cb
19 changed files with 241 additions and 123 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "tetratto"
version = "9.0.0"
version = "10.0.0"
edition = "2024"
[dependencies]

View file

@ -1,5 +1,17 @@
(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
(template
("id" "loading_skeleton")

View file

@ -49,6 +49,7 @@
(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 ns(\"ui\")).IO_DATA_DISABLE_RELOAD = true;
}, 500);"))
console.log(\"created profile timeline\");
}, 1000);"))
(text "{% endblock %}")

View file

@ -1407,6 +1407,22 @@
embed_html:
'<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,
{
@ -1531,16 +1547,6 @@
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
\"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\"],
[
[\"disable_gpa_fun\", \"Disable GPA\"],

View file

@ -1151,82 +1151,90 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
});
// intersection observer infinite scrolling
const obs = (await ns("ui")).IO_DATA_OBSERVER;
if (obs) {
console.log("get lost old observer");
obs.disconnect();
}
self.IO_DATA_OBSERVER = null;
self.IO_DATA_OBSERVER = new IntersectionObserver(
async (entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) {
continue;
}
await self.io_load_data();
break;
self.define(
"io_data_load",
async (_, tmpl, page, paginated_mode = false) => {
// remove old
const obs = self.IO_DATA_OBSERVER;
if (obs) {
console.log("get lost old observer");
obs.disconnect();
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_MARKER = document.querySelector(
"[ui_ident=io_data_marker]",
);
self.IO_DATA_OBSERVER = new IntersectionObserver(
async (entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) {
continue;
}
self.IO_DATA_ELEMENT = document.querySelector(
"[ui_ident=io_data_load]",
);
self.IO_HTML_TMPL = document.getElementById("loading_skeleton");
if (!self.IO_DATA_ELEMENT || !self.IO_DATA_MARKER) {
console.warn(
"ui::io_data_load called, but required elements don't exist",
await self.io_load_data();
break;
}
},
{
root: document.body,
rootMargin: "0px",
threshold: 1,
},
);
return;
}
// ...
self.IO_DATA_MARKER = document.querySelector(
"[ui_ident=io_data_marker]",
);
self.IO_DATA_TMPL = tmpl;
self.IO_DATA_PAGE = page;
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;
self.IO_DATA_ELEMENT = document.querySelector(
"[ui_ident=io_data_load]",
);
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();
}
self.IO_HTML_TMPL = document.getElementById("loading_skeleton");
if (!self.IO_DATA_ELEMENT || !self.IO_DATA_MARKER) {
console.warn(
"ui::io_data_load called, but required elements don't exist",
);
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_DATA_TMPL = tmpl;
self.IO_DATA_PAGE = page;
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;
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 () => {
if (self.IO_DATA_WAITING) {

View file

@ -116,7 +116,7 @@ pub async fn update_user_settings_request(
Json(mut req): Json<UserSettings>,
) -> impl IntoResponse {
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,
None => return Json(Error::NotAllowed.into()),
};
@ -153,7 +153,7 @@ pub async fn update_user_settings_request(
// award achievement
if let Err(e) = data
.add_achievement(&user, AchievementName::EditSettings.into())
.add_achievement(&mut user, AchievementName::EditSettings.into())
.await
{
return Json(e.into());

View file

@ -22,7 +22,7 @@ pub async fn follow_request(
Extension(data): Extension<State>,
) -> impl IntoResponse {
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,
None => return Json(Error::NotAllowed.into()),
};
@ -40,7 +40,7 @@ pub async fn follow_request(
} else {
// create
match data
.create_userfollow(UserFollow::new(user.id, id), false)
.create_userfollow(UserFollow::new(user.id, id), &user, false)
.await
{
Ok(r) => {
@ -59,13 +59,15 @@ pub async fn follow_request(
return Json(e.into());
};
// award achievement
if let Err(e) = data
.add_achievement(&user, AchievementName::FollowUser.into())
.add_achievement(&mut user, AchievementName::FollowUser.into())
.await
{
return Json(e.into());
}
// ...
Json(ApiReturn {
ok: true,
message: "User followed".to_string(),
@ -123,7 +125,7 @@ pub async fn accept_follow_request(
// create follow
match data
.create_userfollow(UserFollow::new(id, user.id), true)
.create_userfollow(UserFollow::new(id, user.id), &user, true)
.await
{
Ok(_) => {

View file

@ -37,7 +37,7 @@ pub async fn create_request(
JsonMultipart(images, req): JsonMultipart<CreatePost>,
) -> impl IntoResponse {
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,
None => return Json(Error::NotAllowed.into()),
};
@ -181,7 +181,7 @@ pub async fn create_request(
// achievements
if let Err(e) = data
.add_achievement(&user, AchievementName::CreatePost.into())
.add_achievement(&mut user, AchievementName::CreatePost.into())
.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(&user, AchievementName::Create50Posts.into())
.add_achievement(&mut user, AchievementName::Create50Posts.into())
.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(&user, AchievementName::Create100Posts.into())
.add_achievement(&mut user, AchievementName::Create100Posts.into())
.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(&user, AchievementName::Create1000Posts.into())
.add_achievement(&mut user, AchievementName::Create1000Posts.into())
.await
{
return Json(e.into());

View file

@ -52,12 +52,23 @@ pub async fn create_request(
// award achievement
if let Some(ref user) = user {
let mut user = user.clone();
if let Err(e) = data
.add_achievement(user, AchievementName::CreateQuestion.into())
.add_achievement(&mut user, AchievementName::CreateQuestion.into())
.await
{
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());
}
}
}
// ...

View file

@ -98,7 +98,7 @@ pub async fn create_request(
Json(props): Json<CreateJournal>,
) -> impl IntoResponse {
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,
None => return Json(Error::NotAllowed.into()),
};
@ -110,7 +110,7 @@ pub async fn create_request(
Ok(x) => {
// award achievement
if let Err(e) = data
.add_achievement(&user, AchievementName::CreateJournal.into())
.add_achievement(&mut user, AchievementName::CreateJournal.into())
.await
{
return Json(e.into());

View file

@ -10,7 +10,10 @@ use axum::{
use axum_extra::extract::CookieJar;
use serde::Deserialize;
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 pathbufd::PathBufD;
@ -447,7 +450,7 @@ pub async fn achievements_request(
Extension(data): Extension<State>,
) -> impl IntoResponse {
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,
None => {
return Err(Html(
@ -458,6 +461,15 @@ pub async fn achievements_request(
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 mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;