fix: use async iteration for Fedify 2.0 attachments/tags, add image lightbox

Fedify 2.0's getAttachments() and getTags() return async iterables, but the
code used synchronous for...of which silently yielded zero results. Changed
to for await...of so media URLs (photo/video/audio) and hashtags are now
properly extracted from incoming posts.

Also replaced the gallery's target=_blank links with an Alpine.js lightbox
modal for full-size image viewing with prev/next navigation and keyboard
support.
This commit is contained in:
Ricardo
2026-02-24 10:25:15 +01:00
parent a95d68c98f
commit cd7d850b44
4 changed files with 105 additions and 7 deletions

View File

@@ -457,7 +457,12 @@
}
.ap-card__gallery-link {
appearance: none;
background: none;
border: 0;
cursor: pointer;
display: block;
padding: 0;
position: relative;
}
@@ -522,6 +527,83 @@
grid-template-rows: 1fr 1fr;
}
/* ==========================================================================
Photo Lightbox
========================================================================== */
[x-cloak] {
display: none !important;
}
.ap-lightbox {
align-items: center;
background: rgba(0, 0, 0, 0.92);
display: flex;
inset: 0;
justify-content: center;
position: fixed;
z-index: 9999;
}
.ap-lightbox__img {
max-height: 90vh;
max-width: 95vw;
object-fit: contain;
}
.ap-lightbox__close {
background: none;
border: 0;
color: white;
cursor: pointer;
font-size: 2rem;
line-height: 1;
padding: var(--space-s);
position: absolute;
right: var(--space-m);
top: var(--space-m);
}
.ap-lightbox__close:hover {
opacity: 0.7;
}
.ap-lightbox__prev,
.ap-lightbox__next {
background: none;
border: 0;
color: white;
cursor: pointer;
font-size: 3rem;
line-height: 1;
padding: var(--space-m);
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.ap-lightbox__prev {
left: var(--space-s);
}
.ap-lightbox__next {
right: var(--space-s);
}
.ap-lightbox__prev:hover,
.ap-lightbox__next:hover {
opacity: 0.7;
}
.ap-lightbox__counter {
bottom: var(--space-m);
color: white;
font-size: var(--font-size-s);
left: 50%;
position: absolute;
transform: translateX(-50%);
}
/* ==========================================================================
Video Embed
========================================================================== */

View File

@@ -185,7 +185,7 @@ export async function extractObjectData(object, options = {}) {
try {
if (typeof object.getTags === "function") {
const tags = await object.getTags();
for (const tag of tags) {
for await (const tag of tags) {
if (tag.name) {
const tagName = tag.name.toString().replace(/^#/, "");
if (tagName) category.push(tagName);
@@ -204,7 +204,7 @@ export async function extractObjectData(object, options = {}) {
try {
if (typeof object.getAttachments === "function") {
const attachments = await object.getAttachments();
for (const att of attachments) {
for await (const att of attachments) {
const mediaUrl = att.url?.href || "";
if (!mediaUrl) continue;

View File

@@ -1,6 +1,6 @@
{
"name": "@rmdes/indiekit-endpoint-activitypub",
"version": "2.0.16",
"version": "2.0.17",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [
"indiekit",

View File

@@ -1,20 +1,36 @@
{# Media attachments partial — included from ap-item-card.njk #}
{# Photo gallery #}
{# Photo gallery with lightbox #}
{% if item.photo and item.photo.length > 0 %}
{% set displayCount = [item.photo.length, 4] | min %}
{% set extraCount = item.photo.length - 4 %}
<div class="ap-card__gallery ap-card__gallery--{{ displayCount }}">
{% set totalPhotos = item.photo.length %}
<div x-data="{ lightbox: false, idx: 0 }" class="ap-card__gallery ap-card__gallery--{{ displayCount }}">
{% for photoUrl in item.photo %}
{% if loop.index0 < 4 %}
<a href="{{ photoUrl }}" target="_blank" rel="noopener" class="ap-card__gallery-link{% if loop.index0 == 3 and extraCount > 0 %} ap-card__gallery-link--more{% endif %}">
<button type="button" @click="idx = {{ loop.index0 }}; lightbox = true" class="ap-card__gallery-link{% if loop.index0 == 3 and extraCount > 0 %} ap-card__gallery-link--more{% endif %}">
<img src="{{ photoUrl }}" alt="" loading="lazy">
{% if loop.index0 == 3 and extraCount > 0 %}
<span class="ap-card__gallery-more">+{{ extraCount }}</span>
{% endif %}
</a>
</button>
{% endif %}
{% endfor %}
{# Lightbox modal — teleported to body to prevent overflow clipping #}
<template x-teleport="body">
<div x-show="lightbox" x-cloak @keydown.escape.window="lightbox = false" @click.self="lightbox = false" class="ap-lightbox" role="dialog" aria-modal="true">
<button type="button" @click="lightbox = false" class="ap-lightbox__close" aria-label="Close">&times;</button>
{% if totalPhotos > 1 %}
<button type="button" @click="idx = (idx - 1 + {{ totalPhotos }}) % {{ totalPhotos }}" class="ap-lightbox__prev" aria-label="Previous image">&lsaquo;</button>
{% endif %}
<img :src="[{% for p in item.photo %}'{{ p }}'{% if not loop.last %},{% endif %}{% endfor %}][idx]" class="ap-lightbox__img" alt="">
{% if totalPhotos > 1 %}
<button type="button" @click="idx = (idx + 1) % {{ totalPhotos }}" class="ap-lightbox__next" aria-label="Next image">&rsaquo;</button>
<div class="ap-lightbox__counter" x-text="(idx + 1) + ' / ' + {{ totalPhotos }}"></div>
{% endif %}
</div>
</template>
</div>
{% endif %}