add: dedicated responses tab for profiles
This commit is contained in:
parent
9ba6320d46
commit
07a23f505b
24 changed files with 332 additions and 55 deletions
|
@ -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_REPLIES: &str = include_str!("./public/html/profile/replies.lisp");
|
||||||
pub const PROFILE_MEDIA: &str = include_str!("./public/html/profile/media.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_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_LIST: &str = include_str!("./public/html/communities/list.lisp");
|
||||||
pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.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/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/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/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/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);
|
write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config --lisp plugins);
|
||||||
|
|
|
@ -76,6 +76,7 @@ version = "1.0.0"
|
||||||
"auth:label.recent_replies" = "Recent replies"
|
"auth:label.recent_replies" = "Recent replies"
|
||||||
"auth:label.recent_posts_with_media" = "Recent posts (with media)"
|
"auth:label.recent_posts_with_media" = "Recent posts (with media)"
|
||||||
"auth:label.posts" = "Posts"
|
"auth:label.posts" = "Posts"
|
||||||
|
"auth:label.responses" = "Answers"
|
||||||
"auth:label.replies" = "Replies"
|
"auth:label.replies" = "Replies"
|
||||||
"auth:label.media" = "Media"
|
"auth:label.media" = "Media"
|
||||||
"auth:label.outbox" = "Outbox"
|
"auth:label.outbox" = "Outbox"
|
||||||
|
|
|
@ -800,7 +800,7 @@
|
||||||
}"))
|
}"))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
|
|
||||||
(text "{% if not is_global and allow_anonymous -%}")
|
(text "{% if not is_global and allow_anonymous and not user -%}")
|
||||||
(div
|
(div
|
||||||
("class" "flex gap-2 items-center")
|
("class" "flex gap-2 items-center")
|
||||||
(input
|
(input
|
||||||
|
@ -1155,10 +1155,8 @@
|
||||||
(icon (text "code"))
|
(icon (text "code"))
|
||||||
(str (text "general:link.source_code")))
|
(str (text "general:link.source_code")))
|
||||||
|
|
||||||
(a
|
(button
|
||||||
("href" "/reference/tetratto/index.html")
|
("onclick" "trigger('me::achievement_link', ['OpenReference', '/reference/tetratto/index.html'])")
|
||||||
("class" "button")
|
|
||||||
("data-turbo" "false")
|
|
||||||
(icon (text "rabbit"))
|
(icon (text "rabbit"))
|
||||||
(str (text "general:link.reference")))
|
(str (text "general:link.reference")))
|
||||||
|
|
||||||
|
|
|
@ -252,10 +252,17 @@
|
||||||
("class" "pillmenu")
|
("class" "pillmenu")
|
||||||
(text "{% if is_self or is_helper or not profile.settings.hide_extra_post_tabs -%}")
|
(text "{% if is_self or is_helper or not profile.settings.hide_extra_post_tabs -%}")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ profile.username }}")
|
("href" "/@{{ profile.username }}?f=true")
|
||||||
("class" "{% if selected == 'posts' -%}active{%- endif %}")
|
("class" "{% if selected == 'posts' -%}active{%- endif %}")
|
||||||
(str (text "auth:label.posts")))
|
(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
|
(a
|
||||||
("href" "/@{{ profile.username }}/replies")
|
("href" "/@{{ profile.username }}/replies")
|
||||||
("class" "{% if selected == 'replies' -%}active{%- endif %}")
|
("class" "{% if selected == 'replies' -%}active{%- endif %}")
|
||||||
|
@ -311,8 +318,9 @@
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"settings:tab.theme\" }}")))
|
(text "{{ text \"settings:tab.theme\" }}")))
|
||||||
(a
|
(a
|
||||||
|
("href" "#")
|
||||||
("data-tab-button" "sessions")
|
("data-tab-button" "sessions")
|
||||||
("href" "#/sessions")
|
("onclick" "trigger('me::achievement_link', ['OpenSessionSettings', '#/sessions'])")
|
||||||
(text "{{ icon \"cookie\" }}")
|
(text "{{ icon \"cookie\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"settings:tab.sessions\" }}")))
|
(text "{{ text \"settings:tab.sessions\" }}")))
|
||||||
|
|
55
crates/app/src/public/html/profile/responses.lisp
Normal file
55
crates/app/src/public/html/profile/responses.lisp
Normal 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 %}")
|
|
@ -757,7 +757,29 @@
|
||||||
(text "{{ icon \"check\" }}")))
|
(text "{{ icon \"check\" }}")))
|
||||||
(span
|
(span
|
||||||
("class" "fade")
|
("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
|
(button
|
||||||
("onclick" "save_settings()")
|
("onclick" "save_settings()")
|
||||||
("id" "save_button")
|
("id" "save_button")
|
||||||
|
@ -1387,6 +1409,7 @@
|
||||||
\"supporter_ad\",
|
\"supporter_ad\",
|
||||||
\"change_avatar\",
|
\"change_avatar\",
|
||||||
\"change_banner\",
|
\"change_banner\",
|
||||||
|
\"default_profile_page\",
|
||||||
]);
|
]);
|
||||||
ui.refresh_container(theme_settings, [
|
ui.refresh_container(theme_settings, [
|
||||||
\"supporter_ad\",
|
\"supporter_ad\",
|
||||||
|
|
|
@ -1363,7 +1363,8 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
|
||||||
JSON.stringify(accepted_warnings),
|
JSON.stringify(accepted_warnings),
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
|
await trigger("me::achievement", ["AcceptProfileWarning"]);
|
||||||
window.history.back();
|
window.history.back();
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,6 +43,12 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.addEventListener("message", async (event) => {
|
socket.addEventListener("message", async (event) => {
|
||||||
|
const sock = await $.sock(stream);
|
||||||
|
|
||||||
|
if (!sock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.data === "Ping") {
|
if (event.data === "Ping") {
|
||||||
return socket.send("Pong");
|
return socket.send("Pong");
|
||||||
}
|
}
|
||||||
|
@ -54,7 +60,7 @@
|
||||||
return console.info(`${stream} ${data.data}`);
|
return console.info(`${stream} ${data.data}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await $.sock(stream)).events.message(data);
|
return sock.events.message(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
return $.STREAMS[stream];
|
return $.STREAMS[stream];
|
||||||
|
|
|
@ -154,7 +154,7 @@ pub async fn update_user_settings_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::EditSettings.into())
|
.add_achievement(&mut user, AchievementName::EditSettings.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
@ -500,7 +500,7 @@ pub async fn enable_totp_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::Enable2fa.into())
|
.add_achievement(&mut user, AchievementName::Enable2fa.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
@ -968,7 +968,7 @@ pub async fn self_serve_achievement_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
// award achievement
|
// 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(_) => Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "Achievement granted".to_string(),
|
message: "Achievement granted".to_string(),
|
||||||
|
|
|
@ -62,7 +62,7 @@ pub async fn follow_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::FollowUser.into())
|
.add_achievement(&mut user, AchievementName::FollowUser.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -27,7 +27,7 @@ pub async fn create_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::CreateDraft.into())
|
.add_achievement(&mut user, AchievementName::CreateDraft.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -181,7 +181,7 @@ pub async fn create_request(
|
||||||
|
|
||||||
// achievements
|
// achievements
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::CreatePost.into())
|
.add_achievement(&mut user, AchievementName::CreatePost.into(), true)
|
||||||
.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(&mut user, AchievementName::Create50Posts.into())
|
.add_achievement(&mut user, AchievementName::Create50Posts.into(), true)
|
||||||
.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(&mut user, AchievementName::Create100Posts.into())
|
.add_achievement(&mut user, AchievementName::Create100Posts.into(), true)
|
||||||
.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(&mut user, AchievementName::Create1000Posts.into())
|
.add_achievement(&mut user, AchievementName::Create1000Posts.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
@ -348,7 +348,7 @@ pub async fn update_content_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::EditPost.into())
|
.add_achievement(&mut user, AchievementName::EditPost.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -55,7 +55,7 @@ pub async fn create_request(
|
||||||
let mut user = user.clone();
|
let mut user = user.clone();
|
||||||
|
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::CreateQuestion.into())
|
.add_achievement(&mut user, AchievementName::CreateQuestion.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
@ -63,7 +63,7 @@ pub async fn create_request(
|
||||||
|
|
||||||
if drawings.len() > 0 {
|
if drawings.len() > 0 {
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::CreateDrawing.into())
|
.add_achievement(&mut user, AchievementName::CreateDrawing.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.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(&mut user, AchievementName::CreateJournal.into())
|
.add_achievement(&mut user, AchievementName::CreateJournal.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -198,7 +198,7 @@ pub async fn update_content_request(
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.add_achievement(&mut user, AchievementName::EditNote.into())
|
.add_achievement(&mut user, AchievementName::EditNote.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
|
|
|
@ -464,7 +464,7 @@ pub async fn achievements_request(
|
||||||
// award achievement
|
// award achievement
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.0
|
.0
|
||||||
.add_achievement(&mut user, AchievementName::OpenAchievements.into())
|
.add_achievement(&mut user, AchievementName::OpenAchievements.into(), true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Err(Html(render_error(e, &jar, &data, &None).await));
|
return Err(Html(render_error(e, &jar, &data, &None).await));
|
||||||
|
@ -633,6 +633,8 @@ pub struct TimelineQuery {
|
||||||
pub paginated: bool,
|
pub paginated: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub before: usize,
|
pub before: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub responses_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/_swiss_army_timeline`
|
/// `/_swiss_army_timeline`
|
||||||
|
@ -680,11 +682,23 @@ pub async fn swiss_army_timeline_request(
|
||||||
check_user_blocked_or_private!(user, other_user, data, jar);
|
check_user_blocked_or_private!(user, other_user, data, jar);
|
||||||
|
|
||||||
if req.tag.is_empty() {
|
if req.tag.is_empty() {
|
||||||
data.0.get_posts_by_user(req.user_id, 12, req.page).await
|
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 {
|
} else {
|
||||||
data.0
|
if req.responses_only {
|
||||||
.get_posts_by_user_tag(req.user_id, &req.tag, 12, req.page)
|
data.0
|
||||||
.await
|
.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 {
|
} else {
|
||||||
// everything else
|
// everything else
|
||||||
|
|
|
@ -179,6 +179,10 @@ pub struct ProfileQuery {
|
||||||
pub warning: bool,
|
pub warning: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tag: String,
|
pub tag: String,
|
||||||
|
#[serde(default, alias = "r")]
|
||||||
|
pub responses_only: bool,
|
||||||
|
#[serde(default, alias = "f")]
|
||||||
|
pub force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -11,7 +11,12 @@ use axum::{
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tera::Context;
|
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 tetratto_shared::hash::hash;
|
||||||
use contrasted::{Color, MINIMUM_CONTRAST_THRESHOLD};
|
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);
|
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
|
// check for warning
|
||||||
if props.warning {
|
if props.warning {
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
|
@ -356,7 +365,13 @@ pub async fn posts_request(
|
||||||
);
|
);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(data.1.render("profile/posts.html", &context).unwrap()))
|
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`
|
/// `/@{username}/replies`
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use super::common::NAME_REGEX;
|
use super::common::NAME_REGEX;
|
||||||
use oiseau::cache::Cache;
|
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::moderation::AuditLogEntry;
|
||||||
use crate::model::oauth::AuthGrant;
|
use crate::model::oauth::AuthGrant;
|
||||||
use crate::model::permissions::SecondaryPermission;
|
use crate::model::permissions::SecondaryPermission;
|
||||||
|
@ -764,7 +766,13 @@ 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: &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 {
|
if user.settings.disable_achievements {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -794,6 +802,15 @@ impl DataManager {
|
||||||
self.update_user_achievements(user.id, user.achievements.to_owned())
|
self.update_user_achievements(user.id, user.achievements.to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// check for final
|
||||||
|
if check_for_final {
|
||||||
|
if user.achievements.len() + 1 == ACHIEVEMENTS {
|
||||||
|
self.add_achievement(user, AchievementName::GetAllOtherAchievements.into(), false)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -242,8 +242,12 @@ impl DataManager {
|
||||||
Ok(if data.role.check(CommunityPermission::REQUESTED) {
|
Ok(if data.role.check(CommunityPermission::REQUESTED) {
|
||||||
"Join request sent".to_string()
|
"Join request sent".to_string()
|
||||||
} else {
|
} else {
|
||||||
self.add_achievement(&mut user.clone(), AchievementName::JoinCommunity.into())
|
self.add_achievement(
|
||||||
.await?;
|
&mut user.clone(),
|
||||||
|
AchievementName::JoinCommunity.into(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
"Community joined".to_string()
|
"Community joined".to_string()
|
||||||
})
|
})
|
||||||
|
|
|
@ -758,6 +758,37 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
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.
|
/// 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))
|
/// 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())
|
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).
|
/// Get all posts from the given community (from most recent).
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -1661,8 +1731,12 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
self.add_achievement(&mut owner, AchievementName::CreatePostWithTitle.into())
|
self.add_achievement(
|
||||||
.await?;
|
&mut owner,
|
||||||
|
AchievementName::CreatePostWithTitle.into(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1803,7 +1877,7 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// award achievement
|
// award achievement
|
||||||
self.add_achievement(&mut owner, AchievementName::CreateRepost.into())
|
self.add_achievement(&mut owner, AchievementName::CreateRepost.into(), true)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -162,26 +162,26 @@ impl DataManager {
|
||||||
// achievements
|
// achievements
|
||||||
if user.id != post.owner {
|
if user.id != post.owner {
|
||||||
let mut owner = self.get_user_by_id(post.owner).await?;
|
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?;
|
.await?;
|
||||||
|
|
||||||
if post.likes >= 9 {
|
if post.likes >= 9 {
|
||||||
self.add_achievement(&mut owner, AchievementName::Get10Likes.into())
|
self.add_achievement(&mut owner, AchievementName::Get10Likes.into(), true)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.likes >= 49 {
|
if post.likes >= 49 {
|
||||||
self.add_achievement(&mut owner, AchievementName::Get50Likes.into())
|
self.add_achievement(&mut owner, AchievementName::Get50Likes.into(), true)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.likes >= 99 {
|
if post.likes >= 99 {
|
||||||
self.add_achievement(&mut owner, AchievementName::Get100Likes.into())
|
self.add_achievement(&mut owner, AchievementName::Get100Likes.into(), true)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.dislikes >= 24 {
|
if post.dislikes >= 24 {
|
||||||
self.add_achievement(&mut owner, AchievementName::Get25Dislikes.into())
|
self.add_achievement(&mut owner, AchievementName::Get25Dislikes.into(), true)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,33 +262,50 @@ impl DataManager {
|
||||||
|
|
||||||
// check if we're staff
|
// check if we're staff
|
||||||
if initiator.permissions.check(FinePermission::STAFF_BADGE) {
|
if initiator.permissions.check(FinePermission::STAFF_BADGE) {
|
||||||
self.add_achievement(&mut other_user, AchievementName::FollowedByStaff.into())
|
self.add_achievement(
|
||||||
.await?;
|
&mut other_user,
|
||||||
|
AchievementName::FollowedByStaff.into(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// other achivements
|
// other achivements
|
||||||
self.add_achievement(&mut other_user, AchievementName::Get1Follower.into())
|
self.add_achievement(&mut other_user, AchievementName::Get1Follower.into(), true)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if other_user.follower_count >= 9 {
|
if other_user.follower_count >= 9 {
|
||||||
self.add_achievement(&mut other_user, AchievementName::Get10Followers.into())
|
self.add_achievement(
|
||||||
.await?;
|
&mut other_user,
|
||||||
|
AchievementName::Get10Followers.into(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if other_user.follower_count >= 49 {
|
if other_user.follower_count >= 49 {
|
||||||
self.add_achievement(&mut other_user, AchievementName::Get50Followers.into())
|
self.add_achievement(
|
||||||
.await?;
|
&mut other_user,
|
||||||
|
AchievementName::Get50Followers.into(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if other_user.follower_count >= 99 {
|
if other_user.follower_count >= 99 {
|
||||||
self.add_achievement(&mut other_user, AchievementName::Get100Followers.into())
|
self.add_achievement(
|
||||||
.await?;
|
&mut other_user,
|
||||||
|
AchievementName::Get100Followers.into(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if initiator.following_count >= 9 {
|
if initiator.following_count >= 9 {
|
||||||
self.add_achievement(
|
self.add_achievement(
|
||||||
&mut initiator.clone(),
|
&mut initiator.clone(),
|
||||||
AchievementName::Follow10Users.into(),
|
AchievementName::Follow10Users.into(),
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
pub struct UserSettings {
|
pub struct UserSettings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -285,6 +299,9 @@ pub struct UserSettings {
|
||||||
/// Automatically hide users that you've blocked on your other accounts from your timelines.
|
/// Automatically hide users that you've blocked on your other accounts from your timelines.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hide_associated_blocked_users: bool,
|
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 {
|
fn mime_avif() -> String {
|
||||||
|
@ -504,10 +521,15 @@ 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 = 30;
|
pub const ACHIEVEMENTS: usize = 34;
|
||||||
/// "self-serve" achievements can be granted by the user through the API.
|
/// "self-serve" achievements can be granted by the user through the API.
|
||||||
pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] =
|
pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[
|
||||||
&[AchievementName::OpenTos, AchievementName::OpenPrivacyPolicy];
|
AchievementName::OpenReference,
|
||||||
|
AchievementName::OpenTos,
|
||||||
|
AchievementName::OpenPrivacyPolicy,
|
||||||
|
AchievementName::AcceptProfileWarning,
|
||||||
|
AchievementName::OpenSessionSettings,
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum AchievementName {
|
pub enum AchievementName {
|
||||||
|
@ -541,6 +563,10 @@ pub enum AchievementName {
|
||||||
CreateRepost,
|
CreateRepost,
|
||||||
OpenTos,
|
OpenTos,
|
||||||
OpenPrivacyPolicy,
|
OpenPrivacyPolicy,
|
||||||
|
OpenReference,
|
||||||
|
GetAllOtherAchievements,
|
||||||
|
AcceptProfileWarning,
|
||||||
|
OpenSessionSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
@ -583,6 +609,10 @@ impl AchievementName {
|
||||||
Self::CreateRepost => "More than a like or comment...",
|
Self::CreateRepost => "More than a like or comment...",
|
||||||
Self::OpenTos => "Well informed!",
|
Self::OpenTos => "Well informed!",
|
||||||
Self::OpenPrivacyPolicy => "Privacy conscious",
|
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::CreateRepost => "Create a repost or quote.",
|
||||||
Self::OpenTos => "Open the terms of service.",
|
Self::OpenTos => "Open the terms of service.",
|
||||||
Self::OpenPrivacyPolicy => "Open the privacy policy.",
|
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::CreateRepost => Common,
|
||||||
Self::OpenTos => Uncommon,
|
Self::OpenTos => Uncommon,
|
||||||
Self::OpenPrivacyPolicy => Uncommon,
|
Self::OpenPrivacyPolicy => Uncommon,
|
||||||
|
Self::OpenReference => Uncommon,
|
||||||
|
Self::GetAllOtherAchievements => Rare,
|
||||||
|
Self::AcceptProfileWarning => Common,
|
||||||
|
Self::OpenSessionSettings => Common,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue