(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" "/community/{{ community.title }}") (text "{{ self::community_avatar(id=community.id, community=community, size=\"48px\") }}") (div ("class" "flex flex-col") (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) -%}") (button ("title" "Like") ("class" "{% if secondary -%}quaternary{% 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 -%}") (button ("title" "Dislike") ("class" "{% if secondary -%}quaternary{% 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) -%} {% if community and show_community and community.id != config.town_square or question %}") (div ("class" "card-nest") (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 {% if secondary -%}secondary{%- endif %}") ("id" "post:{{ post.id }}") ("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.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 post.context.content_warning -%}") (span ("id" "post-content:{{ post.id }}") ("class" "no_p_margin post_content") ("hook" "long") (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 tertiary 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) }} {% else %}") (details ("class" "card tiny tertiary 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") (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 tertiary 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 %}") (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 -%} {{ self::likes(id=post.id, asset_type=\"Post\", likes=post.likes, dislikes=post.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") (a ("href" "/post/{{ post.id }}") ("class" "button camo small") (text "{{ icon \"message-circle\" }}") (span (text "{{ post.comment_count }}"))) (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 }}'])") (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 %} {% 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" "tertiary") ("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 tertiary") ("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 tertiary") ("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 quaternary") ("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 quaternary") ("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 }}")) (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 tertiary 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" "/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") (text "{{ icon \"code\" }}") (span (text "{{ text \"general:link.source_code\" }}"))) ; ; {{ icon "book" }} ; {{ text "general:link.reference" }} ; (div ("class" "title")) (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 quaternary") ("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: 0.25rem; 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 quaternary") ("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 quaternary") ("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 quaternary") ("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 quaternary") ("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, }), }); } };")))) (text "{%- endmacro %}")