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

2362 lines
94 KiB
Common Lisp

(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) -%} {% if user and user.username -%}")
(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 "{%- endif %} {%- 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, is_repost=false) -%} {% if community and show_community and community.id != config.town_square or question %}")
(div
("class" "card-nest post_outer:{{ post.id }} post_outer")
("is_repost" "{{ is_repost }}")
(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 post gap-2 post:{{ post.id }} {% if secondary -%}secondary{%- endif %}")
("data-community" "{{ post.community }}")
("data-ownsup" "{{ owner.permissions|has_supporter }}")
("data-id" "{{ post.id }}")
("hook" "verify_emojis")
(div
("class" "w-full flex gap-2")
(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
("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
(span ("id" "post_content:{{ post.id }}") (text "{{ post.content|markdown|safe }}"))
(text "{% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false, is_repost=true) }} {% 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
("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
(span ("id" "post_content:{{ post.id }}") (text "{{ post.content|markdown|safe }}"))
(text "{% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}")
(div
("class" "card lowered red flex items-center gap-2")
(text "{{ icon \"frown\" }}")
(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\" }}")))
(button
("onclick" "trigger('me::intent_twitter', [trigger('me::gen_share', [{ q: '{{ post.context.answering }}', p: '{{ post.id }}' }, 280, true])])")
(icon (text "bird"))
(span
(text "Twitter")))
(button
("onclick" "trigger('me::intent_bluesky', [trigger('me::gen_share', [{ q: '{{ post.context.answering }}', p: '{{ post.id }}' }, 280, true])])")
(icon (text "cloud"))
(span
(text "BlueSky")))
(text "{%- endif %}")
(text "{% if user.id != post.owner -%}")
(b
("class" "title")
(text "{{ text \"general:label.safety\" }}"))
(button
("class" "red")
("onclick" "trigger('me::report', ['{{ post.id }}', 'post'])")
(text "{{ icon \"flag\" }}")
(span
(text "{{ text \"general:action.report\" }}")))
(text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}")
(b
("class" "title")
(text "{{ text \"general:action.manage\" }}"))
; forge stuff
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
(button
("class" "green")
("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 %}")
; owner stuff
(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(--online)")
(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(--idle)")
(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: var(--offline)")
(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\") }}
{{ self::theme_color(color=user.settings.theme_color_online, css=\"online\") }} {{ self::theme_color(color=user.settings.theme_color_idle, css=\"idle\") }} {{ self::theme_color(color=user.settings.theme_color_offline, css=\"offline\") }} {% 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 question {% if secondary -%}secondary{%- endif %} flex gap-2")
(text "{% if owner.id == 0 or question.context.mask_owner -%}")
(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=\"anonymous\", 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 or question.context.mask_owner -%} {% 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")
("id" "question_content:{{ question.id }}")
(text "{{ question.content|markdown|safe }}"))
; question drawings
(text "{{ self::post_media(upload_ids=question.drawings) }}")
; 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 or question.context.mask_owner) -%}")
(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 "{% if question.context.mask_owner -%}")
(details
("class" "card tiny lowered w-full")
(summary
("class" "w-full flex gap-2 flex-wrap items-center")
(icon (text "venetian-mask"))
(span (text "Unmask")))
(text "{{ self::full_username(user=owner) }}"))
(text "{%- endif %} {%- endif %}")
; ...
(div
("class" "flex gap-2 items-center justify-between"))))
(text "{%- endmacro %} {% macro create_question_form(receiver=\"0\", community=\"\", header=\"\", is_global=false, drawing_enabled=false, allow_anonymous=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")
; carp canvas
(text "{% if drawing_enabled -%}")
(div ("ui_ident" "carp_canvas_field"))
(text "{%- endif %}")
; form
(label
("for" "content")
(text "{{ text \"communities:label.content\" }}"))
(textarea
("type" "text")
("name" "content")
("id" "content")
("placeholder" "content")
("required" "")
("minlength" "2")
("maxlength" "4096")))
(div
("class" "flex w-full justify-between gap-2 flex-collapse")
(div
("class" "flex gap-2")
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))
(text "{% if drawing_enabled -%}")
(button
("class" "lowered")
("ui_ident" "add_drawing")
("onclick" "attach_drawing()")
("type" "button")
(text "{{ text \"communities:action.draw\" }}"))
(button
("class" "lowered red hidden")
("ui_ident" "remove_drawing")
("onclick" "remove_drawing()")
("type" "button")
(text "{{ text \"communities:action.remove_drawing\" }}"))
(script
(text "globalThis.attach_drawing = async () => {
globalThis.gerald = await trigger(\"carp::new\", [document.querySelector(\"[ui_ident=carp_canvas_field]\")]);
globalThis.gerald.create_canvas();
document.querySelector(\"[ui_ident=add_drawing]\").classList.add(\"hidden\");
document.querySelector(\"[ui_ident=remove_drawing]\").classList.remove(\"hidden\");
}
globalThis.remove_drawing = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
document.querySelector(\"[ui_ident=carp_canvas_field]\").innerHTML = \"\";
globalThis.gerald = null;
document.querySelector(\"[ui_ident=add_drawing]\").classList.remove(\"hidden\");
document.querySelector(\"[ui_ident=remove_drawing]\").classList.add(\"hidden\");
}"))
(text "{%- endif %}"))
(text "{% if not is_global and allow_anonymous and not user -%}")
(div
("class" "flex gap-2 items-center")
(input
("type" "checkbox")
("name" "mask_owner")
("id" "mask_owner")
("class" "w-content"))
(label
("for" "mask_owner")
(b (str (text "general:label.send_anonymously")))))
(text "{%- endif %}"))))
(script
(text "globalThis.gerald = null;
async function create_question_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"questions::create\"]);
// create body
const body = new FormData();
if (globalThis.gerald) {
body.append(\"drawing.carpgraph\", new Blob([new Uint8Array(globalThis.gerald.as_carp2())], {
type: \"application/octet-stream\"
}));
}
body.append(
\"body\",
JSON.stringify({
content: e.target.content.value,
receiver: \"{{ receiver }}\",
community: \"{{ community }}\",
is_global: \"{{ is_global }}\" == \"true\",
mask_owner: (e.target.mask_owner || { checked:false }).checked
}),
);
// ...
fetch(\"/api/v1/questions\", {
method: \"POST\",
body,
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
e.target.reset();
if (globalThis.gerald) {
globalThis.gerald.clear();
}
}
});
}"))
(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")
(div
("class" "flex flex-col gap-2")
(span
("class" "no_p_margin")
(text "{{ message.content|markdown|safe }}"))
(div
("class" "flex w-full gap-1 flex-wrap")
("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
("hook" "check_message_reactions")
("hook-arg:id" "{{ message.id }}")
(text "{% for emoji,num in message.reactions -%}")
(button
("class" "small lowered")
("ui_ident" "emoji_{{ emoji }}")
("onclick" "trigger('me::message_react', [event.target.parentElement.parentElement, '{{ message.id }}', '{{ emoji }}'])")
(span (text "{{ emoji|emojis|safe }} {{ num }}")))
(text "{%- endfor %}")
(div
("class" "hidden")
(text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}"))))
(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")))
(text "{% if not user.settings.disable_achievements -%}")
(a
("href" "/achievements")
(icon (text "award"))
(str (text "general:link.achievements")))
(text "{%- endif %}")
(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")))
(button
("onclick" "trigger('me::achievement_link', ['OpenReference', '/reference/tetratto/index.html'])")
(icon (text "rabbit"))
(str (text "general:link.reference")))
(button
("onclick" "trigger('me::achievement_link', ['OpenTos', '{{ config.policies.terms_of_service }}'])")
(icon (text "heart-handshake"))
(text "Terms of service"))
(button
("onclick" "trigger('me::achievement_link', ['OpenPrivacyPolicy', '{{ config.policies.privacy }}'])")
(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, render_button=true, small=false) -%}")
(text "{% if render_button -%}")
(button
("class" "button small {% if not small -%} square {%- endif %} lowered")
("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()")
("title" "Emojis")
("type" "button")
(text "{{ icon \"smile-plus\" }}"))
(text "{%- endif %}")
(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) {
if (window.EMOJI_PICKER_MODE === \"replace\") {
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 += ` :${await (
await fetch(\"/api/v1/lookup_emoji\", {
method: \"POST\",
body: event.detail.unicode,
})
).text()}:`;
}
} else {
if (window.EMOJI_PICKER_MODE === \"replace\") {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value = `:${event.detail.emoji.shortcodes[0]}:`;
} else {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value += ` :${event.detail.emoji.shortcodes[0]}:`;
}
}
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).dispatchEvent(new Event(\"change\"));
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 += `<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 flex-wrap")
(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\" }}"))
(label
("class" "flex items-center gap-1 button lowered")
("title" "Mark as NSFW/hide from public timelines")
("for" "is_nsfw")
(input
("type" "checkbox")
("name" "is_nsfw")
("id" "is_nsfw")
("checked" "{{ user.settings.auto_unlist }}")
("onchange" "POST_INITIAL_SETTINGS['is_nsfw'] = event.target.checked"))
(span (icon (text "eye-closed")))))
(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:
'<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,
}),
});
}
};"))))
; 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 %}"))
(text "{% if selected_note -%}")
; open all details elements above the selected note
(script
("defer" "true")
(text "setTimeout(() => {
let cursor = document.querySelector(\"[ui_ident=active_note]\");
while (cursor) {
if (cursor.nodeName === \"DETAILS\") {
cursor.setAttribute(\"open\", \"true\");
}
cursor = cursor.parentElement;
}
}, 150);"))
(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 "{{ self::notes_list_dir_listing_inner(dir=[0, 0, \"root\"], dirs=journal.dirs, notes=notes, owner=owner, journal=journal, view_mode=view_mode) }}"))
(text "{%- endif %}")
(text "{%- endmacro %}")
(text "{% macro notes_list_dir_listing(dir, dirs, notes, owner, journal, view_mode=false) -%}")
(details
(summary
("class" "button w-full justify-start raised w-full")
(icon (text "folder"))
(text "{{ dir[2] }}"))
(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)")
(text "{{ self::notes_list_dir_listing_inner(dir=dir, dirs=dirs, notes=notes, owner=owner, journal=journal, view_mode=view_mode) }}")))
(text "{%- endmacro %}")
(text "{% macro notes_list_dir_listing_inner(dir, dirs, notes, owner, journal, view_mode=false) -%}")
; child dirs
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
(text "{{ self::notes_list_dir_listing(dir=subdir, dirs=dirs, notes=notes, owner=owner, journal=journal) }}")
(text "{%- endif %} {% endfor %}")
; child notes
(text "{% for note in notes %} {% if note.dir == dir[0] -%} {% if not view_mode or note.title != \"journal.css\" -%}")
(text "{{ self::notes_list_note_listing(note=note, owner=owner, journal=journal) }}")
(text "{%- endif %} {%- endif %} {% endfor %}")
(text "{%- endmacro %}")
(text "{% macro notes_list_note_listing(owner, journal, note) -%}")
(div
("class" "flex flex-row gap-1")
("ui_ident" "{% if selected_note == note.id -%} active_note {%- else -%} inactive_note {%- endif %}")
(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")))
(a
("href" "/journals/{{ journal.id }}/{{ note.id }}#/tags")
(icon (text "tag"))
(str (text "journals:action.edit_tags")))
(button
("onclick" "window.NOTE_MOVER_NOTE_ID = '{{ note.id }}'; document.getElementById('note_mover_dialog').showModal()")
(icon (text "brush-cleaning"))
(str (text "journals:action.move")))
(text "{% if note.is_global -%}")
(a
("class" "button")
("href" "/x/{{ note.title }}")
(icon (text "eye"))
(str (text "journals:action.view")))
(button
("class" "purple")
("onclick" "unpublish_note('{{ note.id }}')")
(icon (text "globe-lock"))
(str (text "journals:action.unpublish")))
(text "{% elif note.title != 'journal.css' %}")
(button
("class" "green")
("onclick" "publish_note('{{ note.id }}')")
(icon (text "globe"))
(str (text "journals:action.publish")))
(text "{%- endif %}")
(button
("onclick" "delete_note('{{ note.id }}')")
("class" "red")
(icon (text "trash"))
(str (text "general:action.delete")))))
(text "{%- endif %}"))
(text "{%- endmacro %}")
(text "{% macro note_tags(note) -%} {% if note and note.tags|length > 0 -%}")
(div
("class" "flex gap-1 flex-wrap")
(text "{% for tag in note.tags %}")
(a
("href" "{% if view_mode -%} /@{{ owner.username }} {%- else -%} /@{{ user.username }} {%- endif -%} /{{ journal.title }}?tag={{ tag }}")
("class" "notification chip")
(span (text "{{ tag }}")))
(text "{% endfor %}"))
(text "{%- endif %} {%- endmacro %}")
(text "{% macro directories_editor(dirs) -%}")
(button
("onclick" "create_directory('0')")
(icon (text "plus"))
(str (text "journals:action.create_root_dir")))
(text "{% for dir in dirs %} {% if dir[1] == 0 -%}")
(text "{{ self::directories_editor_listing(dir=dir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}")
(text "{%- endmacro %}")
(text "{% macro directories_editor_listing(dir, dirs) -%}")
(div
("class" "flex flex-row gap-1")
(button
("class" "justify-start lowered w-full")
(icon (text "folder-open"))
(text "{{ dir[2] }}"))
(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" "create_directory('{{ dir[0] }}')")
(icon (text "plus"))
(str (text "journals:action.create_subdir")))
(button
("onclick" "delete_directory('{{ dir[0] }}')")
("class" "red")
(icon (text "trash"))
(str (text "general:action.delete"))))))
(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)")
; subdir listings
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
(text "{{ self::directories_editor_listing(dir=subdir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}"))
(text "{%- endmacro %}")
(text "{% macro note_mover_dirs(dirs) -%}")
(text "{% for dir in dirs %} {% if dir[1] == 0 -%}")
(text "{{ self::note_mover_dirs_listing(dir=dir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}")
(text "{%- endmacro %}")
(text "{% macro note_mover_dirs_listing(dir, dirs) -%}")
(button
("onclick" "move_note_dir(window.NOTE_MOVER_NOTE_ID, '{{ dir[0] }}'); document.getElementById('note_mover_dialog').close()")
("class" "justify-start lowered w-full")
(icon (text "folder-open"))
(text "{{ dir[2] }}"))
(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)")
; subdir listings
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
(text "{{ self::note_mover_dirs_listing(dir=subdir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}"))
(text "{%- endmacro %}")
(text "{% macro become_supporter_button() -%}")
(p
(text "You're ")
(b
(text "not "))
(text "currently a supporter! No
pressure, but it helps us do some pretty cool
things! As a supporter, you'll get:"))
(ul
("style" "margin-bottom: var(--pad-4)")
(li
(text "Vanity badge on profile"))
(li
(text "No more supporter ads (duh)"))
(li
(text "Ability to upload gif avatars/banners"))
(li
(text "Be an admin/owner of up to 10 communities"))
(li
(text "Use custom CSS on your profile"))
(li
(text "Use community emojis outside of
their community"))
(li
(text "Upload and use gif emojis"))
(li
(text "Create infinite stack timelines"))
(li
(text "Upload images to posts"))
(li
(text "Save infinite post drafts"))
(li
(text "Ability to search through all posts"))
(li
(text "Ability to create forges"))
(li
(text "Create more than 1 app"))
(li
(text "Create up to 10 stack blocks"))
(li
(text "Add unlimited users to stacks"))
(li
(text "Increased proxied image size"))
(li
(text "Create infinite journals"))
(li
(text "Create infinite notes in each journal"))
(li
(text "Publish up to 50 notes"))
(text "{% if config.security.enable_invite_codes -%}")
(li
(text "Create up to 48 invite codes")
(sup (a ("href" "#footnote-1") (text "1"))))
(text "{%- endif %}"))
(a
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
("class" "button")
("target" "_blank")
(text "Become a supporter ({{ config.stripe.supporter_price_text }})"))
(span
("class" "fade")
(text "Please use your")
(b
(text " real email "))
(text "when
completing payment. It is required to manage
your billing settings."))
(text "{% if config.security.enable_invite_codes -%}")
(span
("class" "fade")
("id" "footnote-1")
(b (text "1: ")) (text "After your account is at least 1 month old"))
(text "{%- endif %}")
(text "{%- endmacro %}")