@rmdes/indiekit-endpoint-activitypub 2.0.27 → 2.0.28
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-infinite-scroll.js +183 -0
- package/assets/reader.css +282 -0
- package/index.js +34 -0
- package/lib/controllers/api-timeline.js +170 -0
- package/lib/controllers/explore.js +293 -0
- package/lib/controllers/follow-tag.js +62 -0
- package/lib/controllers/reader.js +11 -0
- package/lib/controllers/tag-timeline.js +147 -0
- package/lib/inbox-listeners.js +27 -0
- package/lib/migrations/separate-mentions.js +88 -0
- package/lib/storage/followed-tags.js +65 -0
- package/lib/storage/timeline.js +15 -2
- package/lib/timeline-store.js +18 -5
- package/locales/en.json +27 -4
- package/package.json +1 -1
- package/views/activitypub-explore.njk +82 -0
- package/views/activitypub-reader.njk +42 -3
- package/views/activitypub-tag-timeline.njk +86 -0
- package/views/layouts/ap-reader.njk +4 -1
- package/views/partials/ap-item-card.njk +20 -5
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infinite scroll — AlpineJS component for AJAX load-more on the timeline
|
|
3
|
+
* Registers the `apInfiniteScroll` Alpine data component.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
document.addEventListener("alpine:init", () => {
|
|
7
|
+
// eslint-disable-next-line no-undef
|
|
8
|
+
Alpine.data("apExploreScroll", () => ({
|
|
9
|
+
loading: false,
|
|
10
|
+
done: false,
|
|
11
|
+
maxId: null,
|
|
12
|
+
instance: "",
|
|
13
|
+
scope: "local",
|
|
14
|
+
observer: null,
|
|
15
|
+
|
|
16
|
+
init() {
|
|
17
|
+
const el = this.$el;
|
|
18
|
+
this.maxId = el.dataset.maxId || null;
|
|
19
|
+
this.instance = el.dataset.instance || "";
|
|
20
|
+
this.scope = el.dataset.scope || "local";
|
|
21
|
+
|
|
22
|
+
if (!this.maxId) {
|
|
23
|
+
this.done = true;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.observer = new IntersectionObserver(
|
|
28
|
+
(entries) => {
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.isIntersecting && !this.loading && !this.done) {
|
|
31
|
+
this.loadMore();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{ rootMargin: "200px" }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (this.$refs.sentinel) {
|
|
39
|
+
this.observer.observe(this.$refs.sentinel);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async loadMore() {
|
|
44
|
+
if (this.loading || this.done || !this.maxId) return;
|
|
45
|
+
|
|
46
|
+
this.loading = true;
|
|
47
|
+
|
|
48
|
+
const timeline = document.getElementById("ap-explore-timeline");
|
|
49
|
+
const mountPath = timeline ? timeline.dataset.mountPath : "";
|
|
50
|
+
|
|
51
|
+
const params = new URLSearchParams({
|
|
52
|
+
instance: this.instance,
|
|
53
|
+
scope: this.scope,
|
|
54
|
+
max_id: this.maxId,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(
|
|
59
|
+
`${mountPath}/admin/reader/api/explore?${params.toString()}`,
|
|
60
|
+
{ headers: { Accept: "application/json" } }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
64
|
+
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
|
|
67
|
+
if (data.html && timeline) {
|
|
68
|
+
timeline.insertAdjacentHTML("beforeend", data.html);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (data.maxId) {
|
|
72
|
+
this.maxId = data.maxId;
|
|
73
|
+
} else {
|
|
74
|
+
this.done = true;
|
|
75
|
+
if (this.observer) this.observer.disconnect();
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error("[ap-explore-scroll] load failed:", err.message);
|
|
79
|
+
} finally {
|
|
80
|
+
this.loading = false;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
destroy() {
|
|
85
|
+
if (this.observer) this.observer.disconnect();
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// eslint-disable-next-line no-undef
|
|
90
|
+
Alpine.data("apInfiniteScroll", () => ({
|
|
91
|
+
loading: false,
|
|
92
|
+
done: false,
|
|
93
|
+
before: null,
|
|
94
|
+
tab: "",
|
|
95
|
+
tag: "",
|
|
96
|
+
observer: null,
|
|
97
|
+
|
|
98
|
+
init() {
|
|
99
|
+
const el = this.$el;
|
|
100
|
+
this.before = el.dataset.before || null;
|
|
101
|
+
this.tab = el.dataset.tab || "";
|
|
102
|
+
this.tag = el.dataset.tag || "";
|
|
103
|
+
|
|
104
|
+
// Hide the no-JS pagination fallback now that JS is active
|
|
105
|
+
const paginationEl =
|
|
106
|
+
document.getElementById("ap-reader-pagination") ||
|
|
107
|
+
document.getElementById("ap-tag-pagination");
|
|
108
|
+
if (paginationEl) {
|
|
109
|
+
paginationEl.style.display = "none";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!this.before) {
|
|
113
|
+
this.done = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Set up IntersectionObserver to auto-load when sentinel comes into view
|
|
118
|
+
this.observer = new IntersectionObserver(
|
|
119
|
+
(entries) => {
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (entry.isIntersecting && !this.loading && !this.done) {
|
|
122
|
+
this.loadMore();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{ rootMargin: "200px" }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (this.$refs.sentinel) {
|
|
130
|
+
this.observer.observe(this.$refs.sentinel);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async loadMore() {
|
|
135
|
+
if (this.loading || this.done || !this.before) return;
|
|
136
|
+
|
|
137
|
+
this.loading = true;
|
|
138
|
+
|
|
139
|
+
const timeline = document.getElementById("ap-timeline");
|
|
140
|
+
const mountPath = timeline ? timeline.dataset.mountPath : "";
|
|
141
|
+
|
|
142
|
+
const params = new URLSearchParams({ before: this.before });
|
|
143
|
+
if (this.tab) params.set("tab", this.tab);
|
|
144
|
+
if (this.tag) params.set("tag", this.tag);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(
|
|
148
|
+
`${mountPath}/admin/reader/api/timeline?${params.toString()}`,
|
|
149
|
+
{ headers: { Accept: "application/json" } }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
153
|
+
|
|
154
|
+
const data = await res.json();
|
|
155
|
+
|
|
156
|
+
if (data.html && timeline) {
|
|
157
|
+
// Append the returned pre-rendered HTML
|
|
158
|
+
timeline.insertAdjacentHTML("beforeend", data.html);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (data.before) {
|
|
162
|
+
this.before = data.before;
|
|
163
|
+
} else {
|
|
164
|
+
// No more items
|
|
165
|
+
this.done = true;
|
|
166
|
+
if (this.observer) this.observer.disconnect();
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error("[ap-infinite-scroll] load failed:", err.message);
|
|
170
|
+
} finally {
|
|
171
|
+
this.loading = false;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
appendItems(/* detail */) {
|
|
176
|
+
// Custom event hook — not used in this implementation but kept for extensibility
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
destroy() {
|
|
180
|
+
if (this.observer) this.observer.disconnect();
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
});
|
package/assets/reader.css
CHANGED
|
@@ -655,6 +655,25 @@
|
|
|
655
655
|
color: var(--color-on-background);
|
|
656
656
|
}
|
|
657
657
|
|
|
658
|
+
.ap-card__mention {
|
|
659
|
+
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
|
660
|
+
border-radius: var(--border-radius-large);
|
|
661
|
+
color: var(--color-accent);
|
|
662
|
+
font-size: var(--font-size-s);
|
|
663
|
+
padding: 2px var(--space-xs);
|
|
664
|
+
text-decoration: none;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.ap-card__mention:hover {
|
|
668
|
+
background: color-mix(in srgb, var(--color-accent) 22%, transparent);
|
|
669
|
+
color: var(--color-accent);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.ap-card__mention--legacy {
|
|
673
|
+
cursor: default;
|
|
674
|
+
opacity: 0.7;
|
|
675
|
+
}
|
|
676
|
+
|
|
658
677
|
/* ==========================================================================
|
|
659
678
|
Interaction Buttons
|
|
660
679
|
========================================================================== */
|
|
@@ -735,6 +754,55 @@
|
|
|
735
754
|
text-decoration: underline;
|
|
736
755
|
}
|
|
737
756
|
|
|
757
|
+
/* Hidden once Alpine is active (JS replaces with infinite scroll) */
|
|
758
|
+
.ap-pagination--js-hidden {
|
|
759
|
+
/* Shown by default for no-JS fallback — Alpine hides via display:none */
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/* ==========================================================================
|
|
763
|
+
Infinite Scroll / Load More
|
|
764
|
+
========================================================================== */
|
|
765
|
+
|
|
766
|
+
.ap-load-more {
|
|
767
|
+
display: flex;
|
|
768
|
+
flex-direction: column;
|
|
769
|
+
align-items: center;
|
|
770
|
+
gap: var(--space-s);
|
|
771
|
+
padding: var(--space-m) 0;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.ap-load-more__sentinel {
|
|
775
|
+
height: 1px;
|
|
776
|
+
width: 100%;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.ap-load-more__btn {
|
|
780
|
+
background: var(--color-offset);
|
|
781
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
782
|
+
border-radius: var(--border-radius-small);
|
|
783
|
+
color: var(--color-on-background);
|
|
784
|
+
cursor: pointer;
|
|
785
|
+
font-size: var(--font-size-s);
|
|
786
|
+
padding: var(--space-xs) var(--space-m);
|
|
787
|
+
transition: background 0.15s;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.ap-load-more__btn:hover:not(:disabled) {
|
|
791
|
+
background: var(--color-offset-variant);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.ap-load-more__btn:disabled {
|
|
795
|
+
cursor: wait;
|
|
796
|
+
opacity: 0.6;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.ap-load-more__done {
|
|
800
|
+
color: var(--color-on-offset);
|
|
801
|
+
font-size: var(--font-size-s);
|
|
802
|
+
margin: 0;
|
|
803
|
+
text-align: center;
|
|
804
|
+
}
|
|
805
|
+
|
|
738
806
|
/* ==========================================================================
|
|
739
807
|
Compose Form
|
|
740
808
|
========================================================================== */
|
|
@@ -1572,6 +1640,204 @@
|
|
|
1572
1640
|
box-shadow: 0 0 0 1px var(--color-primary);
|
|
1573
1641
|
}
|
|
1574
1642
|
|
|
1643
|
+
/* ==========================================================================
|
|
1644
|
+
Tag Timeline Header
|
|
1645
|
+
========================================================================== */
|
|
1646
|
+
|
|
1647
|
+
.ap-tag-header {
|
|
1648
|
+
align-items: flex-start;
|
|
1649
|
+
background: var(--color-offset);
|
|
1650
|
+
border-bottom: var(--border-width-thin) solid var(--color-outline);
|
|
1651
|
+
border-radius: var(--border-radius-small);
|
|
1652
|
+
display: flex;
|
|
1653
|
+
gap: var(--space-m);
|
|
1654
|
+
justify-content: space-between;
|
|
1655
|
+
margin-bottom: var(--space-m);
|
|
1656
|
+
padding: var(--space-m);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
.ap-tag-header__title {
|
|
1660
|
+
font-size: var(--font-size-xl);
|
|
1661
|
+
font-weight: var(--font-weight-bold);
|
|
1662
|
+
margin: 0 0 var(--space-xs);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.ap-tag-header__count {
|
|
1666
|
+
color: var(--color-on-offset);
|
|
1667
|
+
font-size: var(--font-size-s);
|
|
1668
|
+
margin: 0;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
.ap-tag-header__actions {
|
|
1672
|
+
align-items: center;
|
|
1673
|
+
display: flex;
|
|
1674
|
+
flex-shrink: 0;
|
|
1675
|
+
gap: var(--space-s);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
.ap-tag-header__follow-btn {
|
|
1679
|
+
background: var(--color-accent);
|
|
1680
|
+
border: none;
|
|
1681
|
+
border-radius: var(--border-radius-small);
|
|
1682
|
+
color: var(--color-on-accent);
|
|
1683
|
+
cursor: pointer;
|
|
1684
|
+
font-size: var(--font-size-s);
|
|
1685
|
+
padding: var(--space-xs) var(--space-s);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.ap-tag-header__follow-btn:hover {
|
|
1689
|
+
opacity: 0.85;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
.ap-tag-header__unfollow-btn {
|
|
1693
|
+
background: transparent;
|
|
1694
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
1695
|
+
border-radius: var(--border-radius-small);
|
|
1696
|
+
color: var(--color-on-background);
|
|
1697
|
+
cursor: pointer;
|
|
1698
|
+
font-size: var(--font-size-s);
|
|
1699
|
+
padding: var(--space-xs) var(--space-s);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
.ap-tag-header__unfollow-btn:hover {
|
|
1703
|
+
border-color: var(--color-on-background);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
.ap-tag-header__back {
|
|
1707
|
+
color: var(--color-on-offset);
|
|
1708
|
+
font-size: var(--font-size-s);
|
|
1709
|
+
text-decoration: none;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
.ap-tag-header__back:hover {
|
|
1713
|
+
color: var(--color-on-background);
|
|
1714
|
+
text-decoration: underline;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
@media (max-width: 640px) {
|
|
1718
|
+
.ap-tag-header {
|
|
1719
|
+
flex-direction: column;
|
|
1720
|
+
gap: var(--space-s);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.ap-tag-header__actions {
|
|
1724
|
+
flex-wrap: wrap;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/* ==========================================================================
|
|
1729
|
+
Reader Tools Bar (Explore link, etc.)
|
|
1730
|
+
========================================================================== */
|
|
1731
|
+
|
|
1732
|
+
.ap-reader-tools {
|
|
1733
|
+
display: flex;
|
|
1734
|
+
gap: var(--space-s);
|
|
1735
|
+
justify-content: flex-end;
|
|
1736
|
+
margin-bottom: var(--space-s);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.ap-reader-tools__explore {
|
|
1740
|
+
color: var(--color-on-offset);
|
|
1741
|
+
font-size: var(--font-size-s);
|
|
1742
|
+
text-decoration: none;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
.ap-reader-tools__explore:hover {
|
|
1746
|
+
color: var(--color-on-background);
|
|
1747
|
+
text-decoration: underline;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/* ==========================================================================
|
|
1751
|
+
Explore Page
|
|
1752
|
+
========================================================================== */
|
|
1753
|
+
|
|
1754
|
+
.ap-explore-header {
|
|
1755
|
+
margin-bottom: var(--space-m);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
.ap-explore-header__title {
|
|
1759
|
+
font-size: var(--font-size-xl);
|
|
1760
|
+
margin: 0 0 var(--space-xs);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
.ap-explore-header__desc {
|
|
1764
|
+
color: var(--color-on-offset);
|
|
1765
|
+
font-size: var(--font-size-s);
|
|
1766
|
+
margin: 0;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
.ap-explore-form {
|
|
1770
|
+
background: var(--color-offset);
|
|
1771
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
1772
|
+
border-radius: var(--border-radius-small);
|
|
1773
|
+
margin-bottom: var(--space-m);
|
|
1774
|
+
padding: var(--space-m);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.ap-explore-form__row {
|
|
1778
|
+
align-items: center;
|
|
1779
|
+
display: flex;
|
|
1780
|
+
gap: var(--space-s);
|
|
1781
|
+
flex-wrap: wrap;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
.ap-explore-form__input {
|
|
1785
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
1786
|
+
border-radius: var(--border-radius-small);
|
|
1787
|
+
flex: 1;
|
|
1788
|
+
font-size: var(--font-size-base);
|
|
1789
|
+
min-width: 0;
|
|
1790
|
+
padding: var(--space-xs) var(--space-s);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
.ap-explore-form__scope {
|
|
1794
|
+
display: flex;
|
|
1795
|
+
gap: var(--space-s);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
.ap-explore-form__scope-label {
|
|
1799
|
+
align-items: center;
|
|
1800
|
+
cursor: pointer;
|
|
1801
|
+
display: flex;
|
|
1802
|
+
font-size: var(--font-size-s);
|
|
1803
|
+
gap: var(--space-xs);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
.ap-explore-form__btn {
|
|
1807
|
+
background: var(--color-primary);
|
|
1808
|
+
border: none;
|
|
1809
|
+
border-radius: var(--border-radius-small);
|
|
1810
|
+
color: var(--color-on-primary);
|
|
1811
|
+
cursor: pointer;
|
|
1812
|
+
font-size: var(--font-size-s);
|
|
1813
|
+
padding: var(--space-xs) var(--space-m);
|
|
1814
|
+
white-space: nowrap;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
.ap-explore-form__btn:hover {
|
|
1818
|
+
opacity: 0.85;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
.ap-explore-error {
|
|
1822
|
+
background: color-mix(in srgb, var(--color-red45) 10%, transparent);
|
|
1823
|
+
border: var(--border-width-thin) solid var(--color-red45);
|
|
1824
|
+
border-radius: var(--border-radius-small);
|
|
1825
|
+
color: var(--color-red45);
|
|
1826
|
+
margin-bottom: var(--space-m);
|
|
1827
|
+
padding: var(--space-s) var(--space-m);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
@media (max-width: 640px) {
|
|
1831
|
+
.ap-explore-form__row {
|
|
1832
|
+
flex-direction: column;
|
|
1833
|
+
align-items: stretch;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.ap-explore-form__btn {
|
|
1837
|
+
width: 100%;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1575
1841
|
/* Replies — indented from the other side */
|
|
1576
1842
|
.ap-post-detail__replies {
|
|
1577
1843
|
margin-left: var(--space-l);
|
|
@@ -1582,3 +1848,19 @@
|
|
|
1582
1848
|
padding-left: var(--space-m);
|
|
1583
1849
|
margin-bottom: var(--space-xs);
|
|
1584
1850
|
}
|
|
1851
|
+
|
|
1852
|
+
/* Followed tags bar */
|
|
1853
|
+
.ap-followed-tags {
|
|
1854
|
+
display: flex;
|
|
1855
|
+
flex-wrap: wrap;
|
|
1856
|
+
align-items: center;
|
|
1857
|
+
gap: var(--space-xs);
|
|
1858
|
+
padding: var(--space-xs) 0;
|
|
1859
|
+
margin-bottom: var(--space-s);
|
|
1860
|
+
font-size: var(--font-size-s);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
.ap-followed-tags__label {
|
|
1864
|
+
color: var(--color-on-offset);
|
|
1865
|
+
font-weight: 600;
|
|
1866
|
+
}
|
package/index.js
CHANGED
|
@@ -59,6 +59,10 @@ import {
|
|
|
59
59
|
featuredTagsRemoveController,
|
|
60
60
|
} from "./lib/controllers/featured-tags.js";
|
|
61
61
|
import { resolveController } from "./lib/controllers/resolve.js";
|
|
62
|
+
import { tagTimelineController } from "./lib/controllers/tag-timeline.js";
|
|
63
|
+
import { apiTimelineController } from "./lib/controllers/api-timeline.js";
|
|
64
|
+
import { exploreController, exploreApiController } from "./lib/controllers/explore.js";
|
|
65
|
+
import { followTagController, unfollowTagController } from "./lib/controllers/follow-tag.js";
|
|
62
66
|
import { publicProfileController } from "./lib/controllers/public-profile.js";
|
|
63
67
|
import { authorizeInteractionController } from "./lib/controllers/authorize-interaction.js";
|
|
64
68
|
import { myProfileController } from "./lib/controllers/my-profile.js";
|
|
@@ -71,6 +75,7 @@ import {
|
|
|
71
75
|
import { startBatchRefollow } from "./lib/batch-refollow.js";
|
|
72
76
|
import { logActivity } from "./lib/activity-log.js";
|
|
73
77
|
import { scheduleCleanup } from "./lib/timeline-cleanup.js";
|
|
78
|
+
import { runSeparateMentionsMigration } from "./lib/migrations/separate-mentions.js";
|
|
74
79
|
|
|
75
80
|
const defaults = {
|
|
76
81
|
mountPath: "/activitypub",
|
|
@@ -218,6 +223,12 @@ export default class ActivityPubEndpoint {
|
|
|
218
223
|
|
|
219
224
|
router.get("/", dashboardController(mp));
|
|
220
225
|
router.get("/admin/reader", readerController(mp));
|
|
226
|
+
router.get("/admin/reader/tag", tagTimelineController(mp));
|
|
227
|
+
router.get("/admin/reader/api/timeline", apiTimelineController(mp));
|
|
228
|
+
router.get("/admin/reader/explore", exploreController(mp));
|
|
229
|
+
router.get("/admin/reader/api/explore", exploreApiController(mp));
|
|
230
|
+
router.post("/admin/reader/follow-tag", followTagController(mp));
|
|
231
|
+
router.post("/admin/reader/unfollow-tag", unfollowTagController(mp));
|
|
221
232
|
router.get("/admin/reader/notifications", notificationsController(mp));
|
|
222
233
|
router.post("/admin/reader/notifications/mark-read", markAllNotificationsReadController(mp));
|
|
223
234
|
router.post("/admin/reader/notifications/clear", clearAllNotificationsController(mp));
|
|
@@ -855,6 +866,7 @@ export default class ActivityPubEndpoint {
|
|
|
855
866
|
Indiekit.addCollection("ap_blocked");
|
|
856
867
|
Indiekit.addCollection("ap_interactions");
|
|
857
868
|
Indiekit.addCollection("ap_notes");
|
|
869
|
+
Indiekit.addCollection("ap_followed_tags");
|
|
858
870
|
|
|
859
871
|
// Store collection references (posts resolved lazily)
|
|
860
872
|
const indiekitCollections = Indiekit.collections;
|
|
@@ -874,6 +886,7 @@ export default class ActivityPubEndpoint {
|
|
|
874
886
|
ap_blocked: indiekitCollections.get("ap_blocked"),
|
|
875
887
|
ap_interactions: indiekitCollections.get("ap_interactions"),
|
|
876
888
|
ap_notes: indiekitCollections.get("ap_notes"),
|
|
889
|
+
ap_followed_tags: indiekitCollections.get("ap_followed_tags"),
|
|
877
890
|
get posts() {
|
|
878
891
|
return indiekitCollections.get("posts");
|
|
879
892
|
},
|
|
@@ -985,6 +998,18 @@ export default class ActivityPubEndpoint {
|
|
|
985
998
|
{ type: 1 },
|
|
986
999
|
{ background: true },
|
|
987
1000
|
);
|
|
1001
|
+
|
|
1002
|
+
// Followed hashtags — unique on tag (case-insensitive via normalization at write time)
|
|
1003
|
+
this._collections.ap_followed_tags.createIndex(
|
|
1004
|
+
{ tag: 1 },
|
|
1005
|
+
{ unique: true, background: true },
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
// Tag filtering index on timeline
|
|
1009
|
+
this._collections.ap_timeline.createIndex(
|
|
1010
|
+
{ category: 1, published: -1 },
|
|
1011
|
+
{ background: true },
|
|
1012
|
+
);
|
|
988
1013
|
} catch {
|
|
989
1014
|
// Index creation failed — collections not yet available.
|
|
990
1015
|
// Indexes already exist from previous startups; non-fatal.
|
|
@@ -1039,6 +1064,15 @@ export default class ActivityPubEndpoint {
|
|
|
1039
1064
|
});
|
|
1040
1065
|
}, 10_000);
|
|
1041
1066
|
|
|
1067
|
+
// Run one-time migrations (idempotent — safe to run on every startup)
|
|
1068
|
+
runSeparateMentionsMigration(this._collections).then(({ skipped, updated }) => {
|
|
1069
|
+
if (!skipped) {
|
|
1070
|
+
console.log(`[ActivityPub] Migration separate-mentions: updated ${updated} timeline items`);
|
|
1071
|
+
}
|
|
1072
|
+
}).catch((error) => {
|
|
1073
|
+
console.error("[ActivityPub] Migration separate-mentions failed:", error.message);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1042
1076
|
// Schedule timeline retention cleanup (runs on startup + every 24h)
|
|
1043
1077
|
if (this.options.timelineRetention > 0) {
|
|
1044
1078
|
scheduleCleanup(this._collections, this.options.timelineRetention);
|