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;
|
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 */
|
/* Error message */
|
||||||
.ap-card__action-error {
|
.ap-card__action-error {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ export function mapMastodonStatusToItem(status, instance) {
|
|||||||
video,
|
video,
|
||||||
audio,
|
audio,
|
||||||
inReplyTo: status.in_reply_to_id ? `https://${instance}/web/statuses/${status.in_reply_to_id}` : "",
|
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(),
|
createdAt: new Date().toISOString(),
|
||||||
_explore: true,
|
_explore: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -279,6 +279,21 @@ export async function extractObjectData(object, options = {}) {
|
|||||||
// Quote URL — Fedify reads quoteUrl / _misskey_quote / quoteUri
|
// Quote URL — Fedify reads quoteUrl / _misskey_quote / quoteUri
|
||||||
const quoteUrl = object.quoteUrl?.href || "";
|
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
|
// Build base timeline item
|
||||||
const item = {
|
const item = {
|
||||||
uid,
|
uid,
|
||||||
@@ -298,6 +313,7 @@ export async function extractObjectData(object, options = {}) {
|
|||||||
audio,
|
audio,
|
||||||
inReplyTo,
|
inReplyTo,
|
||||||
quoteUrl,
|
quoteUrl,
|
||||||
|
counts,
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"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.",
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"indiekit",
|
"indiekit",
|
||||||
|
|||||||
@@ -149,6 +149,9 @@
|
|||||||
{% set itemUid = item.uid or item.url or item.originalUrl %}
|
{% set itemUid = item.uid or item.url or item.originalUrl %}
|
||||||
{% set isLiked = interactionMap[itemUid].like if interactionMap[itemUid] else false %}
|
{% set isLiked = interactionMap[itemUid].like if interactionMap[itemUid] else false %}
|
||||||
{% set isBoosted = interactionMap[itemUid].boost 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"
|
<footer class="ap-card__actions"
|
||||||
data-item-uid="{{ itemUid }}"
|
data-item-uid="{{ itemUid }}"
|
||||||
data-item-url="{{ itemUrl }}"
|
data-item-url="{{ itemUrl }}"
|
||||||
@@ -160,6 +163,8 @@
|
|||||||
saved: false,
|
saved: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: '',
|
error: '',
|
||||||
|
boostCount: {{ boostCount if boostCount != null else 'null' }},
|
||||||
|
likeCount: {{ likeCount if likeCount != null else 'null' }},
|
||||||
async saveLater() {
|
async saveLater() {
|
||||||
if (this.saved) return;
|
if (this.saved) return;
|
||||||
const el = this.$root;
|
const el = this.$root;
|
||||||
@@ -190,11 +195,11 @@
|
|||||||
const itemUid = el.dataset.itemUid;
|
const itemUid = el.dataset.itemUid;
|
||||||
const csrfToken = el.dataset.csrfToken;
|
const csrfToken = el.dataset.csrfToken;
|
||||||
const basePath = el.dataset.mountPath;
|
const basePath = el.dataset.mountPath;
|
||||||
const prev = { liked: this.liked, boosted: this.boosted };
|
const prev = { liked: this.liked, boosted: this.boosted, boostCount: this.boostCount, likeCount: this.likeCount };
|
||||||
if (action === 'like') this.liked = true;
|
if (action === 'like') { this.liked = true; if (this.likeCount !== null) this.likeCount++; }
|
||||||
else if (action === 'unlike') this.liked = false;
|
else if (action === 'unlike') { this.liked = false; if (this.likeCount !== null && this.likeCount > 0) this.likeCount--; }
|
||||||
else if (action === 'boost') this.boosted = true;
|
else if (action === 'boost') { this.boosted = true; if (this.boostCount !== null) this.boostCount++; }
|
||||||
else if (action === 'unboost') this.boosted = false;
|
else if (action === 'unboost') { this.boosted = false; if (this.boostCount !== null && this.boostCount > 0) this.boostCount--; }
|
||||||
try {
|
try {
|
||||||
const res = await fetch(basePath + '/admin/reader/' + action, {
|
const res = await fetch(basePath + '/admin/reader/' + action, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -208,11 +213,15 @@
|
|||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
this.liked = prev.liked;
|
this.liked = prev.liked;
|
||||||
this.boosted = prev.boosted;
|
this.boosted = prev.boosted;
|
||||||
|
this.boostCount = prev.boostCount;
|
||||||
|
this.likeCount = prev.likeCount;
|
||||||
this.error = data.error || 'Failed';
|
this.error = data.error || 'Failed';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.liked = prev.liked;
|
this.liked = prev.liked;
|
||||||
this.boosted = prev.boosted;
|
this.boosted = prev.boosted;
|
||||||
|
this.boostCount = prev.boostCount;
|
||||||
|
this.likeCount = prev.likeCount;
|
||||||
this.error = e.message;
|
this.error = e.message;
|
||||||
}
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@@ -222,14 +231,14 @@
|
|||||||
<a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUid | urlencode }}"
|
<a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUid | urlencode }}"
|
||||||
class="ap-card__action ap-card__action--reply"
|
class="ap-card__action ap-card__action--reply"
|
||||||
title="{{ __('activitypub.reader.actions.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>
|
</a>
|
||||||
<button class="ap-card__action ap-card__action--boost"
|
<button class="ap-card__action ap-card__action--boost"
|
||||||
:class="{ 'ap-card__action--active': boosted }"
|
:class="{ 'ap-card__action--active': boosted }"
|
||||||
:title="boosted ? '{{ __('activitypub.reader.actions.unboost') }}' : '{{ __('activitypub.reader.actions.boost') }}'"
|
:title="boosted ? '{{ __('activitypub.reader.actions.unboost') }}' : '{{ __('activitypub.reader.actions.boost') }}'"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="interact(boosted ? 'unboost' : 'boost')">
|
@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>
|
||||||
<button class="ap-card__action ap-card__action--like"
|
<button class="ap-card__action ap-card__action--like"
|
||||||
:class="{ 'ap-card__action--active': liked }"
|
:class="{ 'ap-card__action--active': liked }"
|
||||||
@@ -237,7 +246,7 @@
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="interact(liked ? 'unlike' : 'like')">
|
@click="interact(liked ? 'unlike' : 'like')">
|
||||||
<span x-text="liked ? '❤️' : '♥'"></span>
|
<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>
|
</button>
|
||||||
<a href="{{ itemUrl }}" class="ap-card__action ap-card__action--link" target="_blank" rel="noopener">
|
<a href="{{ itemUrl }}" class="ap-card__action ap-card__action--link" target="_blank" rel="noopener">
|
||||||
🔗 {{ __("activitypub.reader.actions.viewOriginal") }}
|
🔗 {{ __("activitypub.reader.actions.viewOriginal") }}
|
||||||
|
|||||||
Reference in New Issue
Block a user