feat: tags.pub global hashtag discovery integration (v3.8.0)

- Add setGlobalFollow/removeGlobalFollow/getFollowedTagsWithState to
  followed-tags storage; unfollowTag now preserves global follow state
- Add followTagGloballyController/unfollowTagGloballyController that
  send AP Follow/Undo via Fedify to tags.pub actor URLs
- Register POST /admin/reader/follow-tag-global and unfollow-tag-global
  routes with plugin reference for Fedify access
- Tag timeline controller passes isGloballyFollowed + error query param
- Tag timeline template adds global follow/unfollow buttons with globe
  indicator and inline error display
- Wire GET /api/v1/followed_tags to return real data with globalFollow state
- Add i18n keys: followGlobally, unfollowGlobally, globallyFollowing,
  globalFollowError
This commit is contained in:
Ricardo
2026-03-22 00:22:47 +01:00
committed by svemagie
parent 97a902bda1
commit a84c6f1abd
8 changed files with 237 additions and 11 deletions

View File

@@ -4,12 +4,15 @@
{# Tag header #}
<header class="ap-tag-header">
<div class="ap-tag-header__info">
<h2 class="ap-tag-header__title">#{{ hashtag }}</h2>
<h2 class="ap-tag-header__title">#{{ hashtag }}{% if isGloballyFollowed %} <span class="ap-tag-header__global-badge" title="{{ __('activitypub.reader.tagTimeline.globallyFollowing') }}">🌐</span>{% endif %}</h2>
<p class="ap-tag-header__count">
{{ __("activitypub.reader.tagTimeline.postsTagged", items.length) }}
</p>
</div>
<div class="ap-tag-header__actions">
{% if error %}
<p class="ap-tag-header__error">{{ __("activitypub.reader.tagTimeline.globalFollowError", error) }}</p>
{% endif %}
{% if isFollowed %}
<form action="{{ mountPath }}/admin/reader/unfollow-tag" method="post" class="ap-tag-header__follow-form">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
@@ -27,6 +30,23 @@
</button>
</form>
{% endif %}
{% if isGloballyFollowed %}
<form action="{{ mountPath }}/admin/reader/unfollow-tag-global" method="post" class="ap-tag-header__follow-form">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<input type="hidden" name="tag" value="{{ hashtag }}">
<button type="submit" class="ap-tag-header__unfollow-btn ap-tag-header__unfollow-btn--global">
🌐 {{ __("activitypub.reader.tagTimeline.unfollowGlobally") }}
</button>
</form>
{% else %}
<form action="{{ mountPath }}/admin/reader/follow-tag-global" method="post" class="ap-tag-header__follow-form">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<input type="hidden" name="tag" value="{{ hashtag }}">
<button type="submit" class="ap-tag-header__follow-btn ap-tag-header__follow-btn--global">
🌐 {{ __("activitypub.reader.tagTimeline.followGlobally") }}
</button>
</form>
{% endif %}
<a href="{{ mountPath }}/admin/reader" class="ap-tag-header__back">
← {{ __("activitypub.reader.title") }}
</a>