@rmdes/indiekit-endpoint-activitypub 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/reader.css CHANGED
@@ -296,6 +296,40 @@
296
296
  max-width: 100%;
297
297
  }
298
298
 
299
+ /* @mentions — styled as subtle pills to distinguish from prose */
300
+ .ap-card__content .h-card,
301
+ .ap-card__content a.u-url.mention {
302
+ color: var(--color-on-offset);
303
+ font-size: var(--font-size-s);
304
+ text-decoration: none;
305
+ }
306
+
307
+ .ap-card__content a.u-url.mention:hover {
308
+ color: var(--color-primary);
309
+ text-decoration: underline;
310
+ }
311
+
312
+ /* Hashtag mentions — subtle tag styling */
313
+ .ap-card__content a.mention.hashtag {
314
+ color: var(--color-on-offset);
315
+ font-size: var(--font-size-s);
316
+ text-decoration: none;
317
+ }
318
+
319
+ .ap-card__content a.mention.hashtag:hover {
320
+ color: var(--color-primary);
321
+ text-decoration: underline;
322
+ }
323
+
324
+ /* Mastodon's invisible/ellipsis spans for long URLs */
325
+ .ap-card__content .invisible {
326
+ display: none;
327
+ }
328
+
329
+ .ap-card__content .ellipsis::after {
330
+ content: "…";
331
+ }
332
+
299
333
  /* ==========================================================================
300
334
  Content Warning
301
335
  ========================================================================== */
@@ -321,6 +321,11 @@ export function registerInboxListeners(inboxChain, options) {
321
321
  const object = await announce.getObject();
322
322
  if (!object) return;
323
323
 
324
+ // Skip non-content objects (Lemmy/PieFed like/create activities
325
+ // that resolve to activity IDs instead of actual Note/Article posts)
326
+ const hasContent = object.content?.toString() || object.name?.toString();
327
+ if (!hasContent) return;
328
+
324
329
  // Get booster actor info
325
330
  const boosterActor = await announce.getActor();
326
331
  const boosterInfo = await extractActorInfo(boosterActor);
@@ -446,7 +451,9 @@ export function registerInboxListeners(inboxChain, options) {
446
451
  const following = await collections.ap_following.findOne({ actorUrl });
447
452
  if (following) {
448
453
  try {
449
- const timelineItem = await extractObjectData(object);
454
+ const timelineItem = await extractObjectData(object, {
455
+ actorFallback: actorObj,
456
+ });
450
457
  await addTimelineItem(collections, timelineItem);
451
458
  } catch (error) {
452
459
  // Log extraction errors but don't fail the entire handler
@@ -87,6 +87,7 @@ export async function extractActorInfo(actor) {
87
87
  * @param {object} options - Extraction options
88
88
  * @param {object} [options.boostedBy] - Actor info for boosts
89
89
  * @param {Date} [options.boostedAt] - Boost timestamp
90
+ * @param {object} [options.actorFallback] - Fedify actor to use when object.getAttributedTo() fails
90
91
  * @returns {Promise<object>} Timeline item data
91
92
  */
92
93
  export async function extractObjectData(object, options = {}) {
@@ -127,7 +128,7 @@ export async function extractObjectData(object, options = {}) {
127
128
  ? String(object.published)
128
129
  : new Date().toISOString();
129
130
 
130
- // Extract author — use async getAttributedTo() for Fedify objects
131
+ // Extract author — try multiple strategies in order of reliability
131
132
  let authorObj = null;
132
133
  try {
133
134
  if (typeof object.getAttributedTo === "function") {
@@ -135,10 +136,39 @@ export async function extractObjectData(object, options = {}) {
135
136
  authorObj = Array.isArray(attr) ? attr[0] : attr;
136
137
  }
137
138
  } catch {
138
- // Fallback: try direct property access for plain objects
139
+ // getAttributedTo() failed (Authorized Fetch, unreachable, etc.)
140
+ }
141
+ // If getAttributedTo() returned nothing, use the actor from the wrapping activity
142
+ if (!authorObj && options.actorFallback) {
143
+ authorObj = options.actorFallback;
144
+ }
145
+ // Try direct property access for plain objects
146
+ if (!authorObj) {
139
147
  authorObj = object.attribution || object.attributedTo || null;
140
148
  }
141
- const author = await extractActorInfo(authorObj);
149
+
150
+ let author;
151
+ if (authorObj) {
152
+ author = await extractActorInfo(authorObj);
153
+ } else {
154
+ // Last resort: use attributionIds (non-fetching) to get at least a URL
155
+ const attrIds = object.attributionIds;
156
+ if (attrIds && attrIds.length > 0) {
157
+ const authorUrl = attrIds[0].href;
158
+ const authorHostname = new URL(authorUrl).hostname;
159
+ // Extract username from URL pattern like /users/name or /@name
160
+ const pathMatch = new URL(authorUrl).pathname.match(/\/@?([^/]+)/);
161
+ const username = pathMatch ? pathMatch[1] : "";
162
+ author = {
163
+ name: username || authorHostname,
164
+ url: authorUrl,
165
+ photo: "",
166
+ handle: username ? `@${username}@${authorHostname}` : "",
167
+ };
168
+ } else {
169
+ author = { name: "Unknown", url: "", photo: "", handle: "" };
170
+ }
171
+ }
142
172
 
143
173
  // Extract tags/categories
144
174
  const category = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -1,5 +1,11 @@
1
1
  {# Timeline item card partial - reusable across timeline and profile views #}
2
2
 
3
+ {# Skip empty cards (e.g. Lemmy/PieFed activity IDs with no actual content) #}
4
+ {% set hasCardContent = item.content and (item.content.html or item.content.text) %}
5
+ {% set hasCardTitle = item.name %}
6
+ {% set hasCardMedia = (item.photo and item.photo.length > 0) or (item.video and item.video.length > 0) or (item.audio and item.audio.length > 0) %}
7
+ {% if hasCardContent or hasCardTitle or hasCardMedia %}
8
+
3
9
  <article class="ap-card{% if item.type %} ap-card--{{ item.type }}{% endif %}{% if item.inReplyTo %} ap-card--reply{% endif %}">
4
10
  {# Boost header if this is a boosted post #}
5
11
  {% if item.type == "boost" and item.boostedBy %}
@@ -167,3 +173,5 @@
167
173
  <div x-show="error" x-text="error" class="ap-card__action-error" x-transition></div>
168
174
  </footer>
169
175
  </article>
176
+
177
+ {% endif %}{# end hasCardContent/hasCardTitle/hasCardMedia guard #}