@rmdes/indiekit-endpoint-activitypub 2.0.16 → 2.0.18

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
@@ -457,7 +457,12 @@
457
457
  }
458
458
 
459
459
  .ap-card__gallery-link {
460
+ appearance: none;
461
+ background: none;
462
+ border: 0;
463
+ cursor: pointer;
460
464
  display: block;
465
+ padding: 0;
461
466
  position: relative;
462
467
  }
463
468
 
@@ -522,6 +527,83 @@
522
527
  grid-template-rows: 1fr 1fr;
523
528
  }
524
529
 
530
+ /* ==========================================================================
531
+ Photo Lightbox
532
+ ========================================================================== */
533
+
534
+ [x-cloak] {
535
+ display: none !important;
536
+ }
537
+
538
+ .ap-lightbox {
539
+ align-items: center;
540
+ background: rgba(0, 0, 0, 0.92);
541
+ display: flex;
542
+ inset: 0;
543
+ justify-content: center;
544
+ position: fixed;
545
+ z-index: 9999;
546
+ }
547
+
548
+ .ap-lightbox__img {
549
+ max-height: 90vh;
550
+ max-width: 95vw;
551
+ object-fit: contain;
552
+ }
553
+
554
+ .ap-lightbox__close {
555
+ background: none;
556
+ border: 0;
557
+ color: white;
558
+ cursor: pointer;
559
+ font-size: 2rem;
560
+ line-height: 1;
561
+ padding: var(--space-s);
562
+ position: absolute;
563
+ right: var(--space-m);
564
+ top: var(--space-m);
565
+ }
566
+
567
+ .ap-lightbox__close:hover {
568
+ opacity: 0.7;
569
+ }
570
+
571
+ .ap-lightbox__prev,
572
+ .ap-lightbox__next {
573
+ background: none;
574
+ border: 0;
575
+ color: white;
576
+ cursor: pointer;
577
+ font-size: 3rem;
578
+ line-height: 1;
579
+ padding: var(--space-m);
580
+ position: absolute;
581
+ top: 50%;
582
+ transform: translateY(-50%);
583
+ }
584
+
585
+ .ap-lightbox__prev {
586
+ left: var(--space-s);
587
+ }
588
+
589
+ .ap-lightbox__next {
590
+ right: var(--space-s);
591
+ }
592
+
593
+ .ap-lightbox__prev:hover,
594
+ .ap-lightbox__next:hover {
595
+ opacity: 0.7;
596
+ }
597
+
598
+ .ap-lightbox__counter {
599
+ bottom: var(--space-m);
600
+ color: white;
601
+ font-size: var(--font-size-s);
602
+ left: 50%;
603
+ position: absolute;
604
+ transform: translateX(-50%);
605
+ }
606
+
525
607
  /* ==========================================================================
526
608
  Video Embed
527
609
  ========================================================================== */
package/index.js CHANGED
@@ -875,107 +875,115 @@ export default class ActivityPubEndpoint {
875
875
  _publicationUrl: this._publicationUrl,
876
876
  };
877
877
 
878
- // TTL index for activity cleanup (MongoDB handles expiry automatically)
879
- const retentionDays = this.options.activityRetentionDays;
880
- if (retentionDays > 0) {
878
+ // Create indexes wrapped in try-catch because collection references
879
+ // may be undefined if MongoDB hasn't finished connecting yet.
880
+ // Indexes are idempotent; they'll be created on next successful startup.
881
+ try {
882
+ // TTL index for activity cleanup (MongoDB handles expiry automatically)
883
+ const retentionDays = this.options.activityRetentionDays;
884
+ if (retentionDays > 0) {
885
+ this._collections.ap_activities.createIndex(
886
+ { receivedAt: 1 },
887
+ { expireAfterSeconds: retentionDays * 86_400 },
888
+ );
889
+ }
890
+
891
+ // Performance indexes for inbox handlers and batch refollow
892
+ this._collections.ap_followers.createIndex(
893
+ { actorUrl: 1 },
894
+ { unique: true, background: true },
895
+ );
896
+ this._collections.ap_following.createIndex(
897
+ { actorUrl: 1 },
898
+ { unique: true, background: true },
899
+ );
900
+ this._collections.ap_following.createIndex(
901
+ { source: 1 },
902
+ { background: true },
903
+ );
881
904
  this._collections.ap_activities.createIndex(
882
- { receivedAt: 1 },
883
- { expireAfterSeconds: retentionDays * 86_400 },
905
+ { objectUrl: 1 },
906
+ { background: true },
907
+ );
908
+ this._collections.ap_activities.createIndex(
909
+ { type: 1, actorUrl: 1, objectUrl: 1 },
910
+ { background: true },
884
911
  );
885
- }
886
-
887
- // Performance indexes for inbox handlers and batch refollow
888
- this._collections.ap_followers.createIndex(
889
- { actorUrl: 1 },
890
- { unique: true, background: true },
891
- );
892
- this._collections.ap_following.createIndex(
893
- { actorUrl: 1 },
894
- { unique: true, background: true },
895
- );
896
- this._collections.ap_following.createIndex(
897
- { source: 1 },
898
- { background: true },
899
- );
900
- this._collections.ap_activities.createIndex(
901
- { objectUrl: 1 },
902
- { background: true },
903
- );
904
- this._collections.ap_activities.createIndex(
905
- { type: 1, actorUrl: 1, objectUrl: 1 },
906
- { background: true },
907
- );
908
-
909
- // Reader indexes (timeline, notifications, moderation, interactions)
910
- this._collections.ap_timeline.createIndex(
911
- { uid: 1 },
912
- { unique: true, background: true },
913
- );
914
- this._collections.ap_timeline.createIndex(
915
- { published: -1 },
916
- { background: true },
917
- );
918
- this._collections.ap_timeline.createIndex(
919
- { "author.url": 1 },
920
- { background: true },
921
- );
922
- this._collections.ap_timeline.createIndex(
923
- { type: 1, published: -1 },
924
- { background: true },
925
- );
926
912
 
927
- this._collections.ap_notifications.createIndex(
928
- { uid: 1 },
929
- { unique: true, background: true },
930
- );
931
- this._collections.ap_notifications.createIndex(
932
- { published: -1 },
933
- { background: true },
934
- );
935
- this._collections.ap_notifications.createIndex(
936
- { read: 1 },
937
- { background: true },
938
- );
939
- this._collections.ap_notifications.createIndex(
940
- { type: 1, published: -1 },
941
- { background: true },
942
- );
913
+ // Reader indexes (timeline, notifications, moderation, interactions)
914
+ this._collections.ap_timeline.createIndex(
915
+ { uid: 1 },
916
+ { unique: true, background: true },
917
+ );
918
+ this._collections.ap_timeline.createIndex(
919
+ { published: -1 },
920
+ { background: true },
921
+ );
922
+ this._collections.ap_timeline.createIndex(
923
+ { "author.url": 1 },
924
+ { background: true },
925
+ );
926
+ this._collections.ap_timeline.createIndex(
927
+ { type: 1, published: -1 },
928
+ { background: true },
929
+ );
943
930
 
944
- // TTL index for notification cleanup
945
- const notifRetention = this.options.notificationRetentionDays;
946
- if (notifRetention > 0) {
947
931
  this._collections.ap_notifications.createIndex(
948
- { createdAt: 1 },
949
- { expireAfterSeconds: notifRetention * 86_400 },
932
+ { uid: 1 },
933
+ { unique: true, background: true },
934
+ );
935
+ this._collections.ap_notifications.createIndex(
936
+ { published: -1 },
937
+ { background: true },
938
+ );
939
+ this._collections.ap_notifications.createIndex(
940
+ { read: 1 },
941
+ { background: true },
942
+ );
943
+ this._collections.ap_notifications.createIndex(
944
+ { type: 1, published: -1 },
945
+ { background: true },
950
946
  );
951
- }
952
947
 
953
- // Drop non-sparse indexes if they exist (created by earlier versions),
954
- // then recreate with sparse:true so multiple null values are allowed.
955
- this._collections.ap_muted.dropIndex("url_1").catch(() => {});
956
- this._collections.ap_muted.dropIndex("keyword_1").catch(() => {});
957
- this._collections.ap_muted.createIndex(
958
- { url: 1 },
959
- { unique: true, sparse: true, background: true },
960
- );
961
- this._collections.ap_muted.createIndex(
962
- { keyword: 1 },
963
- { unique: true, sparse: true, background: true },
964
- );
948
+ // TTL index for notification cleanup
949
+ const notifRetention = this.options.notificationRetentionDays;
950
+ if (notifRetention > 0) {
951
+ this._collections.ap_notifications.createIndex(
952
+ { createdAt: 1 },
953
+ { expireAfterSeconds: notifRetention * 86_400 },
954
+ );
955
+ }
965
956
 
966
- this._collections.ap_blocked.createIndex(
967
- { url: 1 },
968
- { unique: true, background: true },
969
- );
957
+ // Drop non-sparse indexes if they exist (created by earlier versions),
958
+ // then recreate with sparse:true so multiple null values are allowed.
959
+ this._collections.ap_muted.dropIndex("url_1").catch(() => {});
960
+ this._collections.ap_muted.dropIndex("keyword_1").catch(() => {});
961
+ this._collections.ap_muted.createIndex(
962
+ { url: 1 },
963
+ { unique: true, sparse: true, background: true },
964
+ );
965
+ this._collections.ap_muted.createIndex(
966
+ { keyword: 1 },
967
+ { unique: true, sparse: true, background: true },
968
+ );
970
969
 
971
- this._collections.ap_interactions.createIndex(
972
- { objectUrl: 1, type: 1 },
973
- { unique: true, background: true },
974
- );
975
- this._collections.ap_interactions.createIndex(
976
- { type: 1 },
977
- { background: true },
978
- );
970
+ this._collections.ap_blocked.createIndex(
971
+ { url: 1 },
972
+ { unique: true, background: true },
973
+ );
974
+
975
+ this._collections.ap_interactions.createIndex(
976
+ { objectUrl: 1, type: 1 },
977
+ { unique: true, background: true },
978
+ );
979
+ this._collections.ap_interactions.createIndex(
980
+ { type: 1 },
981
+ { background: true },
982
+ );
983
+ } catch {
984
+ // Index creation failed — collections not yet available.
985
+ // Indexes already exist from previous startups; non-fatal.
986
+ }
979
987
 
980
988
  // Seed actor profile from config on first run
981
989
  this._seedProfile().catch((error) => {
@@ -185,7 +185,7 @@ export async function extractObjectData(object, options = {}) {
185
185
  try {
186
186
  if (typeof object.getTags === "function") {
187
187
  const tags = await object.getTags();
188
- for (const tag of tags) {
188
+ for await (const tag of tags) {
189
189
  if (tag.name) {
190
190
  const tagName = tag.name.toString().replace(/^#/, "");
191
191
  if (tagName) category.push(tagName);
@@ -204,7 +204,7 @@ export async function extractObjectData(object, options = {}) {
204
204
  try {
205
205
  if (typeof object.getAttachments === "function") {
206
206
  const attachments = await object.getAttachments();
207
- for (const att of attachments) {
207
+ for await (const att of attachments) {
208
208
  const mediaUrl = att.url?.href || "";
209
209
  if (!mediaUrl) continue;
210
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
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,20 +1,36 @@
1
1
  {# Media attachments partial — included from ap-item-card.njk #}
2
2
 
3
- {# Photo gallery #}
3
+ {# Photo gallery with lightbox #}
4
4
  {% if item.photo and item.photo.length > 0 %}
5
5
  {% set displayCount = [item.photo.length, 4] | min %}
6
6
  {% set extraCount = item.photo.length - 4 %}
7
- <div class="ap-card__gallery ap-card__gallery--{{ displayCount }}">
7
+ {% set totalPhotos = item.photo.length %}
8
+ <div x-data="{ lightbox: false, idx: 0 }" class="ap-card__gallery ap-card__gallery--{{ displayCount }}">
8
9
  {% for photoUrl in item.photo %}
9
10
  {% if loop.index0 < 4 %}
10
- <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 %}">
11
+ <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 %}">
11
12
  <img src="{{ photoUrl }}" alt="" loading="lazy">
12
13
  {% if loop.index0 == 3 and extraCount > 0 %}
13
14
  <span class="ap-card__gallery-more">+{{ extraCount }}</span>
14
15
  {% endif %}
15
- </a>
16
+ </button>
16
17
  {% endif %}
17
18
  {% endfor %}
19
+
20
+ {# Lightbox modal — teleported to body to prevent overflow clipping #}
21
+ <template x-teleport="body">
22
+ <div x-show="lightbox" x-cloak @keydown.escape.window="lightbox = false" @click.self="lightbox = false" class="ap-lightbox" role="dialog" aria-modal="true">
23
+ <button type="button" @click="lightbox = false" class="ap-lightbox__close" aria-label="Close">&times;</button>
24
+ {% if totalPhotos > 1 %}
25
+ <button type="button" @click="idx = (idx - 1 + {{ totalPhotos }}) % {{ totalPhotos }}" class="ap-lightbox__prev" aria-label="Previous image">&lsaquo;</button>
26
+ {% endif %}
27
+ <img :src="[{% for p in item.photo %}'{{ p }}'{% if not loop.last %},{% endif %}{% endfor %}][idx]" class="ap-lightbox__img" alt="">
28
+ {% if totalPhotos > 1 %}
29
+ <button type="button" @click="idx = (idx + 1) % {{ totalPhotos }}" class="ap-lightbox__next" aria-label="Next image">&rsaquo;</button>
30
+ <div class="ap-lightbox__counter" x-text="(idx + 1) + ' / ' + {{ totalPhotos }}"></div>
31
+ {% endif %}
32
+ </div>
33
+ </template>
18
34
  </div>
19
35
  {% endif %}
20
36