{# Macro to render reaction counts summary #} {% macro render_counts(counts, css_class='', element='span') %} {% if counts and counts.total %} <{{ element }} class="{{ css_class }}"> {% if counts.likes %}⭐ {{ counts.likes }}{% endif %} {% if counts.boosts %}🔁 {{ counts.boosts }}{% endif %} {% if counts.replies %}💬 {{ counts.replies }}{% endif %} {% if counts.quotes %}🗣️ {{ counts.quotes }}{% endif %} {% if counts.mentions %}📣 {{ counts.mentions }}{% endif %} {% if counts.webmentions %}🔗 {{ counts.webmentions }}{% endif %} {% if counts.author_replies %}✍️ {{ counts.author_replies }}{% endif %} {% endif %} {% endmacro %} {# Recursive macro to render a thread node and its children #} {% macro render_thread_node(node, depth=0, parent_anchor=None) %} {% set max_indent = 5 %} {% set indent_depth = [depth, max_indent] | min %} {% set current_anchor = get_anchor_id(node) %}
{% if node.reaction_type.value == 'webmention' %} {{ render_webmention(node.item, node.identity, parent_anchor) }} {% elif node.reaction_type.value == 'ap_interaction' %} {{ render_ap_interaction(node.item, node.identity, parent_anchor) }} {% elif node.reaction_type.value == 'author_reply' %} {{ render_author_reply(node.item, parent_anchor) }} {% endif %} {% for child in node.children %} {{ render_thread_node(child, depth + 1, current_anchor) }} {% endfor %}
{% endmacro %} {# Helper to get anchor ID for a node #} {% macro get_anchor_id(node) %} {%- if node.reaction_type.value == 'webmention' -%} wm-{{ node.identity | hash_id }} {%- elif node.reaction_type.value == 'ap_interaction' -%} ap-{{ node.identity | hash_id }} {%- elif node.reaction_type.value == 'author_reply' -%} reply-{{ node.item.full_url | hash_id }} {%- endif -%} {% endmacro %} {# Render media attachments grid (shared by AP and WM) #} {% macro render_media_attachments(metadata, max_visible=4) %} {# Normalize attachments from AP or WM format #} {% set attachments = [] %} {# AP format: metadata.raw_object.attachment #} {% if metadata is mapping and metadata.get('raw_object') %} {% set raw_obj = metadata.get('raw_object') %} {% if raw_obj is mapping and raw_obj.get('attachment') %} {% for att in raw_obj.get('attachment') or [] %} {% if att is mapping %} {% set media_type = att.get('mediaType', '') %} {% set att_type = att.get('type', '') | lower %} {% set url = att.get('url', '') %} {# Handle url as string or object #} {% if url is mapping %} {% set url = url.get('href', '') %} {% endif %} {# Determine type from mediaType first, then fall back to type field #} {% set normalized_type = none %} {% if media_type.startswith('image/') or att_type in ['image', 'document'] and media_type.startswith('image/') %} {% set normalized_type = 'image' %} {% elif media_type.startswith('video/') or att_type == 'video' %} {% set normalized_type = 'video' %} {% elif media_type.startswith('audio/') or att_type == 'audio' %} {% set normalized_type = 'audio' %} {% elif att_type == 'image' %} {% set normalized_type = 'image' %} {% elif att_type == 'document' and not media_type %} {# Guess from URL extension for Document type without mediaType #} {% if url.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg')) %} {% set normalized_type = 'image' %} {% elif url.lower().endswith(('.mp4', '.webm', '.mov', '.avi')) %} {% set normalized_type = 'video' %} {% elif url.lower().endswith(('.mp3', '.ogg', '.wav', '.flac', '.m4a')) %} {% set normalized_type = 'audio' %} {% endif %} {% endif %} {% if normalized_type and url %} {% set _ = attachments.append({ 'type': normalized_type, 'url': url, 'name': att.get('name', ''), 'media_type': media_type }) %} {% endif %} {% endif %} {% endfor %} {% endif %} {% endif %} {# WM format: metadata.mf2.photo/video/audio #} {# Items can be strings (URLs) or objects with value/alt properties #} {% if metadata is mapping and metadata.get('mf2') %} {% set mf2 = metadata.get('mf2') %} {% if mf2 is mapping %} {% for item in mf2.get('photo') or [] %} {% if item %} {% if item is mapping %} {% set url = item.get('value', '') %} {% set alt = item.get('alt', '') %} {% else %} {% set url = item %} {% set alt = '' %} {% endif %} {% if url %} {% set _ = attachments.append({'type': 'image', 'url': url, 'name': alt, 'media_type': ''}) %} {% endif %} {% endif %} {% endfor %} {% for item in mf2.get('video') or [] %} {% if item %} {% if item is mapping %} {% set url = item.get('value', '') %} {% set alt = item.get('alt', '') %} {% else %} {% set url = item %} {% set alt = '' %} {% endif %} {% if url %} {% set _ = attachments.append({'type': 'video', 'url': url, 'name': alt, 'media_type': ''}) %} {% endif %} {% endif %} {% endfor %} {% for item in mf2.get('audio') or [] %} {% if item %} {% if item is mapping %} {% set url = item.get('value', '') %} {% set alt = item.get('alt', '') %} {% else %} {% set url = item %} {% set alt = '' %} {% endif %} {% if url %} {% set _ = attachments.append({'type': 'audio', 'url': url, 'name': alt, 'media_type': ''}) %} {% endif %} {% endif %} {% endfor %} {% endif %} {% endif %} {# Render attachments if any #} {% if attachments %} {% set visible = attachments[:max_visible] %} {% set overflow = attachments[max_visible:] %} {% set visible_count = visible | length %}
{% for att in visible %} {% set safe_url = att.url | safe_url %} {% if safe_url %} {% if att.type == 'image' %} {{ att.name or '' }} {% elif att.type == 'video' %} {% elif att.type == 'audio' %}
{% endif %} {% endif %} {% endfor %}
{% if overflow %} {% set toggle_id = 'media-toggle-' ~ (attachments | string | hash_id) %}
{% for att in overflow %} {% set safe_url = att.url | safe_url %} {% if safe_url %} {% if att.type == 'image' %} {{ att.name or '' }} {% elif att.type == 'video' %} {% elif att.type == 'audio' %}
{% endif %} {% endif %} {% endfor %}
{% endif %}
{% endif %} {% endmacro %} {# Render a single Webmention #} {% macro render_webmention(wm, identity, parent_anchor=None) %} {% set anchor_id = 'wm-' ~ (identity | hash_id) %} {% set source = wm.source or wm.author_url %} {% set source_url = ('https://' ~ utils.hostname(source)) if source and not source.startswith('https://') else source %} {% set source_hostname = utils.hostname(source_url) %} {% set author_name = wm.author_name or 'Anonymous' %}
{% if wm.author_photo %} {% else %} {% include 'icons/user.html' %} {% endif %}
{% if wm.author_url %} {{ author_name }} {% else %} {{ author_name }} {% endif %} {% if wm.mention_type and wm.mention_type.value != 'mention' and wm.mention_type.value != 'reply' %} {% if wm.mention_type.value == 'like' %} {% elif wm.mention_type.value == 'reblog' %} 🔁 {% elif wm.mention_type.value == 'quote' %} 🗣️ {% elif wm.mention_type.value == 'webmention' %} 🔗 {% elif wm.mention_type.value == 'author_reply' %} >✍️ {% endif %} {% endif %} {% if parent_anchor %} {% endif %} {% if wm.published %} {% endif %}
{% if source_url %} {% endif %}
{% if wm.title and wm.title != wm.excerpt %}
{{ wm.title }}
{% endif %} {% if wm.content %}
{{ wm.content | safe }}
{% if wm.content|length > 1000 %} {% endif %} {% elif wm.excerpt %}
{{ wm.excerpt }}
{% if wm.excerpt|length > 1000 %} {% endif %} {% endif %} {# Render mf2 metadata if present #} {% if wm.metadata %} {% set metadata = wm.metadata | fromjson %} {% set mf2 = (metadata.get("mf2") if metadata is mapping else None) | fromjson %} {% if mf2 and mf2 is mapping %} {% set bookmark_of = mf2.get("bookmark_of") or [] %} {% set follow_of = mf2.get("follow_of") or [] %} {% set rsvp = mf2.get("rsvp") %} {% set location = mf2.get("location") %} {% set category = mf2.get("category") or [] %} {% set syndication = mf2.get("syndication") or [] %} {% set has_mf2_fields = bookmark_of or follow_of or rsvp or location or category or syndication %} {% if has_mf2_fields %}
{% if bookmark_of %}
🔖 {% for url in bookmark_of %} {% set bmurl = url | safe_url %} {% if bmurl %} {{ utils.hostname(bmurl) or bmurl }}{% if not loop.last %}, {% endif %} {% endif %} {% endfor %}
{% endif %} {% if follow_of %}
👋 {% for url in follow_of %} {% set furl = url | safe_url %} {% if furl %} {{ utils.hostname(furl) or furl }}{% if not loop.last %}, {% endif %} {% endif %} {% endfor %}
{% endif %} {% if rsvp %}
📅 {{ rsvp }}
{% endif %} {% if location %}
📍 {{ location }}
{% endif %} {% if category %}
🏷️ {% for c in category %} {{ c }}{% if not loop.last %}, {% endif %} {% endfor %}
{% endif %} {% if syndication %}
🔗 {% for url in syndication %} {% set surl = url | safe_url %} {% if surl %} {{ utils.hostname(surl) or surl }}{% if not loop.last %}, {% endif %} {% endif %} {% endfor %}
{% endif %}
{% endif %} {{ render_media_attachments(metadata) }} {% endif %} {% endif %}
{% set wm_counts = interaction_counts.get(wm.source, {}) if interaction_counts is defined else {} %} {{ render_counts(wm_counts, 'interaction-counts') }}
{% endmacro %} {# Render a single AP interaction #} {% macro render_ap_interaction(interaction, identity, parent_anchor=None) %} {% set anchor_id = 'ap-' ~ (identity | hash_id) %} {% set counts = interaction_counts.get(interaction.object_id, {}) if interaction_counts is defined else {} %}
{% if interaction.author_photo %} {% else %} {% include 'icons/user.html' %} {% endif %}
{% if interaction.author_url %} {{ interaction.author_name or interaction.source_actor_id }} {% else %} {{ interaction.author_name or interaction.source_actor_id }} {% endif %} {% if interaction.interaction_type.value != 'reply' and interaction.interaction_type.value != 'mention' %} {% if interaction.interaction_type.value == 'like' %} {% elif interaction.interaction_type.value == 'boost' %} 🔁 {% elif interaction.interaction_type.value == 'quote' %} 🗣️ {% elif interaction.interaction_type.value == 'webmention' %} 🔗 {% elif interaction.interaction_type.value == 'author_reply' %} >✍️ {% endif %} {% endif %} {% if parent_anchor %} {% endif %} {% if interaction.published %} {% endif %}
{% if interaction.author_url %} {% endif %}
{% if interaction.content %}
{{ interaction.content | safe }}
{% if interaction.content|length > 1000 %} {% endif %} {% endif %} {{ render_media_attachments(interaction.metadata) }} {% set ap_author_likes = author_likes_map.get(interaction.object_id, []) if author_likes_map is defined and author_likes_map else [] %} {% if ap_author_likes %}
{% if config.author_photo %} {% endif %} ⭐
{% endif %}
{{ render_counts(counts, 'interaction-counts') }}
{% endmacro %} {# Render an author reply #} {% macro render_author_reply(reply, parent_anchor=None) %} {% set anchor_id = 'reply-' ~ (reply.full_url | hash_id) %}
{% if reply.author_photo %} {% else %} {% include 'icons/user.html' %} {% endif %}
{% if reply.author_url %} {{ reply.author or 'Author' }} {% else %} {{ reply.author or 'Author' }} {% endif %} Author {% if parent_anchor %} {% endif %} {% if reply.published %} {% endif %}
{% if reply.title and reply.title != reply.slug %}
{{ reply.title }}
{% endif %}
{{ reply.content_html | safe }}
{% if reply.content_html|length > 1000 %} {% endif %}
{% set reply_counts = interaction_counts.get(reply.full_url, {}) if interaction_counts is defined else {} %} {{ render_counts(reply_counts, 'interaction-counts') }}
{% endmacro %} {# Main reactions section #} {% if reactions_tree or config.enable_webmentions or config.enable_activitypub %}
{% if not is_unlisted %}

Reactions

{% endif %} {% if (config.enable_webmentions or config.enable_activitypub) and not is_unlisted %}
How to interact with this page
{% if config.enable_webmentions %}

Webmentions

To interact via Webmentions, send an activity that references this URL from a platform that supports Webmentions, such as Lemmy, WordPress with Webmention plugins, or any IndieWeb-compatible site.

{% endif %} {% if config.enable_activitypub %}

ActivityPub

{% endif %}
{% endif %} {{ render_counts(reactions_counts, 'reactions-summary', 'div') }}
{% for node in reactions_tree %} {{ render_thread_node(node, 0) }} {% endfor %}
{% endif %}