From 2676340fbab166afcbe9dcb31e6aed3b50794f5a Mon Sep 17 00:00:00 2001 From: trisua Date: Tue, 24 Jun 2025 13:18:52 -0400 Subject: [PATCH] add: more mod panel stats add: show user invite in mod panel add: ability to share to twitter/bluesky --- crates/app/src/langs/en-US.toml | 1 + crates/app/src/public/html/components.lisp | 52 +++++++++------ crates/app/src/public/html/mod/profile.lisp | 14 ++++ crates/app/src/public/html/mod/stats.lisp | 11 +++- crates/app/src/public/js/atto.js | 9 ++- crates/app/src/public/js/me.js | 72 +++++++++++++++++++++ crates/app/src/routes/pages/mod_panel.rs | 42 ++++++++++++ crates/core/src/database/common.rs | 22 ++++++- 8 files changed, 201 insertions(+), 22 deletions(-) diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 4dacfb5..a83886f 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -182,6 +182,7 @@ version = "1.0.0" "mod_panel:label.warnings" = "Warnings" "mod_panel:label.create_warning" = "Create warning" "mod_panel:label.associations" = "Associations" +"mod_panel:label.invited_by" = "Invited by" "requests:label.requests" = "Requests" "requests:label.community_join_request" = "Community join request" diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 2d27d6d..fb62310 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -132,6 +132,7 @@ ("class" "card flex flex-col post gap-2 post:{{ post.id }} {% if secondary -%}secondary{%- endif %}") ("data-community" "{{ post.community }}") ("data-ownsup" "{{ owner.permissions|has_supporter }}") + ("data-id" "{{ post.id }}") ("hook" "verify_emojis") (div ("class" "w-full flex gap-2") @@ -214,7 +215,7 @@ ("class" "flush") ("href" "/post/{{ post.id }}") (h2 - ("id" "post-content:{{ post.id }}") + ("id" "post_content:{{ post.id }}") ("class" "no_p_margin post_content") ("hook" "long") (text "{{ post.title }}")) @@ -223,7 +224,6 @@ (text "{% else %}") (text "{% if not post.context.content_warning -%}") (span - ("id" "post-content:{{ post.id }}") ("class" "no_p_margin post_content") ("hook" "long") @@ -234,7 +234,8 @@ (text "{%- endif %}") ; content - (text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") + (span ("id" "post_content:{{ post.id }}") (text "{{ post.content|markdown|safe }}")) + (text "{% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") (div ("class" "card lowered red flex items-center gap-2") (text "{{ icon \"frown\" }}") @@ -251,7 +252,6 @@ (div ("class" "flex flex-col gap-2") (span - ("id" "post-content:{{ post.id }}") ("class" "no_p_margin post_content") ("hook" "long") @@ -261,7 +261,8 @@ (text "{% endif %}") ; content - (text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") + (span ("id" "post_content:{{ post.id }}") (text "{{ post.content|markdown|safe }}")) + (text "{% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") (div ("class" "card lowered red flex items-center gap-2") (text "{{ icon \"frown\" }}") @@ -338,7 +339,32 @@ (text "{{ icon \"quote\" }}") (span (text "{{ text \"communities:label.quote_post\" }}"))) + (button + ("onclick" "trigger('me::intent_twitter', [trigger('me::gen_share', [{ q: '{{ post.context.answering }}', p: '{{ post.id }}' }, 280, true])])") + (icon (text "bird")) + (span + (text "Twitter"))) + (button + ("onclick" "trigger('me::intent_bluesky', [trigger('me::gen_share', [{ q: '{{ post.context.answering }}', p: '{{ post.id }}' }, 280, true])])") + (icon (text "cloud")) + (span + (text "BlueSky"))) (text "{%- endif %}") + (text "{% if user.id != post.owner -%}") + (b + ("class" "title") + (text "{{ text \"general:label.safety\" }}")) + (button + ("class" "red") + ("onclick" "trigger('me::report', ['{{ post.id }}', 'post'])") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:action.report\" }}"))) + (text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}") + (b + ("class" "title") + (text "{{ text \"general:action.manage\" }}")) + ; forge stuff (text "{% if community and community.is_forge -%} {% if post.is_open -%}") (button ("class" "green") @@ -354,20 +380,7 @@ (span (text "{{ text \"forge:action.reopen\" }}"))) (text "{%- endif %} {%- endif %}") - (text "{% if user.id != post.owner -%}") - (b - ("class" "title") - (text "{{ text \"general:label.safety\" }}")) - (button - ("class" "red") - ("onclick" "trigger('me::report', ['{{ post.id }}', 'post'])") - (text "{{ icon \"flag\" }}") - (span - (text "{{ text \"general:action.report\" }}"))) - (text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}") - (b - ("class" "title") - (text "{{ text \"general:action.manage\" }}")) + ; owner stuff (text "{% if user.id == post.owner -%}") (a ("href" "/post/{{ post.id }}#/edit") @@ -675,6 +688,7 @@ (span ("class" "no_p_margin") ("style" "font-weight: 500") + ("id" "question_content:{{ question.id }}") (text "{{ question.content|markdown|safe }}")) ; question drawings (text "{{ self::post_media(upload_ids=question.drawings) }}") diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index 28ef09d..8f5e4c6 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -202,6 +202,20 @@ (text "{% for user in associations -%}") (text "{{ components::user_plate(user=user, show_menu=false) }}") (text "{%- endfor %}"))) + (text "{% if invite -%}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"ticket\" }}") + (span + (text "{{ text \"mod_panel:label.invited_by\" }}")))) + (div + ("class" "card lowered flex flex-wrap gap-2") + (text "{{ components::user_plate(user=invite[0], show_menu=false) }}"))) + (text "{%- endif %}") (div ("class" "card-nest w-full") (div diff --git a/crates/app/src/public/html/mod/stats.lisp b/crates/app/src/public/html/mod/stats.lisp index 65e895d..d9ac9f5 100644 --- a/crates/app/src/public/html/mod/stats.lisp +++ b/crates/app/src/public/html/mod/stats.lisp @@ -29,6 +29,15 @@ (b (text "Socket tasks: ")) (span - (text "{{ (active_users_chats + active_users) * 3 }}"))))))) + (text "{{ (active_users_chats + active_users) * 3 }}")))) + + (hr) + (ul + (li (b (text "Users: ")) (span (text "{{ table_users }}"))) + (li (b (text "IP bans: ")) (span (text "{{ table_ipbans }}"))) + (li (b (text "Invite codes: ")) (span (text "{{ table_invite_codes }}"))) + (li (b (text "Posts: ")) (span (text "{{ table_posts }}"))) + (li (b (text "Uploads: ")) (span (text "{{ table_uploads }}"))) + (li (b (text "Communities: ")) (span (text "{{ table_communities }}"))))))) (text "{% endblock %}") diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 56b85f8..5cf338b 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -1227,7 +1227,14 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} ).text(); self.IO_DATA_WAITING = false; - self.IO_DATA_ELEMENT.querySelector("[ui_ident=loading_skel]").remove(); + + const loading_skel = self.IO_DATA_ELEMENT.querySelector( + "[ui_ident=loading_skel]", + ); + + if (loading_skel) { + loading_skel.remove(); + } if ( text.includes(`!`) diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js index 5dbc322..4eb0f69 100644 --- a/crates/app/src/public/js/me.js +++ b/crates/app/src/public/js/me.js @@ -531,6 +531,78 @@ return out; }); + // share intents + self.define( + "gen_share", + ( + _, + ids = { q: "0", p: "0" }, + target_length = 280, + include_link = true, + ) => { + const part_1 = ( + document.getElementById(`question_content:${ids.q}`) || { + innerText: "", + } + ).innerText; + + const part_2 = document.getElementById( + `post_content:${ids.p}`, + ).innerText; + + // ... + const link = + include_link !== false + ? `${window.location.origin}/post/${ids.p}` + : ""; + + const link_size = link.length; + target_length -= link_size; + + let out = ""; + const separator = " — "; + + const part_2_size = target_length / 2 - 1; + const sep_size = separator.length; + const part_1_size = target_length / 2 - sep_size; + + if (part_1 !== "") { + out += + part_1_size > part_1.length + ? part_1 + : part_1.substring(0, part_1_size); + + out += separator; + } + + if (part_2 !== "") { + out += + part_2_size > part_2.length + ? part_2 + : part_2.substring(0, part_2_size); + } + + out += ` ${link}`; + return out; + }, + ); + + self.define("intent_twitter", (_, text) => { + window.open( + `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`, + ); + + trigger("atto::toast", ["success", "Opened intent!"]); + }); + + self.define("intent_bluesky", (_, text) => { + window.open( + `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`, + ); + + trigger("atto::toast", ["success", "Opened intent!"]); + }); + // token switcher self.define("append_associations", (_, tokens) => { fetch("/api/v1/auth/user/me/append_associations", { diff --git a/crates/app/src/routes/pages/mod_panel.rs b/crates/app/src/routes/pages/mod_panel.rs index 860bf0e..7a9b6f7 100644 --- a/crates/app/src/routes/pages/mod_panel.rs +++ b/crates/app/src/routes/pages/mod_panel.rs @@ -194,10 +194,23 @@ pub async fn manage_profile_request( out }; + let invite_code = if profile.invite_code != 0 { + match data.0.get_invite_code_by_id(profile.invite_code).await { + Ok(i) => match data.0.get_user_by_id(i.owner).await { + Ok(u) => Some((u, i)), + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + } + } else { + None + }; + let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; context.insert("profile", &profile); + context.insert("invite", &invite_code); context.insert("associations", &associations); // return @@ -298,6 +311,35 @@ pub async fn stats_request(jar: CookieJar, Extension(data): Extension) -> .unwrap(), ); + context.insert( + "table_users", + &data.0.get_table_row_count("users").await.unwrap_or(0), + ); + context.insert( + "table_posts", + &data.0.get_table_row_count("posts").await.unwrap_or(0), + ); + context.insert( + "table_invite_codes", + &data + .0 + .get_table_row_count("invite_codes") + .await + .unwrap_or(0), + ); + context.insert( + "table_uploads", + &data.0.get_table_row_count("uploads").await.unwrap_or(0), + ); + context.insert( + "table_communities", + &data.0.get_table_row_count("communities").await.unwrap_or(0), + ); + context.insert( + "table_ipbans", + &data.0.get_table_row_count("ipbans").await.unwrap_or(0), + ); + // return Ok(Html(data.1.render("mod/stats.html", &context).unwrap())) } diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index a82e389..45111db 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -1,6 +1,6 @@ use crate::model::{Error, Result}; use super::{DataManager, drivers::common}; -use oiseau::{cache::Cache, execute}; +use oiseau::{cache::Cache, execute, query_row, params}; pub const NAME_REGEX: &str = r"[^\w_\-\.,!]+"; @@ -52,6 +52,26 @@ impl DataManager { Ok(()) } + + pub async fn get_table_row_count(&self, table: &str) -> Result { + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_row!( + &conn, + &format!("SELECT COUNT(*)::int FROM {}", table), + params![], + |x| Ok(x.get::(0)) + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(res.unwrap()) + } } #[macro_export]