tetratto/crates/app/src/public/html/components.lisp

1311 lines
54 KiB
Common Lisp
Raw Normal View History

2025-06-01 12:25:33 -04:00
(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")
("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
(summary
("class" "card flex gap-2 flex-wrap items-center tertiary red w-full")
(text "{{ icon \"triangle-alert\" }}")
(b
(text "{{ post.context.content_warning }}")))
(div
("class" "flex flex-col gap-2")
(span
("id" "post-content:{{ post.id }}")
("class" "no_p_margin")
("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")
(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 }}"))
(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\" }}")))
; <a href="https://trisuaso.github.io/tetratto">
; {{ icon "book" }}
; <span>{{ text "general:link.reference" }}</span>
; </a>
(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 += `<div class=\"card small secondary flex items-center gap-2\" onclick=\"remove_file(${idx})\" style=\"cursor: pointer\">${template.innerHTML.replace(
\".file_name\",
file.name,
)}</div>`;
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:
'<span class=\"fade\">Tags should be separated by a comma.</span>',
},
],
];
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 %}")