diff --git a/Cargo.lock b/Cargo.lock index 3ed31d7..87b81b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3275,7 +3275,7 @@ dependencies = [ [[package]] name = "tetratto" -version = "4.5.0" +version = "5.0.0" dependencies = [ "ammonia", "async-stripe", @@ -3307,7 +3307,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "4.5.0" +version = "5.0.0" dependencies = [ "async-recursion", "base16ct", @@ -3332,7 +3332,7 @@ dependencies = [ [[package]] name = "tetratto-l10n" -version = "4.5.0" +version = "5.0.0" dependencies = [ "pathbufd", "serde", @@ -3341,7 +3341,7 @@ dependencies = [ [[package]] name = "tetratto-shared" -version = "4.5.0" +version = "5.0.0" dependencies = [ "ammonia", "chrono", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 396f908..3f0e132 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto" -version = "4.5.0" +version = "5.0.0" edition = "2024" [features] diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 001e26c..4d295d1 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -678,6 +678,28 @@ button.camo:hover, color: var(--color-text-lowered); } +.hover_left_bar { + position: relative; +} + +.hover_left_bar::after { + top: 0; + left: 0; + width: 5px; + content: ""; + height: 100%; + position: absolute; + background: var(--color-primary); + border-top-left-radius: var(--radius); + border-bottom-left-radius: var(--radius); + opacity: 0%; + transition: opacity 0.15s; +} + +.hover_left_bar:hover::after { + opacity: 100%; +} + /* input */ input, textarea, @@ -707,6 +729,12 @@ select:focus { color: var(--color-text-raised); } +.poll_bar { + background-color: var(--color-primary); + border-radius: var(--radius); + height: 25px; +} + /* pillmenu */ .pillmenu { display: flex; diff --git a/crates/app/src/public/html/communities/create_post.lisp b/crates/app/src/public/html/communities/create_post.lisp index 1f1204a..93d98da 100644 --- a/crates/app/src/public/html/communities/create_post.lisp +++ b/crates/app/src/public/html/communities/create_post.lisp @@ -151,6 +151,13 @@ .querySelector(\"button.primary\") .classList.add(\"hidden\"); + // poll + const poll_data = get_poll_data(); + + if (!poll_data[0]) { + return alert(poll_data[1]); + } + // create body const body = new FormData(); @@ -167,6 +174,7 @@ community: document.getElementById( \"community_to_post_to\", ).selectedOptions[0].value, + poll: poll_data[1], }), ); diff --git a/crates/app/src/public/html/communities/feed.lisp b/crates/app/src/public/html/communities/feed.lisp index 1801fc9..7e817e1 100644 --- a/crates/app/src/public/html/communities/feed.lisp +++ b/crates/app/src/public/html/communities/feed.lisp @@ -11,7 +11,7 @@ (text "{{ text \"communities:label.pinned\" }}"))) (div ("class" "card flex flex-col gap-4") - (text "{% for post in pinned %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {%- endif %} {% endfor %}"))) + (text "{% for post in pinned %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts, poll=post[4]) }} {%- endif %} {% endfor %}"))) (text "{%- endif %}") (div ("class" "card-nest") @@ -22,6 +22,6 @@ (text "{{ text \"communities:label.posts\" }}"))) (div ("class" "card flex flex-col gap-4") - (text "{% for post in feed %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=feed|length) }}")))) + (text "{% for post in feed %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts, poll=post[4]) }} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=feed|length) }}")))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/question.lisp b/crates/app/src/public/html/communities/question.lisp index 2d6eefa..f631481 100644 --- a/crates/app/src/public/html/communities/question.lisp +++ b/crates/app/src/public/html/communities/question.lisp @@ -60,6 +60,13 @@ e.preventDefault(); await trigger(\"atto::debounce\", [\"posts::create\"]); + // poll + const poll_data = get_poll_data(); + + if (!poll_data[0]) { + return alert(poll_data[1]); + } + // create body const body = new FormData(); @@ -75,6 +82,7 @@ content: e.target.content.value, community: community ? community : \"{{ config.town_square }}\", answering, + poll: poll_data[1], }), ); diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 4fa1c06..b183dcc 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -222,6 +222,9 @@ (text "{%- endif %} {%- endif %}")) (text "{{ self::post_media(upload_ids=post.uploads) }}"))) (text "{%- endif %}") + + (text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}") + (div ("class" "flex flex-wrap gap-2 fade") (text "{% for tag in post.context.tags %}") @@ -1205,6 +1208,14 @@ (div ("class" "flex gap-2") (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if not quoting -%} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %} {%- endif %}") + + (button + ("class" "small square quaternary") + ("title" "Add poll") + ("onclick" "document.getElementById('poll_options_dialog').showModal()") + ("type" "button") + (text "{{ icon \"list-todo\" }}")) + (button ("class" "small square quaternary") ("title" "More options") @@ -1323,4 +1334,155 @@ } };")))) +; poll data manager function and dialog +; +; `get_poll_data` returns `[bool, string | PollData]`, where the string in arg 1 +; represents an error message if arg 0 is `false` +(script + (text "window.POLL_OPTION_A = \"\"; + window.POLL_OPTION_B = \"\"; + window.POLL_OPTION_C = \"\"; + window.POLL_OPTION_D = \"\"; + + window.get_poll_data = () => { + if (!POLL_OPTION_A && !POLL_OPTION_B) { + return [true, null]; + } + + if (POLL_OPTION_A && !POLL_OPTION_B || POLL_OPTION_B && !POLL_OPTION_A) { + return [false, \"At least 2 options are required for a poll\"]; + } + + return [true, { + option_a: POLL_OPTION_A, + option_b: POLL_OPTION_B, + option_c: POLL_OPTION_C, + option_d: POLL_OPTION_D + }]; + }")) + +(dialog + ("id" "poll_options_dialog") + (div + ("class" "inner flex flex-col gap-2") + (div + ("id" "poll_options") + ("class" "flex flex-col gap-2") + + (b (text "Attach poll")) + + (div + ("class" "card flex flex-col gap-2") + (span + (b (text "Option A ")) + (span ("class" "fade red") (text "(required)"))) + + (input ("type" "text") ("placeholder" "option A") ("onchange" "window.POLL_OPTION_A = event.target.value"))) + + (div + ("class" "card flex flex-col gap-2") + (span + (b (text "Option B ")) + (span ("class" "fade red") (text "(required)"))) + + (input ("type" "text") ("placeholder" "option B") ("onchange" "window.POLL_OPTION_B = event.target.value"))) + + (div + ("class" "card flex flex-col gap-2") + (b (text "Option C")) + (input ("type" "text") ("placeholder" "option A") ("onchange" "window.POLL_OPTION_C = event.target.value"))) + + (div + ("class" "card flex flex-col gap-2") + (b (text "Option D")) + (input ("type" "text") ("placeholder" "option D") ("onchange" "window.POLL_OPTION_D = event.target.value")))) + (hr) + (div + ("class" "flex justify-between") + (div) + (div + ("class" "flex gap-2") + (button + ("class" "bold red quaternary") + ("onclick" "document.getElementById('poll_options_dialog').close()") + ("type" "button") + (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))))) + +(text "{%- endmacro %}") +(text "{% macro poll(post, poll) -%}") +(div + ("class" "card tertiary w-full flex flex-col gap-2") + (text "{% set total = poll[0].votes_a + poll[0].votes_b + poll[0].votes_c + poll[0].votes_d %}") + + (text "{% if poll[1] -%}") + ; already voted, show results + (span ("class" "fade") (text "You've already voted!")) + + ; option a + (div + ("class" "card w-full flex flex-col gap-2") + (span (text "{{ poll[0].option_a }} ({{ poll[0].votes_a }} votes)")) + (div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_a / total) * 100 }}%"))) + + ; option b + (div + ("class" "card w-full flex flex-col gap-2") + (span (text "{{ poll[0].option_b }} ({{ poll[0].votes_b }} votes)")) + (div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_b / total) * 100 }}%"))) + + ; option c + (text "{% if poll[0].option_c -%}") + (div + ("class" "card w-full flex flex-col gap-2") + (span (text "{{ poll[0].option_c }} ({{ poll[0].votes_c }} votes)")) + (div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_c / total) * 100 }}%"))) + (text "{%- endif %}") + + ; option d + (text "{% if poll[0].option_d -%}") + (div + ("class" "card w-full flex flex-col gap-2") + (span (text "{{ poll[0].option_d }} ({{ poll[0].votes_d }} votes)")) + (div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_d / total) * 100 }}%"))) + (text "{%- endif %}") + (text "{% else %}") + ; not voted yet, just show options so user can vote + + ; option a + (button + ("class" "hover_left_bar tertiary justify-start w-full") + ("onclick" "trigger('me::vote', ['{{ post.id }}', 'A'])") + (icon (text "tally-1")) + (text "{{ poll[0].option_a }}")) + + ; option b + (button + ("class" "hover_left_bar tertiary justify-start w-full") + ("onclick" "trigger('me::vote', ['{{ post.id }}', 'B'])") + (icon (text "tally-2")) + (text "{{ poll[0].option_b }}")) + + ; option c + (text "{% if poll[0].option_c -%}") + (button + ("class" "hover_left_bar tertiary justify-start w-full") + ("onclick" "trigger('me::vote', ['{{ post.id }}', 'C'])") + (icon (text "tally-3")) + (text "{{ poll[0].option_c }}")) + (text "{%- endif %}") + + ; option d + (text "{% if poll[0].option_d -%}") + (button + ("class" "hover_left_bar tertiary justify-start w-full") + ("onclick" "trigger('me::vote', ['{{ post.id }}', 'D'])") + (icon (text "tally-4")) + (text "{{ poll[0].option_d }}")) + (text "{%- endif %}") + (text "{%- endif %}") + + ; show expiration date + totals + (div + ("class" "flex w-full flex-wrap gap-2") + (span ("class" "notification chip") (text "{{ total }} votes")))) (text "{%- endmacro %}") diff --git a/crates/app/src/public/html/misc/requests.lisp b/crates/app/src/public/html/misc/requests.lisp index 1bbd19f..422106b 100644 --- a/crates/app/src/public/html/misc/requests.lisp +++ b/crates/app/src/public/html/misc/requests.lisp @@ -179,6 +179,13 @@ e.preventDefault(); await trigger(\"atto::debounce\", [\"posts::create\"]); + // poll + const poll_data = get_poll_data(); + + if (!poll_data[0]) { + return alert(poll_data[1]); + } + // create body const body = new FormData(); @@ -194,6 +201,7 @@ content: e.target.content.value, community: \"{{ config.town_square }}\", answering, + poll: poll_data[1], }), ); diff --git a/crates/app/src/public/html/post/post.lisp b/crates/app/src/public/html/post/post.lisp index ca0c675..9b510e0 100644 --- a/crates/app/src/public/html/post/post.lisp +++ b/crates/app/src/public/html/post/post.lisp @@ -15,7 +15,7 @@ (text "{%- endif %}") (div ("style" "display: contents;") - (text "{% if post.context.repost and post.context.repost.reposting -%} {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} {%- endif %}")) + (text "{% if post.context.repost and post.context.repost.reposting -%} {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts, poll=poll) }} {%- endif %}")) (text "{% if user and post.context.comments_enabled -%}") (div ("class" "card-nest") @@ -273,13 +273,20 @@ (text "{{ text \"communities:label.replies\" }}"))) (div ("class" "card flex flex-col gap-4") - (text "{% for post in replies %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false) }} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}")))) + (text "{% for post in replies %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, poll=post[4]) }} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}")))) (script (text "async function create_reply_from_form(e) { e.preventDefault(); await trigger(\"atto::debounce\", [\"posts::create\"]); + // poll + const poll_data = get_poll_data(); + + if (!poll_data[0]) { + return alert(poll_data[1]); + } + // create body const body = new FormData(); @@ -295,6 +302,7 @@ content: e.target.content.value, community: \"{{ community.id }}\", replying_to: \"{{ post.id }}\", + poll: poll_data[1], }), ); diff --git a/crates/app/src/public/html/post/quotes.lisp b/crates/app/src/public/html/post/quotes.lisp index 44c4ecc..f128f53 100644 --- a/crates/app/src/public/html/post/quotes.lisp +++ b/crates/app/src/public/html/post/quotes.lisp @@ -64,6 +64,6 @@ (text "{{ text \"communities:label.quotes\" }}"))) (div ("class" "card flex flex-col gap-4") - (text "{% for post in list %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false) }} {% endfor %} {{ components::pagination(page=page, items=list|length, key=\"quotes\", value=\"true\") }}")))) + (text "{% for post in list %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, poll=post[4]) }} {% endfor %} {{ components::pagination(page=page, items=list|length, key=\"quotes\", value=\"true\") }}")))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/media.lisp b/crates/app/src/public/html/profile/media.lisp index a9387cc..7bdbb9d 100644 --- a/crates/app/src/public/html/profile/media.lisp +++ b/crates/app/src/public/html/profile/media.lisp @@ -23,6 +23,6 @@ (text "{%- endif %}")) (div ("class" "card flex flex-col gap-4") - (text "{% for post in posts %} {% 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) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length) }}"))) + (text "{% for post in posts %} {% 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 %} {{ components::pagination(page=page, items=posts|length) }}"))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/posts.lisp b/crates/app/src/public/html/profile/posts.lisp index 83dba21..93d0f0c 100644 --- a/crates/app/src/public/html/profile/posts.lisp +++ b/crates/app/src/public/html/profile/posts.lisp @@ -13,7 +13,7 @@ (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) }} {%- endif %} {%- endif %} {% endfor %}"))) + (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=\"posts\") }}") (div @@ -41,6 +41,6 @@ (text "{%- endif %}")) (div ("class" "card flex flex-col gap-4") - (text "{% for post in posts %} {% 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) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length, key=\"&tag=\", value=tag) }}"))) + (text "{% for post in posts %} {% 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 %} {{ components::pagination(page=page, items=posts|length, key=\"&tag=\", value=tag) }}"))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/replies.lisp b/crates/app/src/public/html/profile/replies.lisp index bb2c9c3..6bc6ba4 100644 --- a/crates/app/src/public/html/profile/replies.lisp +++ b/crates/app/src/public/html/profile/replies.lisp @@ -23,6 +23,6 @@ (text "{%- endif %}")) (div ("class" "card flex flex-col gap-4") - (text "{% for post in posts %} {% 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) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length) }}"))) + (text "{% for post in posts %} {% 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 %} {{ components::pagination(page=page, items=posts|length) }}"))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/stacks/posts.lisp b/crates/app/src/public/html/stacks/posts.lisp index eb86635..38c3dd8 100644 --- a/crates/app/src/public/html/stacks/posts.lisp +++ b/crates/app/src/public/html/stacks/posts.lisp @@ -32,6 +32,6 @@ ("href" "/stacks/{{ stack.id }}/manage#/users") (text "add a user to this stack")) (text "!")) - (text "{%- endif %} {% 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]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")))) + (text "{%- endif %} {% 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 %} {{ components::pagination(page=page, items=list|length) }}")))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/all.lisp b/crates/app/src/public/html/timelines/all.lisp index c9878a8..fb36623 100644 --- a/crates/app/src/public/html/timelines/all.lisp +++ b/crates/app/src/public/html/timelines/all.lisp @@ -30,6 +30,6 @@ (text "{%- endif %}") (div ("class" "card w-full flex flex-col gap-2") - (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[3]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + (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 %} {{ components::pagination(page=page, items=list|length) }}"))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/following.lisp b/crates/app/src/public/html/timelines/following.lisp index 852dca9..abc41d1 100644 --- a/crates/app/src/public/html/timelines/following.lisp +++ b/crates/app/src/public/html/timelines/following.lisp @@ -8,6 +8,6 @@ (text "{{ macros::timelines_nav(selected=\"following\") }} {{ macros::timelines_secondary_nav(posts=\"/following\", questions=\"/following/questions\") }}") (div ("class" "card w-full flex flex-col gap-2") - (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]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + (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 %} {{ components::pagination(page=page, items=list|length) }}"))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/home.lisp b/crates/app/src/public/html/timelines/home.lisp index ed7a66a..09bdef3 100644 --- a/crates/app/src/public/html/timelines/home.lisp +++ b/crates/app/src/public/html/timelines/home.lisp @@ -27,7 +27,7 @@ (text "{% else %}") (div ("class" "card w-full flex flex-col gap-2") - (text "{% for post in list %} {% 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]) }} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")) + (text "{% for post in list %} {% 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 %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")) (text "{%- endif %}")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/popular.lisp b/crates/app/src/public/html/timelines/popular.lisp index 10eda3c..24a8c98 100644 --- a/crates/app/src/public/html/timelines/popular.lisp +++ b/crates/app/src/public/html/timelines/popular.lisp @@ -8,6 +8,6 @@ (text "{{ macros::timelines_nav(selected=\"popular\") }} {{ macros::timelines_secondary_nav(posts=\"/popular\", questions=\"/popular/questions\") }}") (div ("class" "card w-full flex flex-col gap-2") - (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]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + (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 %} {{ components::pagination(page=page, items=list|length) }}"))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/search.lisp b/crates/app/src/public/html/timelines/search.lisp index 649e179..dc6c42d 100644 --- a/crates/app/src/public/html/timelines/search.lisp +++ b/crates/app/src/public/html/timelines/search.lisp @@ -56,6 +56,6 @@ (text "{{ icon \"circle-help\" }}")) (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]) }} {%- 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 "{% 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 %}") diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js index d72a9d3..8634c00 100644 --- a/crates/app/src/public/js/me.js +++ b/crates/app/src/public/js/me.js @@ -104,6 +104,35 @@ }); }); + self.define("vote", async (_, id, option) => { + if ( + !(await trigger("atto::confirm", [ + "Are you sure you want to do this?", + ])) + ) { + return; + } + + fetch(`/api/v1/posts/${id}/poll_vote`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + option, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger("atto::toast", [ + res.ok ? "success" : "error", + res.message, + ]); + + window.location.href = `/post/${id}`; + }); + }); + self.define("react", async (_, element, asset, asset_type, is_like) => { await trigger("atto::debounce", ["reactions::toggle"]); fetch("/api/v1/reactions", { diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index e78a90c..fb6e457 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -717,6 +717,12 @@ pub async fn post_request( Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }; + // check poll + let poll = match data.0.get_post_poll(&post, &user).await { + Ok(q) => q, + Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), + }; + // check permissions let (can_read, can_manage_pins) = check_permissions!(community, jar, data, user); @@ -755,6 +761,7 @@ pub async fn post_request( context.insert("post", &post); context.insert("reposting", &reposting); context.insert("question", &question); + context.insert("poll", &poll); context.insert("replies", &feed); context.insert("page", &props.page); context.insert( diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c8636c9..702f1fe 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-core" -version = "4.5.0" +version = "5.0.0" edition = "2024" [features] diff --git a/crates/core/src/database/polls.rs b/crates/core/src/database/polls.rs index 231f9ac..4abc613 100644 --- a/crates/core/src/database/polls.rs +++ b/crates/core/src/database/polls.rs @@ -21,7 +21,7 @@ impl DataManager { id: get!(x->0(i64)) as usize, owner: get!(x->1(i64)) as usize, created: get!(x->2(i64)) as usize, - expires: get!(x->3(i64)) as usize, + expires: get!(x->3(i32)) as usize, option_a: get!(x->4(String)), option_b: get!(x->5(String)), option_c: get!(x->6(String)), @@ -74,15 +74,15 @@ impl DataManager { &(data.id as i64), &(data.owner as i64), &(data.created as i64), - &(data.expires as i64), + &(data.expires as i32), &data.option_a, &data.option_b, &data.option_c, &data.option_d, - &(data.votes_a as i64), - &(data.votes_b as i64), - &(data.votes_c as i64), - &(data.votes_d as i64), + &(data.votes_a as i32), + &(data.votes_b as i32), + &(data.votes_c as i32), + &(data.votes_d as i32), ] ); @@ -120,6 +120,18 @@ impl DataManager { self.2.remove(format!("atto.poll:{}", id)).await; + // remove votes + let res = execute!( + &conn, + "DELETE FROM pollvotes WHERE poll_id = $1", + &[&(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // ... Ok(()) } @@ -127,15 +139,15 @@ impl DataManager { self.2.remove(format!("atto.poll:{}", poll.id)).await; } - auto_method!(incr_votes_a_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_a + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); - auto_method!(decr_votes_a_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_a - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_a); + auto_method!(incr_votes_a_count()@get_poll_by_id -> "UPDATE polls SET votes_a = votes_a + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); + auto_method!(decr_votes_a_count()@get_poll_by_id -> "UPDATE polls SET votes_a = votes_a - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_a); - auto_method!(incr_votes_b_count()@get_poll_by_id -> "UPDATE users SET votes_b = votes_b + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); - auto_method!(decr_votes_b_count()@get_poll_by_id -> "UPDATE users SET votes_b = votes_b - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_b); + auto_method!(incr_votes_b_count()@get_poll_by_id -> "UPDATE polls SET votes_b = votes_b + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); + auto_method!(decr_votes_b_count()@get_poll_by_id -> "UPDATE polls SET votes_b = votes_b - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_b); - auto_method!(incr_votes_c_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); - auto_method!(decr_votes_c_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_c); + auto_method!(incr_votes_c_count()@get_poll_by_id -> "UPDATE polls SET votes_c = votes_d + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); + auto_method!(decr_votes_c_count()@get_poll_by_id -> "UPDATE polls SET votes_c = votes_d - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_c); - auto_method!(incr_votes_d_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); - auto_method!(decr_votes_d_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_d); + auto_method!(incr_votes_d_count()@get_poll_by_id -> "UPDATE polls SET votes_d = votes_d + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr); + auto_method!(decr_votes_d_count()@get_poll_by_id -> "UPDATE polls SET votes_d = votes_d - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_d); } diff --git a/crates/core/src/database/pollvotes.rs b/crates/core/src/database/pollvotes.rs index 73515c4..456d0f2 100644 --- a/crates/core/src/database/pollvotes.rs +++ b/crates/core/src/database/pollvotes.rs @@ -1,6 +1,6 @@ use super::*; use crate::cache::Cache; -use crate::model::communities::PollVote; +use crate::model::communities::{PollOption, PollVote}; use crate::model::moderation::AuditLogEntry; use crate::model::{Error, Result, auth::User, permissions::FinePermission}; use crate::{auto_method, execute, get, query_row, params}; @@ -45,7 +45,7 @@ impl DataManager { let res = query_row!( &conn, - "SELECT * FROM pollvotes WHERE id = $1 AND poll_id = $2", + "SELECT * FROM pollvotes WHERE owner = $1 AND poll_id = $2", &[&(id as i64), &(poll_id as i64)], |x| { Ok(Self::get_pollvote_from_row(x)) } ); @@ -95,7 +95,7 @@ impl DataManager { &(data.owner as i64), &(data.created as i64), &(data.poll_id as i64), - &(vote_u8 as i64), + &(vote_u8 as i32), ] ); @@ -104,10 +104,20 @@ impl DataManager { } // update poll - self.incr_votes_a_count(poll.id).await?; - self.incr_votes_b_count(poll.id).await?; - self.incr_votes_c_count(poll.id).await?; - self.incr_votes_d_count(poll.id).await?; + match data.vote { + PollOption::A => { + self.incr_votes_a_count(poll.id).await?; + } + PollOption::B => { + self.incr_votes_b_count(poll.id).await?; + } + PollOption::C => { + self.incr_votes_c_count(poll.id).await?; + } + PollOption::D => { + self.incr_votes_d_count(poll.id).await?; + } + }; // ... Ok(data.id) diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 5062cf8..6381ca0 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -245,7 +245,7 @@ impl DataManager { let user = if let Some(ua) = user { ua } else { - return Err(Error::MiscError("Could not get user for pull".to_string())); + return Ok(None); }; if post.poll_id != 0 { @@ -259,7 +259,7 @@ impl DataManager { Err(_) => return Err(Error::MiscError("Invalid poll ID attached".to_string())), })) } else { - return Err(Error::MiscError("Invalid poll ID attached".to_string())); + Ok(None) } } @@ -374,15 +374,10 @@ impl DataManager { Community, Option<(User, Post)>, Option<(Question, User)>, + Option<(Poll, bool)>, )>, > { - let mut out: Vec<( - Post, - User, - Community, - Option<(User, Post)>, - Option<(Question, User)>, - )> = Vec::new(); + let mut out = Vec::new(); let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new(); let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new(); @@ -403,6 +398,7 @@ impl DataManager { community.to_owned(), self.get_post_reposting(&post, ignore_users, user).await, self.get_post_question(&post, ignore_users).await?, + self.get_post_poll(&post, user).await?, )); } else { let ua = self.get_user_by_id(owner).await?; @@ -450,6 +446,7 @@ impl DataManager { community, self.get_post_reposting(&post, ignore_users, user).await, self.get_post_question(&post, ignore_users).await?, + self.get_post_poll(&post, user).await?, )); } } @@ -1423,7 +1420,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, null, $13)", + "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, DEFAULT, $13)", params![ &(data.id as i64), &(data.created as i64), @@ -1541,6 +1538,11 @@ impl DataManager { self.delete_upload(upload).await?; } + // remove poll + if y.poll_id != 0 { + self.delete_poll(y.poll_id, user).await?; + } + // return Ok(()) } diff --git a/crates/core/src/database/stacks.rs b/crates/core/src/database/stacks.rs index c214f38..39b08fc 100644 --- a/crates/core/src/database/stacks.rs +++ b/crates/core/src/database/stacks.rs @@ -1,6 +1,6 @@ use super::*; use crate::cache::Cache; -use crate::model::communities::{Community, Post, Question}; +use crate::model::communities::{Community, Poll, Post, Question}; use crate::model::stacks::{StackMode, StackSort}; use crate::model::{ Error, Result, @@ -51,6 +51,7 @@ impl DataManager { Community, Option<(User, Post)>, Option<(Question, User)>, + Option<(Poll, bool)>, )>, > { let stack = self.get_stack_by_id(id).await?; diff --git a/crates/core/src/model/communities.rs b/crates/core/src/model/communities.rs index 9712e5a..df72d9d 100644 --- a/crates/core/src/model/communities.rs +++ b/crates/core/src/model/communities.rs @@ -425,7 +425,7 @@ impl Poll { /// Poll option (selectors) are stored in the database as numbers 0 to 3. /// /// This enum allows us to convert from these numbers into letters. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum PollOption { A, B, diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index 98312a8..b882fa4 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-l10n" -version = "4.5.0" +version = "5.0.0" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index c161003..0db8015 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-shared" -version = "4.5.0" +version = "5.0.0" edition = "2024" authors.workspace = true repository.workspace = true