mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
Extract shared item-processing.js module with postProcessItems(), applyModerationFilters(), buildInteractionMap(), applyTabFilter(), renderItemCards(), and loadModerationData(). All controllers (reader, api-timeline, explore, hashtag-explore, tag-timeline) now flow through the same pipeline. Unify Alpine.js infinite scroll into single parameterized apInfiniteScroll component configured via data attributes, replacing the separate apExploreScroll component. Also adds fetchAndStoreQuote() for quote enrichment and on-demand quote fetching in post-detail controller. Bump version to 2.5.0. Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
164 lines
7.0 KiB
Plaintext
164 lines
7.0 KiB
Plaintext
{% extends "layouts/ap-reader.njk" %}
|
|
|
|
{% from "heading/macro.njk" import heading with context %}
|
|
{% from "prose/macro.njk" import prose with context %}
|
|
|
|
{% block readercontent %}
|
|
{# Explore link #}
|
|
<div class="ap-reader-tools">
|
|
<a href="{{ mountPath }}/admin/reader/explore" class="ap-reader-tools__explore">
|
|
🔭 {{ __("activitypub.reader.explore.title") }}
|
|
</a>
|
|
</div>
|
|
|
|
{# Followed tags #}
|
|
{% if followedTags and followedTags.length > 0 %}
|
|
<div class="ap-followed-tags">
|
|
<span class="ap-followed-tags__label">{{ __("activitypub.reader.tagTimeline.following") }}:</span>
|
|
{% for tag in followedTags %}
|
|
<a href="{{ mountPath }}/admin/reader/tag?tag={{ tag | urlencode }}" class="ap-card__tag">#{{ tag }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Fediverse lookup with popular accounts autocomplete #}
|
|
<form action="{{ mountPath }}/admin/reader/resolve" method="get" class="ap-lookup"
|
|
x-data="apPopularAccounts('{{ mountPath }}')"
|
|
@submit="onSubmit">
|
|
<div class="ap-lookup-autocomplete">
|
|
<input type="text" name="q" class="ap-lookup__input"
|
|
placeholder="{{ __('activitypub.reader.resolve.placeholder') }}"
|
|
aria-label="{{ __('activitypub.reader.resolve.label') }}"
|
|
x-model="query"
|
|
@focus="loadAccounts()"
|
|
@input.debounce.200ms="filterAccounts()"
|
|
@keydown.arrow-down.prevent="highlightNext()"
|
|
@keydown.arrow-up.prevent="highlightPrev()"
|
|
@keydown.enter="selectHighlighted($event)"
|
|
@keydown.escape="close()"
|
|
@click.away="close()"
|
|
x-ref="input">
|
|
|
|
{# Popular accounts dropdown #}
|
|
<div class="ap-lookup-autocomplete__dropdown" x-show="showResults && suggestions.length > 0" x-cloak>
|
|
<template x-for="(item, index) in suggestions" :key="item.handle">
|
|
<button type="button"
|
|
class="ap-lookup-autocomplete__item"
|
|
:class="{ 'ap-lookup-autocomplete__item--highlighted': index === highlighted }"
|
|
@click="selectItem(item)"
|
|
@mouseenter="highlighted = index">
|
|
<img :src="item.avatar" :alt="item.name" class="ap-lookup-autocomplete__avatar"
|
|
onerror="this.style.display='none'">
|
|
<span class="ap-lookup-autocomplete__info">
|
|
<span class="ap-lookup-autocomplete__name" x-text="item.name"></span>
|
|
<span class="ap-lookup-autocomplete__handle" x-text="item.handle"></span>
|
|
</span>
|
|
<span class="ap-lookup-autocomplete__followers"
|
|
x-text="item.followers.toLocaleString() + ' {{ __("activitypub.reader.resolve.followersLabel") }}'"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="ap-lookup__btn">{{ __("activitypub.reader.resolve.button") }}</button>
|
|
</form>
|
|
|
|
{# Tab navigation #}
|
|
<nav class="ap-tabs" role="tablist">
|
|
<a href="?tab=notes{% if unread %}&unread=1{% endif %}" class="ap-tab{% if tab == 'notes' %} ap-tab--active{% endif %}" role="tab">
|
|
{{ __("activitypub.reader.tabs.notes") }}
|
|
</a>
|
|
<a href="?tab=articles{% if unread %}&unread=1{% endif %}" class="ap-tab{% if tab == 'articles' %} ap-tab--active{% endif %}" role="tab">
|
|
{{ __("activitypub.reader.tabs.articles") }}
|
|
</a>
|
|
<a href="?tab=replies{% if unread %}&unread=1{% endif %}" class="ap-tab{% if tab == 'replies' %} ap-tab--active{% endif %}" role="tab">
|
|
{{ __("activitypub.reader.tabs.replies") }}
|
|
</a>
|
|
<a href="?tab=boosts{% if unread %}&unread=1{% endif %}" class="ap-tab{% if tab == 'boosts' %} ap-tab--active{% endif %}" role="tab">
|
|
{{ __("activitypub.reader.tabs.boosts") }}
|
|
</a>
|
|
<a href="?tab=media{% if unread %}&unread=1{% endif %}" class="ap-tab{% if tab == 'media' %} ap-tab--active{% endif %}" role="tab">
|
|
{{ __("activitypub.reader.tabs.media") }}
|
|
</a>
|
|
<a href="?tab=all{% if unread %}&unread=1{% endif %}" class="ap-tab{% if tab == 'all' %} ap-tab--active{% endif %}" role="tab">
|
|
{{ __("activitypub.reader.tabs.all") }}
|
|
</a>
|
|
<a href="?tab={{ tab }}{% if not unread %}&unread=1{% endif %}" class="ap-tab ap-unread-toggle{% if unread %} ap-unread-toggle--active{% endif %}" title="{% if unread %}Show all posts{% else %}Show unread only{% endif %}">
|
|
{% if unread %}
|
|
All posts
|
|
{% else %}
|
|
Unread{% if unreadTimelineCount %} ({{ unreadTimelineCount }}){% endif %}
|
|
{% endif %}
|
|
</a>
|
|
</nav>
|
|
|
|
{# New posts banner — polls every 30s, shows count of new items #}
|
|
{% if items.length > 0 %}
|
|
<div class="ap-new-posts-banner"
|
|
x-data="apNewPostsBanner()"
|
|
data-newest="{{ items[0].published }}"
|
|
data-tab="{{ tab }}"
|
|
data-mount-path="{{ mountPath }}"
|
|
x-show="count > 0"
|
|
x-cloak>
|
|
<button class="ap-new-posts-banner__btn" @click="loadNew()">
|
|
<span x-text="count + ' new post' + (count !== 1 ? 's' : '')"></span> — Load
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Timeline items with read tracking #}
|
|
{% if items.length > 0 %}
|
|
<div class="ap-timeline"
|
|
id="ap-timeline"
|
|
data-mount-path="{{ mountPath }}"
|
|
data-before="{{ before if before else '' }}"
|
|
data-csrf-token="{{ csrfToken }}"
|
|
x-data="apReadTracker()"
|
|
x-init="init()">
|
|
{% for item in items %}
|
|
{% include "partials/ap-item-card.njk" %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{# Pagination (progressive enhancement — visible without JS, hidden when Alpine active) #}
|
|
{% if before or after %}
|
|
<nav class="ap-pagination ap-pagination--js-hidden" id="ap-reader-pagination">
|
|
{% if after %}
|
|
<a href="?tab={{ tab }}&after={{ after }}" class="ap-pagination__prev">
|
|
{{ __("activitypub.reader.pagination.newer") }}
|
|
</a>
|
|
{% endif %}
|
|
{% if before %}
|
|
<a href="?tab={{ tab }}&before={{ before }}" class="ap-pagination__next">
|
|
{{ __("activitypub.reader.pagination.older") }}
|
|
</a>
|
|
{% endif %}
|
|
</nav>
|
|
{% endif %}
|
|
|
|
{# Infinite scroll load-more sentinel #}
|
|
{% if before %}
|
|
<div class="ap-load-more"
|
|
id="ap-load-more"
|
|
data-cursor="{{ before }}"
|
|
data-api-url="{{ mountPath }}/admin/reader/api/timeline"
|
|
data-cursor-param="before"
|
|
data-cursor-field="before"
|
|
data-timeline-id="ap-timeline"
|
|
data-extra-params='{{ { tab: tab } | dump }}'
|
|
data-hide-pagination="ap-reader-pagination"
|
|
x-data="apInfiniteScroll()"
|
|
x-init="init()">
|
|
<div class="ap-load-more__sentinel" x-ref="sentinel"></div>
|
|
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done">
|
|
<span x-show="!loading">{{ __("activitypub.reader.pagination.loadMore") }}</span>
|
|
<span x-show="loading">{{ __("activitypub.reader.pagination.loading") }}</span>
|
|
</button>
|
|
<p class="ap-load-more__done" x-show="done" x-cloak>{{ __("activitypub.reader.pagination.noMore") }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
{{ prose({ text: __("activitypub.reader.empty") }) }}
|
|
{% endif %}
|
|
{% endblock %}
|