add: forum threads ui
This commit is contained in:
parent
155fe34c6e
commit
3958d5eaef
8 changed files with 260 additions and 15 deletions
|
@ -88,6 +88,8 @@ pub const POST_POST: &str = include_str!("./public/html/post/post.lisp");
|
||||||
pub const POST_REPOSTS: &str = include_str!("./public/html/post/reposts.lisp");
|
pub const POST_REPOSTS: &str = include_str!("./public/html/post/reposts.lisp");
|
||||||
pub const POST_QUOTES: &str = include_str!("./public/html/post/quotes.lisp");
|
pub const POST_QUOTES: &str = include_str!("./public/html/post/quotes.lisp");
|
||||||
pub const POST_LIKES: &str = include_str!("./public/html/post/likes.lisp");
|
pub const POST_LIKES: &str = include_str!("./public/html/post/likes.lisp");
|
||||||
|
pub const POST_FORUM_QUICK_REPLIES: &str =
|
||||||
|
include_str!("./public/html/post/forum_quick_replies.lisp");
|
||||||
|
|
||||||
pub const TIMELINES_HOME: &str = include_str!("./public/html/timelines/home.lisp");
|
pub const TIMELINES_HOME: &str = include_str!("./public/html/timelines/home.lisp");
|
||||||
pub const TIMELINES_POPULAR: &str = include_str!("./public/html/timelines/popular.lisp");
|
pub const TIMELINES_POPULAR: &str = include_str!("./public/html/timelines/popular.lisp");
|
||||||
|
@ -327,6 +329,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
||||||
write_template!(html_path->"post/reposts.html"(crate::assets::POST_REPOSTS) --config=config --lisp plugins);
|
write_template!(html_path->"post/reposts.html"(crate::assets::POST_REPOSTS) --config=config --lisp plugins);
|
||||||
write_template!(html_path->"post/quotes.html"(crate::assets::POST_QUOTES) --config=config --lisp plugins);
|
write_template!(html_path->"post/quotes.html"(crate::assets::POST_QUOTES) --config=config --lisp plugins);
|
||||||
write_template!(html_path->"post/likes.html"(crate::assets::POST_LIKES) --config=config --lisp plugins);
|
write_template!(html_path->"post/likes.html"(crate::assets::POST_LIKES) --config=config --lisp plugins);
|
||||||
|
write_template!(html_path->"post/forum_quick_replies.html"(crate::assets::POST_FORUM_QUICK_REPLIES) --config=config --lisp plugins);
|
||||||
|
|
||||||
write_template!(html_path->"timelines/home.html"(crate::assets::TIMELINES_HOME) -d "timelines" --config=config --lisp plugins);
|
write_template!(html_path->"timelines/home.html"(crate::assets::TIMELINES_HOME) -d "timelines" --config=config --lisp plugins);
|
||||||
write_template!(html_path->"timelines/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config --lisp plugins);
|
write_template!(html_path->"timelines/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config --lisp plugins);
|
||||||
|
|
|
@ -50,6 +50,8 @@ version = "1.0.0"
|
||||||
"general:label.loading" = "Working on it!"
|
"general:label.loading" = "Working on it!"
|
||||||
"general:label.send_anonymously" = "Send anonymously"
|
"general:label.send_anonymously" = "Send anonymously"
|
||||||
"general:label.must_activate_account" = "You need to activate your account!"
|
"general:label.must_activate_account" = "You need to activate your account!"
|
||||||
|
"general:action.load_more" = "Load more"
|
||||||
|
"general:action.show_thread" = "Show thread"
|
||||||
|
|
||||||
"general:label.supporter_motivation" = "Become a supporter!"
|
"general:label.supporter_motivation" = "Become a supporter!"
|
||||||
"general:action.become_supporter" = "Become supporter"
|
"general:action.become_supporter" = "Become supporter"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
@import url("root.css");
|
@import url("root.css");
|
||||||
|
|
||||||
|
/* media gallery */
|
||||||
.media_gallery {
|
.media_gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-columns: 1fr 1fr;
|
grid-auto-columns: 1fr 1fr;
|
||||||
|
@ -312,7 +313,7 @@ button,
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: var(--pad-1) var(--pad-4);
|
padding: 0 var(--pad-4);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -439,7 +440,6 @@ textarea,
|
||||||
select {
|
select {
|
||||||
padding: 0.35rem var(--pad-3);
|
padding: 0.35rem var(--pad-3);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: solid 1px var(--color-super-lowered);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
@ -447,8 +447,10 @@ select {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
/* personality */
|
/* personality */
|
||||||
background: transparent;
|
--background: var(--color-lowered);
|
||||||
color: inherit;
|
border: solid 1px var(--background);
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--color-text-lowered);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
|
@ -458,8 +460,7 @@ textarea {
|
||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
background: var(--color-super-raised);
|
border-color: var(--color-super-lowered);
|
||||||
color: var(--color-text-raised);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll_bar {
|
.poll_bar {
|
||||||
|
@ -902,7 +903,7 @@ dialog::backdrop {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--color-raised);
|
background: var(--color-raised);
|
||||||
border: solid 1px var(--color-super-lowered);
|
/* border: solid 1px var(--color-super-lowered); */
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
top: calc(100% + 5px);
|
top: calc(100% + 5px);
|
||||||
|
@ -1404,3 +1405,55 @@ details.accordion .inner {
|
||||||
background-color: var(--color-primary) !important;
|
background-color: var(--color-primary) !important;
|
||||||
color: var(--color-text-primary) !important;
|
color: var(--color-text-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* threads */
|
||||||
|
.squig {
|
||||||
|
--color: var(--color-super-lowered);
|
||||||
|
--background: var(--color-raised);
|
||||||
|
--size: 10px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squig::before,
|
||||||
|
.squig::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
background-size: var(--size) 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squig::before {
|
||||||
|
top: -2px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, var(--color) 35%, transparent 0),
|
||||||
|
linear-gradient(-45deg, var(--color) 35%, transparent 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.squig::after {
|
||||||
|
top: 0px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, var(--background) 35%, transparent 0),
|
||||||
|
linear-gradient(-45deg, var(--background) 35%, transparent 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread {
|
||||||
|
--pad: 15px;
|
||||||
|
--squig-height: 20px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: var(--pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background: var(--color-super-lowered);
|
||||||
|
height: calc(100% - var(--squig-height));
|
||||||
|
width: 5px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
|
@ -201,7 +201,7 @@
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
||||||
(text "{% macro post_buttons_box(post, community, owner, can_manage_post) -%}")
|
(text "{% macro post_buttons_box(post, community, owner, can_manage_post, show_comments=true) -%}")
|
||||||
(div
|
(div
|
||||||
("class" "flex justify_between items_center gap_2 w_full")
|
("class" "flex justify_between items_center gap_2 w_full")
|
||||||
(text "{% if user -%}")
|
(text "{% if user -%}")
|
||||||
|
@ -220,7 +220,7 @@
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_1 buttons_box")
|
("class" "flex gap_1 buttons_box")
|
||||||
(text "{% if post.context.comments_enabled %}")
|
(text "{% if show_comments and post.context.comments_enabled %}")
|
||||||
(a
|
(a
|
||||||
("href" "/post/{{ post.id }}")
|
("href" "/post/{{ post.id }}")
|
||||||
("class" "button camo small")
|
("class" "button camo small")
|
||||||
|
@ -526,7 +526,7 @@
|
||||||
(text "{{ text \"general:action.delete\" }}"))))))
|
(text "{{ text \"general:action.delete\" }}"))))))
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
||||||
(text "{% macro forum_post(post, owner, community, can_manage_post, poll=false) -%}")
|
(text "{% macro forum_post(post, owner, community, can_manage_post, poll=false, show_show_thread=true) -%}")
|
||||||
(div
|
(div
|
||||||
("class" "card_nest_horizontal_wrapper post post:{{ post.id }}")
|
("class" "card_nest_horizontal_wrapper post post:{{ post.id }}")
|
||||||
("data-community" "{{ post.community }}")
|
("data-community" "{{ post.community }}")
|
||||||
|
@ -580,7 +580,32 @@
|
||||||
(text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}"))
|
(text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}"))
|
||||||
|
|
||||||
(hr ("class" "margin_top"))
|
(hr ("class" "margin_top"))
|
||||||
(text "{{ self::post_buttons_box(post=post, community=community, owner=owner, can_manage_post=can_manage_post) }}"))))
|
(text "{{ self::post_buttons_box(post=post, community=community, owner=owner, can_manage_post=can_manage_post, show_comments=false) }}"))))
|
||||||
|
|
||||||
|
; show thread
|
||||||
|
(text "{% if show_show_thread and post.comment_count > 0 -%}")
|
||||||
|
(div
|
||||||
|
("class" "flex gap_2")
|
||||||
|
(text "{% if post.context.comments_enabled %}")
|
||||||
|
(a
|
||||||
|
("href" "/post/{{ post.id }}")
|
||||||
|
("class" "button lowered")
|
||||||
|
(icon (text "message-circle"))
|
||||||
|
(span
|
||||||
|
(text "{{ post.comment_count }}")))
|
||||||
|
(text "{% endif %}")
|
||||||
|
|
||||||
|
(button
|
||||||
|
("class" "lowered")
|
||||||
|
("onclick" "globalThis.continue_thread(event.target, '{{ post.id }}', 'replies_{{ post.id }}', 0)")
|
||||||
|
(icon (text "chevron-down"))
|
||||||
|
(str (text "general:action.show_thread"))))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
; replies
|
||||||
|
(div
|
||||||
|
("class" "flex flex_col gap_2 hidden thread")
|
||||||
|
("id" "replies_{{ post.id }}"))
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
||||||
(text "{% macro user_card(user) -%}")
|
(text "{% macro user_card(user) -%}")
|
||||||
|
|
24
crates/app/src/public/html/post/forum_quick_replies.lisp
Normal file
24
crates/app/src/public/html/post/forum_quick_replies.lisp
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
(text "{%- import \"components.html\" as components -%} {%- import \"macros.html\" as macros -%}")
|
||||||
|
|
||||||
|
(div
|
||||||
|
("class" "flex flex_col gap_2")
|
||||||
|
("id" "replies_{{ post.id }}_{{ page }}")
|
||||||
|
; replies
|
||||||
|
(text "{% for post in replies -%}")
|
||||||
|
(div
|
||||||
|
("style" "display: contents")
|
||||||
|
(text "{{ components::forum_post(post=post[0], owner=post[1], community=community, can_manage_post=can_manage_posts, poll=post[4]) }}"))
|
||||||
|
(text "{%- endfor %}")
|
||||||
|
|
||||||
|
; load more button
|
||||||
|
(text "{% set len = replies|length %}")
|
||||||
|
(text "{% if len != 0 and (page * 12) + len != post.comment_count -%}")
|
||||||
|
(div
|
||||||
|
(button
|
||||||
|
("class" "lowered")
|
||||||
|
("onclick" "globalThis.continue_thread(event.target, '{{ post.id }}', 'replies_{{ post.id }}', {{ page + 1 }})")
|
||||||
|
(icon (text "chevron-down"))
|
||||||
|
(str (text "general:action.load_more"))))
|
||||||
|
(text "{% else %}")
|
||||||
|
(div ("class" "squig"))
|
||||||
|
(text "{%- endif %}"))
|
|
@ -56,7 +56,7 @@
|
||||||
(text "{% else %}")
|
(text "{% else %}")
|
||||||
(div
|
(div
|
||||||
("style" "display: contents")
|
("style" "display: contents")
|
||||||
(text "{{ components::forum_post(post=post, owner=owner, community=community, can_manage_post=can_manage_posts, poll=poll) }}"))
|
(text "{{ components::forum_post(post=post, owner=owner, community=community, can_manage_post=can_manage_posts, poll=poll, show_show_thread=false) }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
; ...
|
; ...
|
||||||
(text "{% if user and post.context.comments_enabled -%}")
|
(text "{% if user and post.context.comments_enabled -%}")
|
||||||
|
@ -332,7 +332,24 @@
|
||||||
(text "{%- endif %} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}"))))
|
(text "{%- endif %} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}"))))
|
||||||
|
|
||||||
(script
|
(script
|
||||||
(text "async function create_reply_from_form(e) {
|
(text "globalThis.continue_thread = async (target, post_id, id, page = 0) => {
|
||||||
|
const btn_id = `tmp_${window.crypto.randomUUID()}`;
|
||||||
|
target.setAttribute(\"disabled\", \"true\");
|
||||||
|
target.id = btn_id;
|
||||||
|
|
||||||
|
document.getElementById(id).innerHTML +=
|
||||||
|
await (await fetch(`/post/${post_id}/_quick_replies?page=${page}`)).text();
|
||||||
|
document.getElementById(id).classList.remove(\"hidden\");
|
||||||
|
|
||||||
|
await trigger(\"atto::clean_date_codes\");
|
||||||
|
await trigger(\"atto::link_filter\");
|
||||||
|
await trigger(\"atto::hooks::check_reactions\");
|
||||||
|
await trigger(\"atto::hooks::online_indicator\");
|
||||||
|
|
||||||
|
document.getElementById(btn_id).parentElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create_reply_from_form(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await trigger(\"atto::debounce\", [\"posts::create\"]);
|
await trigger(\"atto::debounce\", [\"posts::create\"]);
|
||||||
|
|
||||||
|
|
|
@ -1011,8 +1011,6 @@ pub async fn post_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
let ignore_users = crate::ignore_users_gen!(user, data);
|
|
||||||
|
|
||||||
let feed = match data
|
let feed = match data
|
||||||
.0
|
.0
|
||||||
.get_replies_by_post(
|
.get_replies_by_post(
|
||||||
|
@ -1085,6 +1083,125 @@ pub async fn post_request(
|
||||||
Ok(Html(data.1.render("post/post.html", &context).unwrap()))
|
Ok(Html(data.1.render("post/post.html", &context).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `/post/{id}/_quick_replies`
|
||||||
|
pub async fn forum_quick_replies_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
Query(props): Query<PaginatedQuery>,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = data.read().await;
|
||||||
|
let user = get_user_from_token!(jar, data.0);
|
||||||
|
|
||||||
|
let post = match data.0.get_post_by_id(id).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if post.is_deleted {
|
||||||
|
// act like the post doesn't exist (if missing MANAGE_POSTS)
|
||||||
|
if let Some(ref ua) = user {
|
||||||
|
if !ua.permissions.check(FinePermission::MANAGE_POSTS) {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(
|
||||||
|
Error::GeneralNotFound("post".to_string()),
|
||||||
|
&jar,
|
||||||
|
&data,
|
||||||
|
&user,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(
|
||||||
|
Error::GeneralNotFound("post".to_string()),
|
||||||
|
&jar,
|
||||||
|
&data,
|
||||||
|
&user,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let community = match data.0.get_community_by_id(post.community).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// check permissions
|
||||||
|
let (can_read, can_manage_pins) = check_community_permissions!(community, jar, data, user);
|
||||||
|
|
||||||
|
if !can_read {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let ignore_users = crate::ignore_users_gen!(user, data);
|
||||||
|
|
||||||
|
let feed = match data
|
||||||
|
.0
|
||||||
|
.get_replies_by_post(
|
||||||
|
post.id,
|
||||||
|
12,
|
||||||
|
props.page,
|
||||||
|
if community.is_forum { "ASC" } else { "DESC" },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
|
},
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// init context
|
||||||
|
let lang = get_lang!(jar, data.0);
|
||||||
|
let mut context = initial_context(&data.0.0.0, lang, &user).await;
|
||||||
|
|
||||||
|
let (
|
||||||
|
is_owner,
|
||||||
|
is_joined,
|
||||||
|
is_pending,
|
||||||
|
can_post,
|
||||||
|
can_manage_posts,
|
||||||
|
can_manage_community,
|
||||||
|
can_manage_roles,
|
||||||
|
can_manage_questions,
|
||||||
|
) = community_context_bools!(data, user, community);
|
||||||
|
|
||||||
|
context.insert("post", &post);
|
||||||
|
context.insert("replies", &feed);
|
||||||
|
context.insert("page", &props.page);
|
||||||
|
context.insert("can_manage_pins", &can_manage_pins);
|
||||||
|
|
||||||
|
community_context(
|
||||||
|
&mut context,
|
||||||
|
&community,
|
||||||
|
is_owner,
|
||||||
|
is_joined,
|
||||||
|
is_pending,
|
||||||
|
can_post,
|
||||||
|
can_read,
|
||||||
|
can_manage_posts,
|
||||||
|
can_manage_community,
|
||||||
|
can_manage_roles,
|
||||||
|
can_manage_questions,
|
||||||
|
);
|
||||||
|
|
||||||
|
// return
|
||||||
|
Ok(Html(
|
||||||
|
data.1
|
||||||
|
.render("post/forum_quick_replies.html", &context)
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// `/post/{id}/reposts`
|
/// `/post/{id}/reposts`
|
||||||
pub async fn reposts_request(
|
pub async fn reposts_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
|
|
|
@ -115,6 +115,10 @@ pub fn routes() -> Router {
|
||||||
get(communities::members_request),
|
get(communities::members_request),
|
||||||
)
|
)
|
||||||
.route("/post/{id}", get(communities::post_request))
|
.route("/post/{id}", get(communities::post_request))
|
||||||
|
.route(
|
||||||
|
"/post/{id}/_quick_replies",
|
||||||
|
get(communities::forum_quick_replies_request),
|
||||||
|
)
|
||||||
.route("/post/{id}/reposts", get(communities::reposts_request))
|
.route("/post/{id}/reposts", get(communities::reposts_request))
|
||||||
.route("/post/{id}/likes", get(communities::likes_request))
|
.route("/post/{id}/likes", get(communities::likes_request))
|
||||||
.route("/question/{id}", get(communities::question_request))
|
.route("/question/{id}", get(communities::question_request))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue