{% 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 }}" /> {%- 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 }}" /> {% else %} <img src="/api/v1/communities/{{ id }}/avatar" alt="{{ id }}'s avatar" class="avatar shadow" loading="lazy" style="--size: {{ size }}" /> {% 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 }};" /> {%- 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" /> {% else %} <img src="/api/v1/communities/{{ id }}/banner" alt="{{ id }}'s banner" class="banner shadow" loading="lazy" /> {% endif %} {%- endmacro %} {% macro community_listing_card(community) -%} <a class="card secondary w-full flex items-center gap-4" href="/community/{{ community.title }}" > {{ components::community_avatar(id=community.id, community=community, size="48px") }} <div class="flex flex-col"> <h3 class="name lg:long">{{ community.context.display_name }}</h3> <span class="fade"><b>{{ community.member_count }}</b> members</span> </div> </a> {%- endmacro %} {% macro username(user) -%} <div style="display: contents"> {% if user.settings.display_name %} {{ user.settings.display_name }} {% else %} {{ user.username }} {% endif %} </div> {%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0) -%} <button title="Like" class="camo small" hook_element="reaction.like" onclick="trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', true])" > {{ icon "heart" }} {% if likes > 0 %} <span>{{ likes }}</span> {% endif %} </button> <button title="Dislike" class="camo small" hook_element="reaction.dislike" onclick="trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', false])" > {{ icon "heart-crack" }} {% if dislikes > 0 %} <span>{{ dislikes }}</span> {% endif %} </button> {%- endmacro %} {% macro full_username(user) -%} <div class="flex"> <a href="/@{{ user.username }}" class="flush" style="font-weight: 600"> {{ components::username(user=user) }} </a> {{ components::online_indicator(user=user) }} {% if user.is_verified %} <span title="Verified" style="color: var(--color-primary)" class="flex items-center" > {{ icon "badge-check" }} </span> {% endif %} </div> {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%} <div style="display: contents"> <!-- prettier-ignore --> <div style="display: none" id="repost-content:{{ post.id }}"> {% if repost %} {{ components::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %} <div class="card tertiary red flex items-center gap-2"> {{ icon "frown" }} <span>Could not find original post...</span> </div> {% endif %} </div> {{ components::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post) }} <script> document.getElementById("post-content:{{ post.id }}").innerHTML += document.getElementById("repost-content:{{ post.id }}").innerHTML; document.getElementById("repost-content:{{ post.id }}").remove(); document .getElementById("post:{{ post.id }}") .querySelector(".avatar") .setAttribute("style", "--size: 24px"); document .getElementById("post:{{ post.id }}") .querySelector(".name") .parentElement.prepend( document .getElementById("post:{{ post.id }}") .querySelector(".avatar"), ); </script> </div> {%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false) -%} {% if community and show_community and community.id != config.town_square or question %} <div class="card-nest"> {% if question %} {{ components::question(question=question[0], owner=question[1]) }} {% else %} <div class="card small"> <a href="/api/v1/communities/find/{{ post.community }}" class="flush flex gap-1 items-center" > {{ components::community_avatar(id=post.community, community=community) }} <b> <!-- prettier-ignore --> {% if community.context.display_name %} {{ community.context.display_name }} {% else %} {{ community.title }} {% endif %} </b> {% if post.context.is_pinned or post.context.is_profile_pinned %} {{ icon "pin" }} {% endif %} </a> </div> {% endif %} {% endif %} <div class="card flex flex-col gap-2 {% if secondary %}secondary{% endif %}" id="post:{{ post.id }}" > <div class="w-full flex gap-2"> <a href="/@{{ owner.username }}"> {{ components::avatar(username=owner.username, size="52px", selector_type="username") }} </a> <div class="flex flex-col w-full gap-1"> <div class="flex flex-wrap gap-2 items-center"> <span class="name" >{{ components::full_username(user=owner) }}</span > {% if post.context.edited != 0 %} <div class="flex"> <span class="fade date">{{ post.context.edited }}</span> <sup title="Edited">*</sup> </div> {% else %} <span class="fade date">{{ post.created }}</span> {% endif %} {% if post.context.is_nsfw %} <span title="NSFW post" class="flex items-center" style="color: var(--color-primary)" > {{ icon "square-asterisk" }} </span> {% endif %} {% if post.context.repost and post.context.repost.reposting %} <span title="Repost" class="flex items-center" style="color: var(--color-primary)" > {{ icon "repeat-2" }} </span> {% endif %} {% if post.community == config.town_square %} <span title="Posted to profile" class="flex items-center" style="color: var(--color-primary)" > {{ icon "user-round" }} </span> {% endif %} </div> <span id="post-content:{{ post.id }}" class="no_p_margin" >{{ post.content|markdown|safe }}</span > </div> </div> <div class="flex justify-between items-center gap-2 w-full"> {% if user %} <div class="flex gap-1 reactions_box" hook="check_reactions" hook-arg:id="{{ post.id }}" > <!-- prettier-ignore --> {% if post.context.reactions_enabled %} {% if post.content|length > 0 %} {{ components::likes(id=post.id, asset_type="Post", likes=post.likes, dislikes=post.dislikes) }} {% endif %} {% endif %} {% if post.context.repost and post.context.repost.reposting %} <a href="/post/{{ post.context.repost.reposting }}" class="button small camo" title='{{ text "communities:label.expand_original" }}' > {{ icon "expand" }} </a> {% endif %} </div> {% else %} <div></div> {% endif %} <div class="flex gap-1 buttons_box"> <a href="/post/{{ post.id }}" class="button camo small"> {{ icon "message-circle" }} <span>{{ post.comment_count }}</span> </a> <a href="/post/{{ post.id }}" class="button camo small" target="_blank" > {{ icon "external-link" }} </a> {% if user %} <div class="dropdown"> <button class="camo small" onclick="trigger('atto::hooks::dropdown', [event])" exclude="dropdown" > {{ icon "ellipsis" }} </button> <div class="inner"> {% if config.town_square and post.context.reposts_enabled %} <b class="title">{{ text "general:label.share" }}</b> <button onclick="trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}'])" > {{ icon "repeat-2" }} <span>{{ text "communities:label.repost" }}</span> </button> <button onclick="window.REPOST_ID = '{{ post.id }}'; document.getElementById('quote_dialog').showModal()" > {{ icon "quote" }} <span >{{ text "communities:label.quote_post" }}</span > </button> {% endif %} {% if user.id != post.owner %} <b class="title">{{ text "general:label.safety" }}</b> <button class="red" onclick="trigger('me::report', ['{{ post.id }}', 'post'])" > {{ icon "flag" }} <span>{{ text "general:action.report" }}</span> </button> {% endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %} <b class="title">{{ text "general:action.manage" }}</b> {% if user.id == post.owner %} <a href="/post/{{ post.id }}#/edit"> {{ icon "pen" }} <span >{{ text "communities:label.edit_content" }}</span > </a> {% endif %} <a href="/post/{{ post.id }}#/configure"> {{ icon "settings" }} <span >{{ text "communities:action.configure" }}</span > </a> <button class="red" onclick="trigger('me::remove_post', ['{{ post.id }}'])" > {{ icon "trash" }} <span>{{ text "general:action.delete" }}</span> </button> {% endif %} </div> </div> {% endif %} </div> </div> </div> {% if community and show_community and community.id != config.town_square or question %} </div> {% endif %} {%- endmacro %} {% macro notification(notification) -%} <div class="w-full card-nest"> <div class="card small notif_title flex items-center"> {% 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"></circle> </svg> {% endif %} <b class="no_p_margin">{{ notification.title|markdown|safe }}</b> </div> <div class="card notif_content flex flex-col gap-2"> <span class="no_p_margin" >{{ notification.content|markdown|safe }}</span > <div class="card secondary w-full flex flex-wrap gap-2"> {% if notification.read %} <button class="tertiary" onclick="trigger('me::update_notification_read_status', ['{{ notification.id }}', false])" > {{ icon "undo" }} <span>{{ text "notifs:action.mark_as_unread" }}</span> </button> {% else %} <button class="green tertiary" onclick="trigger('me::update_notification_read_status', ['{{ notification.id }}', true])" > {{ icon "check" }} <span>{{ text "notifs:action.mark_as_read" }}</span> </button> {% endif %} <button class="red tertiary" onclick="trigger('me::remove_notification', ['{{ notification.id }}'])" > {{ icon "trash" }} <span>{{ text "general:action.delete" }}</span> </button> </div> </div> </div> {%- endmacro %} {% macro user_card(user) -%} <a class="card-nest w-full" href="/@{{ user.username }}"> <div class="card small" style="padding: 0"> {{ components::banner(username=user.username, border_radius="0px") }} </div> <div class="card secondary flex items-center gap-4"> {{ components::avatar(username=user.username, size="48px") }} <div class="flex items-center"> <b>{{ components::username(user=user) }}</b> {{ components::online_indicator(user=user) }} </div> </div> </a> {%- endmacro %} {% macro pagination(page=0, items=0, key="", value="") -%} <div class="flex justify-between gap-2 w-full"> {% if page > 0 %} <a class="button quaternary" href="?page={{ page - 1 }}{{ key }}{{ value }}" > {{ icon "arrow-left" }} <span>{{ text "general:link.previous" }}</span> </a> {% else %} <div></div> {% endif %} {% if items != 0 %} <a class="button quaternary" href="?page={{ page + 1 }}{{ key }}{{ value }}" > <span>{{ text "general:link.next" }}</span> {{ icon "arrow-right"}} </a> {% endif %} </div> {%- 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"></circle> </svg> </div> <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"></circle> </svg> </div> <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"></circle> </svg> </div> </div> {% 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"></circle> </svg> </div> {% endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue %} <style> :root, * { --hue: {{ user.settings.theme_hue }} !important; } </style> {% endif %} {% if user.settings.theme_sat %} <style> :root, * { --sat: {{ user.settings.theme_sat }} !important; } </style> {% endif %} {% if user.settings.theme_lit %} <style> :root, * { --lit: {{ user.settings.theme_lit }} !important; } </style> {% endif %} {% if theme_preference %} <script> function match_user_theme() { const pref = "{{ theme_preference }}".toLowerCase(); if (pref === "auto") { return; } document.documentElement.className = pref; } setTimeout(() => { match_user_theme(); }, 150); </script> {% endif %} <!-- prettier-ignore --> <div style="display: none;"> {{ components::theme_color(color=user.settings.theme_color_surface, css="color-surface") }} {{ components::theme_color(color=user.settings.theme_color_text, css="color-text") }} {{ components::theme_color(color=user.settings.theme_color_text_link, css="color-link") }} {{ components::theme_color(color=user.settings.theme_color_lowered, css="color-lowered") }} {{ components::theme_color(color=user.settings.theme_color_text_lowered, css="color-text-lowered") }} {{ components::theme_color(color=user.settings.theme_color_super_lowered, css="color-super-lowered") }} {{ components::theme_color(color=user.settings.theme_color_raised, css="color-raised") }} {{ components::theme_color(color=user.settings.theme_color_text_raised, css="color-text-raised") }} {{ components::theme_color(color=user.settings.theme_color_super_raised, css="color-super-raised") }} {{ components::theme_color(color=user.settings.theme_color_primary, css="color-primary") }} {{ components::theme_color(color=user.settings.theme_color_text_primary, css="color-text-primary") }} {{ components::theme_color(color=user.settings.theme_color_primary_lowered, css="color-primary-lowered") }} {{ components::theme_color(color=user.settings.theme_color_secondary, css="color-secondary") }} {{ components::theme_color(color=user.settings.theme_color_text_secondary, css="color-text-secondary") }} {{ components::theme_color(color=user.settings.theme_color_secondary_lowered, css="color-secondary-lowered") }} {% if user.permissions|has_supporter %} <style>{{ user.settings.theme_custom_css }}</style> {% endif %} </div> {% endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color %} <!-- prettier-ignore --> <style> :root, * { --{{ css }}: {{ color|color }} !important; } </style> {% endif %} {%- endmacro %} {% macro quote_form() -%} {% if config.town_square and user %} <div class="card-nest"> <div class="card small flex flex-col"> <div class="flex items-center gap-2"> {{ icon "quote" }} <span>{{ text "communities:label.quote_post" }}</span> </div> </div> <form class="card flex flex-col gap-2" onsubmit="create_repost_from_form(event)" > <div class="flex flex-col gap-1"> <label for="content">{{ text "communities:label.content" }}</label> <textarea type="text" name="content" id="content" placeholder="content" required minlength="2" maxlength="4096" ></textarea> </div> <button class="primary">{{ text "communities:action.create" }}</button> </form> </div> <script> async function create_repost_from_form(e) { e.preventDefault(); await trigger("atto::debounce", ["posts::create"]); await trigger("me::repost", [ window.REPOST_ID, e.target.content.value, "{{ config.town_square }}", ]); } </script> {% endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false) -%} <div class="card{% if secondary %} secondary{% endif %} flex gap-2"> <a href="/@{{ owner.username }}"> {{ components::avatar(username=owner.username, selector_type="username", size="52px") }} </a> <div class="flex flex-col gap-1"> <div class="flex items-center gap-2 flex-wrap"> <span class="name" >{{ components::full_username(user=owner) }}</span > <span class="date">{{ question.created }}</span> <span title="Question" class="flex items-center" style="color: var(--color-primary)" > {{ icon "message-circle-heart" }} </span> {% if question.community > 0 and show_community %} <a href="/api/v1/communities/find/{{ question.community }}" class="flex items-center" > {{ components::community_avatar(id=question.community, size="24px") }} </a> {% endif %} {% if question.is_global %} <a class="notification chip" href="/question/{{ question.id }}" >{{ question.answer_count }} answers</a > {% endif %} </div> <span class="no_p_margin" style="font-weight: 500" >{{ question.content|markdown|safe }}</span > <div class="flex gap-2 items-center justify-between"></div> </div> </div> {%- endmacro %} {% macro create_question_form(receiver="0", community="", is_global=false) -%} <div class="card-nest"> <div class="card small flex items-center gap-2"> {{ icon "message-circle-heart" }} <span>{{ text "requests:label.ask_question" }}</span> </div> <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 "communities:label.content" }}</label> <textarea type="text" name="content" id="content" placeholder="content" required minlength="2" maxlength="4096" ></textarea> </div> <button class="primary">{{ text "communities:action.create" }}</button> </form> </div> <script> 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(); } }); } </script> {%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false) -%} <div class="card-nest"> {{ components::question(question=question[0], owner=question[1], show_community=false) }} <div class="card flex flex-wrap gap-2{% if secondary %} secondary{% endif %}" > <a href="/question/{{ question[0].id }}" class="button quaternary small" > {{ icon "external-link" }} {% if user %} <span>{{ text "requests:label.answer" }}</span> {% else %} <span>{{ text "general:action.open" }}</span> {% endif %} </a> {% if user %} {% if can_manage_questions or is_helper or question[1].id == user.id %} <button class="quaternary small red" onclick="trigger('me::remove_question', ['{{ question[0].id }}'])" > {{ icon "trash" }} <span>{{ text "general:action.delete" }}</span> </button> {% endif %} {% endif %} </div> </div> {%- endmacro %}