diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 6f9addf..092889f 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -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_QUOTES: &str = include_str!("./public/html/post/quotes.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_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/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/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/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config --lisp plugins); diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index c9e0b93..f995deb 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -50,6 +50,8 @@ version = "1.0.0" "general:label.loading" = "Working on it!" "general:label.send_anonymously" = "Send anonymously" "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:action.become_supporter" = "Become supporter" diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 88a1864..900afd0 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -1,5 +1,6 @@ @import url("root.css"); +/* media gallery */ .media_gallery { display: grid; grid-auto-columns: 1fr 1fr; @@ -312,7 +313,7 @@ button, transition: background 0.15s; width: max-content; height: 32px; - padding: var(--pad-1) var(--pad-4); + padding: 0 var(--pad-4); border-radius: var(--radius); cursor: pointer; display: flex; @@ -439,7 +440,6 @@ textarea, select { padding: 0.35rem var(--pad-3); border-radius: var(--radius); - border: solid 1px var(--color-super-lowered); outline: none; transition: background 0.15s; resize: vertical; @@ -447,8 +447,10 @@ select { font-family: inherit; font-size: 16px; /* personality */ - background: transparent; - color: inherit; + --background: var(--color-lowered); + border: solid 1px var(--background); + background: var(--background); + color: var(--color-text-lowered); } textarea { @@ -458,8 +460,7 @@ textarea { input:focus, textarea:focus, select:focus { - background: var(--color-super-raised); - color: var(--color-text-raised); + border-color: var(--color-super-lowered); } .poll_bar { @@ -902,7 +903,7 @@ dialog::backdrop { display: none; position: absolute; background: var(--color-raised); - border: solid 1px var(--color-super-lowered); + /* border: solid 1px var(--color-super-lowered); */ z-index: 2; border-radius: var(--radius); top: calc(100% + 5px); @@ -1404,3 +1405,55 @@ details.accordion .inner { background-color: var(--color-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); +} diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 8e5f11d..d2b2bf6 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -201,7 +201,7 @@ (text "{%- endif %}") (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 ("class" "flex justify_between items_center gap_2 w_full") (text "{% if user -%}") @@ -220,7 +220,7 @@ (text "{%- endif %}") (div ("class" "flex gap_1 buttons_box") - (text "{% if post.context.comments_enabled %}") + (text "{% if show_comments and post.context.comments_enabled %}") (a ("href" "/post/{{ post.id }}") ("class" "button camo small") @@ -526,7 +526,7 @@ (text "{{ text \"general:action.delete\" }}")))))) (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 ("class" "card_nest_horizontal_wrapper post post:{{ post.id }}") ("data-community" "{{ post.community }}") @@ -580,7 +580,32 @@ (text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}")) (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 "{% macro user_card(user) -%}") diff --git a/crates/app/src/public/html/post/forum_quick_replies.lisp b/crates/app/src/public/html/post/forum_quick_replies.lisp new file mode 100644 index 0000000..383d5ba --- /dev/null +++ b/crates/app/src/public/html/post/forum_quick_replies.lisp @@ -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 %}")) diff --git a/crates/app/src/public/html/post/post.lisp b/crates/app/src/public/html/post/post.lisp index c645ae0..1eae1ab 100644 --- a/crates/app/src/public/html/post/post.lisp +++ b/crates/app/src/public/html/post/post.lisp @@ -56,7 +56,7 @@ (text "{% else %}") (div ("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 "{% if user and post.context.comments_enabled -%}") @@ -332,7 +332,24 @@ (text "{%- endif %} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}")))) (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(); await trigger(\"atto::debounce\", [\"posts::create\"]); diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index 2f0d03c..1e2ce88 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -1011,8 +1011,6 @@ pub async fn post_request( } // ... - let ignore_users = crate::ignore_users_gen!(user, data); - let feed = match data .0 .get_replies_by_post( @@ -1085,6 +1083,125 @@ pub async fn post_request( 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, + Query(props): Query, + Extension(data): Extension, +) -> 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` pub async fn reposts_request( jar: CookieJar, diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index b077d1c..01f24d6 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -115,6 +115,10 @@ pub fn routes() -> Router { get(communities::members_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}/likes", get(communities::likes_request)) .route("/question/{id}", get(communities::question_request))