mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
feat: outbound Delete, visibility addressing, CW/sensitive, polls, Flag reports (v2.10.0)
- Outbound Delete: broadcastDelete() + POST /admin/federation/delete route - Visibility: unlisted + followers-only addressing via defaultVisibility config - Content Warning: outbound sensitive flag + summary as CW text - Polls: inbound Question/poll parsing with progress bar rendering - Flag: inbound report handler with ap_reports collection + Reports tab - Includes DM support files from v2.9.x (messages controller, storage, templates) - Includes coverage audit and high-impact gaps implementation plan Confab-Link: http://localhost:8080/sessions/cc343b15-8d10-43cd-a48f-ca912eb79b83
This commit is contained in:
59
views/activitypub-message-compose.njk
Normal file
59
views/activitypub-message-compose.njk
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "layouts/ap-reader.njk" %}
|
||||
|
||||
{% from "heading/macro.njk" import heading with context %}
|
||||
|
||||
{% block readercontent %}
|
||||
{# Error message #}
|
||||
{% if error %}
|
||||
<div class="ap-compose__error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# Reply context — the message being replied to #}
|
||||
{% if replyContext %}
|
||||
<div class="ap-compose__context">
|
||||
<div class="ap-compose__context-label">{{ __("activitypub.messages.replyingTo") }}</div>
|
||||
<div class="ap-compose__context-author">
|
||||
<a href="{{ replyContext.actorUrl }}">{{ replyContext.actorName }}</a>
|
||||
</div>
|
||||
{% if replyContext.content and (replyContext.content.html or replyContext.content.text) %}
|
||||
<div class="ap-card__content ap-compose__context-text">
|
||||
{{ replyContext.content.html | safe if replyContext.content.html else replyContext.content.text | truncate(300) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ mountPath }}/admin/reader/messages/compose" class="ap-compose__form">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
{% if replyTo %}
|
||||
<input type="hidden" name="replyTo" value="{{ replyTo }}">
|
||||
{% endif %}
|
||||
|
||||
{# Recipient field #}
|
||||
<div class="ap-compose__field">
|
||||
<label for="dm-to" class="ap-compose__label">{{ __("activitypub.messages.recipientLabel") }}</label>
|
||||
<input type="text" id="dm-to" name="to" value="{{ to }}"
|
||||
class="ap-compose__input"
|
||||
placeholder="{{ __('activitypub.messages.recipientPlaceholder') }}"
|
||||
required
|
||||
autocomplete="off">
|
||||
</div>
|
||||
|
||||
{# Content textarea #}
|
||||
<div class="ap-compose__editor">
|
||||
<textarea name="content" class="ap-compose__textarea"
|
||||
rows="6"
|
||||
placeholder="{{ __('activitypub.messages.placeholder') }}"
|
||||
required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="ap-compose__actions">
|
||||
<button type="submit" class="ap-compose__submit">
|
||||
{{ __("activitypub.messages.send") }}
|
||||
</button>
|
||||
<a href="{{ mountPath }}/admin/reader/messages" class="ap-compose__cancel">
|
||||
{{ __("activitypub.compose.cancel") }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
78
views/activitypub-messages.njk
Normal file
78
views/activitypub-messages.njk
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "layouts/ap-reader.njk" %}
|
||||
|
||||
{% from "heading/macro.njk" import heading with context %}
|
||||
{% from "prose/macro.njk" import prose with context %}
|
||||
|
||||
{% block readercontent %}
|
||||
{# Toolbar — compose + mark read + clear all #}
|
||||
{% set msgBase = mountPath + "/admin/reader/messages" %}
|
||||
<div class="ap-notifications__toolbar">
|
||||
<a href="{{ msgBase }}/compose" class="ap-notifications__btn ap-notifications__btn--primary">
|
||||
{{ __("activitypub.messages.compose") }}
|
||||
</a>
|
||||
{% if unreadCount > 0 %}
|
||||
<form method="post" action="{{ msgBase }}/mark-read">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
<button type="submit" class="ap-notifications__btn">{{ __("activitypub.messages.markAllRead") }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ msgBase }}/clear"
|
||||
onsubmit="return confirm('{{ __("activitypub.messages.clearConfirm") }}')">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
<button type="submit" class="ap-notifications__btn ap-notifications__btn--danger">{{ __("activitypub.messages.clearAll") }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="ap-messages__layout">
|
||||
{# Conversation partner sidebar #}
|
||||
{% if partners.length > 0 %}
|
||||
<aside class="ap-messages__sidebar">
|
||||
<a href="{{ msgBase }}" class="ap-messages__partner{% if not activePartner %} ap-messages__partner--active{% endif %}">
|
||||
{{ __("activitypub.messages.allConversations") }}
|
||||
</a>
|
||||
{% for p in partners %}
|
||||
<a href="{{ msgBase }}?partner={{ p._id | urlencode }}"
|
||||
class="ap-messages__partner{% if activePartner == p._id %} ap-messages__partner--active{% endif %}">
|
||||
<span class="ap-messages__partner-avatar" data-avatar-fallback>
|
||||
{% if p.actorPhoto %}
|
||||
<img src="{{ p.actorPhoto }}" alt="{{ p.actorName }}" loading="lazy" crossorigin="anonymous">
|
||||
{% endif %}
|
||||
<span class="ap-messages__partner-initial" aria-hidden="true">{{ p.actorName[0] | upper if p.actorName else "?" }}</span>
|
||||
</span>
|
||||
<span class="ap-messages__partner-info">
|
||||
<span class="ap-messages__partner-name">{{ p.actorName }}</span>
|
||||
{% if p.actorHandle %}
|
||||
<span class="ap-messages__partner-handle">{{ p.actorHandle }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if p.unreadCount > 0 %}
|
||||
<span class="ap-tab__count">{{ p.unreadCount }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
{# Messages list #}
|
||||
<div class="ap-messages__content">
|
||||
{% if items.length > 0 %}
|
||||
<div class="ap-timeline">
|
||||
{% for item in items %}
|
||||
{% include "partials/ap-message-card.njk" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Pagination — preserve active partner #}
|
||||
{% if before %}
|
||||
<nav class="ap-pagination">
|
||||
<a href="?{% if activePartner %}partner={{ activePartner | urlencode }}&{% endif %}before={{ before }}" class="ap-pagination__next">
|
||||
{{ __("activitypub.reader.pagination.older") }}
|
||||
</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ prose({ text: __("activitypub.messages.empty") }) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -23,6 +23,14 @@
|
||||
{{ __("activitypub.notifications.tabs.follows") }}
|
||||
{% if tabCounts.follow %}<span class="ap-tab__count">{{ tabCounts.follow }}</span>{% endif %}
|
||||
</a>
|
||||
<a href="{{ notifBase }}?tab=dm" class="ap-tab{% if tab == 'dm' %} ap-tab--active{% endif %}">
|
||||
{{ __("activitypub.notifications.tabs.dms") }}
|
||||
{% if tabCounts.dm %}<span class="ap-tab__count">{{ tabCounts.dm }}</span>{% endif %}
|
||||
</a>
|
||||
<a href="{{ notifBase }}?tab=report" class="ap-tab{% if tab == 'report' %} ap-tab--active{% endif %}">
|
||||
{{ __("activitypub.notifications.tabs.reports") }}
|
||||
{% if tabCounts.report %}<span class="ap-tab__count">{{ tabCounts.report }}</span>{% endif %}
|
||||
</a>
|
||||
<a href="{{ notifBase }}?tab=all" class="ap-tab{% if tab == 'all' %} ap-tab--active{% endif %}">
|
||||
{{ __("activitypub.notifications.tabs.all") }}
|
||||
{% if tabCounts.all %}<span class="ap-tab__count">{{ tabCounts.all }}</span>{% endif %}
|
||||
|
||||
@@ -100,6 +100,11 @@
|
||||
|
||||
{# Media hidden behind CW #}
|
||||
{% include "partials/ap-item-media.njk" %}
|
||||
|
||||
{# Poll options #}
|
||||
{% if item.type == "question" or (item.pollOptions and item.pollOptions.length > 0) %}
|
||||
{% include "partials/ap-poll-options.njk" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -118,6 +123,11 @@
|
||||
|
||||
{# Media visible directly #}
|
||||
{% include "partials/ap-item-media.njk" %}
|
||||
|
||||
{# Poll options #}
|
||||
{% if item.type == "question" or (item.pollOptions and item.pollOptions.length > 0) %}
|
||||
{% include "partials/ap-poll-options.njk" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Mentions and hashtags #}
|
||||
|
||||
65
views/partials/ap-message-card.njk
Normal file
65
views/partials/ap-message-card.njk
Normal file
@@ -0,0 +1,65 @@
|
||||
{# Message card partial — inbound/outbound DM display #}
|
||||
|
||||
<div class="ap-notification ap-message{% if not item.read %} ap-notification--unread{% endif %}{% if item.direction == 'outbound' %} ap-message--outbound{% endif %}">
|
||||
{# Dismiss button #}
|
||||
<form method="post" action="{{ mountPath }}/admin/reader/messages/delete" class="ap-notification__dismiss">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
<input type="hidden" name="uid" value="{{ item.uid }}">
|
||||
<button type="submit" class="ap-notification__dismiss-btn" title="{{ __('activitypub.messages.delete') }}">×</button>
|
||||
</form>
|
||||
|
||||
{# Avatar — outbound: our profile photo, inbound: sender's photo #}
|
||||
<div class="ap-notification__avatar-wrap" data-avatar-fallback>
|
||||
{% if item.direction == "outbound" and myProfile and myProfile.icon %}
|
||||
<img src="{{ myProfile.icon }}" alt="{{ myProfile.name or 'Me' }}" class="ap-notification__avatar" loading="lazy" crossorigin="anonymous">
|
||||
<span class="ap-notification__avatar ap-notification__avatar--default" aria-hidden="true">{{ (myProfile.name or "M")[0] | upper }}</span>
|
||||
{% else %}
|
||||
{% if item.actorPhoto %}
|
||||
<img src="{{ item.actorPhoto }}" alt="{{ item.actorName }}" class="ap-notification__avatar" loading="lazy" crossorigin="anonymous">
|
||||
{% endif %}
|
||||
<span class="ap-notification__avatar ap-notification__avatar--default" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
|
||||
{% endif %}
|
||||
<span class="ap-notification__type-badge">
|
||||
{% if item.direction == "outbound" %}↗{% else %}✉{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Message body #}
|
||||
<div class="ap-notification__body">
|
||||
<span class="ap-notification__actor">
|
||||
{% if item.direction == "outbound" %}
|
||||
<span class="ap-message__direction">{{ __("activitypub.messages.sentTo") }}</span>
|
||||
{% endif %}
|
||||
<a href="{{ item.actorUrl }}">{{ item.actorName }}</a>
|
||||
{% if item.actorHandle %}
|
||||
<span class="ap-notification__handle">{{ item.actorHandle }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{% if item.content and item.content.html %}
|
||||
<div class="ap-message__content">
|
||||
{{ item.content.html | safe }}
|
||||
</div>
|
||||
{% elif item.content and item.content.text %}
|
||||
<div class="ap-message__content">
|
||||
{{ item.content.text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Reply action (only for inbound messages) #}
|
||||
{% if item.direction == "inbound" %}
|
||||
<div class="ap-notification__actions">
|
||||
<a href="{{ mountPath }}/admin/reader/messages/compose?to={{ item.actorHandle | urlencode }}&replyTo={{ item.uid | urlencode }}" class="ap-notification__reply-btn">
|
||||
↩ {{ __("activitypub.reader.actions.reply") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Timestamp #}
|
||||
{% if item.published %}
|
||||
<time datetime="{{ item.published }}" class="ap-notification__time" x-data x-relative-time>
|
||||
{{ item.published | date("PPp") }}
|
||||
</time>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
{% endif %}
|
||||
<span class="ap-notification__avatar ap-notification__avatar--default" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
|
||||
<span class="ap-notification__type-badge">
|
||||
{% if item.type == "like" %}❤{% elif item.type == "boost" %}🔁{% elif item.type == "follow" %}👤{% elif item.type == "reply" %}💬{% elif item.type == "mention" %}@{% endif %}
|
||||
{% if item.type == "like" %}❤{% elif item.type == "boost" %}🔁{% elif item.type == "follow" %}👤{% elif item.type == "reply" %}💬{% elif item.type == "mention" %}@{% elif item.type == "dm" %}✉{% elif item.type == "report" %}⚑{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
{{ __("activitypub.notifications.repliedTo") }}
|
||||
{% elif item.type == "mention" %}
|
||||
{{ __("activitypub.notifications.mentionedYou") }}
|
||||
{% elif item.type == "dm" %}
|
||||
{{ __("activitypub.messages.sentYouDM") }}
|
||||
{% elif item.type == "report" %}
|
||||
{{ __("activitypub.reports.sentReport") }}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
@@ -60,6 +64,12 @@
|
||||
💬 {{ __("activitypub.notifications.viewThread") }}
|
||||
</a>
|
||||
</div>
|
||||
{% elif item.type == "dm" %}
|
||||
<div class="ap-notification__actions">
|
||||
<a href="{{ mountPath }}/admin/reader/messages?partner={{ item.actorUrl | urlencode }}" class="ap-notification__thread-btn" title="{{ __('activitypub.messages.title') }}">
|
||||
✉ {{ __("activitypub.messages.viewMessage") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
30
views/partials/ap-poll-options.njk
Normal file
30
views/partials/ap-poll-options.njk
Normal file
@@ -0,0 +1,30 @@
|
||||
{# Poll options partial — renders vote results for Question-type posts #}
|
||||
{% if item.pollOptions and item.pollOptions.length > 0 %}
|
||||
{% set totalVotes = 0 %}
|
||||
{% for opt in item.pollOptions %}
|
||||
{% set totalVotes = totalVotes + opt.votes %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="ap-poll">
|
||||
{% for opt in item.pollOptions %}
|
||||
{% set pct = (totalVotes > 0) and ((opt.votes / totalVotes * 100) | round) or 0 %}
|
||||
<div class="ap-poll__option">
|
||||
<div class="ap-poll__bar" style="width: {{ pct }}%"></div>
|
||||
<span class="ap-poll__label">{{ opt.name }}</span>
|
||||
<span class="ap-poll__votes">{{ pct }}%</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="ap-poll__footer">
|
||||
{% if item.votersCount > 0 %}
|
||||
{{ item.votersCount }} {{ __("activitypub.poll.voters") }}
|
||||
{% elif totalVotes > 0 %}
|
||||
{{ totalVotes }} {{ __("activitypub.poll.votes") }}
|
||||
{% endif %}
|
||||
{% if item.pollClosed %}
|
||||
· {{ __("activitypub.poll.closed") }}
|
||||
{% elif item.pollEndTime %}
|
||||
· {{ __("activitypub.poll.endsAt") }} <time datetime="{{ item.pollEndTime }}">{{ item.pollEndTime | date("PPp") }}</time>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user