Files
svemagie d9f26beac5 merge: upstream 6436763..b71b4a9 — settings page, ObjectId pagination, edit history, keyword filters
Key upstream changes merged:
- Settings admin page (ap_settings collection, GET/POST controller, Nunjucks template)
- ObjectId-based status IDs (fixes same-second collision bug in cursor pagination)
- Status edit history stored in ap_status_edits, GET /statuses/:id/history endpoint
- Keyword filters for Mastodon API timelines (apply-filters.js middleware)
- resolve-reply-ids.js helper for threading
- Filter replies from public/local/hashtag timelines
- Non-expiring access tokens
- Media upload improvements (express-fileupload, bridge IndieAuth token)
- Startup gate: defer background tasks until host readiness signal
- Own posts exempt from timeline retention cleanup
- Various Mastodon API fixes (account IDs, update_credentials, interactions)

Conflict resolutions:
- pagination.js: adopted upstream's ObjectId approach (removed cursor-based encodeCursor/decodeCursor)
- statuses.js: adopted upstream's PUT rewrite (cleaner ownership check, edit history storage via Micropub fetch); kept GET /statuses/:id with pinnedIds/replyIdMap; adopted ObjectId-only findTimelineItemById
- compose.js: kept our isDirect/senderActorUrl (DM feature) + added upstream's mediaEndpoint
- activitypub-compose.njk: kept DM notice block + added enctype="multipart/form-data"
- Removed unused processStatusContent function and Update import from statuses.js

Fork-specific changes preserved:
- allowPrivateAddress: true in federation-setup.js
- Canonical Like activity IDs + Like dispatcher in federation-setup.js
- Repost commentary in jf2-to-as2.js
- /api/ap-url endpoint in index.js
- Direct Follow workaround for tags.pub

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:23:54 +02:00

146 lines
5.4 KiB
Plaintext

{% extends "layouts/ap-reader.njk" %}
{% from "heading/macro.njk" import heading with context %}
{% from "textarea/macro.njk" import textarea with context %}
{% from "file-input/macro.njk" import fileInput with context %}
{% from "tag-input/macro.njk" import tagInput with context %}
{% block readercontent %}
{# Reply context — show the post being replied to #}
{% if replyContext %}
<div class="ap-compose__context">
<div class="ap-compose__context-label">{{ __("activitypub.reader.replyingTo") }}</div>
{% if replyContext.author %}
<div class="ap-compose__context-author">
<a href="{{ replyContext.author.url }}">{{ replyContext.author.name }}</a>
</div>
{% endif %}
{% 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 %}
<a href="{{ replyTo }}" class="ap-compose__context-link" target="_blank" rel="noopener">{{ replyTo }}</a>
</div>
{% endif %}
{% if isDirect %}
<div class="ap-compose__dm-notice">
🔒 {{ __("activitypub.compose.directNotice") if __("activitypub.compose.directNotice") != "activitypub.compose.directNotice" else "Direct message — reply stays private" }}
</div>
{% endif %}
<form method="post" action="{{ mountPath }}/admin/reader/compose" class="ap-compose__form" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
{% if replyTo %}
<input type="hidden" name="in-reply-to" value="{{ replyTo }}">
{% endif %}
{% if isDirect %}
<input type="hidden" name="is-direct" value="true">
<input type="hidden" name="sender-actor-url" value="{{ senderActorUrl }}">
{% endif %}
{# Content warning toggle + summary #}
<div class="ap-compose__cw">
<label class="ap-compose__cw-toggle">
<input type="checkbox" name="cw-enabled" id="cw-toggle"
onchange="document.getElementById('cw-text').style.display = this.checked ? 'block' : 'none'">
{{ __("activitypub.compose.cwLabel") }}
</label>
<input type="text" name="summary" id="cw-text" class="ap-compose__cw-input"
placeholder="{{ __('activitypub.compose.cwPlaceholder') }}"
style="display: none">
</div>
{# Rich content editor (EasyMDE with media browser) #}
<div class="ap-compose__editor">
{{ textarea({
name: "content",
label: "Content",
rows: 8,
field: {
attributes: {
editor: true,
"editor-endpoint": mediaEndpoint,
"editor-id": "ap-compose-content",
"editor-locale": application.locale if application else "en",
"editor-image-upload": true
}
}
}) }}
</div>
{# Featured image #}
<div class="ap-compose__media">
{{ fileInput({
name: "photo",
label: "Photo",
field: {
attributes: {
endpoint: mediaEndpoint
}
},
accept: "image/*",
attributes: {
placeholder: "https://"
}
}) }}
</div>
{# Tags / categories #}
<div class="ap-compose__tags">
{{ tagInput({
name: "category",
label: "Tags",
optional: true,
hint: "Separate with commas"
}) }}
</div>
{# Visibility — hidden for direct messages #}
{% if not isDirect %}
<fieldset class="ap-compose__visibility">
<legend>{{ __("activitypub.compose.visibilityLabel") }}</legend>
<label class="ap-compose__visibility-option">
<input type="radio" name="visibility" value="public" checked>
{{ __("activitypub.compose.visibilityPublic") }}
</label>
<label class="ap-compose__visibility-option">
<input type="radio" name="visibility" value="unlisted">
{{ __("activitypub.compose.visibilityUnlisted") }}
</label>
<label class="ap-compose__visibility-option">
<input type="radio" name="visibility" value="followers">
{{ __("activitypub.compose.visibilityFollowers") }}
</label>
</fieldset>
{% endif %}
{# Syndication targets — hidden for direct messages #}
{% if syndicationTargets.length > 0 and not isDirect %}
<fieldset class="ap-compose__syndication">
<legend>{{ __("activitypub.compose.syndicateLabel") }}</legend>
{% for target in syndicationTargets %}
<label class="ap-compose__syndication-target">
<input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}" {{ "checked" if target.defaultChecked }}>
{{ target.name }}
</label>
{% endfor %}
</fieldset>
{% endif %}
<div class="ap-compose__actions">
<button type="submit" class="ap-compose__submit">
{% if isDirect %}
🔒 {{ __("activitypub.compose.submitDirect") if __("activitypub.compose.submitDirect") != "activitypub.compose.submitDirect" else "Send direct reply" }}
{% else %}
{{ __("activitypub.compose.submitMicropub") }}
{% endif %}
</button>
<a href="{{ mountPath }}/admin/reader/notifications" class="ap-compose__cancel">
{{ __("activitypub.compose.cancel") }}
</a>
</div>
</form>
{% endblock %}