2025-06-01 12:25:33 -04:00
( text "{% macro avatar(username, size=\"24px\", selector_type=\"username\") -%}" )
( img
( "title" "{{ username }}'s avatar" )
( "src" "/api/v1/auth/user/{{ username }}/avatar?selector_type={{ selector_type }}" )
( "alt" "@{{ username }}" )
( "class" "avatar shadow" )
( "loading" "lazy" )
( "style" "--size: {{ size }}" ) )
( text "{%- endmacro %} {% macro community_avatar(id, community=false, size=\"24px\") -%} {% if community -%}" )
( img
( "src" "/api/v1/communities/{{ id }}/avatar" )
( "alt" "{{ community.title }}'s avatar" )
( "class" "avatar shadow" )
( "loading" "lazy" )
( "style" "--size: {{ size }}" ) )
( text "{% else %}" )
( img
( "src" "/api/v1/communities/{{ id }}/avatar" )
( "alt" "{{ id }}'s avatar" )
( "class" "avatar shadow" )
( "loading" "lazy" )
( "style" "--size: {{ size }}" ) )
( text "{%- endif %} {%- endmacro %} {% macro banner(username, border_radius=\"var(--radius)\") -%}" )
( img
( "title" "{{ username }}'s banner" )
( "src" "/api/v1/auth/user/{{ username }}/banner" )
( "alt" "@{{ username }}'s banner" )
( "class" "banner shadow w-full" )
( "loading" "lazy" )
( "style" "border-radius: {{ border_radius }};" ) )
( text "{%- endmacro %} {% macro community_banner(id, community=false) -%} {% if community %}" )
( img
( "src" "/api/v1/communities/{{ id }}/banner" )
( "alt" "{{ community.title }}'s banner" )
( "class" "banner shadow" )
( "loading" "lazy" ) )
( text "{% else %}" )
( img
( "src" "/api/v1/communities/{{ id }}/banner" )
( "alt" "{{ id }}'s banner" )
( "class" "banner shadow" )
( "loading" "lazy" ) )
( text "{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%}" )
( a
( "class" "card secondary w-full flex items-center gap-4" )
2025-06-09 16:45:36 -04:00
( "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
( "class" "flex flex-col" )
2025-06-09 16:45:36 -04:00
( div
( "class" "flex gap-2 items-center" )
( text "{% if community.is_forge -%}" )
( icon ( text "anvil" ) )
( text "{%- endif %}" )
( h3
( "class" "name lg:long" )
( text "{{ community.context.display_name }}" ) ) )
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) -%}" )
( button
( "title" "Like" )
2025-06-12 13:53:23 -04:00
( "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 -%}" )
( button
( "title" "Dislike" )
2025-06-12 13:53:23 -04:00
( "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 %}" ) )
( text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%}" )
( div
( "class" "flex items-center" )
( a
( "href" "/@{{ user.username }}" )
( "class" "flush" )
( "style" "font-weight: 600" )
( "target" "_top" )
( text "{{ self::username(user=user) }}" ) )
( text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}" )
( span
( "title" "Verified" )
( "style" "color: var(--color-primary)" )
( "class" "flex items-center" )
( text "{{ icon \"badge-check\" }}" ) )
( text "{%- endif %}" ) )
( text "{%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}" )
( div
( "style" "display: contents" )
( text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}" ) )
2025-06-08 15:34:29 -04:00
( text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false, poll=false, dont_show_title=false) -%} {% if community and show_community and community.id != config.town_square or question %}" )
2025-06-01 12:25:33 -04:00
( div
2025-06-17 01:52:17 -04:00
( "class" "card-nest post_outer:{{ post.id }}" )
2025-06-01 12:25:33 -04:00
( text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}" )
( div
( "class" "card small" )
( a
( "href" "/api/v1/communities/find/{{ post.community }}" )
( "class" "flush flex gap-1 items-center" )
( text "{{ self::community_avatar(id=post.community, community=community) }}" )
( b
( text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}" ) )
( text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- endif %}" ) ) )
( text "{%- endif %} {%- endif %}" )
( div
2025-06-17 01:52:17 -04:00
( "class" "card flex flex-col 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 }}" )
( "hook" "verify_emojis" )
( div
( "class" "w-full flex gap-2" )
( text "{% if not expect_repost -%}" )
( a
( "href" "/@{{ owner.username }}" )
( text "{{ self::avatar(username=owner.username, size=\"52px\", selector_type=\"username\") }}" ) )
( text "{%- endif %}" )
( div
( "class" "flex flex-col w-full gap-1 post_right {% if expect_repost -%}repost{%- endif %}" )
( div
( "class" "flex flex-wrap gap-2 items-center" )
( text "{% if expect_repost -%}" )
( a
( "href" "/@{{ owner.username }}" )
( text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}" ) )
( text "{%- endif %}" )
( span
( "class" "name" )
( text "{{ self::full_username(user=owner) }}" ) )
( text "{% if post.context.edited != 0 -%}" )
( div
( "class" "flex" )
( span
( "class" "fade date" )
( text "{{ post.context.edited }}" ) )
( sup
( "title" "Edited" )
( text "*" ) ) )
( text "{% else %}" )
( span
( "class" "fade date" )
( text "{{ post.created }}" ) )
( text "{%- endif %} {% if post.context.is_nsfw -%}" )
( span
( "title" "NSFW post" )
( "class" "flex items-center" )
( "style" "color: var(--color-primary)" )
( text "{{ icon \"square-asterisk\" }}" ) )
2025-06-15 16:09:02 -04:00
( text "{%- endif %} {% if post.stack -%}" )
2025-06-15 19:04:56 -04:00
( a
2025-06-15 16:09:02 -04:00
( "title" "Posted to a stack you're in" )
2025-06-15 19:04:56 -04:00
( "class" "flex items-center flush" )
2025-06-15 16:09:02 -04:00
( "style" "color: var(--color-primary)" )
2025-06-15 19:04:56 -04:00
( "href" "/stacks/{{ post.stack }}" )
2025-06-15 16:09:02 -04:00
( text "{{ icon \"layers\" }}" ) )
( text "{%- endif %} {% if community and community.is_forge -%} {% if post.is_open -%}" )
2025-06-10 22:02:06 -04:00
( 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 %}" )
2025-06-01 12:25:33 -04:00
( 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 %}" ) )
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 }}" )
( "class" "no_p_margin post_content" )
( "hook" "long" )
( text "{{ post.title }}" ) )
2025-06-12 13:53:23 -04:00
( button ( "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
( "id" "post-content:{{ post.id }}" )
2025-06-03 17:42:29 -04:00
( "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
2025-06-01 12:25:33 -04:00
( text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered red flex items-center gap-2" )
2025-06-01 12:25:33 -04:00
( text "{{ icon \"frown\" }}" )
( span
2025-06-17 14:28:18 -04:00
( 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-06-12 13:53:23 -04:00
( "class" "card tiny lowered w-full" )
2025-06-01 12:25:33 -04:00
( summary
2025-06-03 17:42:29 -04:00
( "class" "red w-full" )
2025-06-01 12:25:33 -04:00
( b
( text "{{ post.context.content_warning }}" ) ) )
( div
( "class" "flex flex-col gap-2" )
( span
( "id" "post-content:{{ post.id }}" )
2025-06-03 17:42:29 -04:00
( "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
2025-06-01 12:25:33 -04:00
( text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}" )
( div
2025-06-12 13:53:23 -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
( "class" "flex flex-wrap gap-2 fade" )
( text "{% for tag in post.context.tags %}" )
( a
( "href" "/@{{ owner.username }}?tag={{ tag }}" )
( "class" "flush fade" )
( text "#{{ tag }}" ) )
( text "{% endfor %}" ) ) ) )
( div
( "class" "flex justify-between items-center gap-2 w-full" )
( text "{% if user -%}" )
( div
( "class" "flex gap-1 reactions_box" )
( "hook" "check_reactions" )
( "hook-arg:id" "{{ post.id }}" )
2025-06-15 18:22:29 -04:00
( 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) }} {%- endif %} {%- endif %} {% if post.context.repost and post.context.repost.reposting -%}" )
2025-06-01 12:25:33 -04:00
( 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-06-03 18:09:12 -04:00
( text "{% if post.context.comments_enabled %}" )
2025-06-01 12:25:33 -04:00
( a
( "href" "/post/{{ post.id }}" )
( "class" "button camo small" )
( text "{{ icon \"message-circle\" }}" )
( span
( text "{{ post.comment_count }}" ) ) )
2025-06-03 18:09:12 -04:00
( text "{% endif %}" )
2025-06-01 12:25:33 -04:00
( a
( "href" "/post/{{ post.id }}" )
( "class" "button camo small" )
( "target" "_blank" )
( text "{{ icon \"external-link\" }}" ) )
( text "{% if user -%}" )
( div
( "class" "dropdown" )
( button
( "class" "camo small" )
( "onclick" "trigger('atto::hooks::dropdown', [event])" )
( "exclude" "dropdown" )
( text "{{ icon \"ellipsis\" }}" ) )
( div
( "class" "inner" )
( text "{% if config.town_square and post.context.reposts_enabled %}" )
( b
( "class" "title" )
( text "{{ text \"general:label.share\" }}" ) )
( button
2025-06-16 18:32:22 -04:00
( "onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}', true])" )
2025-06-01 12:25:33 -04:00
( 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\" }}" ) ) )
2025-06-10 22:02:06 -04:00
( text "{%- endif %}" )
( text "{% if community and community.is_forge -%} {% if post.is_open -%}" )
( button
( "class" "green" )
( "onclick" "trigger('me::update_open', ['{{ post.id }}', false])" )
( text "{{ icon \"circle-check\" }}" )
( span
( text "{{ text \"forge:action.close\" }}" ) ) )
( text "{% else %}" )
( button
( "class" "purple" )
( "onclick" "trigger('me::update_open', ['{{ post.id }}', true])" )
( text "{{ icon \"refresh-ccw-dot\" }}" )
( span
( text "{{ text \"forge:action.reopen\" }}" ) ) )
( text "{%- endif %} {%- endif %}" )
( text "{% if user.id != post.owner -%}" )
2025-06-01 12:25:33 -04:00
( b
( "class" "title" )
( text "{{ text \"general:label.safety\" }}" ) )
( button
( "class" "red" )
( "onclick" "trigger('me::report', ['{{ post.id }}', 'post'])" )
( text "{{ icon \"flag\" }}" )
( span
( text "{{ text \"general:action.report\" }}" ) ) )
( text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}" )
( b
( "class" "title" )
( text "{{ text \"general:action.manage\" }}" ) )
( text "{% if user.id == post.owner -%}" )
( a
( "href" "/post/{{ post.id }}#/edit" )
( text "{{ icon \"pen\" }}" )
( span
( text "{{ text \"communities:label.edit_content\" }}" ) ) )
( text "{%- endif %}" )
( a
( "href" "/post/{{ post.id }}#/configure" )
( text "{{ icon \"settings\" }}" )
( span
( text "{{ text \"communities:action.configure\" }}" ) ) )
( text "{% if not post.is_deleted -%}" )
( button
( "class" "red" )
( "onclick" "trigger('me::remove_post', ['{{ post.id }}'])" )
( text "{{ icon \"trash\" }}" )
( span
( text "{{ text \"general:action.delete\" }}" ) ) )
( text "{%- endif %} {% if is_helper and post.is_deleted -%}" )
( button
( "class" "red" )
( "onclick" "trigger('me::purge_post', ['{{ post.id }}'])" )
( text "{{ icon \"trash-2\" }}" )
( span
( text "{{ text \"general:action.purge\" }}" ) ) )
( button
( "class" "green" )
( "onclick" "trigger('me::restore_post', ['{{ post.id }}'])" )
( text "{{ icon \"undo\" }}" )
( span
( text "{{ text \"general:action.restore\" }}" ) ) )
( text "{%- endif %} {%- endif %}" ) ) )
( text "{%- endif %}" ) ) ) )
( text "{% if community and show_community and community.id != config.town_square or question %}" ) )
( text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0%}" )
( div
( "class" "media_gallery gap-2" )
( text "{% for upload in upload_ids %}" )
( img
( "src" "/api/v1/uploads/{{ upload }}" )
( "alt" "Image upload" )
( "onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])" ) )
( text "{% endfor %}" ) )
( text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}" )
( div
( "class" "w-full card-nest" )
( div
( "class" "card small notif_title flex items-center" )
( text "{% if not notification.read -%}" )
( svg
( "width" "24" )
( "height" "24" )
( "viewBox" "0 0 24 24" )
( "style" "fill: var(--color-link)" )
( circle
( "cx" "12" )
( "cy" "12" )
( "r" "6" ) ) )
( text "{%- endif %}" )
( b
( "class" "no_p_margin" )
( text "{{ notification.title|markdown|safe }}" ) ) )
( div
( "class" "card notif_content flex flex-col gap-2" )
( span
( "class" "no_p_margin" )
( text "{{ notification.content|markdown|safe }}" ) )
( div
( "class" "card secondary w-full flex flex-wrap gap-2" )
( text "{% if notification.read -%}" )
( button
2025-06-12 13:53:23 -04:00
( "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
2025-06-12 13:53:23 -04:00
( "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
2025-06-12 13:53:23 -04:00
( "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\" }}" ) ) ) ) ) )
( text "{%- endmacro %} {% macro user_card(user) -%}" )
( a
( "class" "card-nest w-full" )
( "href" "/@{{ user.username }}" )
( div
( "class" "card small" )
( "style" "padding: 0" )
( text "{{ self::banner(username=user.username, border_radius=\"0px\") }}" ) )
( div
( "class" "card secondary flex items-center gap-4" )
( text "{{ self::avatar(username=user.username, size=\"48px\") }}" )
( div
( "class" "flex items-center" )
( b
( text "{{ self::username(user=user) }}" ) )
( text "{{ self::online_indicator(user=user) }}" ) ) ) )
( text "{%- endmacro %} {% macro pagination(page=0, items=0, key=\"\", value=\"\") -%}" )
( div
( "class" "flex justify-between gap-2 w-full" )
( text "{% if page > 0 -%}" )
( a
2025-06-12 13:53:23 -04:00
( "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
2025-06-12 13:53:23 -04:00
( "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(--color-green)" )
( circle
( "cx" "12" )
( "cy" "12" )
( "r" "6" ) ) ) )
( div
( "style" "display: none" )
( "hook_ui_ident" "idle" )
( "title" "Idle" )
( svg
( "width" "24" )
( "height" "24" )
( "viewBox" "0 0 24 24" )
( "style" "fill: var(--color-yellow)" )
( circle
( "cx" "12" )
( "cy" "12" )
( "r" "6" ) ) ) )
( div
( "style" "display: none" )
( "hook_ui_ident" "offline" )
( "title" "Offline" )
( svg
( "width" "24" )
( "height" "24" )
( "viewBox" "0 0 24 24" )
( "style" "fill: hsl(0, 0%, 50%)" )
( circle
( "cx" "12" )
( "cy" "12" )
( "r" "6" ) ) ) ) )
( text "{% else %}" )
( div
( "title" "Offline" )
( "style" "display: contents" )
( svg
( "width" "24" )
( "height" "24" )
( "viewBox" "0 0 24 24" )
( "style" "fill: hsl(0, 0%, 50%)" )
( circle
( "cx" "12" )
( "cy" "12" )
( "r" "6" ) ) ) )
( text "{%- endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}" )
( style
( text " :root, * {
--hue: {{ user.settings.theme_hue }} !important ;
} " ) )
( text "{%- endif %} {% if user.settings.theme_sat -%}" )
( style
( text " :root, * {
--sat: {{ user.settings.theme_sat }} !important ;
} " ) )
( text "{%- endif %} {% if user.settings.theme_lit -%}" )
( style
( text " :root, * {
--lit: {{ user.settings.theme_lit }} !important ;
} " ) )
( text "{%- endif %} {% if theme_preference -%}" )
( script
( text " function match_user_theme ( ) {
const pref = \"{{ theme_preference }}\".toLowerCase ( ) ;
if ( pref === \"auto\" ) {
return ;
}
document.documentElement.className = pref ;
}
setTimeout ( ( ) => {
match_user_theme ( ) ;
}, 150 ) ;"))
( text "{%- endif %}" )
( div
( "style" "display: none;" )
( text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }} {% if user.permissions|has_supporter -%}" )
( style
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 ;
} " ) )
( text "{%- endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false, profile=false) -%}" )
( div
2025-06-01 19:26:55 -04:00
( "class" "card {% if secondary -%}secondary{%- endif %} flex gap-2" )
2025-06-01 12:25:33 -04:00
( text "{% if owner.id == 0 -%}" )
( span
( text "{% if profile and profile.settings.anonymous_avatar_url -%}" )
( img
( "src" "/api/v1/util/proxy?url={{ profile.settings.anonymous_avatar_url }}" )
( "alt" "anonymous' avatar" )
( "class" "avatar shadow" )
( "loading" "lazy" )
( "style" "--size: 52px" ) )
( text "{% else %} {{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }} {%- endif %}" ) )
( text "{% else %}" )
( a
( "href" "/@{{ owner.username }}" )
( text "{{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }}" ) )
( text "{%- endif %}" )
( div
2025-06-01 19:26:55 -04:00
( "class" "flex flex-col gap-1 w-full" )
2025-06-01 12:25:33 -04:00
( div
( "class" "flex items-center gap-2 flex-wrap" )
( span
( "class" "name" )
( text "{% if owner.id == 0 -%} {% if profile and profile.settings.anonymous_username -%}" )
( span
( "class" "flex items-center gap-2" )
( b
( text "{{ profile.settings.anonymous_username }}" ) )
( span
( "title" "Anonymous user" )
( "class" "flex items-center" )
( "style" "color: var(--color-primary)" )
( text "{{ icon \"drama\" }}" ) ) )
( text "{% else %}" )
( b
( text "anonymous" ) )
( text "{%- endif %} {% else %} {{ self::full_username(user=owner) }} {%- endif %}" ) )
( span
( "class" "date" )
( text "{{ question.created }}" ) )
( span
( "title" "Question" )
( "class" "flex items-center" )
( "style" "color: var(--color-primary)" )
( text "{{ icon \"message-circle-heart\" }}" ) )
( text "{% if question.context.is_nsfw -%}" )
( span
( "title" "NSFW community" )
( "class" "flex items-center" )
( "style" "color: var(--color-primary)" )
( text "{{ icon \"square-asterisk\" }}" ) )
( text "{%- endif %} {% if question.community > 0 and show_community -%}" )
( a
( "href" "/api/v1/communities/find/{{ question.community }}" )
( "class" "flex items-center" )
( text "{{ self::community_avatar(id=question.community, size=\"24px\") }}" ) )
( text "{%- endif %} {% if question.is_global -%}" )
( a
( "class" "notification chip" )
( "href" "/question/{{ question.id }}" )
( text "{{ question.answer_count }} answers" ) )
( text "{%- endif %}" ) )
( span
( "class" "no_p_margin" )
( "style" "font-weight: 500" )
( text "{{ question.content|markdown|safe }}" ) )
2025-06-01 19:26:55 -04:00
; anonymous user ip thing
; this is only shown if the post author is anonymous AND we are a helper
( text "{% if is_helper and owner.id == 0 %}" )
( details
2025-06-12 13:53:23 -04:00
( "class" "card tiny lowered w-full" )
2025-06-01 19:26:55 -04:00
( summary
( "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 }}" ) ) ) )
2025-06-01 19:26:55 -04:00
( text "{% endif %}" )
; ...
2025-06-01 12:25:33 -04:00
( div
( "class" "flex gap-2 items-center justify-between" ) ) ) )
( text "{%- endmacro %} {% macro create_question_form(receiver=\"0\", community=\"\", header=\"\", is_global=false) -%}" )
( div
( "class" "card-nest" )
( div
( "class" "card small flex items-center gap-2" )
( text "{{ icon \"message-circle-heart\" }}" )
( span
( "class" "no_p_margin" )
( text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}" ) ) )
( form
( "class" "card flex flex-col gap-2" )
( "onsubmit" "create_question_from_form(event)" )
( div
( "class" "flex flex-col gap-1" )
( label
( "for" "content" )
( text "{{ text \"communities:label.content\" }}" ) )
( textarea
( "type" "text" )
( "name" "content" )
( "id" "content" )
( "placeholder" "content" )
( "required" "" )
( "minlength" "2" )
( "maxlength" "4096" ) ) )
( button
( "class" "primary" )
( text "{{ text \"communities:action.create\" }}" ) ) ) )
( script
( text " async function create_question_from_form ( e ) {
e.preventDefault ( ) ;
await trigger ( \"atto::debounce\", [\"questions::create\"] ) ;
fetch ( \"/api/v1/questions\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( {
content: e.target.content.value,
receiver: \"{{ receiver }}\",
community: \"{{ community }}\",
is_global: \"{{ is_global }}\" == \"true\",
} ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
if ( res.ok ) {
e.target.reset ( ) ;
}
} ) ;
} " ) )
( text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}" )
( div
( "class" "card-nest" )
( text "{{ self::question(question=question[0], owner=question[1], show_community=show_community) }}" )
( div
( "class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}" )
( div
( "class" "flex gap-1 reactions_box" )
( "hook" "check_reactions" )
( "hook-arg:id" "{{ question[0].id }}" )
( text "{{ self::likes(id=question[0].id, asset_type=\"Question\", likes=question[0].likes, dislikes=question[0].dislikes, secondary=false) }}" ) )
( div
( "class" "flex gap-1 buttons_box" )
( a
( "href" "/question/{{ question[0].id }}" )
( "class" "button small" )
( text "{{ icon \"external-link\" }} {% if user -%}" )
( span
( text "{{ text \"requests:label.answer\" }}" ) )
( text "{% else %}" )
( span
( text "{{ text \"general:action.open\" }}" ) )
( text "{%- endif %}" ) )
( text "{% if user -%} {% if can_manage_questions or is_helper or question[1].id == user.id %}" )
( div
( "class" "dropdown" )
( button
( "class" "camo small" )
( "onclick" "trigger('atto::hooks::dropdown', [event])" )
( "exclude" "dropdown" )
( text "{{ icon \"ellipsis\" }}" ) )
( div
( "class" "inner" )
( button
( "class" "camo small red" )
( "onclick" "trigger('me::remove_question', ['{{ question[0].id }}'])" )
( text "{{ icon \"trash\" }}" )
( span
( text "{{ text \"general:action.delete\" }}" ) ) ) ) )
( text "{%- endif %} {%- endif %}" ) ) ) )
( text "{%- endmacro %} {% macro spotify_playing(state, size=\"60px\") -%} {% if state and state.data %}" )
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center justify-between gap-2 small" )
( div
( "class" "flex items-center gap-2" )
( b
( text "Listening on" ) )
( text "{{ icon \"spotify\" }}" ) )
( span
( "class" "fade date short" )
( text "{{ state.data.timestamp }}" ) ) )
( div
( "class" "card secondary flex gap-2" )
( a
( "href" "{{ state.external_urls.album }}" )
( img
( "src" "{{ state.external_urls.album_img }}" )
( "alt" "Album cover" )
( "loading" "lazy" )
( "class" "avatar" )
( "style" "--size: {{ size }}" ) ) )
( div
( "class" "flex flex-col" )
( h5
( "class" "w-full" )
( a
( "href" "{{ state.external_urls.track }}" )
( "class" "flush" )
( text "{{ state.data.track }}" ) ) )
( span
( "class" "fade" )
( a
( "href" "{{ state.external_urls.artist }}" )
( "class" "flush" )
( text "{{ state.data.artist }}" ) ) )
( span
( "hook" "spotify_time_text" )
( "hook-arg:updated" "{{ state.data.timestamp }}" )
( "hook-arg:progress" "{{ state.data.progress_ms }}" )
( "hook-arg:duration" "{{ state.data.duration_ms }}" )
( "hook-arg:display" "full" ) ) ) ) )
( text "{%- endif %} {%- endmacro %} {% macro last_fm_playing(state, size=\"60px\") -%} {% if state and state.data %}" )
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center justify-between gap-2 small" )
( div
( "class" "flex items-center gap-2" )
( b
( text "Listening on" ) )
( text "{{ icon \"last_fm\" }}" ) )
( span
( "class" "fade date short" )
( text "{{ state.data.timestamp }}" ) ) )
( div
( "class" "card secondary flex gap-2" )
( a
( "href" "{{ state.external_urls.track }}" )
( img
( "src" "{{ state.external_urls.track_img }}" )
( "alt" "Track cover" )
( "loading" "lazy" )
( "class" "avatar" )
( "style" "--size: {{ size }}" ) ) )
( div
( "class" "flex flex-col" )
( h5
( "class" "w-full" )
( a
( "href" "{{ state.external_urls.track }}" )
( "class" "flush" )
( text "{{ state.data.track }}" ) ) )
( span
( "class" "fade" )
( a
( "href" "{{ state.external_urls.artist }}" )
( "class" "flush" )
( text "{{ state.data.artist }}" ) ) )
( text "{% if state.data.duration_ms and state.data.duration_ms != \"0\" -%}" )
( span
( "hook" "spotify_time_text" )
( "hook-arg:updated" "{{ state.data.timestamp }}" )
( "hook-arg:progress" "25000" )
( "hook-arg:duration" "{{ state.data.duration_ms }}" )
( "hook-arg:display" "full" ) )
( text "{%- endif %}" ) ) ) )
( text "{%- endif %} {%- endmacro %} {% macro connection_icon(key) -%}" )
( div
( "style" "display: contents;" )
( text "{% if key == \"Spotify\" -%} {{ icon \"spotify\" }} {% elif key == \"LastFm\" %} {{ icon \"last_fm\" }} {%- endif %}" ) )
( text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %} {% macro message_actions(can_manage_message, owner, message, owner) -%}" )
( div
( "class" "dropdown" )
( button
( "class" "camo small" )
( "onclick" "trigger('atto::hooks::dropdown', [event])" )
( "exclude" "dropdown" )
( text "{{ icon \"ellipsis\" }}" ) )
( div
( "class" "inner" )
( text "{% if can_manage_message or (user and user.id == message.owner) -%}" )
( button
( "class" "red" )
( "onclick" "delete_message('{{ message.id }}')" )
( text "{{ icon \"trash\" }}" )
( span
( text "{{ text \"general:action.delete\" }}" ) ) )
( text "{%- endif %}" )
( button
( "onclick" "window.location.href = `${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`" )
( text "{{ icon \"external-link\" }}" )
( span
( text "{{ text \"general:action.open\" }}" ) ) )
( button
( "onclick" "trigger('atto::copy_text', [`${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`])" )
( text "{{ icon \"copy\" }}" )
( span
( text "{{ text \"general:action.copy_link\" }}" ) ) )
( button
( "onclick" "mention_user('{{ owner.username }}')" )
( text "{{ icon \"at-sign\" }}" )
( span
( text "{{ text \"chats:action.mention_user\" }}" ) ) ) ) )
( text "{%- endmacro %} {% macro message(user, message, can_manage_message=false, grouped=false) -%}" )
( div
( "class" "card secondary message flex gap-2 {% if grouped -%}grouped{%- endif %}" )
( "id" "message-{{ message.id }}" )
( text "{% if not grouped -%}" )
( a
( "href" "/@{{ user.username }}" )
( "target" "_top" )
( text "{{ self::avatar(username=user.username, size=\"42px\") }}" ) )
( text "{%- endif %}" )
( div
( "class" "flex flex-col gap-1 w-full" )
( text "{% if not grouped -%}" )
( div
( "class" "flex gap-2 w-full justify-between flex-wrap" )
( div
( "class" "flex gap-2" )
( text "{{ self::full_username(user=user) }} {% if message.edited != message.created %}" )
( span
( "class" "date" )
( text "{{ message.edited }}" )
( sup
( "title" "Edited" )
( text "*" ) ) )
( text "{% else %}" )
( span
( "class" "date" )
( text "{{ message.created }}" ) )
( text "{%- endif %}" ) )
( div
( "class" "flex gap-2 hidden" )
( text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}" ) ) )
( text "{%- endif %}" )
( div
( "class" "flex w-full gap-2 justify-between" )
( span
( "class" "no_p_margin" )
( text "{{ message.content|markdown|safe }}" ) )
( text "{% if grouped -%}" )
( div
( "class" "hidden" )
( text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}" ) )
( text "{%- endif %}" ) ) ) )
( text "{%- endmacro %} {% macro user_menu() -%}" )
( div
( "class" "inner" )
( b
( "class" "title" )
( text "{{ user.username }}" ) )
( a
( "href" "/@{{ user.username }}" )
( text "{{ icon \"circle-user-round\" }}" )
( span
( text "{{ text \"auth:link.my_profile\" }}" ) ) )
( a
( "href" "/settings" )
( text "{{ icon \"settings\" }}" )
( span
( text "{{ text \"auth:link.settings\" }}" ) ) )
( text "{% if is_helper -%}" )
( b
( "class" "title" )
( text "{{ text \"general:label.mod\" }}" ) )
( a
( "href" "/mod_panel/audit_log" )
( text "{{ icon \"scroll-text\" }}" )
( span
( text "{{ text \"general:link.audit_log\" }}" ) ) )
( a
( "href" "/mod_panel/reports" )
( text "{{ icon \"flag\" }}" )
( span
( text "{{ text \"general:link.reports\" }}" ) ) )
( a
( "href" "/mod_panel/ip_bans" )
( text "{{ icon \"ban\" }}" )
( span
( text "{{ text \"general:link.ip_bans\" }}" ) ) )
( a
( "href" "/mod_panel/stats" )
( text "{{ icon \"chart-line\" }}" )
( span
( text "{{ text \"general:link.stats\" }}" ) ) )
( text "{%- endif %}" )
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" ) ) )
( a
( "href" "/reference/tetratto/index.html" )
( "class" "button" )
( "data-turbo" "false" )
( icon ( text "rabbit" ) )
( str ( text "general:link.reference" ) ) )
2025-06-18 21:00:07 -04:00
( a
( "href" "{{ config.policies.terms_of_service }}" )
( "class" "button" )
( icon ( text "heart-handshake" ) )
( text "Terms of service" ) )
( a
( "href" "{{ config.policies.privacy }}" )
( "class" "button" )
( 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
( "class" "flex items-center gap-2" )
( span
( text "{{ other_user.settings.status }}" ) )
; connection icon
( text "{% if (other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track) or (other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track) %} {{ icon \"music\" }} {% endif %}" ) )
( text "{% elif other_user.connections.LastFm[0].data.name and other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track %}" )
( div
( "class" "flex items-center gap-2" )
( text "{{ icon \"music\" }}" )
( span
( b
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
( "class" "flex items-center gap-2" )
( 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
( "class" "flex gap-2 items-center card tiny user_plate {% if secondary -%}secondary{%- endif %}" )
( a
( "href" "/@{{ user.username }}" )
( text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}" ) )
( div
( "class" "flex justify-center flex-col" )
( "style" "{% if show_menu or show_kick -%}width: 60%{% else %}max-width: 150px{%- endif %}" )
( text "{{ self::full_username(user=user) }}" )
( div
( "class" "user_status" )
( text "{{ self::user_status(other_user=user) }}" ) ) )
( text "{% if show_menu -%}" )
( div
( "class" "dropdown" )
( button
( "class" "camo small square" )
( "onclick" "trigger('atto::hooks::dropdown', [event])" )
( "exclude" "dropdown" )
( text "{{ icon \"settings\" }}" ) )
( text "{{ self::user_menu() }}" ) )
( text "{% elif show_kick %}" )
( div
( "class" "dropdown" )
( "style" "margin-left: auto" )
( button
( "class" "camo small square" )
( "onclick" "trigger('atto::hooks::dropdown', [event])" )
( "exclude" "dropdown" )
( text "{{ icon \"ellipsis\" }}" ) )
( div
( "class" "inner" )
( button
( "class" "red" )
( "onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')" )
( text "{{ icon \"x\" }}" )
( span
( text "{{ text \"chats:action.kick_member\" }}" ) ) ) ) )
( text "{%- endif %}" ) )
( text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}" )
( button
2025-06-12 13:53:23 -04:00
( "class" "button small square 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\" }}" ) )
( text "{% if render_dialog -%}" )
( dialog
( "id" "emoji_dialog" )
( div
( "class" "inner flex flex-col gap-2" )
( script
( "type" "module" )
( "src" "https://unpkg.com/emoji-picker-element@1.22.8/index.js" ) )
( emoji-picker
( "style" "
--border-radius: var ( --radius ) ;
--background: var ( --color-super-raised ) ;
--input-border-radiFus: var ( --radius ) ;
--input-border-color: var ( --color-primary ) ;
--indicator-color: var ( --color-primary ) ;
2025-06-09 16:45:36 -04:00
--emoji-padding: var ( --pad-1 ) ;
2025-06-01 12:25:33 -04:00
box-shadow: 0 0 4px var ( --color-shadow ) ;
" )
( "class" "w-full" ) )
( script
( text " setTimeout ( async ( ) => {
document.querySelector ( \"emoji-picker\" ) . customEmoji =
await trigger ( \"me::emojis\" ) ;
const style = document.createElement ( \"style\" ) ;
style.textContent = ` . custom-emoji { border-radius: 4px !important ; } .category { font-weight: 600; }`;
document
. querySelector ( \"emoji-picker\" )
. shadowRoot.appendChild ( style ) ;
}, 150 ) ;
document
. querySelector ( \"emoji-picker\" )
. addEventListener ( \"emoji-click\", async ( event ) => {
if ( event.detail.skinTone > 0 ) {
document.getElementById (
window.EMOJI_PICKER_TEXT_ID,
) . value += event.detail.unicode ;
document.getElementById ( \"emoji_dialog\" ) . close ( ) ;
return ;
}
if ( event.detail.unicode ) {
document.getElementById (
window.EMOJI_PICKER_TEXT_ID,
) . value += ` :${await (
await fetch ( \"/api/v1/lookup_emoji\", {
method: \"POST\",
body: event.detail.unicode,
} )
) . text ( ) }: ` ;
} else {
document.getElementById (
window.EMOJI_PICKER_TEXT_ID,
) . value += ` :${event.detail.emoji.shortcodes[0]}: ` ;
}
document.getElementById ( \"emoji_dialog\" ) . close ( ) ;
} ) ;"))
( div
( "class" "flex justify-between" )
( div )
( div
( "class" "flex gap-2" )
( button
2025-06-12 13:53:23 -04:00
( "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
2025-06-12 13:53:23 -04:00
( "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 ) {
element.innerHTML += ` <div class=\"card small secondary flex items-center gap-2\" onclick=\"remove_file ( ${idx} ) \" style=\"cursor: pointer\">${template.innerHTML.replace (
\".file_name\",
file.name,
) }</div> ` ;
idx += 1 ;
}
} ;
globalThis.remove_file = ( idx ) => {
const files = Array.from ( input.files ) ;
files.splice ( idx - 1 , 1 ) ;
// update files
const list = new DataTransfer ( ) ;
for ( item of files ) {
list.items.add ( item ) ;
}
input.files = list.files ;
// render
render_file_picker_files ( ) ;
} ;
input.addEventListener ( \"change\", ( ) => {
render_file_picker_files ( ) ;
} ) ;
} ) ( ) ;"))
( text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}" )
( div
( "class" "card w-full supporter_ad" )
( "ui_ident" "supporter_ad" )
( "onclick" "window.location.href = '/settings#/account/billing'" )
( div
( "class" "card w-full flex flex-wrap items-center gap-2 justify-between" )
( text "{% if body -%}" )
( b
( text "{{ body }}" ) )
( text "{% else %}" )
( b
( text "{{ text \"general:label.supporter_motivation\" }}" ) )
( text "{%- endif %}" )
( a
( "href" "/settings#/account/billing" )
( "class" "button small" )
( text "{{ icon \"heart\" }}" )
( span
( text "{{ text \"general:action.become_supporter\" }}" ) ) ) ) )
( text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}" )
( div
( "class" "flex gap-2" )
( text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if not quoting -%} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %} {%- endif %}" )
2025-06-04 17:21:46 -04:00
( button
2025-06-12 13:53:23 -04:00
( "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
2025-06-12 13:53:23 -04:00
( "class" "small square lowered" )
2025-06-01 12:25:33 -04:00
( "title" "More options" )
( "onclick" "document.getElementById('post_options_dialog').showModal()" )
( "type" "button" )
( text "{{ icon \"ellipsis\" }}" ) ) )
( dialog
( "id" "post_options_dialog" )
( div
( "class" "inner flex flex-col gap-2" )
( div
( "id" "post_options" )
( "class" "flex flex-col gap-2" ) )
( hr )
( div
( "class" "flex justify-between" )
( div )
( div
( "class" "flex gap-2" )
( button
2025-06-12 13:53:23 -04:00
( "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: [],
} ;
window.BLANK_INITIAL_SETTINGS = JSON.stringify (
window.POST_INITIAL_SETTINGS,
) ;
const settings_fields = [
[
[
\"comments_enabled\",
\"Allow people to comment on your post\",
],
window.POST_INITIAL_SETTINGS.comments_enabled.toString ( ) ,
\"checkbox\",
],
[
[
\"reposts_enabled\",
\"Allow people to repost/quote your post\",
],
window.POST_INITIAL_SETTINGS.reposts_enabled.toString ( ) ,
\"checkbox\",
],
[
[
\"reactions_enabled\",
\"Allow people to like/dislike your post\",
],
window.POST_INITIAL_SETTINGS.reactions_enabled.toString ( ) ,
\"checkbox\",
],
[
[\"is_nsfw\", \"Hide from public timelines\"],
window.POST_INITIAL_SETTINGS.is_nsfw.toString ( ) ,
\"checkbox\",
],
[
[\"content_warning\", \"Content warning\"],
window.POST_INITIAL_SETTINGS.content_warning,
\"textarea\",
],
[
[\"tags\", \"Tags\"],
window.POST_INITIAL_SETTINGS.tags,
\"input\",
{
embed_html:
'<span class=\"fade\">Tags should be separated by a comma.</span> ',
},
],
] ;
document.getElementById ( \"post_options\" ) . innerHTML = \"\" ;
trigger ( \"ui::generate_settings_ui\", [
document.getElementById ( \"post_options\" ) ,
settings_fields,
window.POST_INITIAL_SETTINGS,
{
tags: ( new_tags ) => {
window.POST_INITIAL_SETTINGS.tags = new_tags
. split ( \",\" )
. map ( ( t ) => t.trim ( ) ) ;
},
},
] ) ;
}, 250 ) ;
globalThis.update_settings_maybe = async ( id ) => {
if (
JSON.stringify ( window.POST_INITIAL_SETTINGS ) !==
window.BLANK_INITIAL_SETTINGS
) {
await fetch ( ` /api/v1/posts/${id}/context ` , {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( {
context: window.POST_INITIAL_SETTINGS,
} ) ,
} ) ;
}
} ;"))))
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 = \"\" ;
2025-06-05 16:23:57 -04:00
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\"] ;
}
2025-06-05 16:23:57 -04:00
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,
2025-06-05 16:23:57 -04:00
option_d: POLL_OPTION_D,
expires: POLL_EXPIRES,
2025-06-04 17:21:46 -04:00
}] ;
} " ) )
( dialog
( "id" "poll_options_dialog" )
( div
( "class" "inner flex flex-col gap-2" )
( div
( "id" "poll_options" )
( "class" "flex flex-col gap-2" )
( b ( text "Attach poll" ) )
( div
( "class" "card flex flex-col gap-2" )
( span
( b ( text "Option A " ) )
( span ( "class" "fade red" ) ( text "(required)" ) ) )
( input ( "type" "text" ) ( "placeholder" "option A" ) ( "onchange" "window.POLL_OPTION_A = event.target.value" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( span
( b ( text "Option B " ) )
( span ( "class" "fade red" ) ( text "(required)" ) ) )
( input ( "type" "text" ) ( "placeholder" "option B" ) ( "onchange" "window.POLL_OPTION_B = event.target.value" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( b ( text "Option C" ) )
( input ( "type" "text" ) ( "placeholder" "option A" ) ( "onchange" "window.POLL_OPTION_C = event.target.value" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( b ( text "Option D" ) )
2025-06-05 16:23:57 -04:00
( input ( "type" "text" ) ( "placeholder" "option D" ) ( "onchange" "window.POLL_OPTION_D = event.target.value" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( b ( text "Expires" ) )
( input ( "type" "date" ) ( "onchange" "window.POLL_EXPIRES = event.target.valueAsDate.getTime() - new Date().getTime()" ) ) ) )
2025-06-04 17:21:46 -04:00
( hr )
( div
( "class" "flex justify-between" )
( div )
( div
( "class" "flex gap-2" )
( button
2025-06-12 13:53:23 -04:00
( "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-06-12 13:53:23 -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 %}" )
2025-06-05 16:23:57 -04:00
( 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
2025-06-05 16:23:57 -04:00
( text "{% if poll[1] %}" )
2025-06-04 17:21:46 -04:00
( span ( "class" "fade" ) ( text "You've already voted!" ) )
2025-06-05 16:23:57 -04:00
( text "{% elif poll[2] %}" )
( span ( "class" "fade" ) ( text "Poll ended!" ) )
( text "{% endif %}" )
2025-06-04 17:21:46 -04:00
; option a
( div
( "class" "card w-full flex flex-col gap-2" )
( span ( text "{{ poll[0].option_a }} ({{ poll[0].votes_a }} votes)" ) )
( div ( "class" "poll_bar" ) ( "style" "width: {{ (poll[0].votes_a / total) * 100 }}%" ) ) )
; option b
( div
( "class" "card w-full flex flex-col gap-2" )
( span ( text "{{ poll[0].option_b }} ({{ poll[0].votes_b }} votes)" ) )
( div ( "class" "poll_bar" ) ( "style" "width: {{ (poll[0].votes_b / total) * 100 }}%" ) ) )
; option c
( text "{% if poll[0].option_c -%}" )
( div
( "class" "card w-full flex flex-col gap-2" )
( span ( text "{{ poll[0].option_c }} ({{ poll[0].votes_c }} votes)" ) )
( div ( "class" "poll_bar" ) ( "style" "width: {{ (poll[0].votes_c / total) * 100 }}%" ) ) )
( text "{%- endif %}" )
; option d
( text "{% if poll[0].option_d -%}" )
( div
( "class" "card w-full flex flex-col gap-2" )
( span ( text "{{ poll[0].option_d }} ({{ poll[0].votes_d }} votes)" ) )
( div ( "class" "poll_bar" ) ( "style" "width: {{ (poll[0].votes_d / total) * 100 }}%" ) ) )
( text "{%- endif %}" )
( text "{% else %}" )
; not voted yet, just show options so user can vote
; option a
( button
2025-06-12 13:53:23 -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-06-12 13:53:23 -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-06-12 13:53:23 -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-06-12 13:53:23 -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
( "class" "flex w-full flex-wrap gap-2" )
2025-06-05 16:23:57 -04:00
( span ( "class" "notification chip" ) ( text "{{ total }} votes" ) )
2025-06-05 16:34:07 -04:00
( text "{% if not poll[2] -%}" )
2025-06-05 16:23:57 -04:00
( span
( "class" "notification chip" )
( 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 %}" )
2025-06-09 16:45:36 -04:00
( text "{% macro community_info(community) %}" )
( div
( "class" "card-nest flex flex-col" )
( div
( "id" "bio" )
( "class" "card small no_p_margin" )
( text "{{ community.context.description|markdown|safe }}" ) )
( div
( "class" "card flex flex-col gap-2" )
( div
( "class" "w-full flex justify-between items-center" )
( span
( "class" "notification chip" )
( text "ID" ) )
( button
( "title" "Copy" )
( "onclick" "trigger('atto::copy_text', ['{{ community.id }}'])" )
( "class" "camo small" )
( text "{{ icon \"copy\" }}" ) ) )
( div
( "class" "w-full flex justify-between items-center" )
( span
( "class" "notification chip" )
( text "Created " ) )
( span
( "class" "date" )
( text "{{ community.created }}" ) ) )
( div
( "class" "w-full flex justify-between items-center" )
( span
( "class" "notification chip" )
( text "Members" ) )
( a
( "href" "/community/{{ community.title }}/members" )
( text "{{ community.member_count }}" ) ) )
( div
( "class" "w-full flex justify-between items-center" )
( span
( "class" "notification chip" )
( text "Posts" ) )
( a
( "href" "/community/{{ community.title }}" )
( text "{{ community.post_count }}" ) ) )
( div
( "class" "w-full flex justify-between items-center" )
( span
( "class" "notification chip" )
( text "Score" ) )
( div
( "class" "flex gap-2" )
( b
( text "{{ community.likes - community.dislikes }}" ) )
( text "{% if user -%}" )
( div
( "class" "flex gap-1 reactions_box" )
( "hook" "check_reactions" )
( "hook-arg:id" "{{ community.id }}" )
( text "{{ components::likes(id=community.id, asset_type=\"Community\", likes=community.likes, dislikes=community.dislikes) }}" ) )
( text "{%- endif %}" ) ) ) ) )
( text "{% endmacro %}" )
( text "{% macro community_actions(community) -%}" )
( text "{% if user -%}" )
( div
( "class" "card flex gap-2 flex-wrap" )
( "id" "join_or_leave" )
( text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}" )
( button
( "class" "primary" )
( "onclick" "join_community()" )
( text "{{ icon \"circle-plus\" }}" )
( span
( text "{{ text \"communities:action.join\" }}" ) ) )
( script
( text " globalThis.join_community = ( ) => {
fetch (
\"/api/v1/communities/{{ community.id }}/join\",
{
method: \"POST\",
},
)
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
setTimeout ( ( ) => {
window.location.reload ( ) ;
}, 150 ) ;
} ) ;
} ;"))
( text "{% else %}" )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered red" )
2025-06-09 16:45:36 -04:00
( "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
2025-06-12 13:53:23 -04:00
( "class" "lowered red" )
2025-06-09 16:45:36 -04:00
( "onclick" "leave_community()" )
( text "{{ icon \"circle-minus\" }}" )
( span
( text "{{ text \"communities:action.leave\" }}" ) ) )
( a
( "href" "/chats/{{ community.id }}/0" )
2025-06-12 13:53:23 -04:00
( "class" "button lowered" )
2025-06-09 16:45:36 -04:00
( text "{{ icon \"message-circle\" }}" )
( span
( text "{{ text \"communities:label.chats\" }}" ) ) )
( text "{% if user and can_post -%}" )
( a
( "href" "/communities/intents/post?community={{ community.id }}" )
2025-06-12 13:53:23 -04:00
( "class" "button lowered" )
2025-06-09 16:45:36 -04:00
( "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" )
2025-06-12 13:53:23 -04:00
( "class" "button lowered" )
2025-06-09 16:45:36 -04:00
( text "{{ icon \"message-circle\" }}" )
( span
( text "{{ text \"communities:label.chats\" }}" ) ) )
( a
( "href" "/communities/intents/post?community={{ community.id }}" )
2025-06-12 13:53:23 -04:00
( "class" "button lowered" )
2025-06-09 16:45:36 -04:00
( "data-turbo" "false" )
( text "{{ icon \"plus\" }}" )
( span
( text "{{ text \"general:action.post\" }}" ) ) )
( text "{%- endif %} {% if can_manage_community or is_manager -%}" )
( a
( "href" "/community/{{ community.id }}/manage" )
( "class" "button primary" )
( text "{{ icon \"settings\" }}" )
( span
( text "{{ text \"communities:action.configure\" }}" ) ) )
( text "{%- endif %}" ) )
( text "{%- endif %}" )
( text "{%- endmacro %}" )
2025-06-10 22:02:06 -04:00
( text "{% macro ticket(post, owner) -%}" )
( div
( "href" "/post/{{ post.id }}" )
( "class" "card secondary w-fill flex flex-col gap-2" )
( div
( "class" "flex gap-2 items-center" )
; user info
( a
( "href" "/@{{ owner.username }}" )
( text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}" ) )
( span
( "class" "name" )
( text "{{ self::full_username(user=owner) }}" ) )
; timestamp
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 }}" )
( "class" "flush flex gap-2 items-center" )
; open/closed icon
( text "{% if community and community.is_forge -%} {% if post.is_open -%}" )
( span
( "title" "Open" )
( "class" "flex items-center green" )
( text "{{ icon \"circle-dot\" }}" ) )
( text "{% else %}" )
( span
( "title" "Closed" )
( "class" "flex items-center purple" )
( text "{{ icon \"circle-check\" }}" ) )
( text "{%- endif %} {%- endif %}" )
( h4
( "class" "no_p_margin" )
( text "{{ post.title|markdown|safe }}" ) ) ) )
( text "{%- endmacro %}" )
2025-06-15 11:52:44 -04:00
( text "{% macro stack_listing(stack) -%}" )
( a
( "href" "/stacks/{{ stack.id }}" )
( "class" "card secondary flex flex-col gap-2" )
( div
( "class" "flex items-center gap-2" )
( text "{{ icon \"list\" }}" )
( b
( text "{{ stack.name }}" ) ) )
( span
( text "Created " )
( span
( "class" "date" )
( text "{{ stack.created }}" ) )
( text "; {{ stack.privacy }}; {{ stack.users|length }} users" ) ) )
( text "{%- endmacro %}" )