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

2709 lines
106 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 %}")
(text "{% if community.is_forum -%}")
(icon (text "square-library"))
(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 flex_wrap items_center")
(a
("href" "/@{{ user.username }}")
("class" "flush flex gap_1")
("style" "font-weight: 600")
("target" "_top")
(text "{% if user.settings.private_profile -%}")
(span
("title" "Private")
("class" "flex items_center")
(icon (text "lock")))
(text "{%- endif %}")
(text "{% if user.permissions|has_banned -%}")
(del ("class" "fade") (text "{{ self::username(user=user) }}"))
(text "{% else %}")
(text "{{ self::username(user=user) }}")
(text "{%- endif %}"))
(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 %} {% if user.permissions|has_supporter -%}")
(span
("title" "Supporter")
("style" "color: var(--color-primary);")
("class" "flex items_center")
(text "{{ icon \"star\" }}"))
(text "{%- endif %} {% if user.checkouts|length > 0 -%}")
(span
("title" "Donator")
("style" "color: var(--color-primary);")
("class" "flex items_center")
(text "{{ icon \"hand-heart\" }}"))
(text "{%- endif %} {% if user.permissions|has_staff_badge -%}")
(span
("title" "Staff")
("style" "color: var(--color-primary);")
("class" "flex items_center")
(text "{{ icon \"shield-user\" }}"))
(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 %}")
(text "{% macro post_info(post, community) -%}")
; info about the post: edited, date, etc.
(text "{% if post.context.edited != 0 -%}")
(div
("class" "flex")
(span
("class" "fade date w_content")
(text "{{ post.context.edited }}"))
(sup
("title" "Edited")
(text "*")))
(text "{% else %}")
(span
("class" "fade date w_content")
(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.full_unlist -%}")
(span
("title" "Unlisted")
("class" "flex items_center")
("style" "color: var(--color-primary)")
(icon (text "eye-off")))
(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 "{%- endmacro %}")
(text "{% macro post_buttons_box(post, community, owner, can_manage_post, show_comments=true) -%}")
(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 show_comments and 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\" }}"))
(div
("class" "dropdown")
(button
("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
("title" "More options")
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
(text "{% if config.town_square and post.context.reposts_enabled %}")
(b
("class" "title")
(text "{{ text \"general:label.share\" }}"))
(text "{% if user -%}")
(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 owner.settings.enable_questions -%}")
(a
("class" "button")
("href" "/@{{ owner.username }}?asking_about={{ post.id }}")
(icon (text "reply"))
(span
(str (text "communities:label.ask_about_this"))))
(text "{%- endif %}")
(text "{%- endif %}")
(text "{% if user and 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 and (user.id == post.owner) or is_helper or can_manage_post %}")
(b
("class" "title")
(text "{{ text \"general:action.manage\" }}"))
; forge stuff
(text "{% if user and 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 and 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 "{%- endmacro %}")
(text "{% 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], asking_about=question[2], 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 "{%- 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 {% if community.is_forum -%}flex_collapse{%- endif %}")
(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 "{{ self::post_info(post=post, community=community) }}")
(text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- 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 ("title" "View post content") ("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 %}"))))
(text "{{ self::post_buttons_box(post=post, community=community, owner=owner, can_manage_post=can_manage_post) }}"))
(text "{% if community and show_community and community.id != config.town_square or question %}"))
(text "{%- endif %} {%- endmacro %}")
(text "{% 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 gap_2 justify_between items_center")
(div
("class" "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 }}")))
(span ("class" "date") (text "{{ notification.created }}")))
(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 %}")
(text "{% macro forum_post(post, owner, community, can_manage_post, poll=false, show_show_thread=true) -%}")
(div
("class" "card_nest_horizontal_wrapper post post:{{ post.id }}")
("data-community" "{{ post.community }}")
("data-ownsup" "{{ owner.permissions|has_supporter }}")
("data-id" "{{ post.id }}")
("hook" "verify_emojis")
(div
("class" "card_nest_horizontal_header flex justify_between gap_2")
(div
("class" "flex items_center gap_2")
(a
("href" "/@{{ owner.username }}")
("class" "mobile")
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
(span
("class" "name")
(text "{{ self::full_username(user=owner) }}"))
(a
("href" "/community/{{ community.title }}/topic/{{ post.topic }}")
("class" "flush flex gap_1 items_center smaller_avatar")
(text "{{ self::community_avatar(id=post.community, community=community, size=\"18px\") }}")))
(div
("class" "flex gap_2")
(text "{{ self::post_info(post=post, community=community) }}")))
(div
("class" "card_nest_horizontal")
; author info
(div
("class" "side flex flex_col gap_2 desktop")
("style" "min-width: 200px")
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"64px\", selector_type=\"username\") }}"))
(div
("class" "fade flex flex_col")
("style" "font-size: 12px")
(span (text "Joined: ") (span ("class" "date") (text "{{ owner.created }}")))
(span (text "Posts: ") (text "{{ owner.post_count }}"))
(span (text "Followers: ") (text "{{ owner.follower_count }}"))
(span (text "Following: ") (text "{{ owner.following_count }}"))))
; post
(div
("class" "flex flex_col gap_2 side w_full")
(div
("class" "flex flex_col gap_2")
("style" "flex: 1 0 auto")
(b ("class" "no_p_margin") (text "{{ post.title|markdown|safe }}"))
(span ("class" "no_p_margin") (text "{{ post.content|markdown|safe }}"))
(text "{{ self::post_media(upload_ids=post.uploads) }}")
(text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}"))
(hr ("class" "margin_top"))
(text "{{ self::post_buttons_box(post=post, community=community, owner=owner, can_manage_post=can_manage_post, show_comments=false) }}"))))
; show thread
(text "{% if show_show_thread and post.comment_count > 0 -%}")
(div
("class" "flex gap_2")
(text "{% if post.context.comments_enabled %}")
(a
("href" "/post/{{ post.id }}")
("class" "button lowered")
(icon (text "message-circle"))
(span
(text "{{ post.comment_count }}")))
(text "{% endif %}")
(button
("class" "lowered")
("onclick" "globalThis.continue_thread(event.target, '{{ post.id }}', 'replies_{{ post.id }}', 0)")
(icon (text "chevron-down"))
(str (text "general:action.show_thread"))))
(text "{%- endif %}")
; replies
(div
("class" "flex flex_col gap_2 hidden thread")
("id" "replies_{{ post.id }}"))
(text "{%- endmacro %}")
(text "{% 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, asking_about=false, 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) }}")
; asking about
(text "{% if asking_about -%}")
(text "{{ self::post(post=asking_about[1], owner=asking_about[0], secondary=not secondary, show_community=false) }}")
(text "{%- endif %}")
; 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
("id" "create_question_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
(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 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;
// asking about
globalThis.asking_about = new URLSearchParams(window.location.search).get(\"asking_about\") || \"\";
if (asking_about) {
document.getElementById(\"create_question_form\").innerHTML +=
`<hr /><span class=\"fade\">Asking about: <a href=\"/post/${asking_about}\" target=\"_blank\">${asking_about}</a> <a href=\"?\" class=\"red\">(cancel)</a></span>`;
}
// ...
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,
asking_about,
}),
);
// ...
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], asking_about=false, 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")
("title" "More options")
(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")
("title" "More options")
(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\" }}")))
(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")
("title" "More options")
(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 %}")
(text "{% 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 %}")
(text "{% 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")
("title" "More options")
(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: [],
full_unlist: false,
};
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\",
// ],
[
[\"full_unlist\", \"Unlist from timelines\"],
window.POST_INITIAL_SETTINGS.full_unlist.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 flex items_center gap_1")
(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
("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\" }}")))
(text "{% if not community.is_forum -%}")
(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 %}")
(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")
("title" "More options")
(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")
("title" "More options")
(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")
("title" "More options")
(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")
("title" "More options")
(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 "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"))
(li
(text "Create infinite Littleweb sites"))
(li
(text "Create infinite Littleweb domains"))
(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_links.supporter }}?client_reference_id={{ user.id }}")
("class" "button")
("target" "_blank")
(text "Become a supporter ({{ config.stripe.price_texts.supporter }})"))
(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 %}")
(text "{% macro get_developer_pass_button() -%}")
(p
(text "You currently do not hold a developer pass. With a developer pass, you'll get:"))
(ul
("style" "margin-bottom: var(--pad-4)")
(li
(text "Increased app storage limit (500 KB->25 MB)"))
(li
(text "Ability to create forges"))
(li
(text "Ability to create more than 1 app"))
(li
(text "Developer pass profile badge")))
(a
("href" "{{ config.stripe.payment_links.dev_pass }}?client_reference_id={{ user.id }}")
("class" "button")
("target" "_blank")
(text "Continue ({{ config.stripe.price_texts.dev_pass }})"))
(span
("class" "fade")
(text "Please use your")
(b
(text " real email "))
(text "when completing payment. It is required to manage your billing settings. If you're already a supporter, please use the same email you used there."))
(text "{%- endmacro %}")
(text "{% macro developer_pass_ad(body) -%} {% if config.stripe and not has_developer_pass %}")
(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")
(b
(text "{{ body }}"))
(a
("href" "/settings#/account/billing")
("class" "button small")
(icon (text "arrow-right"))
(span
(str (text "dialog:action.continue"))))))
(text "{%- endif %} {%- endmacro %}")
(text "{% macro letter_listing(letter, owner) -%}")
(div
("class" "card lowered flex gap_2 flex_row")
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
(div
("class" "flex flex_col")
(text "{{ self::full_username(user=owner) }}")
(div
("class" "flex items_center gap_2")
; read status
(text "{% if user.id in letter.read_by -%}")
(div ("class" "flex items_center green") (icon (text "mail-check")))
(text "{% else %}")
(div ("class" "flex items_center") (icon (text "mail")))
(text "{%- endif %}")
; subject
(a ("class" "flush") ("href" "/mail/letter/{{ letter.id }}") (b (text "{{ letter.subject }}"))))))
(text "{%- endmacro %}")
(text "{% macro letter(letter, owner, show_subject=true) -%}")
(div
("class" "card_nest")
(text "{% if show_subject -%}")
(div
("class" "card flex gap_2 flex_row")
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
(div
("class" "flex flex_col")
(text "{{ self::full_username(user=owner) }}")
(span
(b (text "{{ letter.subject }}"))
(text "{% if letter.replying_to -%}")
(a
("href" "/mail/letter/{{ letter.replying_to }}")
(text " (up)"))
(text "{%- endif %}"))
(div
("class" "flex flex_wrap gap_2")
(text "{% for receiver in letter.receivers %}")
(a
("href" "/api/v1/auth/user/find/{{ receiver }}")
(text "{{ components::avatar(username=receiver, selector_type=\"id\", size=\"18px\") }}"))
(text "{%- endfor %}"))))
(text "{% else %}")
(div
("class" "card small flex gap_2 flex_row")
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"24px\") }}"))
(text "{{ self::full_username(user=owner) }}"))
(text "{%- endif %}")
(div
("class" "card flex flex_col gap_2")
(text "{{ letter.content|markdown|safe }}")
(hr)
(div
("class" "flex gap_2 items_center")
(a
("class" "button small lowered")
("href" "/mail/compose?receivers={{ owner.username }}&subject=Re%3A%20{{ letter.subject }}&replying_to={{ letter.id }}")
("title" "Reply")
(icon (text "reply")))
(a
("class" "button small lowered")
("href" "/mail/compose?receivers={{ owner.username }}{% for receiver in letter.receivers %},id%3A{{ receiver }}{% endfor %}&subject=Re%3A%20{{ letter.subject }}&replying_to={{ letter.id }}")
("title" "Reply all")
(icon (text "reply-all")))
(text "{% if user and letter.owner == user.id -%}")
(button
("class" "small lowered red")
("onclick" "delete_letter('{{ letter.id }}')")
("title" "Delete")
(icon (text "trash")))
(text "{%- endif %}"))))
(text "{%- endmacro %}")
(text "{% macro topic_display(id, topic, community, show_description=true) -%}")
(div
("class" "flex items_center gap_2")
(svg
("width" "12")
("height" "12")
("viewBox" "0 0 12 12")
("style" "fill: {% if topic.color == \"#000000\" -%} var(--color-primary) {%- else -%} {{ topic.color }} {%- endif %}; margin-top: 3.5px")
(circle
("cx" "6")
("cy" "6")
("r" "6")))
(a
("href" "/community/{{ community.title }}/topic/{{ id }}")
("class" "flush")
(b (text "{{ topic.title }}"))))
(text "{% if show_description -%}")
(span ("class" "no_p_margin") (text "{{ topic.description|markdown|safe }}"))
(text "{%- endif %}")
(text "{%- endmacro %}")
(text "{% macro topic_post_display(post, owner, is_pinned=false, community=false) -%}")
(tr
(text "{% if community %}")
(td
(a
("href" "/community/{{ community.title }}/topic/{{ post.topic }}")
("class" "flex gap_1 items_center w_content")
(text "{{ self::community_avatar(id=post.community, community=community) }}")
(span
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))))
(text "{%- endif %}")
(td
("class" "flex gap_1")
(a
("href" "/post/{{ post.id }}")
(text "{% if is_pinned -%}Sticky: {% endif %}")
(text "{{ post.title }}"))
(span ("class" "fade") (text "by"))
(text "{{ self::full_username(user=owner) }}"))
(td (text "{{ post.comment_count }}"))
(td (text "{{ ((post.likes + 1) / (post.dislikes + 1))|round(method=\"ceil\", precision=2) }}"))
(td (span ("class" "date short") (text "{{ post.created }}"))))
(text "{%- endmacro %}")
(text "{% macro product_listing_card(product, owner=false, edit=false) -%}")
(a
("class" "card button lowered w_full flex flex_col gap_2")
("href" "/product/{{ product.id }}{% if edit -%} /edit {%- endif %}")
(text "{% if owner -%}")
(text "{{ self::full_username(user=owner) }}")
(text "{%- endif %}")
(h3
("class" "flex gap_2 items_center {% if not product.on_sale -%} fade {%- endif %}")
("style" "height: 24px; text-decoration: {% if not product.on_sale -%} line-through {%- else -%} none {%- endif %}")
(icon (text "package"))
(text "{{ product.title }}"))
(h4
("class" "flex gap_2 items_center")
("style" "height: 18px")
(icon (text "badge-cent"))
(text "{{ product.price }}")))
(text "{%- endmacro %}")