(text "{% macro avatar(username, size=\"24px\", selector_type=\"username\") -%}") (img ("title" "{{ username }}'s avatar") ("src" "/api/v1/auth/user/{{ username }}/avatar?selector_type={{ selector_type }}") ("alt" "@{{ username }}") ("class" "avatar shadow") ("loading" "lazy") ("style" "--size: {{ size }}")) (text "{%- endmacro %} {% macro community_avatar(id, community=false, size=\"24px\") -%} {% if community -%}") (img ("src" "/api/v1/communities/{{ id }}/avatar") ("alt" "{{ community.title }}'s avatar") ("class" "avatar shadow") ("loading" "lazy") ("style" "--size: {{ size }}")) (text "{% else %}") (img ("src" "/api/v1/communities/{{ id }}/avatar") ("alt" "{{ id }}'s avatar") ("class" "avatar shadow") ("loading" "lazy") ("style" "--size: {{ size }}")) (text "{%- endif %} {%- endmacro %} {% macro banner(username, border_radius=\"var(--radius)\") -%}") (img ("title" "{{ username }}'s banner") ("src" "/api/v1/auth/user/{{ username }}/banner") ("alt" "@{{ username }}'s banner") ("class" "banner shadow w-full") ("loading" "lazy") ("style" "border-radius: {{ border_radius }};")) (text "{%- endmacro %} {% macro community_banner(id, community=false) -%} {% if community %}") (img ("src" "/api/v1/communities/{{ id }}/banner") ("alt" "{{ community.title }}'s banner") ("class" "banner shadow") ("loading" "lazy")) (text "{% else %}") (img ("src" "/api/v1/communities/{{ id }}/banner") ("alt" "{{ id }}'s banner") ("class" "banner shadow") ("loading" "lazy")) (text "{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%}") (a ("class" "card secondary w-full flex items-center gap-4") ("href" "{% if community.is_forge -%}/forge/{{ community.title }}{% else %}/community/{{ community.title }}{%- endif %}") (text "{{ self::community_avatar(id=community.id, community=community, size=\"48px\") }}") (div ("class" "flex flex-col") (div ("class" "flex gap-2 items-center") (text "{% if community.is_forge -%}") (icon (text "anvil")) (text "{%- endif %}") (h3 ("class" "name lg:long") (text "{{ community.context.display_name }}"))) (span ("class" "fade") (b (text "{{ community.member_count }} ")) (text "members")))) (text "{%- endmacro %} {% macro username(user) -%}") (div ("style" "display: contents") (text "{% if user.settings.display_name -%} {{ user.settings.display_name }} {% else %} {{ user.username }} {%- endif %}")) (text "{%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0, secondary=false, disable_dislikes=false) -%}") (button ("title" "Like") ("class" "{% if secondary -%}lowered{% else %}camo{%- endif %} small") ("hook_element" "reaction.like") ("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', true])") (text "{{ icon \"heart\" }} {% if likes > 0 -%}") (span (text "{{ likes }}")) (text "{%- endif %}")) (text "{% if not user or not user.settings.hide_dislikes and not disable_dislikes -%}") (button ("title" "Dislike") ("class" "{% if secondary -%}lowered{% else %}camo{%- endif %} small") ("hook_element" "reaction.dislike") ("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', false])") (text "{{ icon \"heart-crack\" }} {% if dislikes > 0 -%}") (span (text "{{ dislikes }}")) (text "{%- endif %}")) (text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%}") (div ("class" "flex items-center") (a ("href" "/@{{ user.username }}") ("class" "flush") ("style" "font-weight: 600") ("target" "_top") (text "{{ self::username(user=user) }}")) (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}") (span ("title" "Verified") ("style" "color: var(--color-primary)") ("class" "flex items-center") (text "{{ icon \"badge-check\" }}")) (text "{%- endif %}")) (text "{%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}") (div ("style" "display: contents") (text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}")) (text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false, poll=false, dont_show_title=false) -%} {% if community and show_community and community.id != config.town_square or question %}") (div ("class" "card-nest post_outer:{{ post.id }}") (text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}") (div ("class" "card small") (a ("href" "/api/v1/communities/find/{{ post.community }}") ("class" "flush flex gap-1 items-center") (text "{{ self::community_avatar(id=post.community, community=community) }}") (b (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) (text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- endif %}"))) (text "{%- endif %} {%- endif %}") (div ("class" "card flex flex-col gap-2 post:{{ post.id }} {% if secondary -%}secondary{%- endif %}") ("data-community" "{{ post.community }}") ("data-ownsup" "{{ owner.permissions|has_supporter }}") ("hook" "verify_emojis") (div ("class" "w-full flex gap-2") (text "{% if not expect_repost -%}") (a ("href" "/@{{ owner.username }}") (text "{{ self::avatar(username=owner.username, size=\"52px\", selector_type=\"username\") }}")) (text "{%- endif %}") (div ("class" "flex flex-col w-full gap-1 post_right {% if expect_repost -%}repost{%- endif %}") (div ("class" "flex flex-wrap gap-2 items-center") (text "{% if expect_repost -%}") (a ("href" "/@{{ owner.username }}") (text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}")) (text "{%- endif %}") (span ("class" "name") (text "{{ self::full_username(user=owner) }}")) (text "{% if post.context.edited != 0 -%}") (div ("class" "flex") (span ("class" "fade date") (text "{{ post.context.edited }}")) (sup ("title" "Edited") (text "*"))) (text "{% else %}") (span ("class" "fade date") (text "{{ post.created }}")) (text "{%- endif %} {% if post.context.is_nsfw -%}") (span ("title" "NSFW post") ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"square-asterisk\" }}")) (text "{%- endif %} {% if post.stack -%}") (a ("title" "Posted to a stack you're in") ("class" "flex items-center flush") ("style" "color: var(--color-primary)") ("href" "/stacks/{{ post.stack }}") (text "{{ icon \"layers\" }}")) (text "{%- endif %} {% if community and community.is_forge -%} {% if post.is_open -%}") (span ("title" "Open") ("class" "flex items-center green") (text "{{ icon \"circle-dot\" }}")) (text "{% else %}") (span ("title" "Closed") ("class" "flex items-center purple") (text "{{ icon \"circle-check\" }}")) (text "{%- endif %} {%- endif %}") (text "{% if post.context.repost and post.context.repost.reposting %}") (span ("title" "Repost") ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"repeat-2\" }}")) (text "{%- endif %} {% if post.community == config.town_square -%}") (span ("title" "Posted to profile") ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"user-round\" }}")) (text "{%- endif %} {% if post.is_deleted -%}") (span ("title" "Deleted") ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"trash-2\" }}")) (text "{%- endif %}")) (text "{% if not dont_show_title and post.title and community and community.context.enable_titles -%}") ; post has a title AND whatever is rendering this component wants to see it (a ("class" "flush") ("href" "/post/{{ post.id }}") (h2 ("id" "post-content:{{ post.id }}") ("class" "no_p_margin post_content") ("hook" "long") (text "{{ post.title }}")) (button ("class" "small lowered") (icon (text "ellipsis")))) (text "{% else %}") (text "{% if not post.context.content_warning -%}") (span ("id" "post-content:{{ post.id }}") ("class" "no_p_margin post_content") ("hook" "long") ; title (text "{% if post.title and community and community.context.enable_titles -%}") (h2 (text "{{ post.title }}")) (hr ("class" "margin") ("style" "margin-top: var(--pad-2)")) (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 %}") (div ("class" "card lowered red flex items-center gap-2") (text "{{ icon \"frown\" }}") (span (str (text "general:label.could_not_find_post")))) (text "{%- endif %} {%- endif %}")) (text "{{ self::post_media(upload_ids=post.uploads) }} {% else %}") (details ("class" "card tiny lowered w-full") (summary ("class" "red w-full") (b (text "{{ post.context.content_warning }}"))) (div ("class" "flex flex-col gap-2") (span ("id" "post-content:{{ post.id }}") ("class" "no_p_margin post_content") ("hook" "long") ; title (text "{% if post.title and community and community.settings.enable_titles %}") (h2 (text "{{ post.title }}")) (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 %}") (div ("class" "card lowered red flex items-center gap-2") (text "{{ icon \"frown\" }}") (span (text "Could not find original post..."))) (text "{%- endif %} {%- endif %}")) (text "{{ self::post_media(upload_ids=post.uploads) }}"))) (text "{%- endif %} {%- 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 %}") (a ("href" "/@{{ owner.username }}?tag={{ tag }}") ("class" "flush fade") (text "#{{ tag }}")) (text "{% endfor %}")))) (div ("class" "flex justify-between items-center gap-2 w-full") (text "{% if user -%}") (div ("class" "flex gap-1 reactions_box") ("hook" "check_reactions") ("hook-arg:id" "{{ post.id }}") (text "{% if post.context.reactions_enabled -%} {% if post.content|length > 0 or post.uploads|length > 0 -%} {{ self::likes(id=post.id, asset_type=\"Post\", likes=post.likes, dislikes=post.dislikes, disable_dislikes=owner.settings.hide_dislikes) }} {%- endif %} {%- endif %} {% if post.context.repost and post.context.repost.reposting -%}") (a ("href" "/post/{{ post.context.repost.reposting }}") ("class" "button small camo") (text "{{ icon \"expand\" }}")) (text "{%- endif %}")) (text "{% else %}") (div) (text "{%- endif %}") (div ("class" "flex gap-1 buttons_box") (text "{% if post.context.comments_enabled %}") (a ("href" "/post/{{ post.id }}") ("class" "button camo small") (text "{{ icon \"message-circle\" }}") (span (text "{{ post.comment_count }}"))) (text "{% endif %}") (a ("href" "/post/{{ post.id }}") ("class" "button camo small") ("target" "_blank") (text "{{ icon \"external-link\" }}")) (text "{% if user -%}") (div ("class" "dropdown") (button ("class" "camo small") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (text "{% if config.town_square and post.context.reposts_enabled %}") (b ("class" "title") (text "{{ text \"general:label.share\" }}")) (button ("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}', true])") (text "{{ icon \"repeat-2\" }}") (span (text "{{ text \"communities:label.repost\" }}"))) (a ("class" "button") ("href" "/communities/intents/post?quote={{ post.id }}") (text "{{ icon \"quote\" }}") (span (text "{{ text \"communities:label.quote_post\" }}"))) (text "{%- endif %}") (text "{% if community and community.is_forge -%} {% if post.is_open -%}") (button ("class" "green") ("onclick" "trigger('me::update_open', ['{{ post.id }}', false])") (text "{{ icon \"circle-check\" }}") (span (text "{{ text \"forge:action.close\" }}"))) (text "{% else %}") (button ("class" "purple") ("onclick" "trigger('me::update_open', ['{{ post.id }}', true])") (text "{{ icon \"refresh-ccw-dot\" }}") (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\" }}")) (text "{% if user.id == post.owner -%}") (a ("href" "/post/{{ post.id }}#/edit") (text "{{ icon \"pen\" }}") (span (text "{{ text \"communities:label.edit_content\" }}"))) (text "{%- endif %}") (a ("href" "/post/{{ post.id }}#/configure") (text "{{ icon \"settings\" }}") (span (text "{{ text \"communities:action.configure\" }}"))) (text "{% if not post.is_deleted -%}") (button ("class" "red") ("onclick" "trigger('me::remove_post', ['{{ post.id }}'])") (text "{{ icon \"trash\" }}") (span (text "{{ text \"general:action.delete\" }}"))) (text "{%- endif %} {% if is_helper and post.is_deleted -%}") (button ("class" "red") ("onclick" "trigger('me::purge_post', ['{{ post.id }}'])") (text "{{ icon \"trash-2\" }}") (span (text "{{ text \"general:action.purge\" }}"))) (button ("class" "green") ("onclick" "trigger('me::restore_post', ['{{ post.id }}'])") (text "{{ icon \"undo\" }}") (span (text "{{ text \"general:action.restore\" }}"))) (text "{%- endif %} {%- endif %}"))) (text "{%- endif %}")))) (text "{% if community and show_community and community.id != config.town_square or question %}")) (text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0%}") (div ("class" "media_gallery gap-2") (text "{% for upload in upload_ids %}") (img ("src" "/api/v1/uploads/{{ upload }}") ("alt" "Image upload") ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])")) (text "{% endfor %}")) (text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}") (div ("class" "w-full card-nest") (div ("class" "card small notif_title flex items-center") (text "{% if not notification.read -%}") (svg ("width" "24") ("height" "24") ("viewBox" "0 0 24 24") ("style" "fill: var(--color-link)") (circle ("cx" "12") ("cy" "12") ("r" "6"))) (text "{%- endif %}") (b ("class" "no_p_margin") (text "{{ notification.title|markdown|safe }}"))) (div ("class" "card notif_content flex flex-col gap-2") (span ("class" "no_p_margin") (text "{{ notification.content|markdown|safe }}")) (div ("class" "card secondary w-full flex flex-wrap gap-2") (text "{% if notification.read -%}") (button ("class" "raised") ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', false])") (text "{{ icon \"undo\" }}") (span (text "{{ text \"notifs:action.mark_as_unread\" }}"))) (text "{% else %}") (button ("class" "green raised") ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', true])") (text "{{ icon \"check\" }}") (span (text "{{ text \"notifs:action.mark_as_read\" }}"))) (text "{%- endif %}") (button ("class" "red raised") ("onclick" "trigger('me::remove_notification', ['{{ notification.id }}'])") (text "{{ icon \"trash\" }}") (span (text "{{ text \"general:action.delete\" }}")))))) (text "{%- endmacro %} {% macro user_card(user) -%}") (a ("class" "card-nest w-full") ("href" "/@{{ user.username }}") (div ("class" "card small") ("style" "padding: 0") (text "{{ self::banner(username=user.username, border_radius=\"0px\") }}")) (div ("class" "card secondary flex items-center gap-4") (text "{{ self::avatar(username=user.username, size=\"48px\") }}") (div ("class" "flex items-center") (b (text "{{ self::username(user=user) }}")) (text "{{ self::online_indicator(user=user) }}")))) (text "{%- endmacro %} {% macro pagination(page=0, items=0, key=\"\", value=\"\") -%}") (div ("class" "flex justify-between gap-2 w-full") (text "{% if page > 0 -%}") (a ("class" "button lowered") ("href" "?page={{ page - 1 }}{{ key }}{{ value }}") (text "{{ icon \"arrow-left\" }}") (span (text "{{ text \"general:link.previous\" }}"))) (text "{% else %}") (div) (text "{%- endif %} {% if items != 0 -%}") (a ("class" "button lowered") ("href" "?page={{ page + 1 }}{{ key }}{{ value }}") (span (text "{{ text \"general:link.next\" }}")) (text "{{ icon \"arrow-right\" }}")) (text "{%- endif %}")) (text "{%- endmacro %} {% macro online_indicator(user) -%} {% if not user.settings.private_last_seen or is_helper %}") (div ("class" "online_indicator") ("style" "display: contents") ("hook" "online_indicator") ("hook-arg:last_seen" "{{ user.last_seen }}") (div ("style" "display: none") ("hook_ui_ident" "online") ("title" "Online") (svg ("width" "24") ("height" "24") ("viewBox" "0 0 24 24") ("style" "fill: var(--color-green)") (circle ("cx" "12") ("cy" "12") ("r" "6")))) (div ("style" "display: none") ("hook_ui_ident" "idle") ("title" "Idle") (svg ("width" "24") ("height" "24") ("viewBox" "0 0 24 24") ("style" "fill: var(--color-yellow)") (circle ("cx" "12") ("cy" "12") ("r" "6")))) (div ("style" "display: none") ("hook_ui_ident" "offline") ("title" "Offline") (svg ("width" "24") ("height" "24") ("viewBox" "0 0 24 24") ("style" "fill: hsl(0, 0%, 50%)") (circle ("cx" "12") ("cy" "12") ("r" "6"))))) (text "{% else %}") (div ("title" "Offline") ("style" "display: contents") (svg ("width" "24") ("height" "24") ("viewBox" "0 0 24 24") ("style" "fill: hsl(0, 0%, 50%)") (circle ("cx" "12") ("cy" "12") ("r" "6")))) (text "{%- endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}") (style (text ":root, * { --hue: {{ user.settings.theme_hue }} !important; }")) (text "{%- endif %} {% if user.settings.theme_sat -%}") (style (text ":root, * { --sat: {{ user.settings.theme_sat }} !important; }")) (text "{%- endif %} {% if user.settings.theme_lit -%}") (style (text ":root, * { --lit: {{ user.settings.theme_lit }} !important; }")) (text "{%- endif %} {% if theme_preference -%}") (script (text "function match_user_theme() { const pref = \"{{ theme_preference }}\".toLowerCase(); if (pref === \"auto\") { return; } document.documentElement.className = pref; } setTimeout(() => { match_user_theme(); }, 150);")) (text "{%- endif %}") (div ("style" "display: none;") (text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }} {% if user.permissions|has_supporter -%}") (style (text "{{ user.settings.theme_custom_css|remove_script_tags|safe }}")) (text "{%- endif %}")) (text "{%- endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color -%}") (style (text ":root, * { --{{ css }}: {{ color|color }} !important; }")) (text "{%- endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false, profile=false) -%}") (div ("class" "card {% if secondary -%}secondary{%- endif %} flex gap-2") (text "{% if owner.id == 0 -%}") (span (text "{% if profile and profile.settings.anonymous_avatar_url -%}") (img ("src" "/api/v1/util/proxy?url={{ profile.settings.anonymous_avatar_url }}") ("alt" "anonymous' avatar") ("class" "avatar shadow") ("loading" "lazy") ("style" "--size: 52px")) (text "{% else %} {{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }} {%- endif %}")) (text "{% else %}") (a ("href" "/@{{ owner.username }}") (text "{{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }}")) (text "{%- endif %}") (div ("class" "flex flex-col gap-1 w-full") (div ("class" "flex items-center gap-2 flex-wrap") (span ("class" "name") (text "{% if owner.id == 0 -%} {% if profile and profile.settings.anonymous_username -%}") (span ("class" "flex items-center gap-2") (b (text "{{ profile.settings.anonymous_username }}")) (span ("title" "Anonymous user") ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"drama\" }}"))) (text "{% else %}") (b (text "anonymous")) (text "{%- endif %} {% else %} {{ self::full_username(user=owner) }} {%- endif %}")) (span ("class" "date") (text "{{ question.created }}")) (span ("title" "Question") ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"message-circle-heart\" }}")) (text "{% if question.context.is_nsfw -%}") (span ("title" "NSFW community") ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"square-asterisk\" }}")) (text "{%- endif %} {% if question.community > 0 and show_community -%}") (a ("href" "/api/v1/communities/find/{{ question.community }}") ("class" "flex items-center") (text "{{ self::community_avatar(id=question.community, size=\"24px\") }}")) (text "{%- endif %} {% if question.is_global -%}") (a ("class" "notification chip") ("href" "/question/{{ question.id }}") (text "{{ question.answer_count }} answers")) (text "{%- endif %}")) (span ("class" "no_p_margin") ("style" "font-weight: 500") (text "{{ question.content|markdown|safe }}")) ; anonymous user ip thing ; this is only shown if the post author is anonymous AND we are a helper (text "{% if is_helper and owner.id == 0 %}") (details ("class" "card tiny lowered w-full") (summary ("class" "w-full flex gap-2 flex-wrap items-center") (icon (text "shield")) (span (text "View IP"))) (pre (code (text "{{ question.ip }}")))) (text "{% endif %}") ; ... (div ("class" "flex gap-2 items-center justify-between")))) (text "{%- endmacro %} {% macro create_question_form(receiver=\"0\", community=\"\", header=\"\", is_global=false) -%}") (div ("class" "card-nest") (div ("class" "card small flex items-center gap-2") (text "{{ icon \"message-circle-heart\" }}") (span ("class" "no_p_margin") (text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}"))) (form ("class" "card flex flex-col gap-2") ("onsubmit" "create_question_from_form(event)") (div ("class" "flex flex-col gap-1") (label ("for" "content") (text "{{ text \"communities:label.content\" }}")) (textarea ("type" "text") ("name" "content") ("id" "content") ("placeholder" "content") ("required" "") ("minlength" "2") ("maxlength" "4096"))) (button ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) (script (text "async function create_question_from_form(e) { e.preventDefault(); await trigger(\"atto::debounce\", [\"questions::create\"]); fetch(\"/api/v1/questions\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ content: e.target.content.value, receiver: \"{{ receiver }}\", community: \"{{ community }}\", is_global: \"{{ is_global }}\" == \"true\", }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { e.target.reset(); } }); }")) (text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}") (div ("class" "card-nest") (text "{{ self::question(question=question[0], owner=question[1], show_community=show_community) }}") (div ("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}") (div ("class" "flex gap-1 reactions_box") ("hook" "check_reactions") ("hook-arg:id" "{{ question[0].id }}") (text "{{ self::likes(id=question[0].id, asset_type=\"Question\", likes=question[0].likes, dislikes=question[0].dislikes, secondary=false) }}")) (div ("class" "flex gap-1 buttons_box") (a ("href" "/question/{{ question[0].id }}") ("class" "button small") (text "{{ icon \"external-link\" }} {% if user -%}") (span (text "{{ text \"requests:label.answer\" }}")) (text "{% else %}") (span (text "{{ text \"general:action.open\" }}")) (text "{%- endif %}")) (text "{% if user -%} {% if can_manage_questions or is_helper or question[1].id == user.id %}") (div ("class" "dropdown") (button ("class" "camo small") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (button ("class" "camo small red") ("onclick" "trigger('me::remove_question', ['{{ question[0].id }}'])") (text "{{ icon \"trash\" }}") (span (text "{{ text \"general:action.delete\" }}"))))) (text "{%- endif %} {%- endif %}")))) (text "{%- endmacro %} {% macro spotify_playing(state, size=\"60px\") -%} {% if state and state.data %}") (div ("class" "card-nest") (div ("class" "card flex items-center justify-between gap-2 small") (div ("class" "flex items-center gap-2") (b (text "Listening on")) (text "{{ icon \"spotify\" }}")) (span ("class" "fade date short") (text "{{ state.data.timestamp }}"))) (div ("class" "card secondary flex gap-2") (a ("href" "{{ state.external_urls.album }}") (img ("src" "{{ state.external_urls.album_img }}") ("alt" "Album cover") ("loading" "lazy") ("class" "avatar") ("style" "--size: {{ size }}"))) (div ("class" "flex flex-col") (h5 ("class" "w-full") (a ("href" "{{ state.external_urls.track }}") ("class" "flush") (text "{{ state.data.track }}"))) (span ("class" "fade") (a ("href" "{{ state.external_urls.artist }}") ("class" "flush") (text "{{ state.data.artist }}"))) (span ("hook" "spotify_time_text") ("hook-arg:updated" "{{ state.data.timestamp }}") ("hook-arg:progress" "{{ state.data.progress_ms }}") ("hook-arg:duration" "{{ state.data.duration_ms }}") ("hook-arg:display" "full"))))) (text "{%- endif %} {%- endmacro %} {% macro last_fm_playing(state, size=\"60px\") -%} {% if state and state.data %}") (div ("class" "card-nest") (div ("class" "card flex items-center justify-between gap-2 small") (div ("class" "flex items-center gap-2") (b (text "Listening on")) (text "{{ icon \"last_fm\" }}")) (span ("class" "fade date short") (text "{{ state.data.timestamp }}"))) (div ("class" "card secondary flex gap-2") (a ("href" "{{ state.external_urls.track }}") (img ("src" "{{ state.external_urls.track_img }}") ("alt" "Track cover") ("loading" "lazy") ("class" "avatar") ("style" "--size: {{ size }}"))) (div ("class" "flex flex-col") (h5 ("class" "w-full") (a ("href" "{{ state.external_urls.track }}") ("class" "flush") (text "{{ state.data.track }}"))) (span ("class" "fade") (a ("href" "{{ state.external_urls.artist }}") ("class" "flush") (text "{{ state.data.artist }}"))) (text "{% if state.data.duration_ms and state.data.duration_ms != \"0\" -%}") (span ("hook" "spotify_time_text") ("hook-arg:updated" "{{ state.data.timestamp }}") ("hook-arg:progress" "25000") ("hook-arg:duration" "{{ state.data.duration_ms }}") ("hook-arg:display" "full")) (text "{%- endif %}")))) (text "{%- endif %} {%- endmacro %} {% macro connection_icon(key) -%}") (div ("style" "display: contents;") (text "{% if key == \"Spotify\" -%} {{ icon \"spotify\" }} {% elif key == \"LastFm\" %} {{ icon \"last_fm\" }} {%- endif %}")) (text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %} {% macro message_actions(can_manage_message, owner, message, owner) -%}") (div ("class" "dropdown") (button ("class" "camo small") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (text "{% if can_manage_message or (user and user.id == message.owner) -%}") (button ("class" "red") ("onclick" "delete_message('{{ message.id }}')") (text "{{ icon \"trash\" }}") (span (text "{{ text \"general:action.delete\" }}"))) (text "{%- endif %}") (button ("onclick" "window.location.href = `${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`") (text "{{ icon \"external-link\" }}") (span (text "{{ text \"general:action.open\" }}"))) (button ("onclick" "trigger('atto::copy_text', [`${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`])") (text "{{ icon \"copy\" }}") (span (text "{{ text \"general:action.copy_link\" }}"))) (button ("onclick" "mention_user('{{ owner.username }}')") (text "{{ icon \"at-sign\" }}") (span (text "{{ text \"chats:action.mention_user\" }}"))))) (text "{%- endmacro %} {% macro message(user, message, can_manage_message=false, grouped=false) -%}") (div ("class" "card secondary message flex gap-2 {% if grouped -%}grouped{%- endif %}") ("id" "message-{{ message.id }}") (text "{% if not grouped -%}") (a ("href" "/@{{ user.username }}") ("target" "_top") (text "{{ self::avatar(username=user.username, size=\"42px\") }}")) (text "{%- endif %}") (div ("class" "flex flex-col gap-1 w-full") (text "{% if not grouped -%}") (div ("class" "flex gap-2 w-full justify-between flex-wrap") (div ("class" "flex gap-2") (text "{{ self::full_username(user=user) }} {% if message.edited != message.created %}") (span ("class" "date") (text "{{ message.edited }}") (sup ("title" "Edited") (text "*"))) (text "{% else %}") (span ("class" "date") (text "{{ message.created }}")) (text "{%- endif %}")) (div ("class" "flex gap-2 hidden") (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}"))) (text "{%- endif %}") (div ("class" "flex w-full gap-2 justify-between") (span ("class" "no_p_margin") (text "{{ message.content|markdown|safe }}")) (text "{% if grouped -%}") (div ("class" "hidden") (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")) (text "{%- endif %}")))) (text "{%- endmacro %} {% macro user_menu() -%}") (div ("class" "inner") (b ("class" "title") (text "{{ user.username }}")) (a ("href" "/@{{ user.username }}") (text "{{ icon \"circle-user-round\" }}") (span (text "{{ text \"auth:link.my_profile\" }}"))) (a ("href" "/journals/0/0") (icon (text "notebook")) (str (text "general:link.journals"))) (a ("href" "/settings") (text "{{ icon \"settings\" }}") (span (text "{{ text \"auth:link.settings\" }}"))) (text "{% if is_helper -%}") (b ("class" "title") (text "{{ text \"general:label.mod\" }}")) (a ("href" "/mod_panel/audit_log") (text "{{ icon \"scroll-text\" }}") (span (text "{{ text \"general:link.audit_log\" }}"))) (a ("href" "/mod_panel/reports") (text "{{ icon \"flag\" }}") (span (text "{{ text \"general:link.reports\" }}"))) (a ("href" "/mod_panel/ip_bans") (text "{{ icon \"ban\" }}") (span (text "{{ text \"general:link.ip_bans\" }}"))) (a ("href" "/mod_panel/stats") (text "{{ icon \"chart-line\" }}") (span (text "{{ text \"general:link.stats\" }}"))) (text "{%- endif %}") (b ("class" "title") (text "{{ config.name }}")) (a ("href" "https://trisua.com/t/tetratto") ("class" "button") (icon (text "code")) (str (text "general:link.source_code"))) (a ("href" "/reference/tetratto/index.html") ("class" "button") ("data-turbo" "false") (icon (text "rabbit")) (str (text "general:link.reference"))) (a ("href" "{{ config.policies.terms_of_service }}") ("class" "button") (icon (text "heart-handshake")) (text "Terms of service")) (a ("href" "{{ config.policies.privacy }}") ("class" "button") (icon (text "cookie")) (text "Privacy policy")) (b ("class" "title") (str (text "general:label.account"))) (button ("onclick" "trigger('me::switch_account')") (text "{{ icon \"ellipsis\" }}") (span (text "{{ text \"general:action.switch_account\" }}"))) (button ("class" "red") ("onclick" "trigger('me::logout')") (text "{{ icon \"log-out\" }}") (span (text "{{ text \"auth:action.logout\" }}")))) (text "{%- endmacro %} {% macro user_status(other_user) -%} {% if other_user.settings.status %}") (div ("class" "flex items-center gap-2") (span (text "{{ other_user.settings.status }}")) ; connection icon (text "{% if (other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track) or (other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track) %} {{ icon \"music\" }} {% endif %}")) (text "{% elif other_user.connections.LastFm[0].data.name and other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track %}") (div ("class" "flex items-center gap-2") (text "{{ icon \"music\" }}") (span (b (text "Listening to ")) (text "{{ other_user.connections.LastFm[1].data.artist }}"))) (text "{% elif other_user.connections.Spotify[0].data.name and other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track %}") (div ("class" "flex items-center gap-2") (text "{{ icon \"music\" }}") (span (b (text "Listening to ")) (text "{{ other_user.connections.Spotify[1].data.artist }}"))) (text "{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false) -%}") (div ("class" "flex gap-2 items-center card tiny user_plate {% if secondary -%}secondary{%- endif %}") (a ("href" "/@{{ user.username }}") (text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}")) (div ("class" "flex justify-center flex-col") ("style" "{% if show_menu or show_kick -%}width: 60%{% else %}max-width: 150px{%- endif %}") (text "{{ self::full_username(user=user) }}") (div ("class" "user_status") (text "{{ self::user_status(other_user=user) }}"))) (text "{% if show_menu -%}") (div ("class" "dropdown") (button ("class" "camo small square") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") (text "{{ icon \"settings\" }}")) (text "{{ self::user_menu() }}")) (text "{% elif show_kick %}") (div ("class" "dropdown") ("style" "margin-left: auto") (button ("class" "camo small square") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (button ("class" "red") ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')") (text "{{ icon \"x\" }}") (span (text "{{ text \"chats:action.kick_member\" }}"))))) (text "{%- endif %}")) (text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}") (button ("class" "button small square lowered") ("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()") ("title" "Emojis") ("type" "button") (text "{{ icon \"smile-plus\" }}")) (text "{% if render_dialog -%}") (dialog ("id" "emoji_dialog") (div ("class" "inner flex flex-col gap-2") (script ("type" "module") ("src" "https://unpkg.com/emoji-picker-element@1.22.8/index.js")) (emoji-picker ("style" " --border-radius: var(--radius); --background: var(--color-super-raised); --input-border-radiFus: var(--radius); --input-border-color: var(--color-primary); --indicator-color: var(--color-primary); --emoji-padding: var(--pad-1); box-shadow: 0 0 4px var(--color-shadow); ") ("class" "w-full")) (script (text "setTimeout(async () => { document.querySelector(\"emoji-picker\").customEmoji = await trigger(\"me::emojis\"); const style = document.createElement(\"style\"); style.textContent = `.custom-emoji { border-radius: 4px !important; } .category { font-weight: 600; }`; document .querySelector(\"emoji-picker\") .shadowRoot.appendChild(style); }, 150); document .querySelector(\"emoji-picker\") .addEventListener(\"emoji-click\", async (event) => { if (event.detail.skinTone > 0) { document.getElementById( window.EMOJI_PICKER_TEXT_ID, ).value += event.detail.unicode; document.getElementById(\"emoji_dialog\").close(); return; } if (event.detail.unicode) { document.getElementById( window.EMOJI_PICKER_TEXT_ID, ).value += ` :${await ( await fetch(\"/api/v1/lookup_emoji\", { method: \"POST\", body: event.detail.unicode, }) ).text()}:`; } else { document.getElementById( window.EMOJI_PICKER_TEXT_ID, ).value += ` :${event.detail.emoji.shortcodes[0]}:`; } document.getElementById(\"emoji_dialog\").close(); });")) (div ("class" "flex justify-between") (div) (div ("class" "flex gap-2") (button ("class" "bold red lowered") ("onclick" "document.getElementById('emoji_dialog').close()") ("type" "button") (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))))) (text "{%- endif %} {%- endmacro %} {% macro file_picker(files_list_id) -%}") (button ("class" "button small square lowered") ("onclick" "pick_file()") ("title" "Images") ("type" "button") (text "{{ icon \"image-up\" }}")) (input ("type" "file") ("multiple" "") ("accept" "image/png,image/jpeg,image/avif,image/webp") ("style" "display: none") ("name" "file_picker")) (div ("style" "display: none") ("id" "file_template") (text "{{ icon \"image\" }}") (b ("class" "name shorter") ("style" "overflow-wrap: normal") (text ".file_name"))) (script (text "(() => { const input = document.querySelector(\"input[name=file_picker]\"); const element = document.getElementById(\"{{ files_list_id }}\"); const template = document.getElementById(\"file_template\"); globalThis.pick_file = () => { input.click(); }; globalThis.render_file_picker_files = () => { element.innerHTML = \"\"; let idx = 0; for (const file of input.files) { element.innerHTML += `
${template.innerHTML.replace( \".file_name\", file.name, )}
`; idx += 1; } }; globalThis.remove_file = (idx) => { const files = Array.from(input.files); files.splice(idx - 1, 1); // update files const list = new DataTransfer(); for (item of files) { list.items.add(item); } input.files = list.files; // render render_file_picker_files(); }; input.addEventListener(\"change\", () => { render_file_picker_files(); }); })();")) (text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}") (div ("class" "card w-full supporter_ad") ("ui_ident" "supporter_ad") ("onclick" "window.location.href = '/settings#/account/billing'") (div ("class" "card w-full flex flex-wrap items-center gap-2 justify-between") (text "{% if body -%}") (b (text "{{ body }}")) (text "{% else %}") (b (text "{{ text \"general:label.supporter_motivation\" }}")) (text "{%- endif %}") (a ("href" "/settings#/account/billing") ("class" "button small") (text "{{ icon \"heart\" }}") (span (text "{{ text \"general:action.become_supporter\" }}"))))) (text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}") (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 lowered") ("title" "Add poll") ("onclick" "document.getElementById('poll_options_dialog').showModal()") ("type" "button") (text "{{ icon \"list-todo\" }}")) (button ("class" "small square lowered") ("title" "More options") ("onclick" "document.getElementById('post_options_dialog').showModal()") ("type" "button") (text "{{ icon \"ellipsis\" }}"))) (dialog ("id" "post_options_dialog") (div ("class" "inner flex flex-col gap-2") (div ("id" "post_options") ("class" "flex flex-col gap-2")) (hr) (div ("class" "flex justify-between") (div) (div ("class" "flex gap-2") (button ("class" "bold red lowered") ("onclick" "document.getElementById('post_options_dialog').close()") ("type" "button") (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))) (script (text "setTimeout(() => { window.POST_INITIAL_SETTINGS = { comments_enabled: true, reposts_enabled: true, reactions_enabled: true, is_nsfw: false, content_warning: \"\", tags: [], }; window.BLANK_INITIAL_SETTINGS = JSON.stringify( window.POST_INITIAL_SETTINGS, ); const settings_fields = [ [ [ \"comments_enabled\", \"Allow people to comment on your post\", ], window.POST_INITIAL_SETTINGS.comments_enabled.toString(), \"checkbox\", ], [ [ \"reposts_enabled\", \"Allow people to repost/quote your post\", ], window.POST_INITIAL_SETTINGS.reposts_enabled.toString(), \"checkbox\", ], [ [ \"reactions_enabled\", \"Allow people to like/dislike your post\", ], window.POST_INITIAL_SETTINGS.reactions_enabled.toString(), \"checkbox\", ], [ [\"is_nsfw\", \"Hide from public timelines\"], window.POST_INITIAL_SETTINGS.is_nsfw.toString(), \"checkbox\", ], [ [\"content_warning\", \"Content warning\"], window.POST_INITIAL_SETTINGS.content_warning, \"textarea\", ], [ [\"tags\", \"Tags\"], window.POST_INITIAL_SETTINGS.tags, \"input\", { embed_html: 'Tags should be separated by a comma.', }, ], ]; document.getElementById(\"post_options\").innerHTML = \"\"; trigger(\"ui::generate_settings_ui\", [ document.getElementById(\"post_options\"), settings_fields, window.POST_INITIAL_SETTINGS, { tags: (new_tags) => { window.POST_INITIAL_SETTINGS.tags = new_tags .split(\",\") .map((t) => t.trim()); }, }, ]); }, 250); globalThis.update_settings_maybe = async (id) => { if ( JSON.stringify(window.POST_INITIAL_SETTINGS) !== window.BLANK_INITIAL_SETTINGS ) { await fetch(`/api/v1/posts/${id}/context`, { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ context: window.POST_INITIAL_SETTINGS, }), }); } };")))) ; 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.POLL_EXPIRES = null; 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\"]; } if (POLL_EXPIRES < 0) { return [false, \"Polls cannot time travel\"]; } return [true, { option_a: POLL_OPTION_A, option_b: POLL_OPTION_B, option_c: POLL_OPTION_C, option_d: POLL_OPTION_D, expires: POLL_EXPIRES, }]; }")) (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"))) (div ("class" "card flex flex-col gap-2") (b (text "Expires")) (input ("type" "date") ("onchange" "window.POLL_EXPIRES = event.target.valueAsDate.getTime() - new Date().getTime()")))) (hr) (div ("class" "flex justify-between") (div) (div ("class" "flex gap-2") (button ("class" "bold red lowered") ("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 lowered 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] or poll[2] or user and user.id == poll[0].owner -%}") ; already voted, show results (text "{% if poll[1] %}") (span ("class" "fade") (text "You've already voted!")) (text "{% elif poll[2] %}") (span ("class" "fade") (text "Poll ended!")) (text "{% endif %}") ; 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 raised justify-start w-full poll_option") ("onclick" "trigger('me::vote', ['{{ post.id }}', 'A'])") (icon (text "tally-1")) (text "{{ poll[0].option_a }}")) ; option b (button ("class" "hover_left_bar raised justify-start w-full poll_option") ("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 raised justify-start w-full poll_option") ("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 raised justify-start w-full poll_option") ("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 "{% if not poll[2] -%}") (span ("class" "notification chip") (text "Expires in ") (span ("class" "poll_date") ("data-created" "{{ poll[0].created }}") ("data-expires" "{{ poll[0].expires }}"))) (text "{%- endif %}"))) (text "{%- endmacro %}") (text "{% macro community_info(community) %}") (div ("class" "card-nest flex flex-col") (div ("id" "bio") ("class" "card small no_p_margin") (text "{{ community.context.description|markdown|safe }}")) (div ("class" "card flex flex-col gap-2") (div ("class" "w-full flex justify-between items-center") (span ("class" "notification chip") (text "ID")) (button ("title" "Copy") ("onclick" "trigger('atto::copy_text', ['{{ community.id }}'])") ("class" "camo small") (text "{{ icon \"copy\" }}"))) (div ("class" "w-full flex justify-between items-center") (span ("class" "notification chip") (text "Created ")) (span ("class" "date") (text "{{ community.created }}"))) (div ("class" "w-full flex justify-between items-center") (span ("class" "notification chip") (text "Members")) (a ("href" "/community/{{ community.title }}/members") (text "{{ community.member_count }}"))) (div ("class" "w-full flex justify-between items-center") (span ("class" "notification chip") (text "Posts")) (a ("href" "/community/{{ community.title }}") (text "{{ community.post_count }}"))) (div ("class" "w-full flex justify-between items-center") (span ("class" "notification chip") (text "Score")) (div ("class" "flex gap-2") (b (text "{{ community.likes - community.dislikes }}")) (text "{% if user -%}") (div ("class" "flex gap-1 reactions_box") ("hook" "check_reactions") ("hook-arg:id" "{{ community.id }}") (text "{{ components::likes(id=community.id, asset_type=\"Community\", likes=community.likes, dislikes=community.dislikes) }}")) (text "{%- endif %}"))))) (text "{% endmacro %}") (text "{% macro community_actions(community) -%}") (text "{% if user -%}") (div ("class" "card flex gap-2 flex-wrap") ("id" "join_or_leave") (text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}") (button ("class" "primary") ("onclick" "join_community()") (text "{{ icon \"circle-plus\" }}") (span (text "{{ text \"communities:action.join\" }}"))) (script (text "globalThis.join_community = () => { fetch( \"/api/v1/communities/{{ community.id }}/join\", { method: \"POST\", }, ) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); setTimeout(() => { window.location.reload(); }, 150); }); };")) (text "{% else %}") (button ("class" "lowered red") ("onclick" "cancel_request()") (text "{{ icon \"x\" }}") (span (text "{{ text \"communities:action.cancel_request\" }}"))) (script (text "globalThis.cancel_request = async () => { if ( !(await trigger(\"atto::confirm\", [ \"Are you sure you would like to do this?\", ])) ) { return; } fetch( \"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\", { method: \"DELETE\", }, ) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); setTimeout(() => { window.location.reload(); }, 150); }); };")) (text "{%- endif %} {% else %}") (button ("class" "lowered red") ("onclick" "leave_community()") (text "{{ icon \"circle-minus\" }}") (span (text "{{ text \"communities:action.leave\" }}"))) (a ("href" "/chats/{{ community.id }}/0") ("class" "button lowered") (text "{{ icon \"message-circle\" }}") (span (text "{{ text \"communities:label.chats\" }}"))) (text "{% if user and can_post -%}") (a ("href" "/communities/intents/post?community={{ community.id }}") ("class" "button lowered") ("data-turbo" "false") (text "{{ icon \"plus\" }}") (span (text "{{ text \"general:action.post\" }}"))) (text "{%- endif %}") (script (text "globalThis.leave_community = async () => { if ( !(await trigger(\"atto::confirm\", [ \"Are you sure you would like to do this?\", ])) ) { return; } fetch( \"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\", { method: \"DELETE\", }, ) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); setTimeout(() => { window.location.reload(); }, 150); }); };")) (text "{%- endif %} {% else %}") (a ("href" "/chats/{{ community.id }}/0") ("class" "button lowered") (text "{{ icon \"message-circle\" }}") (span (text "{{ text \"communities:label.chats\" }}"))) (a ("href" "/communities/intents/post?community={{ community.id }}") ("class" "button lowered") ("data-turbo" "false") (text "{{ icon \"plus\" }}") (span (text "{{ text \"general:action.post\" }}"))) (text "{%- endif %} {% if can_manage_community or is_manager -%}") (a ("href" "/community/{{ community.id }}/manage") ("class" "button primary") (text "{{ icon \"settings\" }}") (span (text "{{ text \"communities:action.configure\" }}"))) (text "{%- endif %}")) (text "{%- endif %}") (text "{%- endmacro %}") (text "{% macro ticket(post, owner) -%}") (div ("href" "/post/{{ post.id }}") ("class" "card secondary w-fill flex flex-col gap-2") (div ("class" "flex gap-2 items-center") ; user info (a ("href" "/@{{ owner.username }}") (text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}")) (span ("class" "name") (text "{{ self::full_username(user=owner) }}")) ; timestamp (span ("class" "date") (text "{{ post.created }}")) ; pinned (text "{% if post.context.is_pinned -%}") (icon (text "pin")) (text "{%- endif %}")) ; post title (a ("href" "/post/{{ post.id }}") ("class" "flush flex gap-2 items-center") ; open/closed icon (text "{% if community and community.is_forge -%} {% if post.is_open -%}") (span ("title" "Open") ("class" "flex items-center green") (text "{{ icon \"circle-dot\" }}")) (text "{% else %}") (span ("title" "Closed") ("class" "flex items-center purple") (text "{{ icon \"circle-check\" }}")) (text "{%- endif %} {%- endif %}") (h4 ("class" "no_p_margin") (text "{{ post.title|markdown|safe }}")))) (text "{%- endmacro %}") (text "{% macro stack_listing(stack) -%}") (a ("href" "/stacks/{{ stack.id }}") ("class" "card secondary flex flex-col gap-2") (div ("class" "flex items-center gap-2") (text "{{ icon \"list\" }}") (b (text "{{ stack.name }}"))) (span (text "Created ") (span ("class" "date") (text "{{ stack.created }}")) (text "; {{ stack.privacy }}; {{ stack.users|length }} users"))) (text "{%- endmacro %}") (text "{% macro journal_listing(journal, notes, selected_note, selected_journal, view_mode=false, owner=false) -%}") (text "{% if selected_journal != journal.id -%}") ; not selected (div ("class" "flex flex-row gap-1") (a ("href" "/journals/{{ journal.id }}/0?view={{ view_mode }}") ("class" "button justify-start lowered w-full") (icon (text "notebook")) (text "{{ journal.title }}")) (div ("class" "dropdown") (button ("class" "big_icon lowered") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "width: 32px") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (button ("onclick" "delete_journal('{{ journal.id }}')") ("class" "red") (text "{{ icon \"trash\" }}") (span (text "{{ text \"general:action.delete\" }}")))))) (text "{% else %}") ; selected (div ("class" "flex flex-row gap-1") (button ("class" "justify-start lowered w-full") (icon (text "arrow-down")) (text "{{ journal.title }}")) (text "{% if user and user.id == journal.owner -%}") (div ("class" "dropdown") (button ("class" "big_icon lowered") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "width: 32px") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (a ("class" "button") ("href" "/journals/{{ journal.id }}/0?view={{ view_mode }}") (icon (text "house")) (str (text "general:link.home"))) (button ("onclick" "delete_journal('{{ journal.id }}')") ("class" "red") (icon (text "trash")) (str (text "general:action.delete"))))) (text "{%- endif %}")) (div ("class" "flex flex-col gap-2") ("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)") ; create note (text "{% if user and user.id == journal.owner -%}") (button ("class" "lowered justify-start w-full") ("onclick" "create_note()") (icon (text "plus")) (str (text "journals:action.create_note"))) (text "{%- endif %}") ; note listings (text "{% for note in notes %}") (div ("class" "flex flex-row gap-1") (a ("href" "{% if owner -%} /@{{ owner.username }}/{{ journal.title }}/{{ note.title }} {%- else -%} /journals/{{ journal.id }}/{{ note.id }} {%- endif %}") ("class" "button justify-start w-full {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}") (icon (text "file-text")) (text "{{ note.title }}")) (text "{% if user and user.id == journal.owner -%}") (div ("class" "dropdown") (button ("class" "big_icon {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "width: 32px") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (button ("onclick" "change_note_title('{{ note.id }}')") (icon (text "pencil")) (str (text "chats:action.rename"))) (button ("onclick" "delete_note('{{ note.id }}')") ("class" "red") (icon (text "trash")) (str (text "general:action.delete"))))) (text "{%- endif %}")) (text "{% endfor %}")) (text "{%- endif %}") (text "{%- endmacro %}")