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

2689 lines
105 KiB
Common Lisp
Raw Normal View History

2025-06-01 12:25:33 -04:00
(text "{% macro avatar(username, size=\"24px\", selector_type=\"username\") -%}")
(img
("title" "{{ username }}'s avatar")
("src" "/api/v1/auth/user/{{ username }}/avatar?selector_type={{ selector_type }}")
("alt" "@{{ username }}")
("class" "avatar shadow")
("loading" "lazy")
("style" "--size: {{ size }}"))
(text "{%- endmacro %} {% macro community_avatar(id, community=false, size=\"24px\") -%} {% if community -%}")
(img
("src" "/api/v1/communities/{{ id }}/avatar")
("alt" "{{ community.title }}'s avatar")
("class" "avatar shadow")
("loading" "lazy")
("style" "--size: {{ size }}"))
(text "{% else %}")
(img
("src" "/api/v1/communities/{{ id }}/avatar")
("alt" "{{ id }}'s avatar")
("class" "avatar shadow")
("loading" "lazy")
("style" "--size: {{ size }}"))
(text "{%- endif %} {%- endmacro %} {% macro banner(username, border_radius=\"var(--radius)\") -%}")
(img
("title" "{{ username }}'s banner")
("src" "/api/v1/auth/user/{{ username }}/banner")
("alt" "@{{ username }}'s banner")
2025-08-03 23:24:57 -04:00
("class" "banner shadow w_full")
2025-06-01 12:25:33 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "card secondary w_full flex items_center gap_4")
("href" "{% if community.is_forge -%}/forge/{{ community.title }}{% else %}/community/{{ community.title }}{%- endif %}")
2025-06-01 12:25:33 -04:00
(text "{{ self::community_avatar(id=community.id, community=community, size=\"48px\") }}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col")
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 items_center")
(text "{% if community.is_forge -%}")
(icon (text "anvil"))
(text "{%- endif %}")
2025-08-02 17:29:26 -04:00
(text "{% if community.is_forum -%}")
(icon (text "square-library"))
(text "{%- endif %}")
(h3
("class" "name lg:long")
(text "{{ community.context.display_name }}")))
2025-06-01 12:25:33 -04:00
(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) -%}")
2025-06-01 12:25:33 -04:00
(button
("title" "Like")
("class" "{% if secondary -%}lowered{% else %}camo{%- endif %} small")
2025-06-01 12:25:33 -04:00
("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 -%}")
2025-06-01 12:25:33 -04:00
(button
("title" "Dislike")
("class" "{% if secondary -%}lowered{% else %}camo{%- endif %} small")
2025-06-01 12:25:33 -04:00
("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 %}"))
2025-06-22 00:04:32 -04:00
(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%} {% if user and user.username -%}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-06-01 12:25:33 -04:00
(a
("href" "/@{{ user.username }}")
("class" "flush")
("style" "font-weight: 600")
("target" "_top")
2025-07-16 20:18:39 -04:00
(text "{% if user.permissions|has_banned -%}")
(del ("class" "fade") (text "{{ self::username(user=user) }}"))
(text "{% else %}")
(text "{{ self::username(user=user) }}")
(text "{%- endif %}"))
2025-06-01 12:25:33 -04:00
(text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}")
(span
("title" "Verified")
("style" "color: var(--color-primary)")
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"badge-check\" }}"))
2025-08-02 16:04:50 -04:00
(text "{%- endif %} {% if user.permissions|has_supporter -%}")
(span
("title" "Supporter")
("style" "color: var(--color-primary);")
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-08-02 16:04:50 -04:00
(text "{{ icon \"star\" }}"))
2025-07-18 20:04:26 -04:00
(text "{%- endif %} {% if user.permissions|has_staff_badge -%}")
(span
("title" "Staff")
("style" "color: var(--color-primary);")
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-07-18 20:04:26 -04:00
(text "{{ icon \"shield-user\" }}"))
2025-06-01 12:25:33 -04:00
(text "{%- endif %}"))
2025-06-22 00:04:32 -04:00
(text "{%- endif %} {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}")
2025-06-01 12:25:33 -04:00
(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) }}"))
2025-08-03 23:24:57 -04:00
(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 %}")
2025-08-05 16:33:53 -04:00
(text "{% macro post_buttons_box(post, community, owner, can_manage_post, show_comments=true) -%}")
2025-08-03 23:24:57 -04:00
(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")
2025-08-05 16:33:53 -04:00
(text "{% if show_comments and post.context.comments_enabled %}")
2025-08-03 23:24:57 -04:00
(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 %}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest post_outer:{{ post.id }} post_outer")
2025-06-27 13:36:10 -04:00
("is_repost" "{{ is_repost }}")
2025-07-13 18:42:08 -04:00
(text "{% if question -%} {{ self::question(question=question[0], owner=question[1], asking_about=question[2], profile=owner) }} {% else %}")
2025-06-01 12:25:33 -04:00
(div
("class" "card small")
(a
("href" "/api/v1/communities/find/{{ post.community }}")
2025-08-03 23:24:57 -04:00
("class" "flush flex gap_1 items_center")
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col post gap_2 post:{{ post.id }} {% if secondary -%}secondary{%- endif %}")
2025-06-01 12:25:33 -04:00
("data-community" "{{ post.community }}")
("data-ownsup" "{{ owner.permissions|has_supporter }}")
("data-id" "{{ post.id }}")
2025-06-01 12:25:33 -04:00
("hook" "verify_emojis")
(div
2025-08-03 23:24:57 -04:00
("class" "w_full flex gap_2 {% if community.is_forum -%}flex_collapse{%- endif %}")
2025-06-01 12:25:33 -04:00
(text "{% if not expect_repost -%}")
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"52px\", selector_type=\"username\") }}"))
(text "{%- endif %}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col w_full gap_1 post_right {% if expect_repost -%}repost{%- endif %}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_wrap gap_2 items_center")
2025-06-01 12:25:33 -04:00
(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) }}"))
2025-08-03 23:24:57 -04:00
(text "{{ self::post_info(post=post, community=community) }}"))
2025-06-08 15:34:29 -04:00
(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 }}")
2025-06-08 15:34:29 -04:00
("class" "no_p_margin post_content")
("hook" "long")
(text "{{ post.title }}"))
2025-07-13 23:15:00 -04:00
(button ("title" "View post content") ("class" "small lowered") (icon (text "ellipsis"))))
2025-06-08 15:34:29 -04:00
(text "{% else %}")
2025-06-01 12:25:33 -04:00
(text "{% if not post.context.content_warning -%}")
(span
("class" "no_p_margin post_content")
2025-06-01 12:25:33 -04:00
("hook" "long")
2025-06-08 15:34:29 -04:00
; title
(text "{% if post.title and community and community.context.enable_titles -%}")
(h2 (text "{{ post.title }}"))
2025-06-10 22:02:06 -04:00
(hr ("class" "margin") ("style" "margin-top: var(--pad-2)"))
2025-06-08 15:34:29 -04:00
(text "{%- endif %}")
; content
(span ("id" "post_content:{{ post.id }}") (text "{{ post.content|markdown|safe }}"))
2025-06-27 13:36:10 -04:00
(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 %}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered red flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"frown\" }}")
(span
(str (text "general:label.could_not_find_post"))))
2025-06-01 12:25:33 -04:00
(text "{%- endif %} {%- endif %}"))
(text "{{ self::post_media(upload_ids=post.uploads) }} {% else %}")
(details
2025-08-03 23:24:57 -04:00
("class" "card tiny lowered w_full")
2025-06-01 12:25:33 -04:00
(summary
2025-08-03 23:24:57 -04:00
("class" "red w_full")
2025-06-01 12:25:33 -04:00
(b
(text "{{ post.context.content_warning }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
(span
("class" "no_p_margin post_content")
2025-06-01 12:25:33 -04:00
("hook" "long")
2025-06-08 15:34:29 -04:00
; 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 %}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered red flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"frown\" }}")
(span
(text "Could not find original post...")))
(text "{%- endif %} {%- endif %}"))
(text "{{ self::post_media(upload_ids=post.uploads) }}")))
2025-06-08 15:34:29 -04:00
(text "{%- endif %} {%- endif %}")
2025-06-04 17:21:46 -04:00
(text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_wrap gap_2 fade")
2025-06-01 12:25:33 -04:00
(text "{% for tag in post.context.tags %}")
(a
("href" "/@{{ owner.username }}?tag={{ tag }}")
("class" "flush fade")
(text "#{{ tag }}"))
(text "{% endfor %}"))))
2025-08-03 23:24:57 -04:00
(text "{{ self::post_buttons_box(post=post, community=community, owner=owner, can_manage_post=can_manage_post) }}"))
2025-06-01 12:25:33 -04:00
(text "{% if community and show_community and community.id != config.town_square or question %}"))
2025-08-03 23:24:57 -04:00
(text "{%- endif %} {%- endmacro %}")
2025-06-01 12:25:33 -04:00
2025-08-03 23:24:57 -04:00
(text "{% macro post_media(upload_ids) -%} {% if upload_ids|length > 0 -%}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "media_gallery gap_2")
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "w_full card_nest")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card small notif_title flex gap_2 justify_between items_center")
2025-08-02 16:04:50 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-08-02 16:04:50 -04:00
(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 }}")))
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card notif_content flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
(span
("class" "no_p_margin")
(text "{{ notification.content|markdown|safe }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "card secondary w_full flex flex_wrap gap_2")
2025-06-01 12:25:33 -04:00
(text "{% if notification.read -%}")
(button
("class" "raised")
2025-06-01 12:25:33 -04:00
("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")
2025-06-01 12:25:33 -04:00
("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")
2025-06-01 12:25:33 -04:00
("onclick" "trigger('me::remove_notification', ['{{ notification.id }}'])")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"general:action.delete\" }}"))))))
2025-08-03 23:24:57 -04:00
(text "{%- endmacro %}")
2025-06-01 12:25:33 -04:00
2025-08-05 16:33:53 -04:00
(text "{% macro forum_post(post, owner, community, can_manage_post, poll=false, show_show_thread=true) -%}")
2025-08-03 23:24:57 -04:00
(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")
2025-08-04 12:12:04 -04:00
(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) }}")))
2025-08-03 23:24:57 -04:00
(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")
2025-08-04 14:58:36 -04:00
(b ("class" "no_p_margin") (text "{{ post.title|markdown|safe }}"))
2025-08-03 23:24:57 -04:00
(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"))
2025-08-05 16:33:53 -04:00
(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 }}"))
2025-08-03 23:24:57 -04:00
(text "{%- endmacro %}")
(text "{% macro user_card(user) -%}")
2025-06-01 12:25:33 -04:00
(a
2025-08-03 23:24:57 -04:00
("class" "card_nest w_full")
2025-06-01 12:25:33 -04:00
("href" "/@{{ user.username }}")
(div
("class" "card small")
("style" "padding: 0")
(text "{{ self::banner(username=user.username, border_radius=\"0px\") }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "card secondary flex items_center gap_4")
2025-06-01 12:25:33 -04:00
(text "{{ self::avatar(username=user.username, size=\"48px\") }}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-06-01 12:25:33 -04:00
(b
(text "{{ self::username(user=user) }}"))
(text "{{ self::online_indicator(user=user) }}"))))
(text "{%- endmacro %} {% macro pagination(page=0, items=0, key=\"\", value=\"\") -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex justify_between gap_2 w_full")
2025-06-01 12:25:33 -04:00
(text "{% if page > 0 -%}")
(a
("class" "button lowered")
2025-06-01 12:25:33 -04:00
("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")
2025-06-01 12:25:33 -04:00
("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)")
2025-06-01 12:25:33 -04:00
(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)")
2025-06-01 12:25:33 -04:00
(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)")
2025-06-01 12:25:33 -04:00
(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 -%}")
2025-06-01 12:25:33 -04:00
(style
2025-06-14 14:45:52 -04:00
(text "{{ user.settings.theme_custom_css|remove_script_tags|safe }}"))
2025-06-01 12:25:33 -04:00
(text "{%- endif %}"))
(text "{%- endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color -%}")
(style
(text ":root,
* {
--{{ css }}: {{ color|color }} !important;
}"))
2025-07-13 18:42:08 -04:00
(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, asking_about=false, show_community=true, secondary=false, profile=false) -%}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card question {% if secondary -%}secondary{%- endif %} flex gap_2")
(text "{% if owner.id == 0 or question.context.mask_owner -%}")
2025-06-01 12:25:33 -04:00
(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 %}"))
2025-06-01 12:25:33 -04:00
(text "{% else %}")
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }}"))
(text "{%- endif %}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1 w_full")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2 flex_wrap")
2025-06-01 12:25:33 -04:00
(span
("class" "name")
(text "{% if owner.id == 0 or question.context.mask_owner -%} {% if profile and profile.settings.anonymous_username -%}")
2025-06-01 12:25:33 -04:00
(span
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(b
(text "{{ profile.settings.anonymous_username }}"))
(span
("title" "Anonymous user")
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-06-01 12:25:33 -04:00
("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")
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-06-01 12:25:33 -04:00
("style" "color: var(--color-primary)")
(text "{{ icon \"message-circle-heart\" }}"))
(text "{% if question.context.is_nsfw -%}")
(span
("title" "NSFW community")
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-06-01 12:25:33 -04:00
("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 }}")
2025-08-03 23:24:57 -04:00
("class" "flex items_center")
2025-06-01 12:25:33 -04:00
(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 }}")
2025-06-01 12:25:33 -04:00
(text "{{ question.content|markdown|safe }}"))
2025-06-20 17:40:55 -04:00
; question drawings
(text "{{ self::post_media(upload_ids=question.drawings) }}")
2025-07-13 18:42:08 -04:00
; 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
2025-08-03 23:24:57 -04:00
("class" "card tiny lowered w_full")
(summary
2025-08-03 23:24:57 -04:00
("class" "w_full flex gap_2 flex_wrap items_center")
(icon (text "shield"))
(span (text "View IP")))
2025-06-02 20:33:51 -04:00
(pre (code (text "{{ question.ip }}"))))
(text "{% if question.context.mask_owner -%}")
(details
2025-08-03 23:24:57 -04:00
("class" "card tiny lowered w_full")
(summary
2025-08-03 23:24:57 -04:00
("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 %}")
; ...
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 items_center justify_between"))))
2025-06-01 12:25:33 -04:00
(text "{%- endmacro %} {% macro create_question_form(receiver=\"0\", community=\"\", header=\"\", is_global=false, drawing_enabled=false, allow_anonymous=false) -%}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card small flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"message-circle-heart\" }}")
(span
("class" "no_p_margin")
(text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}")))
(form
2025-07-13 18:42:08 -04:00
("id" "create_question_form")
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("onsubmit" "create_question_from_form(event)")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1")
2025-06-20 17:40:55 -04:00
; carp canvas
(text "{% if drawing_enabled -%}")
(div ("ui_ident" "carp_canvas_field"))
(text "{%- endif %}")
; form
2025-06-01 12:25:33 -04:00
(label
("for" "content")
(text "{{ text \"communities:label.content\" }}"))
(textarea
("type" "text")
("name" "content")
("id" "content")
("placeholder" "content")
("required" "")
("minlength" "2")
("maxlength" "4096")))
2025-06-20 17:40:55 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex w_full justify_between gap_2 flex_collapse")
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
(button
(text "{{ text \"communities:action.create\" }}"))
2025-06-20 17:40:55 -04:00
(text "{% if drawing_enabled -%}")
(button
("class" "lowered")
("ui_ident" "add_drawing")
("onclick" "attach_drawing()")
("type" "button")
(text "{{ text \"communities:action.draw\" }}"))
2025-06-20 17:40:55 -04:00
(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\");
2025-06-20 17:40:55 -04:00
}
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 %}"))
2025-06-20 17:40:55 -04:00
2025-07-08 14:36:14 -04:00
(text "{% if not is_global and allow_anonymous and user -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 items_center")
(input
("type" "checkbox")
("name" "mask_owner")
("id" "mask_owner")
2025-08-03 23:24:57 -04:00
("class" "w_content"))
(label
("for" "mask_owner")
(b (str (text "general:label.send_anonymously")))))
2025-06-20 17:40:55 -04:00
(text "{%- endif %}"))))
2025-06-01 12:25:33 -04:00
(script
2025-06-20 17:40:55 -04:00
(text "globalThis.gerald = null;
2025-07-13 18:42:08 -04:00
// asking about
2025-07-13 19:58:59 -04:00
globalThis.asking_about = new URLSearchParams(window.location.search).get(\"asking_about\") || \"\";
2025-07-13 18:42:08 -04:00
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>`;
}
// ...
2025-06-20 17:40:55 -04:00
async function create_question_from_form(e) {
2025-06-01 12:25:33 -04:00
e.preventDefault();
await trigger(\"atto::debounce\", [\"questions::create\"]);
2025-06-20 17:40:55 -04:00
// 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({
2025-06-01 12:25:33 -04:00
content: e.target.content.value,
receiver: \"{{ receiver }}\",
community: \"{{ community }}\",
is_global: \"{{ is_global }}\" == \"true\",
2025-07-13 18:42:08 -04:00
mask_owner: (e.target.mask_owner || { checked:false }).checked,
asking_about,
2025-06-01 12:25:33 -04:00
}),
2025-06-20 17:40:55 -04:00
);
// ...
fetch(\"/api/v1/questions\", {
method: \"POST\",
body,
2025-06-01 12:25:33 -04:00
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
e.target.reset();
2025-06-20 17:40:55 -04:00
if (globalThis.gerald) {
globalThis.gerald.clear();
}
2025-06-01 12:25:33 -04:00
}
});
}"))
(text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-07-13 18:42:08 -04:00
(text "{{ self::question(question=question[0], owner=question[1], asking_about=false, show_community=show_community) }}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "small card flex justify_between flex_wrap gap_2{% if secondary -%} secondary{%- endif %}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_1 reactions_box")
2025-06-01 12:25:33 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "flex gap_1 buttons_box")
2025-06-01 12:25:33 -04:00
(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")
2025-07-13 23:15:00 -04:00
("title" "More options")
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card flex items_center justify_between gap_2 small")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(b
(text "Listening on"))
(text "{{ icon \"spotify\" }}"))
(span
("class" "fade date short")
(text "{{ state.data.timestamp }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "card secondary flex gap_2")
2025-06-01 12:25:33 -04:00
(a
("href" "{{ state.external_urls.album }}")
(img
("src" "{{ state.external_urls.album_img }}")
("alt" "Album cover")
("loading" "lazy")
("class" "avatar")
("style" "--size: {{ size }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col")
2025-06-01 12:25:33 -04:00
(h5
2025-08-03 23:24:57 -04:00
("class" "w_full")
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card flex items_center justify_between gap_2 small")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(b
(text "Listening on"))
(text "{{ icon \"last_fm\" }}"))
(span
("class" "fade date short")
(text "{{ state.data.timestamp }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "card secondary flex gap_2")
2025-06-01 12:25:33 -04:00
(a
("href" "{{ state.external_urls.track }}")
(img
("src" "{{ state.external_urls.track_img }}")
("alt" "Track cover")
("loading" "lazy")
("class" "avatar")
("style" "--size: {{ size }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col")
2025-06-01 12:25:33 -04:00
(h5
2025-08-03 23:24:57 -04:00
("class" "w_full")
2025-06-01 12:25:33 -04:00
(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")
2025-07-13 23:15:00 -04:00
("title" "More options")
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card secondary message flex gap_2 {% if grouped -%}grouped{%- endif %}")
2025-06-01 12:25:33 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1 w_full")
2025-06-01 12:25:33 -04:00
(text "{% if not grouped -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 w_full justify_between flex_wrap")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 hidden")
2025-06-01 12:25:33 -04:00
(text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")))
(text "{%- endif %}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex w_full gap_2 justify_between")
2025-06-21 03:11:29 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-21 03:11:29 -04:00
(span
("class" "no_p_margin")
(text "{{ message.content|markdown|safe }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "flex w_full gap_1 flex_wrap")
2025-06-21 03:11:29 -04:00
("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) }}"))))
2025-06-01 12:25:33 -04:00
(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\" }}")))
2025-06-27 14:21:42 -04:00
(text "{% if not user.settings.disable_achievements -%}")
2025-06-27 03:45:50 -04:00
(a
("href" "/achievements")
(icon (text "award"))
(str (text "general:link.achievements")))
2025-06-27 14:21:42 -04:00
(text "{%- endif %}")
2025-06-01 12:25:33 -04:00
(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 %}")
2025-06-14 14:45:52 -04:00
(b ("class" "title") (text "{{ config.name }}"))
2025-06-01 12:25:33 -04:00
(a
("href" "https://trisua.com/t/tetratto")
2025-06-14 14:45:52 -04:00
("class" "button")
(icon (text "code"))
(str (text "general:link.source_code")))
(button
("onclick" "trigger('me::achievement_link', ['OpenReference', '/reference/tetratto/index.html'])")
2025-06-14 14:45:52 -04:00
(icon (text "rabbit"))
(str (text "general:link.reference")))
2025-06-30 18:49:41 -04:00
(button
("onclick" "trigger('me::achievement_link', ['OpenTos', '{{ config.policies.terms_of_service }}'])")
(icon (text "heart-handshake"))
(text "Terms of service"))
2025-06-30 18:49:41 -04:00
(button
("onclick" "trigger('me::achievement_link', ['OpenPrivacyPolicy', '{{ config.policies.privacy }}'])")
(icon (text "cookie"))
(text "Privacy policy"))
2025-06-14 14:45:52 -04:00
(b ("class" "title") (str (text "general:label.account")))
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"music\" }}")
(span
(b
2025-06-01 20:24:05 -04:00
(text "Listening to "))
2025-06-01 12:25:33 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"music\" }}")
(span
(b
2025-06-01 20:24:05 -04:00
(text "Listening to "))
2025-06-01 12:25:33 -04:00
(text "{{ other_user.connections.Spotify[1].data.artist }}")))
(text "{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false) -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 items_center card tiny user_plate {% if secondary -%}secondary{%- endif %}")
2025-06-01 12:25:33 -04:00
(a
("href" "/@{{ user.username }}")
(text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "flex justify_center flex_col")
2025-06-01 12:25:33 -04:00
("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")
2025-07-13 23:15:00 -04:00
("title" "More options")
2025-06-01 12:25:33 -04:00
(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 %}"))
2025-06-21 03:11:29 -04:00
(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false, render_button=true, small=false) -%}")
(text "{% if render_button -%}")
2025-06-01 12:25:33 -04:00
(button
2025-06-21 03:11:29 -04:00
("class" "button small {% if not small -%} square {%- endif %} lowered")
2025-06-01 12:25:33 -04:00
("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()")
("title" "Emojis")
("type" "button")
(text "{{ icon \"smile-plus\" }}"))
2025-06-21 03:11:29 -04:00
(text "{%- endif %}")
2025-06-01 12:25:33 -04:00
(text "{% if render_dialog -%}")
(dialog
("id" "emoji_dialog")
(div
2025-08-03 23:24:57 -04:00
("class" "inner flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
(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);
2025-06-01 12:25:33 -04:00
box-shadow: 0 0 4px var(--color-shadow);
")
2025-08-03 23:24:57 -04:00
("class" "w_full"))
2025-06-01 12:25:33 -04:00
(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) {
2025-06-21 03:11:29 -04:00
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()}:`;
}
2025-06-01 12:25:33 -04:00
} else {
2025-06-21 03:11:29 -04:00
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]}:`;
}
2025-06-01 12:25:33 -04:00
}
2025-06-21 03:11:29 -04:00
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).dispatchEvent(new Event(\"change\"));
2025-06-01 12:25:33 -04:00
document.getElementById(\"emoji_dialog\").close();
});"))
(div
2025-08-03 23:24:57 -04:00
("class" "flex justify_between")
2025-06-01 12:25:33 -04:00
(div)
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
2025-06-01 12:25:33 -04:00
(button
("class" "bold red lowered")
2025-06-01 12:25:33 -04:00
("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")
2025-06-01 12:25:33 -04:00
("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) {
2025-08-03 23:24:57 -04:00
element.innerHTML += `<div class=\"card small secondary flex items_center gap_2\" onclick=\"remove_file(${idx})\" style=\"cursor: pointer\">${template.innerHTML.replace(
2025-06-01 12:25:33 -04:00
\".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();
});
})();"))
2025-07-18 14:52:00 -04:00
(text "{%- endmacro %}")
(text "{% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card w_full supporter_ad")
2025-06-01 12:25:33 -04:00
("ui_ident" "supporter_ad")
("onclick" "window.location.href = '/settings#/account/billing'")
(div
2025-08-03 23:24:57 -04:00
("class" "card w_full flex flex_wrap items_center gap_2 justify_between")
2025-06-01 12:25:33 -04:00
(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\" }}")))))
2025-07-18 14:52:00 -04:00
(text "{%- endif %} {%- endmacro %}")
2025-06-01 12:25:33 -04:00
2025-07-18 14:52:00 -04:00
(text "{% macro create_post_options() -%}")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 flex_wrap")
2025-06-01 12:25:33 -04:00
(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 %}")
2025-06-04 17:21:46 -04:00
(button
("class" "small square lowered")
2025-06-04 17:21:46 -04:00
("title" "Add poll")
("onclick" "document.getElementById('poll_options_dialog').showModal()")
("type" "button")
(text "{{ icon \"list-todo\" }}"))
2025-06-01 12:25:33 -04:00
(button
("class" "small square lowered")
2025-06-01 12:25:33 -04:00
("title" "More options")
("onclick" "document.getElementById('post_options_dialog').showModal()")
("type" "button")
2025-07-13 23:15:00 -04:00
("title" "More options")
(text "{{ icon \"ellipsis\" }}"))
(label
2025-08-03 23:24:57 -04:00
("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")))))
2025-06-01 12:25:33 -04:00
(dialog
("id" "post_options_dialog")
(div
2025-08-03 23:24:57 -04:00
("class" "inner flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
(div
("id" "post_options")
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2"))
2025-06-01 12:25:33 -04:00
(hr)
(div
2025-08-03 23:24:57 -04:00
("class" "flex justify_between")
2025-06-01 12:25:33 -04:00
(div)
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
2025-06-01 12:25:33 -04:00
(button
("class" "bold red lowered")
2025-06-01 12:25:33 -04:00
("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: [],
2025-07-10 18:43:54 -04:00
full_unlist: false,
2025-06-01 12:25:33 -04:00
};
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\",
// ],
2025-07-10 18:43:54 -04:00
[
[\"full_unlist\", \"Unlist from timelines\"],
window.POST_INITIAL_SETTINGS.full_unlist.toString(),
\"checkbox\",
],
2025-06-01 12:25:33 -04:00
[
[\"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,
}),
});
}
};"))))
2025-06-04 17:21:46 -04:00
; 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;
2025-06-04 17:21:46 -04:00
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\"];
}
2025-06-04 17:21:46 -04:00
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,
2025-06-04 17:21:46 -04:00
}];
}"))
(dialog
("id" "poll_options_dialog")
(div
2025-08-03 23:24:57 -04:00
("class" "inner flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(div
("id" "poll_options")
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(b (text "Attach poll"))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(b (text "Option C"))
(input ("type" "text") ("placeholder" "option A") ("onchange" "window.POLL_OPTION_C = event.target.value")))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(b (text "Option D"))
(input ("type" "text") ("placeholder" "option D") ("onchange" "window.POLL_OPTION_D = event.target.value")))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
(b (text "Expires"))
(input ("type" "date") ("onchange" "window.POLL_EXPIRES = event.target.valueAsDate.getTime() - new Date().getTime()"))))
2025-06-04 17:21:46 -04:00
(hr)
(div
2025-08-03 23:24:57 -04:00
("class" "flex justify_between")
2025-06-04 17:21:46 -04:00
(div)
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
2025-06-04 17:21:46 -04:00
(button
("class" "bold red lowered")
2025-06-04 17:21:46 -04:00
("onclick" "document.getElementById('poll_options_dialog').close()")
("type" "button")
(text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}"))))))
(text "{%- endmacro %}")
(text "{% macro poll(post, poll) -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered w_full flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(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 -%}")
2025-06-04 17:21:46 -04:00
; already voted, show results
(text "{% if poll[1] %}")
2025-06-04 17:21:46 -04:00
(span ("class" "fade") (text "You've already voted!"))
(text "{% elif poll[2] %}")
(span ("class" "fade") (text "Poll ended!"))
(text "{% endif %}")
2025-06-04 17:21:46 -04:00
; option a
(div
2025-08-03 23:24:57 -04:00
("class" "card w_full flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card w_full flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card w_full flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card w_full flex flex_col gap_2")
2025-06-04 17:21:46 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "hover_left_bar raised justify-start w_full poll_option")
2025-06-04 17:21:46 -04:00
("onclick" "trigger('me::vote', ['{{ post.id }}', 'A'])")
(icon (text "tally-1"))
(text "{{ poll[0].option_a }}"))
; option b
(button
2025-08-03 23:24:57 -04:00
("class" "hover_left_bar raised justify-start w_full poll_option")
2025-06-04 17:21:46 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "hover_left_bar raised justify-start w_full poll_option")
2025-06-04 17:21:46 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "hover_left_bar raised justify-start w_full poll_option")
2025-06-04 17:21:46 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "flex w_full flex_wrap gap_2")
(span ("class" "notification chip") (text "{{ total }} votes"))
2025-06-05 16:34:07 -04:00
(text "{% if not poll[2] -%}")
(span
2025-08-03 23:24:57 -04:00
("class" "notification chip flex items_center gap_1")
2025-07-15 00:08:49 -04:00
(text "Expires in")
(span
("class" "poll_date")
("data-created" "{{ poll[0].created }}")
2025-06-05 16:34:07 -04:00
("data-expires" "{{ poll[0].expires }}")))
(text "{%- endif %}")))
2025-06-01 12:25:33 -04:00
(text "{%- endmacro %}")
(text "{% macro community_info(community) %}")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest flex flex_col")
(div
("id" "bio")
("class" "card small no_p_margin")
(text "{{ community.context.description|markdown|safe }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
(div
2025-08-03 23:24:57 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "w_full flex justify_between items_center")
(span
("class" "notification chip")
(text "Created "))
(span
("class" "date")
(text "{{ community.created }}")))
(div
2025-08-03 23:24:57 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "w_full flex justify_between items_center")
(span
("class" "notification chip")
(text "Posts"))
(a
("href" "/community/{{ community.title }}")
(text "{{ community.post_count }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "w_full flex justify_between items_center")
(span
("class" "notification chip")
(text "Score"))
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
(b
(text "{{ community.likes - community.dislikes }}"))
(text "{% if user -%}")
(div
2025-08-03 23:24:57 -04:00
("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
2025-08-03 23:24:57 -04:00
("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\" }}")))
2025-08-04 12:12:04 -04:00
(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\" }}")))
2025-08-04 12:12:04 -04:00
(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 %}")
2025-06-10 22:02:06 -04:00
(text "{% macro ticket(post, owner) -%}")
(div
("href" "/post/{{ post.id }}")
2025-08-03 23:24:57 -04:00
("class" "card secondary w-fill flex flex_col gap_2")
2025-06-10 22:02:06 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 items_center")
2025-06-10 22:02:06 -04:00
; 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
2025-06-10 22:12:22 -04:00
(span ("class" "date") (text "{{ post.created }}"))
; pinned
(text "{% if post.context.is_pinned -%}")
(icon (text "pin"))
(text "{%- endif %}"))
2025-06-10 22:02:06 -04:00
; post title
(a
("href" "/post/{{ post.id }}")
2025-08-03 23:24:57 -04:00
("class" "flush flex gap_2 items_center")
2025-06-10 22:02:06 -04:00
; open/closed icon
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
(span
("title" "Open")
2025-08-03 23:24:57 -04:00
("class" "flex items_center green")
2025-06-10 22:02:06 -04:00
(text "{{ icon \"circle-dot\" }}"))
(text "{% else %}")
(span
("title" "Closed")
2025-08-03 23:24:57 -04:00
("class" "flex items_center purple")
2025-06-10 22:02:06 -04:00
(text "{{ icon \"circle-check\" }}"))
(text "{%- endif %} {%- endif %}")
(h4
("class" "no_p_margin")
(text "{{ post.title|markdown|safe }}"))))
(text "{%- endmacro %}")
2025-06-15 11:52:44 -04:00
(text "{% macro stack_listing(stack) -%}")
(a
("href" "/stacks/{{ stack.id }}")
2025-08-03 23:24:57 -04:00
("class" "card secondary flex flex_col gap_2")
2025-06-15 11:52:44 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-15 11:52:44 -04:00
(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 %}")
2025-06-19 15:48:04 -04:00
(text "{% macro journal_listing(journal, notes, selected_note, selected_journal, view_mode=false, owner=false) -%}")
(text "{% if selected_journal != journal.id -%}")
; not selected
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_row gap_1")
2025-06-19 15:48:04 -04:00
(a
("href" "/journals/{{ journal.id }}/0?view={{ view_mode }}")
2025-08-03 23:24:57 -04:00
("class" "button justify-start lowered w_full")
2025-06-19 15:48:04 -04:00
(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")
2025-07-13 23:15:00 -04:00
("title" "More options")
2025-06-19 15:48:04 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex flex_row gap_1")
2025-06-19 15:48:04 -04:00
(button
2025-08-03 23:24:57 -04:00
("class" "justify-start lowered w_full")
2025-06-19 15:48:04 -04:00
(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")
2025-07-13 23:15:00 -04:00
("title" "More options")
2025-06-19 15:48:04 -04:00
(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 %}"))
2025-06-21 19:44:28 -04:00
(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 %}")
2025-06-19 15:48:04 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-19 15:48:04 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "lowered justify-start w_full")
2025-06-19 15:48:04 -04:00
("onclick" "create_note()")
(icon (text "plus"))
(str (text "journals:action.create_note")))
(text "{%- endif %}")
; note listings
2025-06-21 19:44:28 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "button w_full justify-start raised w_full")
2025-06-21 19:44:28 -04:00
(icon (text "folder"))
(text "{{ dir[2] }}"))
2025-06-19 15:48:04 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-21 19:44:28 -04:00
("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 %}")
2025-06-19 15:48:04 -04:00
2025-06-21 19:44:28 -04:00
; 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
2025-08-03 23:24:57 -04:00
("class" "flex flex_row gap_1")
2025-06-21 19:44:28 -04:00
("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 %}")
2025-08-03 23:24:57 -04:00
("class" "button justify-start w_full {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}")
2025-06-21 19:44:28 -04:00
(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")
2025-07-13 23:15:00 -04:00
("title" "More options")
2025-06-21 19:44:28 -04:00
(text "{{ icon \"ellipsis\" }}"))
2025-06-19 15:48:04 -04:00
(div
2025-06-21 19:44:28 -04:00
("class" "inner")
2025-06-19 15:48:04 -04:00
(button
2025-06-21 19:44:28 -04:00
("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")))
2025-06-26 02:56:22 -04:00
(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 %}")
2025-06-21 19:44:28 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex gap_1 flex_wrap")
2025-06-21 19:44:28 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex flex_row gap_1")
2025-06-21 19:44:28 -04:00
(button
2025-08-03 23:24:57 -04:00
("class" "justify-start lowered w_full")
2025-06-21 19:44:28 -04:00
(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")
2025-07-13 23:15:00 -04:00
("title" "More options")
2025-06-21 19:44:28 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-21 19:44:28 -04:00
("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()")
2025-08-03 23:24:57 -04:00
("class" "justify-start lowered w_full")
2025-06-21 19:44:28 -04:00
(icon (text "folder-open"))
(text "{{ dir[2] }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-21 19:44:28 -04:00
("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) }}")
2025-06-19 19:27:42 -04:00
(text "{%- endif %} {% endfor %}"))
2025-06-19 15:48:04 -04:00
(text "{%- endmacro %}")
2025-07-03 21:56:21 -04:00
(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"))
2025-07-08 13:35:23 -04:00
(li
(text "Create infinite Littleweb sites"))
(li
(text "Create infinite Littleweb domains"))
2025-07-03 21:56:21 -04:00
(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
2025-07-12 16:30:57 -04:00
("href" "{{ config.stripe.payment_links.supporter }}?client_reference_id={{ user.id }}")
2025-07-03 21:56:21 -04:00
("class" "button")
("target" "_blank")
2025-07-18 14:52:00 -04:00
(text "Become a supporter ({{ config.stripe.price_texts.supporter }})"))
2025-07-03 21:56:21 -04:00
(span
("class" "fade")
(text "Please use your")
(b
(text " real email "))
2025-07-18 14:52:00 -04:00
(text "when completing payment. It is required to manage your billing settings."))
2025-07-03 21:56:21 -04:00
(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 %}")
2025-07-18 14:52:00 -04:00
(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)"))
2025-07-18 14:52:00 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card w_full supporter_ad")
2025-07-18 14:52:00 -04:00
("ui_ident" "supporter_ad")
("onclick" "window.location.href = '/settings#/account/billing'")
(div
2025-08-03 23:24:57 -04:00
("class" "card w_full flex flex_wrap items_center gap_2 justify_between")
2025-07-18 14:52:00 -04:00
(b
(text "{{ body }}"))
(a
("href" "/settings#/account/billing")
("class" "button small")
(icon (text "arrow-right"))
(span
(str (text "dialog:action.continue"))))))
(text "{%- endif %} {%- endmacro %}")
2025-08-02 16:04:50 -04:00
(text "{% macro letter_listing(letter, owner) -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered flex gap_2 flex_row")
2025-08-02 16:04:50 -04:00
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col")
2025-08-02 16:04:50 -04:00
(text "{{ self::full_username(user=owner) }}")
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-08-02 16:04:50 -04:00
; read status
(text "{% if user.id in letter.read_by -%}")
2025-08-03 23:24:57 -04:00
(div ("class" "flex items_center green") (icon (text "mail-check")))
2025-08-02 16:04:50 -04:00
(text "{% else %}")
2025-08-03 23:24:57 -04:00
(div ("class" "flex items_center") (icon (text "mail")))
2025-08-02 16:04:50 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-08-02 16:04:50 -04:00
(text "{% if show_subject -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "card flex gap_2 flex_row")
2025-08-02 16:04:50 -04:00
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col")
2025-08-02 16:04:50 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex flex_wrap gap_2")
2025-08-02 16:04:50 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card small flex gap_2 flex_row")
2025-08-02 16:04:50 -04:00
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"24px\") }}"))
(text "{{ self::full_username(user=owner) }}"))
(text "{%- endif %}")
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-08-02 16:04:50 -04:00
(text "{{ letter.content|markdown|safe }}")
(hr)
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 items_center")
2025-08-02 16:04:50 -04:00
(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")
2025-08-02 17:29:26 -04:00
("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 }}")
2025-08-02 16:04:50 -04:00
("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 %}")
2025-08-04 12:12:04 -04:00
(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 %}")
2025-08-04 14:24:25 -04:00
(text "{% macro topic_post_display(post, owner, is_pinned=false, community=false) -%}")
2025-08-04 12:12:04 -04:00
(tr
2025-08-04 14:24:25 -04:00
(text "{% if community %}")
(td
(a
("href" "/community/{{ community.title }}/topic/{{ post.topic }}")
("class" "flex gap_1 items_center")
(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 %}")
2025-08-04 12:12:04 -04:00
(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) }}"))
2025-08-04 23:29:24 -04:00
(td (span ("class" "date short") (text "{{ post.created }}"))))
2025-08-04 12:12:04 -04:00
(text "{%- endmacro %}")