Files
indiekit-endpoint-microsub/views/item.njk
Ricardo 1512bcecb2 feat: add CSS stacking avatar fallback to all templates (v1.0.49)
Phase 3 frontend harmonization: apply the same avatar fallback pattern
used in ActivityPub to all Microsub templates. A fallback initials span
is always rendered; when the <img> loads, it covers the fallback via
absolute positioning. Broken images are removed by event delegation,
revealing the initials underneath.

Templates updated: item-card, item, actor, author (3 avatar sizes:
40px card, 48px detail, 80px profile). Error delegation script added
to reader layout.

Confab-Link: http://localhost:8080/sessions/bb4a6ec4-b711-48cd-b3d7-942ec2a9851d
2026-03-13 13:53:45 +01:00

209 lines
7.4 KiB
Plaintext

{% extends "layouts/reader.njk" %}
{% block reader %}
<article class="ms-item">
<a href="{{ backUrl or (baseUrl + '/channels') }}" class="back-link">
{{ icon("previous") }} {{ __("Back") }}
</a>
{% if item.author %}
<header class="ms-item__author">
<div class="ms-item__avatar-wrap" data-avatar-fallback>
{% if item.author.photo %}
<img src="{{ item.author.photo }}"
alt=""
class="ms-item__author-photo"
width="48"
height="48"
loading="lazy">
{% endif %}
<span class="ms-item__author-photo ms-item__author-photo--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
</div>
<div class="ms-item__author-info">
<span class="ms-item__author-name">
{% if item.author.url %}
<a href="{{ item.author.url }}" target="_blank" rel="noopener">{{ item.author.name or item.author.url }}</a>
{% else %}
{{ item.author.name or "Unknown" }}
{% endif %}
</span>
{% if item.published %}
<time datetime="{{ item.published }}" class="ms-item__date">
{{ item.published | date("PPPp", { locale: locale, timeZone: application.timeZone }) }}
</time>
{% endif %}
</div>
</header>
{% endif %}
{# Context for interactions #}
{% if item["in-reply-to"] or item["like-of"] or item["repost-of"] or item["bookmark-of"] %}
<div class="ms-item__context">
{% if item["in-reply-to"] and item["in-reply-to"].length > 0 %}
<p class="ms-item__context-label">
{{ icon("reply") }} {{ __("Reply to") }}:
<a href="{{ item['in-reply-to'][0] }}" target="_blank" rel="noopener">
{{ item["in-reply-to"][0] | replace("https://", "") | replace("http://", "") }}
</a>
</p>
{% endif %}
{% if item["like-of"] and item["like-of"].length > 0 %}
<p class="ms-item__context-label">
{{ icon("like") }} {{ __("Liked") }}:
<a href="{{ item['like-of'][0] }}" target="_blank" rel="noopener">
{{ item["like-of"][0] | replace("https://", "") | replace("http://", "") }}
</a>
</p>
{% endif %}
{% if item["repost-of"] and item["repost-of"].length > 0 %}
<p class="ms-item__context-label">
{{ icon("repost") }} {{ __("Reposted") }}:
<a href="{{ item['repost-of'][0] }}" target="_blank" rel="noopener">
{{ item["repost-of"][0] | replace("https://", "") | replace("http://", "") }}
</a>
</p>
{% endif %}
{% if item["bookmark-of"] and item["bookmark-of"].length > 0 %}
<p class="ms-item__context-label">
{{ icon("bookmark") }} {{ __("Bookmarked") }}:
<a href="{{ item['bookmark-of'][0] }}" target="_blank" rel="noopener">
{{ item["bookmark-of"][0] | replace("https://", "") | replace("http://", "") }}
</a>
</p>
{% endif %}
</div>
{% endif %}
{% if item.name %}
<h2 class="ms-item__title">{{ item.name }}</h2>
{% endif %}
{% if item.content %}
<div class="ms-item__content prose">
{% if item.content.html %}
{{ item.content.html | safe }}
{% else %}
{{ item.content.text }}
{% endif %}
</div>
{% endif %}
{# Categories #}
{% if item.category and item.category.length > 0 %}
<div class="ms-item-card__categories">
{% for cat in item.category %}
<span class="ms-item-card__category">#{{ cat | replace("#", "") }}</span>
{% endfor %}
</div>
{% endif %}
{# Photos #}
{% if item.photo and item.photo.length > 0 %}
<div class="ms-item__photos">
{% for photo in item.photo %}
<a href="{{ photo }}" target="_blank" rel="noopener">
<img src="{{ photo }}" alt="" class="ms-item__photo" loading="lazy">
</a>
{% endfor %}
</div>
{% endif %}
{# Video #}
{% if item.video and item.video.length > 0 %}
<div class="ms-item__media">
{% for video in item.video %}
<video src="{{ video }}"
controls
preload="metadata"
{% if item.photo and item.photo.length > 0 %}poster="{{ item.photo[0] }}"{% endif %}>
</video>
{% endfor %}
</div>
{% endif %}
{# Audio #}
{% if item.audio and item.audio.length > 0 %}
<div class="ms-item__media">
{% for audio in item.audio %}
<audio src="{{ audio }}" controls preload="metadata"></audio>
{% endfor %}
</div>
{% endif %}
<footer class="ms-item__actions">
{% if item.url %}
<a href="{{ item.url }}" class="button button--secondary button--small" target="_blank" rel="noopener">
{{ icon("syndicate") }} {{ __("microsub.item.viewOriginal") }}
</a>
{% endif %}
<a href="{{ baseUrl }}/compose?reply={{ item.url | urlencode }}" class="button button--secondary button--small">
{{ icon("reply") }} {{ __("microsub.item.reply") }}
</a>
<a href="{{ baseUrl }}/compose?like={{ item.url | urlencode }}" class="button button--secondary button--small">
{{ icon("like") }} {{ __("microsub.item.like") }}
</a>
<a href="{{ baseUrl }}/compose?repost={{ item.url | urlencode }}" class="button button--secondary button--small">
{{ icon("repost") }} {{ __("microsub.item.repost") }}
</a>
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="button button--secondary button--small">
{{ icon("bookmark") }} {{ __("microsub.item.bookmark") }}
</a>
{% if not item._is_read %}
<button type="button"
class="button button--secondary button--small ms-item__mark-read"
data-item-id="{{ item._id }}"
data-channel="{{ channel.uid }}">
{{ icon("tick") }} {{ __("microsub.timeline.markRead") }}
</button>
{% endif %}
</footer>
</article>
<script type="module">
// Handle mark-read button
const markReadBtn = document.querySelector('.ms-item__mark-read');
if (markReadBtn) {
markReadBtn.addEventListener('click', async () => {
const itemId = markReadBtn.dataset.itemId;
const channelUid = markReadBtn.dataset.channel;
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader.*$/, '');
markReadBtn.disabled = true;
markReadBtn.textContent = 'Marking...';
try {
const formData = new URLSearchParams();
formData.append('action', 'timeline');
formData.append('method', 'mark_read');
formData.append('channel', channelUid);
formData.append('entry', itemId);
const response = await fetch(microsubApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
credentials: 'same-origin'
});
if (response.ok) {
markReadBtn.textContent = 'Marked as read';
markReadBtn.classList.add('button--success');
setTimeout(() => {
markReadBtn.remove();
}, 1500);
} else {
markReadBtn.textContent = 'Failed';
markReadBtn.disabled = false;
}
} catch (error) {
console.error('Error:', error);
markReadBtn.textContent = 'Error';
markReadBtn.disabled = false;
}
});
}
</script>
{% endblock %}