mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
Add a dedicated fediverse reader view with: - Timeline view showing posts from followed accounts with threading, content warnings, boosts, and media display - Compose form with dual-path posting (quick AP reply + Micropub blog post) - Native AP interactions (like, boost, reply, follow/unfollow) - Notifications view for likes, boosts, follows, mentions, replies - Moderation tools (mute/block actors, keyword filters) - Remote actor profile pages with follow state - Automatic timeline cleanup with configurable retention - CSRF protection, XSS prevention, input validation throughout Removes Microsub bridge dependency — AP content now lives in its own MongoDB collections (ap_timeline, ap_notifications, ap_interactions, ap_muted, ap_blocked). Bumps version to 1.1.0.
119 lines
4.9 KiB
Plaintext
119 lines
4.9 KiB
Plaintext
{% extends "layouts/reader.njk" %}
|
|
|
|
{% from "heading/macro.njk" import heading with context %}
|
|
{% from "prose/macro.njk" import prose with context %}
|
|
|
|
{% block content %}
|
|
{{ heading({
|
|
text: title,
|
|
level: 1,
|
|
parent: { text: __("activitypub.reader.title"), href: mountPath + "/admin/reader" }
|
|
}) }}
|
|
|
|
{# Blocked actors #}
|
|
<section class="ap-moderation__section">
|
|
<h2>{{ __("activitypub.moderation.blockedTitle") }}</h2>
|
|
{% if blocked.length > 0 %}
|
|
<ul class="ap-moderation__list">
|
|
{% for entry in blocked %}
|
|
<li class="ap-moderation__entry"
|
|
x-data="{ removing: false }">
|
|
<a href="{{ entry.url }}">{{ entry.url }}</a>
|
|
<button class="ap-moderation__remove"
|
|
:disabled="removing"
|
|
@click="
|
|
removing = true;
|
|
fetch('{{ mountPath }}/admin/reader/unblock', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrfToken }}' },
|
|
body: JSON.stringify({ url: '{{ entry.url }}' })
|
|
}).then(r => r.json()).then(d => { if (d.success) $el.closest('li').remove(); else removing = false; }).catch(() => removing = false);
|
|
">{{ __("activitypub.moderation.unblock") }}</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
{{ prose({ text: __("activitypub.moderation.noBlocked") }) }}
|
|
{% endif %}
|
|
</section>
|
|
|
|
{# Muted actors #}
|
|
<section class="ap-moderation__section">
|
|
<h2>{{ __("activitypub.moderation.mutedActorsTitle") }}</h2>
|
|
{% set mutedActors = muted | selectattr("url") %}
|
|
{% if mutedActors | length > 0 %}
|
|
<ul class="ap-moderation__list">
|
|
{% for entry in mutedActors %}
|
|
<li class="ap-moderation__entry"
|
|
x-data="{ removing: false }">
|
|
<a href="{{ entry.url }}">{{ entry.url }}</a>
|
|
<button class="ap-moderation__remove"
|
|
:disabled="removing"
|
|
@click="
|
|
removing = true;
|
|
fetch('{{ mountPath }}/admin/reader/unmute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrfToken }}' },
|
|
body: JSON.stringify({ url: '{{ entry.url }}' })
|
|
}).then(r => r.json()).then(d => { if (d.success) $el.closest('li').remove(); else removing = false; }).catch(() => removing = false);
|
|
">{{ __("activitypub.moderation.unmute") }}</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
{{ prose({ text: __("activitypub.moderation.noMutedActors") }) }}
|
|
{% endif %}
|
|
</section>
|
|
|
|
{# Muted keywords #}
|
|
<section class="ap-moderation__section">
|
|
<h2>{{ __("activitypub.moderation.mutedKeywordsTitle") }}</h2>
|
|
{% set mutedKeywords = muted | selectattr("keyword") %}
|
|
{% if mutedKeywords | length > 0 %}
|
|
<ul class="ap-moderation__list">
|
|
{% for entry in mutedKeywords %}
|
|
<li class="ap-moderation__entry"
|
|
x-data="{ removing: false }">
|
|
<code>{{ entry.keyword }}</code>
|
|
<button class="ap-moderation__remove"
|
|
:disabled="removing"
|
|
@click="
|
|
removing = true;
|
|
fetch('{{ mountPath }}/admin/reader/unmute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrfToken }}' },
|
|
body: JSON.stringify({ keyword: '{{ entry.keyword }}' })
|
|
}).then(r => r.json()).then(d => { if (d.success) $el.closest('li').remove(); else removing = false; }).catch(() => removing = false);
|
|
">{{ __("activitypub.moderation.unmute") }}</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
{{ prose({ text: __("activitypub.moderation.noMutedKeywords") }) }}
|
|
{% endif %}
|
|
</section>
|
|
|
|
{# Add keyword mute form #}
|
|
<section class="ap-moderation__section">
|
|
<h2>{{ __("activitypub.moderation.addKeywordTitle") }}</h2>
|
|
<form class="ap-moderation__add-form"
|
|
x-data="{ keyword: '', submitting: false }"
|
|
@submit.prevent="
|
|
if (!keyword.trim()) return;
|
|
submitting = true;
|
|
fetch('{{ mountPath }}/admin/reader/mute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrfToken }}' },
|
|
body: JSON.stringify({ keyword: keyword.trim() })
|
|
}).then(r => r.json()).then(d => { if (d.success) location.reload(); submitting = false; }).catch(() => submitting = false);
|
|
">
|
|
<input type="text" x-model="keyword"
|
|
placeholder="{{ __('activitypub.moderation.keywordPlaceholder') }}"
|
|
class="ap-moderation__input">
|
|
<button type="submit" :disabled="submitting" class="ap-moderation__add-btn">
|
|
{{ __("activitypub.moderation.addKeyword") }}
|
|
</button>
|
|
</form>
|
|
</section>
|
|
{% endblock %}
|