mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
feat: interaction counts on timeline cards (Release 5)
Extract reply/boost/like counts from AP Collections (getReplies, getLikes, getShares) and Mastodon API (replies_count, reblogs_count, favourites_count). Display counts next to interaction buttons with optimistic updates on like/boost actions. Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
This commit is contained in:
@@ -763,6 +763,14 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Interaction counts */
|
||||
.ap-card__count {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-on-offset);
|
||||
margin-left: 0.25rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Error message */
|
||||
.ap-card__action-error {
|
||||
color: var(--color-error);
|
||||
|
||||
@@ -133,6 +133,11 @@ export function mapMastodonStatusToItem(status, instance) {
|
||||
video,
|
||||
audio,
|
||||
inReplyTo: status.in_reply_to_id ? `https://${instance}/web/statuses/${status.in_reply_to_id}` : "",
|
||||
counts: {
|
||||
replies: status.replies_count ?? null,
|
||||
boosts: status.reblogs_count ?? null,
|
||||
likes: status.favourites_count ?? null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
_explore: true,
|
||||
};
|
||||
|
||||
@@ -279,6 +279,21 @@ export async function extractObjectData(object, options = {}) {
|
||||
// Quote URL — Fedify reads quoteUrl / _misskey_quote / quoteUri
|
||||
const quoteUrl = object.quoteUrl?.href || "";
|
||||
|
||||
// Interaction counts — extract from AP Collection objects
|
||||
const counts = { replies: null, boosts: null, likes: null };
|
||||
try {
|
||||
const replies = await object.getReplies?.(loaderOpts);
|
||||
if (replies?.totalItems != null) counts.replies = replies.totalItems;
|
||||
} catch { /* ignore — collection may not exist */ }
|
||||
try {
|
||||
const likes = await object.getLikes?.(loaderOpts);
|
||||
if (likes?.totalItems != null) counts.likes = likes.totalItems;
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
const shares = await object.getShares?.(loaderOpts);
|
||||
if (shares?.totalItems != null) counts.boosts = shares.totalItems;
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Build base timeline item
|
||||
const item = {
|
||||
uid,
|
||||
@@ -298,6 +313,7 @@ export async function extractObjectData(object, options = {}) {
|
||||
audio,
|
||||
inReplyTo,
|
||||
quoteUrl,
|
||||
counts,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||
"version": "2.5.3",
|
||||
"version": "2.5.4",
|
||||
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||
"keywords": [
|
||||
"indiekit",
|
||||
|
||||
@@ -149,6 +149,9 @@
|
||||
{% set itemUid = item.uid or item.url or item.originalUrl %}
|
||||
{% set isLiked = interactionMap[itemUid].like if interactionMap[itemUid] else false %}
|
||||
{% set isBoosted = interactionMap[itemUid].boost if interactionMap[itemUid] else false %}
|
||||
{% set replyCount = item.counts.replies if item.counts and item.counts.replies != null else null %}
|
||||
{% set boostCount = item.counts.boosts if item.counts and item.counts.boosts != null else null %}
|
||||
{% set likeCount = item.counts.likes if item.counts and item.counts.likes != null else null %}
|
||||
<footer class="ap-card__actions"
|
||||
data-item-uid="{{ itemUid }}"
|
||||
data-item-url="{{ itemUrl }}"
|
||||
@@ -160,6 +163,8 @@
|
||||
saved: false,
|
||||
loading: false,
|
||||
error: '',
|
||||
boostCount: {{ boostCount if boostCount != null else 'null' }},
|
||||
likeCount: {{ likeCount if likeCount != null else 'null' }},
|
||||
async saveLater() {
|
||||
if (this.saved) return;
|
||||
const el = this.$root;
|
||||
@@ -190,11 +195,11 @@
|
||||
const itemUid = el.dataset.itemUid;
|
||||
const csrfToken = el.dataset.csrfToken;
|
||||
const basePath = el.dataset.mountPath;
|
||||
const prev = { liked: this.liked, boosted: this.boosted };
|
||||
if (action === 'like') this.liked = true;
|
||||
else if (action === 'unlike') this.liked = false;
|
||||
else if (action === 'boost') this.boosted = true;
|
||||
else if (action === 'unboost') this.boosted = false;
|
||||
const prev = { liked: this.liked, boosted: this.boosted, boostCount: this.boostCount, likeCount: this.likeCount };
|
||||
if (action === 'like') { this.liked = true; if (this.likeCount !== null) this.likeCount++; }
|
||||
else if (action === 'unlike') { this.liked = false; if (this.likeCount !== null && this.likeCount > 0) this.likeCount--; }
|
||||
else if (action === 'boost') { this.boosted = true; if (this.boostCount !== null) this.boostCount++; }
|
||||
else if (action === 'unboost') { this.boosted = false; if (this.boostCount !== null && this.boostCount > 0) this.boostCount--; }
|
||||
try {
|
||||
const res = await fetch(basePath + '/admin/reader/' + action, {
|
||||
method: 'POST',
|
||||
@@ -208,11 +213,15 @@
|
||||
if (!data.success) {
|
||||
this.liked = prev.liked;
|
||||
this.boosted = prev.boosted;
|
||||
this.boostCount = prev.boostCount;
|
||||
this.likeCount = prev.likeCount;
|
||||
this.error = data.error || 'Failed';
|
||||
}
|
||||
} catch (e) {
|
||||
this.liked = prev.liked;
|
||||
this.boosted = prev.boosted;
|
||||
this.boostCount = prev.boostCount;
|
||||
this.likeCount = prev.likeCount;
|
||||
this.error = e.message;
|
||||
}
|
||||
this.loading = false;
|
||||
@@ -222,14 +231,14 @@
|
||||
<a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUid | urlencode }}"
|
||||
class="ap-card__action ap-card__action--reply"
|
||||
title="{{ __('activitypub.reader.actions.reply') }}">
|
||||
↩ {{ __("activitypub.reader.actions.reply") }}
|
||||
↩ {{ __("activitypub.reader.actions.reply") }}{% if replyCount != null %}<span class="ap-card__count">{{ replyCount }}</span>{% endif %}
|
||||
</a>
|
||||
<button class="ap-card__action ap-card__action--boost"
|
||||
:class="{ 'ap-card__action--active': boosted }"
|
||||
:title="boosted ? '{{ __('activitypub.reader.actions.unboost') }}' : '{{ __('activitypub.reader.actions.boost') }}'"
|
||||
:disabled="loading"
|
||||
@click="interact(boosted ? 'unboost' : 'boost')">
|
||||
🔁 <span x-text="boosted ? '{{ __('activitypub.reader.actions.boosted') }}' : '{{ __('activitypub.reader.actions.boost') }}'"></span>
|
||||
🔁 <span x-text="boosted ? '{{ __('activitypub.reader.actions.boosted') }}' : '{{ __('activitypub.reader.actions.boost') }}'"></span><template x-if="boostCount !== null"><span class="ap-card__count" x-text="boostCount"></span></template>
|
||||
</button>
|
||||
<button class="ap-card__action ap-card__action--like"
|
||||
:class="{ 'ap-card__action--active': liked }"
|
||||
@@ -237,7 +246,7 @@
|
||||
:disabled="loading"
|
||||
@click="interact(liked ? 'unlike' : 'like')">
|
||||
<span x-text="liked ? '❤️' : '♥'"></span>
|
||||
<span x-text="liked ? '{{ __('activitypub.reader.actions.liked') }}' : '{{ __('activitypub.reader.actions.like') }}'"></span>
|
||||
<span x-text="liked ? '{{ __('activitypub.reader.actions.liked') }}' : '{{ __('activitypub.reader.actions.like') }}'"></span><template x-if="likeCount !== null"><span class="ap-card__count" x-text="likeCount"></span></template>
|
||||
</button>
|
||||
<a href="{{ itemUrl }}" class="ap-card__action ap-card__action--link" target="_blank" rel="noopener">
|
||||
🔗 {{ __("activitypub.reader.actions.viewOriginal") }}
|
||||
|
||||
Reference in New Issue
Block a user