2261 lines
90 KiB
Common Lisp
2261 lines
90 KiB
Common Lisp
(text "{% macro avatar(username, size=\"24px\", selector_type=\"username\") -%}")
|
|
(img
|
|
("title" "{{ username }}'s avatar")
|
|
("src" "/api/v1/auth/user/{{ username }}/avatar?selector_type={{ selector_type }}")
|
|
("alt" "@{{ username }}")
|
|
("class" "avatar shadow")
|
|
("loading" "lazy")
|
|
("style" "--size: {{ size }}"))
|
|
|
|
(text "{%- endmacro %} {% macro community_avatar(id, community=false, size=\"24px\") -%} {% if community -%}")
|
|
(img
|
|
("src" "/api/v1/communities/{{ id }}/avatar")
|
|
("alt" "{{ community.title }}'s avatar")
|
|
("class" "avatar shadow")
|
|
("loading" "lazy")
|
|
("style" "--size: {{ size }}"))
|
|
|
|
(text "{% else %}")
|
|
(img
|
|
("src" "/api/v1/communities/{{ id }}/avatar")
|
|
("alt" "{{ id }}'s avatar")
|
|
("class" "avatar shadow")
|
|
("loading" "lazy")
|
|
("style" "--size: {{ size }}"))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro banner(username, border_radius=\"var(--radius)\") -%}")
|
|
(img
|
|
("title" "{{ username }}'s banner")
|
|
("src" "/api/v1/auth/user/{{ username }}/banner")
|
|
("alt" "@{{ username }}'s banner")
|
|
("class" "banner shadow w-full")
|
|
("loading" "lazy")
|
|
("style" "border-radius: {{ border_radius }};"))
|
|
|
|
(text "{%- endmacro %} {% macro community_banner(id, community=false) -%} {% if community %}")
|
|
(img
|
|
("src" "/api/v1/communities/{{ id }}/banner")
|
|
("alt" "{{ community.title }}'s banner")
|
|
("class" "banner shadow")
|
|
("loading" "lazy"))
|
|
|
|
(text "{% else %}")
|
|
(img
|
|
("src" "/api/v1/communities/{{ id }}/banner")
|
|
("alt" "{{ id }}'s banner")
|
|
("class" "banner shadow")
|
|
("loading" "lazy"))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%}")
|
|
(a
|
|
("class" "card secondary w-full flex items-center gap-4")
|
|
("href" "{% if community.is_forge -%}/forge/{{ community.title }}{% else %}/community/{{ community.title }}{%- endif %}")
|
|
(text "{{ self::community_avatar(id=community.id, community=community, size=\"48px\") }}")
|
|
(div
|
|
("class" "flex flex-col")
|
|
(div
|
|
("class" "flex gap-2 items-center")
|
|
(text "{% if community.is_forge -%}")
|
|
(icon (text "anvil"))
|
|
(text "{%- endif %}")
|
|
(h3
|
|
("class" "name lg:long")
|
|
(text "{{ community.context.display_name }}")))
|
|
(span
|
|
("class" "fade")
|
|
(b
|
|
(text "{{ community.member_count }} "))
|
|
(text "members"))))
|
|
|
|
(text "{%- endmacro %} {% macro username(user) -%}")
|
|
(div
|
|
("style" "display: contents")
|
|
(text "{% if user.settings.display_name -%} {{ user.settings.display_name }} {% else %} {{ user.username }} {%- endif %}"))
|
|
|
|
(text "{%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0, secondary=false, disable_dislikes=false) -%}")
|
|
(button
|
|
("title" "Like")
|
|
("class" "{% if secondary -%}lowered{% else %}camo{%- endif %} small")
|
|
("hook_element" "reaction.like")
|
|
("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', true])")
|
|
(text "{{ icon \"heart\" }} {% if likes > 0 -%}")
|
|
(span
|
|
(text "{{ likes }}"))
|
|
(text "{%- endif %}"))
|
|
|
|
(text "{% if not user or not user.settings.hide_dislikes and not disable_dislikes -%}")
|
|
(button
|
|
("title" "Dislike")
|
|
("class" "{% if secondary -%}lowered{% else %}camo{%- endif %} small")
|
|
("hook_element" "reaction.dislike")
|
|
("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', false])")
|
|
(text "{{ icon \"heart-crack\" }} {% if dislikes > 0 -%}")
|
|
(span
|
|
(text "{{ dislikes }}"))
|
|
(text "{%- endif %}"))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%} {% if user and user.username -%}")
|
|
(div
|
|
("class" "flex items-center")
|
|
(a
|
|
("href" "/@{{ user.username }}")
|
|
("class" "flush")
|
|
("style" "font-weight: 600")
|
|
("target" "_top")
|
|
(text "{{ self::username(user=user) }}"))
|
|
(text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}")
|
|
(span
|
|
("title" "Verified")
|
|
("style" "color: var(--color-primary)")
|
|
("class" "flex items-center")
|
|
(text "{{ icon \"badge-check\" }}"))
|
|
(text "{%- endif %}"))
|
|
(text "{%- endif %} {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}")
|
|
(div
|
|
("style" "display: contents")
|
|
(text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}"))
|
|
(text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false, poll=false, dont_show_title=false, is_repost=false) -%} {% if community and show_community and community.id != config.town_square or question %}")
|
|
(div
|
|
("class" "card-nest post_outer:{{ post.id }} post_outer")
|
|
("is_repost" "{{ is_repost }}")
|
|
(text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}")
|
|
(div
|
|
("class" "card small")
|
|
(a
|
|
("href" "/api/v1/communities/find/{{ post.community }}")
|
|
("class" "flush flex gap-1 items-center")
|
|
(text "{{ self::community_avatar(id=post.community, community=community) }}")
|
|
(b
|
|
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
|
|
(text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- endif %}")))
|
|
(text "{%- endif %} {%- endif %}")
|
|
(div
|
|
("class" "card flex flex-col post gap-2 post:{{ post.id }} {% if secondary -%}secondary{%- endif %}")
|
|
("data-community" "{{ post.community }}")
|
|
("data-ownsup" "{{ owner.permissions|has_supporter }}")
|
|
("data-id" "{{ post.id }}")
|
|
("hook" "verify_emojis")
|
|
(div
|
|
("class" "w-full flex gap-2")
|
|
(text "{% if not expect_repost -%}")
|
|
(a
|
|
("href" "/@{{ owner.username }}")
|
|
(text "{{ self::avatar(username=owner.username, size=\"52px\", selector_type=\"username\") }}"))
|
|
(text "{%- endif %}")
|
|
(div
|
|
("class" "flex flex-col w-full gap-1 post_right {% if expect_repost -%}repost{%- endif %}")
|
|
(div
|
|
("class" "flex flex-wrap gap-2 items-center")
|
|
(text "{% if expect_repost -%}")
|
|
(a
|
|
("href" "/@{{ owner.username }}")
|
|
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
|
|
(text "{%- endif %}")
|
|
(span
|
|
("class" "name")
|
|
(text "{{ self::full_username(user=owner) }}"))
|
|
(text "{% if post.context.edited != 0 -%}")
|
|
(div
|
|
("class" "flex")
|
|
(span
|
|
("class" "fade date")
|
|
(text "{{ post.context.edited }}"))
|
|
(sup
|
|
("title" "Edited")
|
|
(text "*")))
|
|
(text "{% else %}")
|
|
(span
|
|
("class" "fade date")
|
|
(text "{{ post.created }}"))
|
|
(text "{%- endif %} {% if post.context.is_nsfw -%}")
|
|
(span
|
|
("title" "NSFW post")
|
|
("class" "flex items-center")
|
|
("style" "color: var(--color-primary)")
|
|
(text "{{ icon \"square-asterisk\" }}"))
|
|
(text "{%- endif %} {% if post.stack -%}")
|
|
(a
|
|
("title" "Posted to a stack you're in")
|
|
("class" "flex items-center flush")
|
|
("style" "color: var(--color-primary)")
|
|
("href" "/stacks/{{ post.stack }}")
|
|
(text "{{ icon \"layers\" }}"))
|
|
(text "{%- endif %} {% if community and community.is_forge -%} {% if post.is_open -%}")
|
|
(span
|
|
("title" "Open")
|
|
("class" "flex items-center green")
|
|
(text "{{ icon \"circle-dot\" }}"))
|
|
(text "{% else %}")
|
|
(span
|
|
("title" "Closed")
|
|
("class" "flex items-center purple")
|
|
(text "{{ icon \"circle-check\" }}"))
|
|
(text "{%- endif %} {%- endif %}")
|
|
(text "{% if post.context.repost and post.context.repost.reposting %}")
|
|
(span
|
|
("title" "Repost")
|
|
("class" "flex items-center")
|
|
("style" "color: var(--color-primary)")
|
|
(text "{{ icon \"repeat-2\" }}"))
|
|
(text "{%- endif %} {% if post.community == config.town_square -%}")
|
|
(span
|
|
("title" "Posted to profile")
|
|
("class" "flex items-center")
|
|
("style" "color: var(--color-primary)")
|
|
(text "{{ icon \"user-round\" }}"))
|
|
(text "{%- endif %} {% if post.is_deleted -%}")
|
|
(span
|
|
("title" "Deleted")
|
|
("class" "flex items-center")
|
|
("style" "color: var(--color-primary)")
|
|
(text "{{ icon \"trash-2\" }}"))
|
|
(text "{%- endif %}"))
|
|
(text "{% if not dont_show_title and post.title and community and community.context.enable_titles -%}")
|
|
; post has a title AND whatever is rendering this component wants to see it
|
|
(a
|
|
("class" "flush")
|
|
("href" "/post/{{ post.id }}")
|
|
(h2
|
|
("id" "post_content:{{ post.id }}")
|
|
("class" "no_p_margin post_content")
|
|
("hook" "long")
|
|
(text "{{ post.title }}"))
|
|
|
|
(button ("class" "small lowered") (icon (text "ellipsis"))))
|
|
(text "{% else %}")
|
|
(text "{% if not post.context.content_warning -%}")
|
|
(span
|
|
("class" "no_p_margin post_content")
|
|
("hook" "long")
|
|
|
|
; title
|
|
(text "{% if post.title and community and community.context.enable_titles -%}")
|
|
(h2 (text "{{ post.title }}"))
|
|
(hr ("class" "margin") ("style" "margin-top: var(--pad-2)"))
|
|
(text "{%- endif %}")
|
|
|
|
; content
|
|
(span ("id" "post_content:{{ post.id }}") (text "{{ post.content|markdown|safe }}"))
|
|
(text "{% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false, is_repost=true) }} {% else %}")
|
|
(div
|
|
("class" "card lowered red flex items-center gap-2")
|
|
(text "{{ icon \"frown\" }}")
|
|
(span
|
|
(str (text "general:label.could_not_find_post"))))
|
|
(text "{%- endif %} {%- endif %}"))
|
|
(text "{{ self::post_media(upload_ids=post.uploads) }} {% else %}")
|
|
(details
|
|
("class" "card tiny lowered w-full")
|
|
(summary
|
|
("class" "red w-full")
|
|
(b
|
|
(text "{{ post.context.content_warning }}")))
|
|
(div
|
|
("class" "flex flex-col gap-2")
|
|
(span
|
|
("class" "no_p_margin post_content")
|
|
("hook" "long")
|
|
|
|
; title
|
|
(text "{% if post.title and community and community.settings.enable_titles %}")
|
|
(h2 (text "{{ post.title }}"))
|
|
(text "{% endif %}")
|
|
|
|
; content
|
|
(span ("id" "post_content:{{ post.id }}") (text "{{ post.content|markdown|safe }}"))
|
|
(text "{% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}")
|
|
(div
|
|
("class" "card lowered red flex items-center gap-2")
|
|
(text "{{ icon \"frown\" }}")
|
|
(span
|
|
(text "Could not find original post...")))
|
|
(text "{%- endif %} {%- endif %}"))
|
|
(text "{{ self::post_media(upload_ids=post.uploads) }}")))
|
|
(text "{%- endif %} {%- endif %}")
|
|
|
|
(text "{% if poll -%} {{ self::poll(post=post, poll=poll) }} {%- endif %}")
|
|
|
|
(div
|
|
("class" "flex flex-wrap gap-2 fade")
|
|
(text "{% for tag in post.context.tags %}")
|
|
(a
|
|
("href" "/@{{ owner.username }}?tag={{ tag }}")
|
|
("class" "flush fade")
|
|
(text "#{{ tag }}"))
|
|
(text "{% endfor %}"))))
|
|
(div
|
|
("class" "flex justify-between items-center gap-2 w-full")
|
|
(text "{% if user -%}")
|
|
(div
|
|
("class" "flex gap-1 reactions_box")
|
|
("hook" "check_reactions")
|
|
("hook-arg:id" "{{ post.id }}")
|
|
(text "{% if post.context.reactions_enabled -%} {% if post.content|length > 0 or post.uploads|length > 0 -%} {{ self::likes(id=post.id, asset_type=\"Post\", likes=post.likes, dislikes=post.dislikes, disable_dislikes=owner.settings.hide_dislikes) }} {%- endif %} {%- endif %} {% if post.context.repost and post.context.repost.reposting -%}")
|
|
(a
|
|
("href" "/post/{{ post.context.repost.reposting }}")
|
|
("class" "button small camo")
|
|
(text "{{ icon \"expand\" }}"))
|
|
(text "{%- endif %}"))
|
|
(text "{% else %}")
|
|
(div)
|
|
(text "{%- endif %}")
|
|
(div
|
|
("class" "flex gap-1 buttons_box")
|
|
(text "{% if post.context.comments_enabled %}")
|
|
(a
|
|
("href" "/post/{{ post.id }}")
|
|
("class" "button camo small")
|
|
(text "{{ icon \"message-circle\" }}")
|
|
(span
|
|
(text "{{ post.comment_count }}")))
|
|
(text "{% endif %}")
|
|
|
|
(a
|
|
("href" "/post/{{ post.id }}")
|
|
("class" "button camo small")
|
|
("target" "_blank")
|
|
(text "{{ icon \"external-link\" }}"))
|
|
(text "{% if user -%}")
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "camo small")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(text "{% if config.town_square and post.context.reposts_enabled %}")
|
|
(b
|
|
("class" "title")
|
|
(text "{{ text \"general:label.share\" }}"))
|
|
(button
|
|
("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}', true])")
|
|
(text "{{ icon \"repeat-2\" }}")
|
|
(span
|
|
(text "{{ text \"communities:label.repost\" }}")))
|
|
(a
|
|
("class" "button")
|
|
("href" "/communities/intents/post?quote={{ post.id }}")
|
|
(text "{{ icon \"quote\" }}")
|
|
(span
|
|
(text "{{ text \"communities:label.quote_post\" }}")))
|
|
(button
|
|
("onclick" "trigger('me::intent_twitter', [trigger('me::gen_share', [{ q: '{{ post.context.answering }}', p: '{{ post.id }}' }, 280, true])])")
|
|
(icon (text "bird"))
|
|
(span
|
|
(text "Twitter")))
|
|
(button
|
|
("onclick" "trigger('me::intent_bluesky', [trigger('me::gen_share', [{ q: '{{ post.context.answering }}', p: '{{ post.id }}' }, 280, true])])")
|
|
(icon (text "cloud"))
|
|
(span
|
|
(text "BlueSky")))
|
|
(text "{%- endif %}")
|
|
(text "{% if user.id != post.owner -%}")
|
|
(b
|
|
("class" "title")
|
|
(text "{{ text \"general:label.safety\" }}"))
|
|
(button
|
|
("class" "red")
|
|
("onclick" "trigger('me::report', ['{{ post.id }}', 'post'])")
|
|
(text "{{ icon \"flag\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.report\" }}")))
|
|
(text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}")
|
|
(b
|
|
("class" "title")
|
|
(text "{{ text \"general:action.manage\" }}"))
|
|
; forge stuff
|
|
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
|
|
(button
|
|
("class" "green")
|
|
("onclick" "trigger('me::update_open', ['{{ post.id }}', false])")
|
|
(text "{{ icon \"circle-check\" }}")
|
|
(span
|
|
(text "{{ text \"forge:action.close\" }}")))
|
|
(text "{% else %}")
|
|
(button
|
|
("class" "purple")
|
|
("onclick" "trigger('me::update_open', ['{{ post.id }}', true])")
|
|
(text "{{ icon \"refresh-ccw-dot\" }}")
|
|
(span
|
|
(text "{{ text \"forge:action.reopen\" }}")))
|
|
(text "{%- endif %} {%- endif %}")
|
|
; owner stuff
|
|
(text "{% if user.id == post.owner -%}")
|
|
(a
|
|
("href" "/post/{{ post.id }}#/edit")
|
|
(text "{{ icon \"pen\" }}")
|
|
(span
|
|
(text "{{ text \"communities:label.edit_content\" }}")))
|
|
(text "{%- endif %}")
|
|
(a
|
|
("href" "/post/{{ post.id }}#/configure")
|
|
(text "{{ icon \"settings\" }}")
|
|
(span
|
|
(text "{{ text \"communities:action.configure\" }}")))
|
|
(text "{% if not post.is_deleted -%}")
|
|
(button
|
|
("class" "red")
|
|
("onclick" "trigger('me::remove_post', ['{{ post.id }}'])")
|
|
(text "{{ icon \"trash\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.delete\" }}")))
|
|
(text "{%- endif %} {% if is_helper and post.is_deleted -%}")
|
|
(button
|
|
("class" "red")
|
|
("onclick" "trigger('me::purge_post', ['{{ post.id }}'])")
|
|
(text "{{ icon \"trash-2\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.purge\" }}")))
|
|
(button
|
|
("class" "green")
|
|
("onclick" "trigger('me::restore_post', ['{{ post.id }}'])")
|
|
(text "{{ icon \"undo\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.restore\" }}")))
|
|
(text "{%- endif %} {%- endif %}")))
|
|
(text "{%- endif %}"))))
|
|
(text "{% if community and show_community and community.id != config.town_square or question %}"))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0 -%}")
|
|
(div
|
|
("class" "media_gallery gap-2")
|
|
(text "{% for upload in upload_ids %}")
|
|
(img
|
|
("src" "/api/v1/uploads/{{ upload }}")
|
|
("alt" "Image upload")
|
|
("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])"))
|
|
(text "{% endfor %}"))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}")
|
|
(div
|
|
("class" "w-full card-nest")
|
|
(div
|
|
("class" "card small notif_title flex items-center")
|
|
(text "{% if not notification.read -%}")
|
|
(svg
|
|
("width" "24")
|
|
("height" "24")
|
|
("viewBox" "0 0 24 24")
|
|
("style" "fill: var(--color-link)")
|
|
(circle
|
|
("cx" "12")
|
|
("cy" "12")
|
|
("r" "6")))
|
|
(text "{%- endif %}")
|
|
(b
|
|
("class" "no_p_margin")
|
|
(text "{{ notification.title|markdown|safe }}")))
|
|
(div
|
|
("class" "card notif_content flex flex-col gap-2")
|
|
(span
|
|
("class" "no_p_margin")
|
|
(text "{{ notification.content|markdown|safe }}"))
|
|
(div
|
|
("class" "card secondary w-full flex flex-wrap gap-2")
|
|
(text "{% if notification.read -%}")
|
|
(button
|
|
("class" "raised")
|
|
("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', false])")
|
|
(text "{{ icon \"undo\" }}")
|
|
(span
|
|
(text "{{ text \"notifs:action.mark_as_unread\" }}")))
|
|
(text "{% else %}")
|
|
(button
|
|
("class" "green raised")
|
|
("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', true])")
|
|
(text "{{ icon \"check\" }}")
|
|
(span
|
|
(text "{{ text \"notifs:action.mark_as_read\" }}")))
|
|
(text "{%- endif %}")
|
|
(button
|
|
("class" "red raised")
|
|
("onclick" "trigger('me::remove_notification', ['{{ notification.id }}'])")
|
|
(text "{{ icon \"trash\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.delete\" }}"))))))
|
|
|
|
(text "{%- endmacro %} {% macro user_card(user) -%}")
|
|
(a
|
|
("class" "card-nest w-full")
|
|
("href" "/@{{ user.username }}")
|
|
(div
|
|
("class" "card small")
|
|
("style" "padding: 0")
|
|
(text "{{ self::banner(username=user.username, border_radius=\"0px\") }}"))
|
|
(div
|
|
("class" "card secondary flex items-center gap-4")
|
|
(text "{{ self::avatar(username=user.username, size=\"48px\") }}")
|
|
(div
|
|
("class" "flex items-center")
|
|
(b
|
|
(text "{{ self::username(user=user) }}"))
|
|
(text "{{ self::online_indicator(user=user) }}"))))
|
|
|
|
(text "{%- endmacro %} {% macro pagination(page=0, items=0, key=\"\", value=\"\") -%}")
|
|
(div
|
|
("class" "flex justify-between gap-2 w-full")
|
|
(text "{% if page > 0 -%}")
|
|
(a
|
|
("class" "button lowered")
|
|
("href" "?page={{ page - 1 }}{{ key }}{{ value }}")
|
|
(text "{{ icon \"arrow-left\" }}")
|
|
(span
|
|
(text "{{ text \"general:link.previous\" }}")))
|
|
(text "{% else %}")
|
|
(div)
|
|
(text "{%- endif %} {% if items != 0 -%}")
|
|
(a
|
|
("class" "button lowered")
|
|
("href" "?page={{ page + 1 }}{{ key }}{{ value }}")
|
|
(span
|
|
(text "{{ text \"general:link.next\" }}"))
|
|
(text "{{ icon \"arrow-right\" }}"))
|
|
(text "{%- endif %}"))
|
|
|
|
(text "{%- endmacro %} {% macro online_indicator(user) -%} {% if not user.settings.private_last_seen or is_helper %}")
|
|
(div
|
|
("class" "online_indicator")
|
|
("style" "display: contents")
|
|
("hook" "online_indicator")
|
|
("hook-arg:last_seen" "{{ user.last_seen }}")
|
|
(div
|
|
("style" "display: none")
|
|
("hook_ui_ident" "online")
|
|
("title" "Online")
|
|
(svg
|
|
("width" "24")
|
|
("height" "24")
|
|
("viewBox" "0 0 24 24")
|
|
("style" "fill: var(--color-green)")
|
|
(circle
|
|
("cx" "12")
|
|
("cy" "12")
|
|
("r" "6"))))
|
|
(div
|
|
("style" "display: none")
|
|
("hook_ui_ident" "idle")
|
|
("title" "Idle")
|
|
(svg
|
|
("width" "24")
|
|
("height" "24")
|
|
("viewBox" "0 0 24 24")
|
|
("style" "fill: var(--color-yellow)")
|
|
(circle
|
|
("cx" "12")
|
|
("cy" "12")
|
|
("r" "6"))))
|
|
(div
|
|
("style" "display: none")
|
|
("hook_ui_ident" "offline")
|
|
("title" "Offline")
|
|
(svg
|
|
("width" "24")
|
|
("height" "24")
|
|
("viewBox" "0 0 24 24")
|
|
("style" "fill: hsl(0, 0%, 50%)")
|
|
(circle
|
|
("cx" "12")
|
|
("cy" "12")
|
|
("r" "6")))))
|
|
|
|
(text "{% else %}")
|
|
(div
|
|
("title" "Offline")
|
|
("style" "display: contents")
|
|
(svg
|
|
("width" "24")
|
|
("height" "24")
|
|
("viewBox" "0 0 24 24")
|
|
("style" "fill: hsl(0, 0%, 50%)")
|
|
(circle
|
|
("cx" "12")
|
|
("cy" "12")
|
|
("r" "6"))))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}")
|
|
(style
|
|
(text ":root, * {
|
|
--hue: {{ user.settings.theme_hue }} !important;
|
|
}"))
|
|
|
|
(text "{%- endif %} {% if user.settings.theme_sat -%}")
|
|
(style
|
|
(text ":root, * {
|
|
--sat: {{ user.settings.theme_sat }} !important;
|
|
}"))
|
|
|
|
(text "{%- endif %} {% if user.settings.theme_lit -%}")
|
|
(style
|
|
(text ":root, * {
|
|
--lit: {{ user.settings.theme_lit }} !important;
|
|
}"))
|
|
|
|
(text "{%- endif %} {% if theme_preference -%}")
|
|
(script
|
|
(text "function match_user_theme() {
|
|
const pref = \"{{ theme_preference }}\".toLowerCase();
|
|
|
|
if (pref === \"auto\") {
|
|
return;
|
|
}
|
|
|
|
document.documentElement.className = pref;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
match_user_theme();
|
|
}, 150);"))
|
|
|
|
(text "{%- endif %}")
|
|
(div
|
|
("style" "display: none;")
|
|
(text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }} {% if user.permissions|has_supporter -%}")
|
|
(style
|
|
(text "{{ user.settings.theme_custom_css|remove_script_tags|safe }}"))
|
|
(text "{%- endif %}"))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color -%}")
|
|
(style
|
|
(text ":root,
|
|
* {
|
|
--{{ css }}: {{ color|color }} !important;
|
|
}"))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false, profile=false) -%}")
|
|
(div
|
|
("class" "card question {% if secondary -%}secondary{%- endif %} flex gap-2")
|
|
(text "{% if owner.id == 0 -%}")
|
|
(span
|
|
(text "{% if profile and profile.settings.anonymous_avatar_url -%}")
|
|
(img
|
|
("src" "/api/v1/util/proxy?url={{ profile.settings.anonymous_avatar_url }}")
|
|
("alt" "anonymous' avatar")
|
|
("class" "avatar shadow")
|
|
("loading" "lazy")
|
|
("style" "--size: 52px"))
|
|
(text "{% else %} {{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }} {%- endif %}"))
|
|
(text "{% else %}")
|
|
(a
|
|
("href" "/@{{ owner.username }}")
|
|
(text "{{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }}"))
|
|
(text "{%- endif %}")
|
|
(div
|
|
("class" "flex flex-col gap-1 w-full")
|
|
(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")
|
|
("id" "question_content:{{ question.id }}")
|
|
(text "{{ question.content|markdown|safe }}"))
|
|
; question drawings
|
|
(text "{{ self::post_media(upload_ids=question.drawings) }}")
|
|
; anonymous user ip thing
|
|
; this is only shown if the post author is anonymous AND we are a helper
|
|
(text "{% if is_helper and owner.id == 0 %}")
|
|
(details
|
|
("class" "card tiny lowered w-full")
|
|
(summary
|
|
("class" "w-full flex gap-2 flex-wrap items-center")
|
|
(icon (text "shield"))
|
|
(span (text "View IP")))
|
|
|
|
(pre (code (text "{{ question.ip }}"))))
|
|
(text "{% endif %}")
|
|
; ...
|
|
(div
|
|
("class" "flex gap-2 items-center justify-between"))))
|
|
|
|
(text "{%- endmacro %} {% macro create_question_form(receiver=\"0\", community=\"\", header=\"\", is_global=false, drawing_enabled=false) -%}")
|
|
(div
|
|
("class" "card-nest")
|
|
(div
|
|
("class" "card small flex items-center gap-2")
|
|
(text "{{ icon \"message-circle-heart\" }}")
|
|
(span
|
|
("class" "no_p_margin")
|
|
(text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}")))
|
|
(form
|
|
("class" "card flex flex-col gap-2")
|
|
("onsubmit" "create_question_from_form(event)")
|
|
(div
|
|
("class" "flex flex-col gap-1")
|
|
; carp canvas
|
|
(text "{% if drawing_enabled -%}")
|
|
(div ("ui_ident" "carp_canvas_field"))
|
|
(text "{%- endif %}")
|
|
|
|
; form
|
|
(label
|
|
("for" "content")
|
|
(text "{{ text \"communities:label.content\" }}"))
|
|
(textarea
|
|
("type" "text")
|
|
("name" "content")
|
|
("id" "content")
|
|
("placeholder" "content")
|
|
("required" "")
|
|
("minlength" "2")
|
|
("maxlength" "4096")))
|
|
(div
|
|
("class" "flex gap-2")
|
|
(button
|
|
("class" "primary")
|
|
(text "{{ text \"communities:action.create\" }}"))
|
|
|
|
(text "{% if drawing_enabled -%}")
|
|
(button
|
|
("class" "lowered")
|
|
("ui_ident" "add_drawing")
|
|
("onclick" "attach_drawing()")
|
|
("type" "button")
|
|
(text "{{ text \"communities:action.draw\" }}"))
|
|
|
|
(button
|
|
("class" "lowered red hidden")
|
|
("ui_ident" "remove_drawing")
|
|
("onclick" "remove_drawing()")
|
|
("type" "button")
|
|
(text "{{ text \"communities:action.remove_drawing\" }}"))
|
|
|
|
(script
|
|
(text "globalThis.attach_drawing = async () => {
|
|
globalThis.gerald = await trigger(\"carp::new\", [document.querySelector(\"[ui_ident=carp_canvas_field]\")]);
|
|
globalThis.gerald.create_canvas();
|
|
|
|
document.querySelector(\"[ui_ident=add_drawing]\").classList.add(\"hidden\");
|
|
document.querySelector(\"[ui_ident=remove_drawing]\").classList.remove(\"hidden\");
|
|
}
|
|
|
|
globalThis.remove_drawing = async () => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
document.querySelector(\"[ui_ident=carp_canvas_field]\").innerHTML = \"\";
|
|
globalThis.gerald = null;
|
|
|
|
document.querySelector(\"[ui_ident=add_drawing]\").classList.remove(\"hidden\");
|
|
document.querySelector(\"[ui_ident=remove_drawing]\").classList.add(\"hidden\");
|
|
}"))
|
|
(text "{%- endif %}"))))
|
|
|
|
(script
|
|
(text "globalThis.gerald = null;
|
|
async function create_question_from_form(e) {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"questions::create\"]);
|
|
|
|
// create body
|
|
const body = new FormData();
|
|
|
|
if (globalThis.gerald) {
|
|
body.append(\"drawing.carpgraph\", new Blob([new Uint8Array(globalThis.gerald.as_carp2())], {
|
|
type: \"application/octet-stream\"
|
|
}));
|
|
}
|
|
|
|
|
|
body.append(
|
|
\"body\",
|
|
JSON.stringify({
|
|
content: e.target.content.value,
|
|
receiver: \"{{ receiver }}\",
|
|
community: \"{{ community }}\",
|
|
is_global: \"{{ is_global }}\" == \"true\",
|
|
}),
|
|
);
|
|
|
|
// ...
|
|
fetch(\"/api/v1/questions\", {
|
|
method: \"POST\",
|
|
body,
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
e.target.reset();
|
|
|
|
if (globalThis.gerald) {
|
|
globalThis.gerald.clear();
|
|
}
|
|
}
|
|
});
|
|
}"))
|
|
|
|
(text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}")
|
|
(div
|
|
("class" "card-nest")
|
|
(text "{{ self::question(question=question[0], owner=question[1], show_community=show_community) }}")
|
|
(div
|
|
("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}")
|
|
(div
|
|
("class" "flex gap-1 reactions_box")
|
|
("hook" "check_reactions")
|
|
("hook-arg:id" "{{ question[0].id }}")
|
|
(text "{{ self::likes(id=question[0].id, asset_type=\"Question\", likes=question[0].likes, dislikes=question[0].dislikes, secondary=false) }}"))
|
|
(div
|
|
("class" "flex gap-1 buttons_box")
|
|
(a
|
|
("href" "/question/{{ question[0].id }}")
|
|
("class" "button small")
|
|
(text "{{ icon \"external-link\" }} {% if user -%}")
|
|
(span
|
|
(text "{{ text \"requests:label.answer\" }}"))
|
|
(text "{% else %}")
|
|
(span
|
|
(text "{{ text \"general:action.open\" }}"))
|
|
(text "{%- endif %}"))
|
|
(text "{% if user -%} {% if can_manage_questions or is_helper or question[1].id == user.id %}")
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "camo small")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(button
|
|
("class" "camo small red")
|
|
("onclick" "trigger('me::remove_question', ['{{ question[0].id }}'])")
|
|
(text "{{ icon \"trash\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.delete\" }}")))))
|
|
(text "{%- endif %} {%- endif %}"))))
|
|
|
|
(text "{%- endmacro %} {% macro spotify_playing(state, size=\"60px\") -%} {% if state and state.data %}")
|
|
(div
|
|
("class" "card-nest")
|
|
(div
|
|
("class" "card flex items-center justify-between gap-2 small")
|
|
(div
|
|
("class" "flex items-center gap-2")
|
|
(b
|
|
(text "Listening on"))
|
|
(text "{{ icon \"spotify\" }}"))
|
|
(span
|
|
("class" "fade date short")
|
|
(text "{{ state.data.timestamp }}")))
|
|
(div
|
|
("class" "card secondary flex gap-2")
|
|
(a
|
|
("href" "{{ state.external_urls.album }}")
|
|
(img
|
|
("src" "{{ state.external_urls.album_img }}")
|
|
("alt" "Album cover")
|
|
("loading" "lazy")
|
|
("class" "avatar")
|
|
("style" "--size: {{ size }}")))
|
|
(div
|
|
("class" "flex flex-col")
|
|
(h5
|
|
("class" "w-full")
|
|
(a
|
|
("href" "{{ state.external_urls.track }}")
|
|
("class" "flush")
|
|
(text "{{ state.data.track }}")))
|
|
(span
|
|
("class" "fade")
|
|
(a
|
|
("href" "{{ state.external_urls.artist }}")
|
|
("class" "flush")
|
|
(text "{{ state.data.artist }}")))
|
|
(span
|
|
("hook" "spotify_time_text")
|
|
("hook-arg:updated" "{{ state.data.timestamp }}")
|
|
("hook-arg:progress" "{{ state.data.progress_ms }}")
|
|
("hook-arg:duration" "{{ state.data.duration_ms }}")
|
|
("hook-arg:display" "full")))))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro last_fm_playing(state, size=\"60px\") -%} {% if state and state.data %}")
|
|
(div
|
|
("class" "card-nest")
|
|
(div
|
|
("class" "card flex items-center justify-between gap-2 small")
|
|
(div
|
|
("class" "flex items-center gap-2")
|
|
(b
|
|
(text "Listening on"))
|
|
(text "{{ icon \"last_fm\" }}"))
|
|
(span
|
|
("class" "fade date short")
|
|
(text "{{ state.data.timestamp }}")))
|
|
(div
|
|
("class" "card secondary flex gap-2")
|
|
(a
|
|
("href" "{{ state.external_urls.track }}")
|
|
(img
|
|
("src" "{{ state.external_urls.track_img }}")
|
|
("alt" "Track cover")
|
|
("loading" "lazy")
|
|
("class" "avatar")
|
|
("style" "--size: {{ size }}")))
|
|
(div
|
|
("class" "flex flex-col")
|
|
(h5
|
|
("class" "w-full")
|
|
(a
|
|
("href" "{{ state.external_urls.track }}")
|
|
("class" "flush")
|
|
(text "{{ state.data.track }}")))
|
|
(span
|
|
("class" "fade")
|
|
(a
|
|
("href" "{{ state.external_urls.artist }}")
|
|
("class" "flush")
|
|
(text "{{ state.data.artist }}")))
|
|
(text "{% if state.data.duration_ms and state.data.duration_ms != \"0\" -%}")
|
|
(span
|
|
("hook" "spotify_time_text")
|
|
("hook-arg:updated" "{{ state.data.timestamp }}")
|
|
("hook-arg:progress" "25000")
|
|
("hook-arg:duration" "{{ state.data.duration_ms }}")
|
|
("hook-arg:display" "full"))
|
|
(text "{%- endif %}"))))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro connection_icon(key) -%}")
|
|
(div
|
|
("style" "display: contents;")
|
|
(text "{% if key == \"Spotify\" -%} {{ icon \"spotify\" }} {% elif key == \"LastFm\" %} {{ icon \"last_fm\" }} {%- endif %}"))
|
|
|
|
(text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %} {% macro message_actions(can_manage_message, owner, message, owner) -%}")
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "camo small")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(text "{% if can_manage_message or (user and user.id == message.owner) -%}")
|
|
(button
|
|
("class" "red")
|
|
("onclick" "delete_message('{{ message.id }}')")
|
|
(text "{{ icon \"trash\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.delete\" }}")))
|
|
(text "{%- endif %}")
|
|
(button
|
|
("onclick" "window.location.href = `${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`")
|
|
(text "{{ icon \"external-link\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.open\" }}")))
|
|
(button
|
|
("onclick" "trigger('atto::copy_text', [`${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`])")
|
|
(text "{{ icon \"copy\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.copy_link\" }}")))
|
|
(button
|
|
("onclick" "mention_user('{{ owner.username }}')")
|
|
(text "{{ icon \"at-sign\" }}")
|
|
(span
|
|
(text "{{ text \"chats:action.mention_user\" }}")))))
|
|
|
|
(text "{%- endmacro %} {% macro message(user, message, can_manage_message=false, grouped=false) -%}")
|
|
(div
|
|
("class" "card secondary message flex gap-2 {% if grouped -%}grouped{%- endif %}")
|
|
("id" "message-{{ message.id }}")
|
|
(text "{% if not grouped -%}")
|
|
(a
|
|
("href" "/@{{ user.username }}")
|
|
("target" "_top")
|
|
(text "{{ self::avatar(username=user.username, size=\"42px\") }}"))
|
|
(text "{%- endif %}")
|
|
(div
|
|
("class" "flex flex-col gap-1 w-full")
|
|
(text "{% if not grouped -%}")
|
|
(div
|
|
("class" "flex gap-2 w-full justify-between flex-wrap")
|
|
(div
|
|
("class" "flex gap-2")
|
|
(text "{{ self::full_username(user=user) }} {% if message.edited != message.created %}")
|
|
(span
|
|
("class" "date")
|
|
(text "{{ message.edited }}")
|
|
(sup
|
|
("title" "Edited")
|
|
(text "*")))
|
|
(text "{% else %}")
|
|
(span
|
|
("class" "date")
|
|
(text "{{ message.created }}"))
|
|
(text "{%- endif %}"))
|
|
(div
|
|
("class" "flex gap-2 hidden")
|
|
(text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")))
|
|
(text "{%- endif %}")
|
|
(div
|
|
("class" "flex w-full gap-2 justify-between")
|
|
(div
|
|
("class" "flex flex-col gap-2")
|
|
(span
|
|
("class" "no_p_margin")
|
|
(text "{{ message.content|markdown|safe }}"))
|
|
|
|
(div
|
|
("class" "flex w-full gap-1 flex-wrap")
|
|
("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
|
|
("hook" "check_message_reactions")
|
|
("hook-arg:id" "{{ message.id }}")
|
|
|
|
(text "{% for emoji,num in message.reactions -%}")
|
|
(button
|
|
("class" "small lowered")
|
|
("ui_ident" "emoji_{{ emoji }}")
|
|
("onclick" "trigger('me::message_react', [event.target.parentElement.parentElement, '{{ message.id }}', '{{ emoji }}'])")
|
|
(span (text "{{ emoji|emojis|safe }} {{ num }}")))
|
|
(text "{%- endfor %}")
|
|
|
|
(div
|
|
("class" "hidden")
|
|
(text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}"))))
|
|
(text "{% if grouped -%}")
|
|
(div
|
|
("class" "hidden")
|
|
(text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}"))
|
|
(text "{%- endif %}"))))
|
|
|
|
(text "{%- endmacro %} {% macro user_menu() -%}")
|
|
(div
|
|
("class" "inner")
|
|
(b
|
|
("class" "title")
|
|
(text "{{ user.username }}"))
|
|
(a
|
|
("href" "/@{{ user.username }}")
|
|
(text "{{ icon \"circle-user-round\" }}")
|
|
(span
|
|
(text "{{ text \"auth:link.my_profile\" }}")))
|
|
(a
|
|
("href" "/journals/0/0")
|
|
(icon (text "notebook"))
|
|
(str (text "general:link.journals")))
|
|
(text "{% if not user.settings.disable_achievements -%}")
|
|
(a
|
|
("href" "/achievements")
|
|
(icon (text "award"))
|
|
(str (text "general:link.achievements")))
|
|
(text "{%- endif %}")
|
|
(a
|
|
("href" "/settings")
|
|
(text "{{ icon \"settings\" }}")
|
|
(span
|
|
(text "{{ text \"auth:link.settings\" }}")))
|
|
(text "{% if is_helper -%}")
|
|
(b
|
|
("class" "title")
|
|
(text "{{ text \"general:label.mod\" }}"))
|
|
(a
|
|
("href" "/mod_panel/audit_log")
|
|
(text "{{ icon \"scroll-text\" }}")
|
|
(span
|
|
(text "{{ text \"general:link.audit_log\" }}")))
|
|
(a
|
|
("href" "/mod_panel/reports")
|
|
(text "{{ icon \"flag\" }}")
|
|
(span
|
|
(text "{{ text \"general:link.reports\" }}")))
|
|
(a
|
|
("href" "/mod_panel/ip_bans")
|
|
(text "{{ icon \"ban\" }}")
|
|
(span
|
|
(text "{{ text \"general:link.ip_bans\" }}")))
|
|
(a
|
|
("href" "/mod_panel/stats")
|
|
(text "{{ icon \"chart-line\" }}")
|
|
(span
|
|
(text "{{ text \"general:link.stats\" }}")))
|
|
(text "{%- endif %}")
|
|
(b ("class" "title") (text "{{ config.name }}"))
|
|
(a
|
|
("href" "https://trisua.com/t/tetratto")
|
|
("class" "button")
|
|
(icon (text "code"))
|
|
(str (text "general:link.source_code")))
|
|
|
|
(a
|
|
("href" "/reference/tetratto/index.html")
|
|
("class" "button")
|
|
("data-turbo" "false")
|
|
(icon (text "rabbit"))
|
|
(str (text "general:link.reference")))
|
|
|
|
(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"))
|
|
(b ("class" "title") (str (text "general:label.account")))
|
|
(button
|
|
("onclick" "trigger('me::switch_account')")
|
|
(text "{{ icon \"ellipsis\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.switch_account\" }}")))
|
|
(button
|
|
("class" "red")
|
|
("onclick" "trigger('me::logout')")
|
|
(text "{{ icon \"log-out\" }}")
|
|
(span
|
|
(text "{{ text \"auth:action.logout\" }}"))))
|
|
|
|
(text "{%- endmacro %} {% macro user_status(other_user) -%} {% if other_user.settings.status %}")
|
|
(div
|
|
("class" "flex items-center gap-2")
|
|
(span
|
|
(text "{{ other_user.settings.status }}"))
|
|
; connection icon
|
|
(text "{% if (other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track) or (other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track) %} {{ icon \"music\" }} {% endif %}"))
|
|
|
|
(text "{% elif other_user.connections.LastFm[0].data.name and other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track %}")
|
|
(div
|
|
("class" "flex items-center gap-2")
|
|
(text "{{ icon \"music\" }}")
|
|
(span
|
|
(b
|
|
(text "Listening to "))
|
|
(text "{{ other_user.connections.LastFm[1].data.artist }}")))
|
|
|
|
(text "{% elif other_user.connections.Spotify[0].data.name and other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track %}")
|
|
(div
|
|
("class" "flex items-center gap-2")
|
|
(text "{{ icon \"music\" }}")
|
|
(span
|
|
(b
|
|
(text "Listening to "))
|
|
(text "{{ other_user.connections.Spotify[1].data.artist }}")))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false) -%}")
|
|
(div
|
|
("class" "flex gap-2 items-center card tiny user_plate {% if secondary -%}secondary{%- endif %}")
|
|
(a
|
|
("href" "/@{{ user.username }}")
|
|
(text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}"))
|
|
(div
|
|
("class" "flex justify-center flex-col")
|
|
("style" "{% if show_menu or show_kick -%}width: 60%{% else %}max-width: 150px{%- endif %}")
|
|
(text "{{ self::full_username(user=user) }}")
|
|
(div
|
|
("class" "user_status")
|
|
(text "{{ self::user_status(other_user=user) }}")))
|
|
(text "{% if show_menu -%}")
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "camo small square")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
(text "{{ icon \"settings\" }}"))
|
|
(text "{{ self::user_menu() }}"))
|
|
(text "{% elif show_kick %}")
|
|
(div
|
|
("class" "dropdown")
|
|
("style" "margin-left: auto")
|
|
(button
|
|
("class" "camo small square")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(button
|
|
("class" "red")
|
|
("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')")
|
|
(text "{{ icon \"x\" }}")
|
|
(span
|
|
(text "{{ text \"chats:action.kick_member\" }}")))))
|
|
(text "{%- endif %}"))
|
|
|
|
(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false, render_button=true, small=false) -%}")
|
|
(text "{% if render_button -%}")
|
|
(button
|
|
("class" "button small {% if not small -%} square {%- endif %} lowered")
|
|
("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()")
|
|
("title" "Emojis")
|
|
("type" "button")
|
|
(text "{{ icon \"smile-plus\" }}"))
|
|
(text "{%- endif %}")
|
|
|
|
(text "{% if render_dialog -%}")
|
|
(dialog
|
|
("id" "emoji_dialog")
|
|
(div
|
|
("class" "inner flex flex-col gap-2")
|
|
(script
|
|
("type" "module")
|
|
("src" "https://unpkg.com/emoji-picker-element@1.22.8/index.js"))
|
|
(emoji-picker
|
|
("style" "
|
|
--border-radius: var(--radius);
|
|
--background: var(--color-super-raised);
|
|
--input-border-radiFus: var(--radius);
|
|
--input-border-color: var(--color-primary);
|
|
--indicator-color: var(--color-primary);
|
|
--emoji-padding: var(--pad-1);
|
|
box-shadow: 0 0 4px var(--color-shadow);
|
|
")
|
|
("class" "w-full"))
|
|
(script
|
|
(text "setTimeout(async () => {
|
|
document.querySelector(\"emoji-picker\").customEmoji =
|
|
await trigger(\"me::emojis\");
|
|
|
|
const style = document.createElement(\"style\");
|
|
style.textContent = `.custom-emoji { border-radius: 4px !important; } .category { font-weight: 600; }`;
|
|
document
|
|
.querySelector(\"emoji-picker\")
|
|
.shadowRoot.appendChild(style);
|
|
}, 150);
|
|
|
|
document
|
|
.querySelector(\"emoji-picker\")
|
|
.addEventListener(\"emoji-click\", async (event) => {
|
|
if (event.detail.skinTone > 0) {
|
|
document.getElementById(
|
|
window.EMOJI_PICKER_TEXT_ID,
|
|
).value += event.detail.unicode;
|
|
|
|
document.getElementById(\"emoji_dialog\").close();
|
|
return;
|
|
}
|
|
|
|
if (event.detail.unicode) {
|
|
if (window.EMOJI_PICKER_MODE === \"replace\") {
|
|
document.getElementById(
|
|
window.EMOJI_PICKER_TEXT_ID,
|
|
).value = `:${await (
|
|
await fetch(\"/api/v1/lookup_emoji\", {
|
|
method: \"POST\",
|
|
body: event.detail.unicode,
|
|
})
|
|
).text()}:`;
|
|
} else {
|
|
document.getElementById(
|
|
window.EMOJI_PICKER_TEXT_ID,
|
|
).value += ` :${await (
|
|
await fetch(\"/api/v1/lookup_emoji\", {
|
|
method: \"POST\",
|
|
body: event.detail.unicode,
|
|
})
|
|
).text()}:`;
|
|
}
|
|
} else {
|
|
if (window.EMOJI_PICKER_MODE === \"replace\") {
|
|
document.getElementById(
|
|
window.EMOJI_PICKER_TEXT_ID,
|
|
).value = `:${event.detail.emoji.shortcodes[0]}:`;
|
|
} else {
|
|
document.getElementById(
|
|
window.EMOJI_PICKER_TEXT_ID,
|
|
).value += ` :${event.detail.emoji.shortcodes[0]}:`;
|
|
}
|
|
}
|
|
|
|
document.getElementById(
|
|
window.EMOJI_PICKER_TEXT_ID,
|
|
).dispatchEvent(new Event(\"change\"));
|
|
|
|
document.getElementById(\"emoji_dialog\").close();
|
|
});"))
|
|
(div
|
|
("class" "flex justify-between")
|
|
(div)
|
|
(div
|
|
("class" "flex gap-2")
|
|
(button
|
|
("class" "bold red lowered")
|
|
("onclick" "document.getElementById('emoji_dialog').close()")
|
|
("type" "button")
|
|
(text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}"))))))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro file_picker(files_list_id) -%}")
|
|
(button
|
|
("class" "button small square lowered")
|
|
("onclick" "pick_file()")
|
|
("title" "Images")
|
|
("type" "button")
|
|
(text "{{ icon \"image-up\" }}"))
|
|
|
|
(input
|
|
("type" "file")
|
|
("multiple" "")
|
|
("accept" "image/png,image/jpeg,image/avif,image/webp")
|
|
("style" "display: none")
|
|
("name" "file_picker"))
|
|
|
|
(div
|
|
("style" "display: none")
|
|
("id" "file_template")
|
|
(text "{{ icon \"image\" }}")
|
|
(b
|
|
("class" "name shorter")
|
|
("style" "overflow-wrap: normal")
|
|
(text ".file_name")))
|
|
|
|
(script
|
|
(text "(() => {
|
|
const input = document.querySelector(\"input[name=file_picker]\");
|
|
const element = document.getElementById(\"{{ files_list_id }}\");
|
|
const template = document.getElementById(\"file_template\");
|
|
|
|
globalThis.pick_file = () => {
|
|
input.click();
|
|
};
|
|
|
|
globalThis.render_file_picker_files = () => {
|
|
element.innerHTML = \"\";
|
|
|
|
let idx = 0;
|
|
for (const file of input.files) {
|
|
element.innerHTML += `<div class=\"card small secondary flex items-center gap-2\" onclick=\"remove_file(${idx})\" style=\"cursor: pointer\">${template.innerHTML.replace(
|
|
\".file_name\",
|
|
file.name,
|
|
)}</div>`;
|
|
|
|
idx += 1;
|
|
}
|
|
};
|
|
|
|
globalThis.remove_file = (idx) => {
|
|
const files = Array.from(input.files);
|
|
files.splice(idx - 1, 1);
|
|
|
|
// update files
|
|
const list = new DataTransfer();
|
|
|
|
for (item of files) {
|
|
list.items.add(item);
|
|
}
|
|
|
|
input.files = list.files;
|
|
|
|
// render
|
|
render_file_picker_files();
|
|
};
|
|
|
|
input.addEventListener(\"change\", () => {
|
|
render_file_picker_files();
|
|
});
|
|
})();"))
|
|
|
|
(text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}")
|
|
(div
|
|
("class" "card w-full supporter_ad")
|
|
("ui_ident" "supporter_ad")
|
|
("onclick" "window.location.href = '/settings#/account/billing'")
|
|
(div
|
|
("class" "card w-full flex flex-wrap items-center gap-2 justify-between")
|
|
(text "{% if body -%}")
|
|
(b
|
|
(text "{{ body }}"))
|
|
(text "{% else %}")
|
|
(b
|
|
(text "{{ text \"general:label.supporter_motivation\" }}"))
|
|
(text "{%- endif %}")
|
|
(a
|
|
("href" "/settings#/account/billing")
|
|
("class" "button small")
|
|
(text "{{ icon \"heart\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.become_supporter\" }}")))))
|
|
|
|
(text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}")
|
|
(div
|
|
("class" "flex gap-2 flex-wrap")
|
|
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if not quoting -%} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %} {%- endif %}")
|
|
|
|
(button
|
|
("class" "small square lowered")
|
|
("title" "Add poll")
|
|
("onclick" "document.getElementById('poll_options_dialog').showModal()")
|
|
("type" "button")
|
|
(text "{{ icon \"list-todo\" }}"))
|
|
|
|
(button
|
|
("class" "small square lowered")
|
|
("title" "More options")
|
|
("onclick" "document.getElementById('post_options_dialog').showModal()")
|
|
("type" "button")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
|
|
(label
|
|
("class" "flex items-center gap-1 button lowered")
|
|
("title" "Mark as NSFW/hide from public timelines")
|
|
("for" "is_nsfw")
|
|
(input
|
|
("type" "checkbox")
|
|
("name" "is_nsfw")
|
|
("id" "is_nsfw")
|
|
("checked" "{{ user.settings.auto_unlist }}")
|
|
("onchange" "POST_INITIAL_SETTINGS['is_nsfw'] = event.target.checked"))
|
|
|
|
(span (icon (text "eye-closed")))))
|
|
|
|
(dialog
|
|
("id" "post_options_dialog")
|
|
(div
|
|
("class" "inner flex flex-col gap-2")
|
|
(div
|
|
("id" "post_options")
|
|
("class" "flex flex-col gap-2"))
|
|
(hr)
|
|
(div
|
|
("class" "flex justify-between")
|
|
(div)
|
|
(div
|
|
("class" "flex gap-2")
|
|
(button
|
|
("class" "bold red lowered")
|
|
("onclick" "document.getElementById('post_options_dialog').close()")
|
|
("type" "button")
|
|
(text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}"))))
|
|
(script
|
|
(text "setTimeout(() => {
|
|
window.POST_INITIAL_SETTINGS = {
|
|
comments_enabled: true,
|
|
reposts_enabled: true,
|
|
reactions_enabled: true,
|
|
is_nsfw: false,
|
|
content_warning: \"\",
|
|
tags: [],
|
|
};
|
|
|
|
window.BLANK_INITIAL_SETTINGS = JSON.stringify(
|
|
window.POST_INITIAL_SETTINGS,
|
|
);
|
|
|
|
const settings_fields = [
|
|
[
|
|
[
|
|
\"comments_enabled\",
|
|
\"Allow people to comment on your post\",
|
|
],
|
|
window.POST_INITIAL_SETTINGS.comments_enabled.toString(),
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"reposts_enabled\",
|
|
\"Allow people to repost/quote your post\",
|
|
],
|
|
window.POST_INITIAL_SETTINGS.reposts_enabled.toString(),
|
|
\"checkbox\",
|
|
],
|
|
[
|
|
[
|
|
\"reactions_enabled\",
|
|
\"Allow people to like/dislike your post\",
|
|
],
|
|
window.POST_INITIAL_SETTINGS.reactions_enabled.toString(),
|
|
\"checkbox\",
|
|
],
|
|
// [
|
|
// [\"is_nsfw\", \"Hide from public timelines\"],
|
|
// window.POST_INITIAL_SETTINGS.is_nsfw.toString(),
|
|
// \"checkbox\",
|
|
// ],
|
|
[
|
|
[\"content_warning\", \"Content warning\"],
|
|
window.POST_INITIAL_SETTINGS.content_warning,
|
|
\"textarea\",
|
|
],
|
|
[
|
|
[\"tags\", \"Tags\"],
|
|
window.POST_INITIAL_SETTINGS.tags,
|
|
\"input\",
|
|
{
|
|
embed_html:
|
|
'<span class=\"fade\">Tags should be separated by a comma.</span>',
|
|
},
|
|
],
|
|
];
|
|
|
|
document.getElementById(\"post_options\").innerHTML = \"\";
|
|
trigger(\"ui::generate_settings_ui\", [
|
|
document.getElementById(\"post_options\"),
|
|
settings_fields,
|
|
window.POST_INITIAL_SETTINGS,
|
|
{
|
|
tags: (new_tags) => {
|
|
window.POST_INITIAL_SETTINGS.tags = new_tags
|
|
.split(\",\")
|
|
.map((t) => t.trim());
|
|
},
|
|
},
|
|
]);
|
|
}, 250);
|
|
|
|
globalThis.update_settings_maybe = async (id) => {
|
|
if (
|
|
JSON.stringify(window.POST_INITIAL_SETTINGS) !==
|
|
window.BLANK_INITIAL_SETTINGS
|
|
) {
|
|
await fetch(`/api/v1/posts/${id}/context`, {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
context: window.POST_INITIAL_SETTINGS,
|
|
}),
|
|
});
|
|
}
|
|
};"))))
|
|
|
|
; poll data manager function and dialog
|
|
;
|
|
; `get_poll_data` returns `[bool, string | PollData]`, where the string in arg 1
|
|
; represents an error message if arg 0 is `false`
|
|
(script
|
|
(text "window.POLL_OPTION_A = \"\";
|
|
window.POLL_OPTION_B = \"\";
|
|
window.POLL_OPTION_C = \"\";
|
|
window.POLL_OPTION_D = \"\";
|
|
window.POLL_EXPIRES = null;
|
|
|
|
window.get_poll_data = () => {
|
|
if (!POLL_OPTION_A && !POLL_OPTION_B) {
|
|
return [true, null];
|
|
}
|
|
|
|
if (POLL_OPTION_A && !POLL_OPTION_B || POLL_OPTION_B && !POLL_OPTION_A) {
|
|
return [false, \"At least 2 options are required for a poll\"];
|
|
}
|
|
|
|
if (POLL_EXPIRES < 0) {
|
|
return [false, \"Polls cannot time travel\"];
|
|
}
|
|
|
|
return [true, {
|
|
option_a: POLL_OPTION_A,
|
|
option_b: POLL_OPTION_B,
|
|
option_c: POLL_OPTION_C,
|
|
option_d: POLL_OPTION_D,
|
|
expires: POLL_EXPIRES,
|
|
}];
|
|
}"))
|
|
|
|
(dialog
|
|
("id" "poll_options_dialog")
|
|
(div
|
|
("class" "inner flex flex-col gap-2")
|
|
(div
|
|
("id" "poll_options")
|
|
("class" "flex flex-col gap-2")
|
|
|
|
(b (text "Attach poll"))
|
|
|
|
(div
|
|
("class" "card flex flex-col gap-2")
|
|
(span
|
|
(b (text "Option A "))
|
|
(span ("class" "fade red") (text "(required)")))
|
|
|
|
(input ("type" "text") ("placeholder" "option A") ("onchange" "window.POLL_OPTION_A = event.target.value")))
|
|
|
|
(div
|
|
("class" "card flex flex-col gap-2")
|
|
(span
|
|
(b (text "Option B "))
|
|
(span ("class" "fade red") (text "(required)")))
|
|
|
|
(input ("type" "text") ("placeholder" "option B") ("onchange" "window.POLL_OPTION_B = event.target.value")))
|
|
|
|
(div
|
|
("class" "card flex flex-col gap-2")
|
|
(b (text "Option C"))
|
|
(input ("type" "text") ("placeholder" "option A") ("onchange" "window.POLL_OPTION_C = event.target.value")))
|
|
|
|
(div
|
|
("class" "card flex flex-col gap-2")
|
|
(b (text "Option D"))
|
|
(input ("type" "text") ("placeholder" "option D") ("onchange" "window.POLL_OPTION_D = event.target.value")))
|
|
|
|
(div
|
|
("class" "card flex flex-col gap-2")
|
|
(b (text "Expires"))
|
|
(input ("type" "date") ("onchange" "window.POLL_EXPIRES = event.target.valueAsDate.getTime() - new Date().getTime()"))))
|
|
(hr)
|
|
(div
|
|
("class" "flex justify-between")
|
|
(div)
|
|
(div
|
|
("class" "flex gap-2")
|
|
(button
|
|
("class" "bold red lowered")
|
|
("onclick" "document.getElementById('poll_options_dialog').close()")
|
|
("type" "button")
|
|
(text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}"))))))
|
|
|
|
(text "{%- endmacro %}")
|
|
(text "{% macro poll(post, poll) -%}")
|
|
(div
|
|
("class" "card lowered w-full flex flex-col gap-2")
|
|
(text "{% set total = poll[0].votes_a + poll[0].votes_b + poll[0].votes_c + poll[0].votes_d %}")
|
|
|
|
(text "{% if poll[1] or poll[2] or user and user.id == poll[0].owner -%}")
|
|
; already voted, show results
|
|
(text "{% if poll[1] %}")
|
|
(span ("class" "fade") (text "You've already voted!"))
|
|
(text "{% elif poll[2] %}")
|
|
(span ("class" "fade") (text "Poll ended!"))
|
|
(text "{% endif %}")
|
|
|
|
; option a
|
|
(div
|
|
("class" "card w-full flex flex-col gap-2")
|
|
(span (text "{{ poll[0].option_a }} ({{ poll[0].votes_a }} votes)"))
|
|
(div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_a / total) * 100 }}%")))
|
|
|
|
; option b
|
|
(div
|
|
("class" "card w-full flex flex-col gap-2")
|
|
(span (text "{{ poll[0].option_b }} ({{ poll[0].votes_b }} votes)"))
|
|
(div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_b / total) * 100 }}%")))
|
|
|
|
; option c
|
|
(text "{% if poll[0].option_c -%}")
|
|
(div
|
|
("class" "card w-full flex flex-col gap-2")
|
|
(span (text "{{ poll[0].option_c }} ({{ poll[0].votes_c }} votes)"))
|
|
(div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_c / total) * 100 }}%")))
|
|
(text "{%- endif %}")
|
|
|
|
; option d
|
|
(text "{% if poll[0].option_d -%}")
|
|
(div
|
|
("class" "card w-full flex flex-col gap-2")
|
|
(span (text "{{ poll[0].option_d }} ({{ poll[0].votes_d }} votes)"))
|
|
(div ("class" "poll_bar") ("style" "width: {{ (poll[0].votes_d / total) * 100 }}%")))
|
|
(text "{%- endif %}")
|
|
(text "{% else %}")
|
|
; not voted yet, just show options so user can vote
|
|
|
|
; option a
|
|
(button
|
|
("class" "hover_left_bar raised justify-start w-full poll_option")
|
|
("onclick" "trigger('me::vote', ['{{ post.id }}', 'A'])")
|
|
(icon (text "tally-1"))
|
|
(text "{{ poll[0].option_a }}"))
|
|
|
|
; option b
|
|
(button
|
|
("class" "hover_left_bar raised justify-start w-full poll_option")
|
|
("onclick" "trigger('me::vote', ['{{ post.id }}', 'B'])")
|
|
(icon (text "tally-2"))
|
|
(text "{{ poll[0].option_b }}"))
|
|
|
|
; option c
|
|
(text "{% if poll[0].option_c -%}")
|
|
(button
|
|
("class" "hover_left_bar raised justify-start w-full poll_option")
|
|
("onclick" "trigger('me::vote', ['{{ post.id }}', 'C'])")
|
|
(icon (text "tally-3"))
|
|
(text "{{ poll[0].option_c }}"))
|
|
(text "{%- endif %}")
|
|
|
|
; option d
|
|
(text "{% if poll[0].option_d -%}")
|
|
(button
|
|
("class" "hover_left_bar raised justify-start w-full poll_option")
|
|
("onclick" "trigger('me::vote', ['{{ post.id }}', 'D'])")
|
|
(icon (text "tally-4"))
|
|
(text "{{ poll[0].option_d }}"))
|
|
(text "{%- endif %}")
|
|
(text "{%- endif %}")
|
|
|
|
; show expiration date + totals
|
|
(div
|
|
("class" "flex w-full flex-wrap gap-2")
|
|
(span ("class" "notification chip") (text "{{ total }} votes"))
|
|
(text "{% if not poll[2] -%}")
|
|
(span
|
|
("class" "notification chip")
|
|
(text "Expires in ")
|
|
(span
|
|
("class" "poll_date")
|
|
("data-created" "{{ poll[0].created }}")
|
|
("data-expires" "{{ poll[0].expires }}")))
|
|
(text "{%- endif %}")))
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro community_info(community) %}")
|
|
(div
|
|
("class" "card-nest flex flex-col")
|
|
(div
|
|
("id" "bio")
|
|
("class" "card small no_p_margin")
|
|
(text "{{ community.context.description|markdown|safe }}"))
|
|
(div
|
|
("class" "card flex flex-col gap-2")
|
|
(div
|
|
("class" "w-full flex justify-between items-center")
|
|
(span
|
|
("class" "notification chip")
|
|
(text "ID"))
|
|
(button
|
|
("title" "Copy")
|
|
("onclick" "trigger('atto::copy_text', ['{{ community.id }}'])")
|
|
("class" "camo small")
|
|
(text "{{ icon \"copy\" }}")))
|
|
(div
|
|
("class" "w-full flex justify-between items-center")
|
|
(span
|
|
("class" "notification chip")
|
|
(text "Created "))
|
|
(span
|
|
("class" "date")
|
|
(text "{{ community.created }}")))
|
|
(div
|
|
("class" "w-full flex justify-between items-center")
|
|
(span
|
|
("class" "notification chip")
|
|
(text "Members"))
|
|
(a
|
|
("href" "/community/{{ community.title }}/members")
|
|
(text "{{ community.member_count }}")))
|
|
(div
|
|
("class" "w-full flex justify-between items-center")
|
|
(span
|
|
("class" "notification chip")
|
|
(text "Posts"))
|
|
(a
|
|
("href" "/community/{{ community.title }}")
|
|
(text "{{ community.post_count }}")))
|
|
(div
|
|
("class" "w-full flex justify-between items-center")
|
|
(span
|
|
("class" "notification chip")
|
|
(text "Score"))
|
|
(div
|
|
("class" "flex gap-2")
|
|
(b
|
|
(text "{{ community.likes - community.dislikes }}"))
|
|
(text "{% if user -%}")
|
|
(div
|
|
("class" "flex gap-1 reactions_box")
|
|
("hook" "check_reactions")
|
|
("hook-arg:id" "{{ community.id }}")
|
|
(text "{{ components::likes(id=community.id, asset_type=\"Community\", likes=community.likes, dislikes=community.dislikes) }}"))
|
|
(text "{%- endif %}")))))
|
|
(text "{% endmacro %}")
|
|
|
|
(text "{% macro community_actions(community) -%}")
|
|
(text "{% if user -%}")
|
|
(div
|
|
("class" "card flex gap-2 flex-wrap")
|
|
("id" "join_or_leave")
|
|
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
|
|
(button
|
|
("class" "primary")
|
|
("onclick" "join_community()")
|
|
(text "{{ icon \"circle-plus\" }}")
|
|
(span
|
|
(text "{{ text \"communities:action.join\" }}")))
|
|
(script
|
|
(text "globalThis.join_community = () => {
|
|
fetch(
|
|
\"/api/v1/communities/{{ community.id }}/join\",
|
|
{
|
|
method: \"POST\",
|
|
},
|
|
)
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 150);
|
|
});
|
|
};"))
|
|
(text "{% else %}")
|
|
(button
|
|
("class" "lowered red")
|
|
("onclick" "cancel_request()")
|
|
(text "{{ icon \"x\" }}")
|
|
(span
|
|
(text "{{ text \"communities:action.cancel_request\" }}")))
|
|
(script
|
|
(text "globalThis.cancel_request = async () => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(
|
|
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
|
|
{
|
|
method: \"DELETE\",
|
|
},
|
|
)
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 150);
|
|
});
|
|
};"))
|
|
(text "{%- endif %} {% else %}")
|
|
(button
|
|
("class" "lowered red")
|
|
("onclick" "leave_community()")
|
|
(text "{{ icon \"circle-minus\" }}")
|
|
(span
|
|
(text "{{ text \"communities:action.leave\" }}")))
|
|
(a
|
|
("href" "/chats/{{ community.id }}/0")
|
|
("class" "button lowered")
|
|
(text "{{ icon \"message-circle\" }}")
|
|
(span
|
|
(text "{{ text \"communities:label.chats\" }}")))
|
|
(text "{% if user and can_post -%}")
|
|
(a
|
|
("href" "/communities/intents/post?community={{ community.id }}")
|
|
("class" "button lowered")
|
|
("data-turbo" "false")
|
|
(text "{{ icon \"plus\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.post\" }}")))
|
|
(text "{%- endif %}")
|
|
(script
|
|
(text "globalThis.leave_community = async () => {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(
|
|
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
|
|
{
|
|
method: \"DELETE\",
|
|
},
|
|
)
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 150);
|
|
});
|
|
};"))
|
|
(text "{%- endif %} {% else %}")
|
|
(a
|
|
("href" "/chats/{{ community.id }}/0")
|
|
("class" "button lowered")
|
|
(text "{{ icon \"message-circle\" }}")
|
|
(span
|
|
(text "{{ text \"communities:label.chats\" }}")))
|
|
(a
|
|
("href" "/communities/intents/post?community={{ community.id }}")
|
|
("class" "button lowered")
|
|
("data-turbo" "false")
|
|
(text "{{ icon \"plus\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.post\" }}")))
|
|
(text "{%- endif %} {% if can_manage_community or is_manager -%}")
|
|
(a
|
|
("href" "/community/{{ community.id }}/manage")
|
|
("class" "button primary")
|
|
(text "{{ icon \"settings\" }}")
|
|
(span
|
|
(text "{{ text \"communities:action.configure\" }}")))
|
|
(text "{%- endif %}"))
|
|
(text "{%- endif %}")
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro ticket(post, owner) -%}")
|
|
(div
|
|
("href" "/post/{{ post.id }}")
|
|
("class" "card secondary w-fill flex flex-col gap-2")
|
|
(div
|
|
("class" "flex gap-2 items-center")
|
|
; user info
|
|
(a
|
|
("href" "/@{{ owner.username }}")
|
|
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
|
|
(span
|
|
("class" "name")
|
|
(text "{{ self::full_username(user=owner) }}"))
|
|
|
|
; timestamp
|
|
(span ("class" "date") (text "{{ post.created }}"))
|
|
|
|
; pinned
|
|
(text "{% if post.context.is_pinned -%}")
|
|
(icon (text "pin"))
|
|
(text "{%- endif %}"))
|
|
|
|
; post title
|
|
(a
|
|
("href" "/post/{{ post.id }}")
|
|
("class" "flush flex gap-2 items-center")
|
|
; open/closed icon
|
|
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
|
|
(span
|
|
("title" "Open")
|
|
("class" "flex items-center green")
|
|
(text "{{ icon \"circle-dot\" }}"))
|
|
(text "{% else %}")
|
|
(span
|
|
("title" "Closed")
|
|
("class" "flex items-center purple")
|
|
(text "{{ icon \"circle-check\" }}"))
|
|
(text "{%- endif %} {%- endif %}")
|
|
|
|
(h4
|
|
("class" "no_p_margin")
|
|
(text "{{ post.title|markdown|safe }}"))))
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro stack_listing(stack) -%}")
|
|
(a
|
|
("href" "/stacks/{{ stack.id }}")
|
|
("class" "card secondary flex flex-col gap-2")
|
|
(div
|
|
("class" "flex items-center gap-2")
|
|
(text "{{ icon \"list\" }}")
|
|
(b
|
|
(text "{{ stack.name }}")))
|
|
(span
|
|
(text "Created ")
|
|
(span
|
|
("class" "date")
|
|
(text "{{ stack.created }}"))
|
|
(text "; {{ stack.privacy }}; {{ stack.users|length }} users")))
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro journal_listing(journal, notes, selected_note, selected_journal, view_mode=false, owner=false) -%}")
|
|
(text "{% if selected_journal != journal.id -%}")
|
|
; not selected
|
|
(div
|
|
("class" "flex flex-row gap-1")
|
|
(a
|
|
("href" "/journals/{{ journal.id }}/0?view={{ view_mode }}")
|
|
("class" "button justify-start lowered w-full")
|
|
(icon (text "notebook"))
|
|
(text "{{ journal.title }}"))
|
|
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "big_icon lowered")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
("style" "width: 32px")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(button
|
|
("onclick" "delete_journal('{{ journal.id }}')")
|
|
("class" "red")
|
|
(text "{{ icon \"trash\" }}")
|
|
(span
|
|
(text "{{ text \"general:action.delete\" }}"))))))
|
|
(text "{% else %}")
|
|
; selected
|
|
(div
|
|
("class" "flex flex-row gap-1")
|
|
(button
|
|
("class" "justify-start lowered w-full")
|
|
(icon (text "arrow-down"))
|
|
(text "{{ journal.title }}"))
|
|
|
|
(text "{% if user and user.id == journal.owner -%}")
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "big_icon lowered")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
("style" "width: 32px")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(a
|
|
("class" "button")
|
|
("href" "/journals/{{ journal.id }}/0?view={{ view_mode }}")
|
|
(icon (text "house"))
|
|
(str (text "general:link.home")))
|
|
(button
|
|
("onclick" "delete_journal('{{ journal.id }}')")
|
|
("class" "red")
|
|
(icon (text "trash"))
|
|
(str (text "general:action.delete")))))
|
|
(text "{%- endif %}"))
|
|
|
|
(text "{% if selected_note -%}")
|
|
; open all details elements above the selected note
|
|
(script
|
|
("defer" "true")
|
|
(text "setTimeout(() => {
|
|
let cursor = document.querySelector(\"[ui_ident=active_note]\");
|
|
while (cursor) {
|
|
if (cursor.nodeName === \"DETAILS\") {
|
|
cursor.setAttribute(\"open\", \"true\");
|
|
}
|
|
|
|
cursor = cursor.parentElement;
|
|
}
|
|
}, 150);"))
|
|
(text "{%- endif %}")
|
|
|
|
(div
|
|
("class" "flex flex-col gap-2")
|
|
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
|
|
; create note
|
|
(text "{% if user and user.id == journal.owner -%}")
|
|
(button
|
|
("class" "lowered justify-start w-full")
|
|
("onclick" "create_note()")
|
|
(icon (text "plus"))
|
|
(str (text "journals:action.create_note")))
|
|
(text "{%- endif %}")
|
|
|
|
; note listings
|
|
(text "{{ self::notes_list_dir_listing_inner(dir=[0, 0, \"root\"], dirs=journal.dirs, notes=notes, owner=owner, journal=journal, view_mode=view_mode) }}"))
|
|
(text "{%- endif %}")
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro notes_list_dir_listing(dir, dirs, notes, owner, journal, view_mode=false) -%}")
|
|
(details
|
|
(summary
|
|
("class" "button w-full justify-start raised w-full")
|
|
(icon (text "folder"))
|
|
(text "{{ dir[2] }}"))
|
|
|
|
(div
|
|
("class" "flex flex-col gap-2")
|
|
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
|
|
(text "{{ self::notes_list_dir_listing_inner(dir=dir, dirs=dirs, notes=notes, owner=owner, journal=journal, view_mode=view_mode) }}")))
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro notes_list_dir_listing_inner(dir, dirs, notes, owner, journal, view_mode=false) -%}")
|
|
; child dirs
|
|
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
|
|
(text "{{ self::notes_list_dir_listing(dir=subdir, dirs=dirs, notes=notes, owner=owner, journal=journal) }}")
|
|
(text "{%- endif %} {% endfor %}")
|
|
|
|
; child notes
|
|
(text "{% for note in notes %} {% if note.dir == dir[0] -%} {% if not view_mode or note.title != \"journal.css\" -%}")
|
|
(text "{{ self::notes_list_note_listing(note=note, owner=owner, journal=journal) }}")
|
|
(text "{%- endif %} {%- endif %} {% endfor %}")
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro notes_list_note_listing(owner, journal, note) -%}")
|
|
(div
|
|
("class" "flex flex-row gap-1")
|
|
("ui_ident" "{% if selected_note == note.id -%} active_note {%- else -%} inactive_note {%- endif %}")
|
|
(a
|
|
("href" "{% if owner -%} /@{{ owner.username }}/{{ journal.title }}/{{ note.title }} {%- else -%} /journals/{{ journal.id }}/{{ note.id }} {%- endif %}")
|
|
("class" "button justify-start w-full {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}")
|
|
(icon (text "file-text"))
|
|
(text "{{ note.title }}"))
|
|
|
|
(text "{% if user and user.id == journal.owner -%}")
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "big_icon {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
("style" "width: 32px")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(button
|
|
("onclick" "change_note_title('{{ note.id }}')")
|
|
(icon (text "pencil"))
|
|
(str (text "chats:action.rename")))
|
|
(a
|
|
("href" "/journals/{{ journal.id }}/{{ note.id }}#/tags")
|
|
(icon (text "tag"))
|
|
(str (text "journals:action.edit_tags")))
|
|
(button
|
|
("onclick" "window.NOTE_MOVER_NOTE_ID = '{{ note.id }}'; document.getElementById('note_mover_dialog').showModal()")
|
|
(icon (text "brush-cleaning"))
|
|
(str (text "journals:action.move")))
|
|
(text "{% if note.is_global -%}")
|
|
(a
|
|
("class" "button")
|
|
("href" "/x/{{ note.title }}")
|
|
(icon (text "eye"))
|
|
(str (text "journals:action.view")))
|
|
|
|
(button
|
|
("class" "purple")
|
|
("onclick" "unpublish_note('{{ note.id }}')")
|
|
(icon (text "globe-lock"))
|
|
(str (text "journals:action.unpublish")))
|
|
(text "{% elif note.title != 'journal.css' %}")
|
|
(button
|
|
("class" "green")
|
|
("onclick" "publish_note('{{ note.id }}')")
|
|
(icon (text "globe"))
|
|
(str (text "journals:action.publish")))
|
|
(text "{%- endif %}")
|
|
(button
|
|
("onclick" "delete_note('{{ note.id }}')")
|
|
("class" "red")
|
|
(icon (text "trash"))
|
|
(str (text "general:action.delete")))))
|
|
(text "{%- endif %}"))
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro note_tags(note) -%} {% if note and note.tags|length > 0 -%}")
|
|
(div
|
|
("class" "flex gap-1 flex-wrap")
|
|
(text "{% for tag in note.tags %}")
|
|
(a
|
|
("href" "{% if view_mode -%} /@{{ owner.username }} {%- else -%} /@{{ user.username }} {%- endif -%} /{{ journal.title }}?tag={{ tag }}")
|
|
("class" "notification chip")
|
|
(span (text "{{ tag }}")))
|
|
(text "{% endfor %}"))
|
|
(text "{%- endif %} {%- endmacro %}")
|
|
|
|
(text "{% macro directories_editor(dirs) -%}")
|
|
(button
|
|
("onclick" "create_directory('0')")
|
|
(icon (text "plus"))
|
|
(str (text "journals:action.create_root_dir")))
|
|
|
|
(text "{% for dir in dirs %} {% if dir[1] == 0 -%}")
|
|
(text "{{ self::directories_editor_listing(dir=dir, dirs=dirs) }}")
|
|
(text "{%- endif %} {% endfor %}")
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro directories_editor_listing(dir, dirs) -%}")
|
|
(div
|
|
("class" "flex flex-row gap-1")
|
|
(button
|
|
("class" "justify-start lowered w-full")
|
|
(icon (text "folder-open"))
|
|
(text "{{ dir[2] }}"))
|
|
|
|
(div
|
|
("class" "dropdown")
|
|
(button
|
|
("class" "big_icon lowered")
|
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
|
("exclude" "dropdown")
|
|
("style" "width: 32px")
|
|
(text "{{ icon \"ellipsis\" }}"))
|
|
(div
|
|
("class" "inner")
|
|
(button
|
|
("onclick" "create_directory('{{ dir[0] }}')")
|
|
(icon (text "plus"))
|
|
(str (text "journals:action.create_subdir")))
|
|
(button
|
|
("onclick" "delete_directory('{{ dir[0] }}')")
|
|
("class" "red")
|
|
(icon (text "trash"))
|
|
(str (text "general:action.delete"))))))
|
|
|
|
(div
|
|
("class" "flex flex-col gap-2")
|
|
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
|
|
; subdir listings
|
|
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
|
|
(text "{{ self::directories_editor_listing(dir=subdir, dirs=dirs) }}")
|
|
(text "{%- endif %} {% endfor %}"))
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro note_mover_dirs(dirs) -%}")
|
|
(text "{% for dir in dirs %} {% if dir[1] == 0 -%}")
|
|
(text "{{ self::note_mover_dirs_listing(dir=dir, dirs=dirs) }}")
|
|
(text "{%- endif %} {% endfor %}")
|
|
(text "{%- endmacro %}")
|
|
|
|
(text "{% macro note_mover_dirs_listing(dir, dirs) -%}")
|
|
(button
|
|
("onclick" "move_note_dir(window.NOTE_MOVER_NOTE_ID, '{{ dir[0] }}'); document.getElementById('note_mover_dialog').close()")
|
|
("class" "justify-start lowered w-full")
|
|
(icon (text "folder-open"))
|
|
(text "{{ dir[2] }}"))
|
|
|
|
(div
|
|
("class" "flex flex-col gap-2")
|
|
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
|
|
; subdir listings
|
|
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
|
|
(text "{{ self::note_mover_dirs_listing(dir=subdir, dirs=dirs) }}")
|
|
(text "{%- endif %} {% endfor %}"))
|
|
(text "{%- endmacro %}")
|