add: better user settings page
This commit is contained in:
parent
e8cc541f45
commit
4735832cef
16 changed files with 2398 additions and 2241 deletions
|
@ -80,6 +80,7 @@ version = "1.0.0"
|
|||
"auth:label.relationship" = "Relationship"
|
||||
"auth:label.joined_communities" = "Joined communities"
|
||||
"auth:label.recent_posts" = "Recent posts"
|
||||
"auth:label.recent_answers" = "Recent answers"
|
||||
"auth:label.recent_with_tag" = "Recent posts (with tag)"
|
||||
"auth:label.recent_replies" = "Recent replies"
|
||||
"auth:label.recent_posts_with_media" = "Recent posts (with media)"
|
||||
|
@ -176,7 +177,7 @@ version = "1.0.0"
|
|||
"settings:tab.profile" = "Profile"
|
||||
"settings:tab.theme" = "Theme"
|
||||
"settings:tab.sessions" = "Sessions"
|
||||
"settings:tab.connections" = "Connections"
|
||||
"settings:tab.grants" = "Grants"
|
||||
"settings:tab.images" = "Images"
|
||||
"settings:tab.presets" = "Presets"
|
||||
"settings:label.change_password" = "Change password"
|
||||
|
|
|
@ -85,13 +85,18 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Lexend";
|
||||
src: url("/public/fonts/lexend_variable.woff2") format("woff2");
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.15px;
|
||||
font-family:
|
||||
"Inter", "Poppins", "Roboto", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Lexend", "Inter", "Poppins", "Roboto", ui-sans-serif, system-ui,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
|
|
|
@ -466,7 +466,7 @@ button.camo:hover,
|
|||
input,
|
||||
textarea,
|
||||
select {
|
||||
padding: 0.35rem var(--pad-3);
|
||||
padding: var(--pad-2) var(--pad-3);
|
||||
border-radius: var(--radius);
|
||||
outline: none;
|
||||
transition: background 0.15s;
|
||||
|
@ -481,6 +481,10 @@ select {
|
|||
color: var(--color-text-lowered);
|
||||
}
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
@ -564,10 +568,27 @@ input[type="checkbox"]:checked {
|
|||
background-image: url("/icons/check.svg");
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.round_form input,
|
||||
.round_form .square {
|
||||
border-radius: var(--circle);
|
||||
}
|
||||
|
||||
.round_form input {
|
||||
padding: var(--pad-2) var(--pad-4);
|
||||
}
|
||||
|
||||
.round_form input[type="file"] {
|
||||
padding: 0 var(--pad-4);
|
||||
}
|
||||
|
||||
/* pillmenu */
|
||||
.pillmenu {
|
||||
display: flex;
|
||||
|
@ -874,12 +895,12 @@ nav .button:not(.title):not(.active):hover {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.mobile_nav .pillmenu a:first-of-type {
|
||||
.mobile_nav .pillmenu a:not(.dropdown *):first-of-type {
|
||||
border-top-left-radius: var(--radius) !important;
|
||||
border-bottom-left-radius: var(--radius) !important;
|
||||
}
|
||||
|
||||
.mobile_nav .pillmenu a:last-of-type {
|
||||
.mobile_nav .pillmenu a:not(.dropdown *):last-of-type {
|
||||
border-top-right-radius: var(--radius) !important;
|
||||
border-bottom-right-radius: var(--radius) !important;
|
||||
}
|
||||
|
@ -1033,14 +1054,14 @@ dialog:is(.dark *)::backdrop {
|
|||
}
|
||||
|
||||
.dropdown .inner .active::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
top: 10%;
|
||||
left: 5px;
|
||||
width: 5px;
|
||||
content: "";
|
||||
height: 100%;
|
||||
height: 80%;
|
||||
position: absolute;
|
||||
background: var(--color-primary);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--circle);
|
||||
}
|
||||
|
||||
.dropdown:not(nav *):has(.inner.open) button:not(.inner button) {
|
||||
|
@ -1085,7 +1106,7 @@ dialog:is(.dark *)::backdrop {
|
|||
width: max-content;
|
||||
max-width: calc(100dvw - var(--pad-4));
|
||||
border-radius: var(--radius);
|
||||
padding: var(--pad-3) var(--pad-4);
|
||||
padding: var(--pad-2) var(--pad-3);
|
||||
animation: popin ease-in-out 1 0.15s running;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -1502,3 +1523,47 @@ details.accordion .inner {
|
|||
top: 0;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* menus */
|
||||
menu {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
menu a {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
text-decoration: none !important;
|
||||
background: var(--color-raised);
|
||||
color: var(--color-text-raised);
|
||||
padding: var(--pad-2) var(--pad-3);
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--pad-2);
|
||||
}
|
||||
|
||||
menu a:hover {
|
||||
background: var(--color-super-raised);
|
||||
}
|
||||
|
||||
menu a.active {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
menu.col {
|
||||
flex-direction: column;
|
||||
width: 25rem;
|
||||
max-width: 100%;
|
||||
padding: var(--pad-3) 0;
|
||||
}
|
||||
|
||||
menu a:first-child {
|
||||
border-top-left-radius: var(--radius);
|
||||
border-top-right-radius: var(--radius);
|
||||
}
|
||||
|
||||
menu a:last-child {
|
||||
border-bottom-left-radius: var(--radius);
|
||||
border-bottom-right-radius: var(--radius);
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
`<b>${message}.</b> You can now close this tab.`;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = \"/settings#/connections\";
|
||||
window.location.href = \"/settings#/grants\";
|
||||
}, 500);
|
||||
}, 1000);"))
|
||||
|
||||
|
@ -75,7 +75,7 @@
|
|||
`<b>${message}.</b> You can now close this tab.`;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = \"/settings#/connections\";
|
||||
window.location.href = \"/settings#/grants\";
|
||||
}, 500);
|
||||
}, 1000);"))
|
||||
|
||||
|
|
|
@ -188,7 +188,7 @@
|
|||
(b
|
||||
(text "{{ text \"settings:label.change_avatar\" }}")))
|
||||
(form
|
||||
("class" "card flex gap_2 flex_row flex_wrap items_center")
|
||||
("class" "card big_icon flex gap_2 flex_row flex_wrap items_center")
|
||||
("method" "post")
|
||||
("enctype" "multipart/form-data")
|
||||
("onsubmit" "upload_avatar(event)")
|
||||
|
@ -199,6 +199,7 @@
|
|||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||
("class" "w_content"))
|
||||
(button
|
||||
("class" "small square big_icon")
|
||||
(text "{{ icon \"check\" }}"))))
|
||||
(div
|
||||
("class" "card_nest")
|
||||
|
@ -208,7 +209,7 @@
|
|||
(b
|
||||
(text "{{ text \"settings:label.change_banner\" }}")))
|
||||
(form
|
||||
("class" "card flex flex_col gap_2")
|
||||
("class" "card big_icon flex flex_col gap_2")
|
||||
("method" "post")
|
||||
("enctype" "multipart/form-data")
|
||||
("onsubmit" "upload_banner(event)")
|
||||
|
@ -221,6 +222,7 @@
|
|||
("accept" "image/png,image/jpeg,image/avif,image/webp")
|
||||
("class" "w_content"))
|
||||
(button
|
||||
("class" "small square big_icon")
|
||||
(text "{{ icon \"check\" }}")))
|
||||
(span
|
||||
("class" "fade")
|
||||
|
|
|
@ -18,17 +18,7 @@
|
|||
(span
|
||||
("class" "desktop")
|
||||
(str (text "general:link.home"))))
|
||||
|
||||
(text "{% if user -%}")
|
||||
(a
|
||||
("href" "/communities")
|
||||
("class" "button {% if selected == 'communities' -%}active{%- endif %}")
|
||||
(icon (text "book-heart"))
|
||||
(span
|
||||
("class" "desktop")
|
||||
(str (text "general:link.communities"))))
|
||||
|
||||
(text "{%- endif %} {%- endif %}"))
|
||||
(text "{%- endif %}"))
|
||||
|
||||
(div
|
||||
("class" "flex nav_side")
|
||||
|
@ -71,6 +61,10 @@
|
|||
|
||||
(div
|
||||
("class" "inner")
|
||||
(a
|
||||
("href" "/communities")
|
||||
(icon (text "book-heart"))
|
||||
(str (text "general:link.communities")))
|
||||
(a
|
||||
("href" "/chats/0/0")
|
||||
(icon (text "message-circle"))
|
||||
|
@ -385,9 +379,9 @@
|
|||
(span
|
||||
(text "{{ text \"settings:tab.sessions\" }}")))
|
||||
(a
|
||||
("data-tab-button" "connections")
|
||||
("href" "#/connections")
|
||||
("data-tab-button" "grants")
|
||||
("href" "#/grants")
|
||||
(text "{{ icon \"cable\" }}")
|
||||
(span
|
||||
(text "{{ text \"settings:tab.connections\" }}")))
|
||||
(text "{{ text \"settings:tab.grants\" }}")))
|
||||
(text "{%- endmacro %}")
|
||||
|
|
|
@ -20,13 +20,36 @@
|
|||
(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 "{% if not tag -%}")
|
||||
(div
|
||||
("class" "flex gap_2")
|
||||
(div
|
||||
("class" "dropdown")
|
||||
(button
|
||||
("class" "lowered small")
|
||||
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
||||
("exclude" "dropdown")
|
||||
(icon (text "arrow-down-up"))
|
||||
(text "{{ order }}"))
|
||||
(div
|
||||
("class" "inner")
|
||||
(a
|
||||
("href" "?o=Recent&f=true")
|
||||
(icon (text "calendar-arrow-down"))
|
||||
(span (text "Recent")))
|
||||
(a
|
||||
("href" "?o=Popular&f=true")
|
||||
(icon (text "trending-up"))
|
||||
(span (text "Popular")))))
|
||||
|
||||
(text "{% if user -%}")
|
||||
(a
|
||||
("href" "/search?profile={{ profile.id }}")
|
||||
("class" "button lowered small")
|
||||
(text "{{ icon \"search\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:link.search\" }}")))
|
||||
(text "{%- endif %}"))
|
||||
(text "{%- endif %}"))
|
||||
(div
|
||||
("class" "card w_full flex flex_col gap_2")
|
||||
|
@ -42,7 +65,7 @@
|
|||
(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 }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
|
||||
await trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&order={{ order }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
|
||||
(await ns(\"ui\")).IO_DATA_DISABLE_RELOAD = true;
|
||||
console.log(\"created profile timeline\");
|
||||
}, 1000);"))
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
("class" "flex gap_2 items_center")
|
||||
(text "{% if not tag -%} {{ icon \"clock\" }}")
|
||||
(span
|
||||
(text "{{ text \"auth:label.recent_posts\" }}"))
|
||||
(text "{{ text \"auth:label.recent_answers\" }}"))
|
||||
(text "{% else %} {{ icon \"tag\" }}")
|
||||
(span
|
||||
(text "{{ text \"auth:label.recent_with_tag\" }}: ")
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,7 +23,7 @@
|
|||
("class" "card w_full flex flex_col gap_2")
|
||||
(text "{% if not profile and not user.permissions|has_supporter -%} {{ components::supporter_ad(body=\"Become a supporter for full-site search!\") }} {% else %}")
|
||||
(form
|
||||
("class" "flex flex_col gap_2")
|
||||
("class" "flex flex_col gap_2 round_form")
|
||||
(div
|
||||
("class" "flex flex_row gap_2")
|
||||
(input
|
||||
|
@ -57,5 +57,4 @@
|
|||
(text "{%- endif %}"))))
|
||||
(text "{%- endif %}")
|
||||
(text "{% for post in list %} {% 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) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], poll=post[5]) }} {%- endif %} {%- endif %} {% endfor %} {% if profile -%} {{ components::pagination(page=page, items=list|length, key=\"&profile=\" ~ profile.id, value=\"&query=\" ~ query) }} {% else %} {{ components::pagination(page=page, items=list|length, key=\"&query=\" ~ query) }} {%- endif %}"))))
|
||||
|
||||
(text "{% endblock %}")
|
||||
|
|
|
@ -217,21 +217,24 @@ pub async fn add_user_request(
|
|||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
|
||||
// check block status
|
||||
if data
|
||||
.get_userblock_by_initiator_receiver(other_user.id, user.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
// add user
|
||||
// get stack
|
||||
let mut stack = match data.get_stack_by_id(id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
// check block status
|
||||
if stack.mode != StackMode::BlockList {
|
||||
if data
|
||||
.get_userblock_by_initiator_receiver(other_user.id, user.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
}
|
||||
|
||||
// add user
|
||||
if stack.users.contains(&other_user.id) {
|
||||
return Json(Error::MiscError("This user is already in this stack".to_string()).into());
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use axum::{
|
|||
Extension,
|
||||
};
|
||||
use crate::cookie::CookieJar;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tetratto_core::{
|
||||
database::FullPost,
|
||||
model::{
|
||||
|
@ -670,6 +670,20 @@ pub async fn search_request(
|
|||
))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
|
||||
pub enum TimelineOrderMode {
|
||||
/// Ordered by creation date.
|
||||
Recent,
|
||||
/// Ordered by likes - dislikes.
|
||||
Popular,
|
||||
}
|
||||
|
||||
impl Default for TimelineOrderMode {
|
||||
fn default() -> Self {
|
||||
Self::Recent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TimelineQuery {
|
||||
#[serde(default)]
|
||||
|
@ -688,6 +702,8 @@ pub struct TimelineQuery {
|
|||
pub before: usize,
|
||||
#[serde(default)]
|
||||
pub responses_only: bool,
|
||||
#[serde(default)]
|
||||
pub order: TimelineOrderMode,
|
||||
}
|
||||
|
||||
async fn swiss_army_timeline(
|
||||
|
@ -737,7 +753,13 @@ async fn swiss_army_timeline(
|
|||
.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
|
||||
if req.order == TimelineOrderMode::Recent {
|
||||
data.0.get_posts_by_user(req.user_id, 12, req.page).await
|
||||
} else {
|
||||
data.0
|
||||
.get_popular_posts_by_user(req.user_id, 12, req.page)
|
||||
.await
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if req.responses_only {
|
||||
|
|
|
@ -16,7 +16,7 @@ use axum::{
|
|||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use crate::cookie::CookieJar;
|
||||
use crate::{cookie::CookieJar, routes::pages::misc::TimelineOrderMode};
|
||||
use serde::Deserialize;
|
||||
use tetratto_core::model::{Error, auth::User};
|
||||
use crate::{assets::initial_context, get_lang, InnerState};
|
||||
|
@ -222,6 +222,8 @@ pub struct ProfileQuery {
|
|||
pub responses_only: bool,
|
||||
#[serde(default, alias = "f")]
|
||||
pub force: bool,
|
||||
#[serde(default, alias = "o")]
|
||||
pub order: TimelineOrderMode,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
|
@ -380,6 +380,7 @@ pub async fn posts_request(
|
|||
context.insert("pinned", &pinned);
|
||||
context.insert("page", &props.page);
|
||||
context.insert("tag", &props.tag);
|
||||
context.insert("order", &props.order);
|
||||
profile_context(
|
||||
&mut context,
|
||||
&user,
|
||||
|
|
|
@ -783,6 +783,37 @@ impl DataManager {
|
|||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all posts from the given user (sorted by likes - dislikes).
|
||||
///
|
||||
/// # 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_popular_posts_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 ORDER BY (likes - dislikes) DESC, 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())
|
||||
}
|
||||
|
||||
/// Get all posts (that are answering a question) from the given user (from most recent).
|
||||
///
|
||||
/// # Arguments
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue