@rmdes/indiekit-endpoint-microsub 1.0.56 → 1.0.58
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.js +408 -0
- package/index.js +37 -36
- package/lib/cache/redis.js +12 -3
- package/lib/controllers/reader/actor.js +142 -0
- package/lib/controllers/reader/channel.js +301 -0
- package/lib/controllers/reader/compose.js +242 -0
- package/lib/controllers/reader/deck.js +129 -0
- package/lib/controllers/reader/feed-repair.js +117 -0
- package/lib/controllers/reader/feed.js +246 -0
- package/lib/controllers/reader/index.js +126 -0
- package/lib/controllers/reader/search.js +157 -0
- package/lib/controllers/reader/timeline.js +251 -0
- package/lib/controllers/timeline.js +4 -2
- package/lib/feeds/atom.js +1 -1
- package/lib/feeds/fetcher.js +1 -30
- package/lib/feeds/hfeed.js +1 -1
- package/lib/feeds/jsonfeed.js +1 -1
- package/lib/feeds/normalizer-hfeed.js +209 -0
- package/lib/feeds/normalizer-jsonfeed.js +171 -0
- package/lib/feeds/normalizer-rss.js +178 -0
- package/lib/feeds/normalizer.js +20 -560
- package/lib/feeds/rss.js +1 -1
- package/lib/polling/processor.js +3 -17
- package/lib/storage/items-read-state.js +287 -0
- package/lib/storage/items-retention.js +174 -0
- package/lib/storage/items-search.js +34 -0
- package/lib/storage/items.js +99 -590
- package/lib/storage/read-state.js +1 -1
- package/lib/utils/async-handler.js +7 -0
- package/lib/utils/html.js +25 -0
- package/lib/utils/source-type.js +28 -0
- package/lib/webmention/processor.js +1 -1
- package/locales/de.json +3 -0
- package/locales/en.json +2 -0
- package/locales/es-419.json +3 -0
- package/locales/es.json +3 -0
- package/locales/fr.json +3 -0
- package/locales/hi.json +3 -0
- package/locales/id.json +3 -0
- package/locales/it.json +3 -0
- package/locales/nl.json +3 -0
- package/locales/pl.json +3 -0
- package/locales/pt-BR.json +3 -0
- package/locales/pt.json +3 -0
- package/locales/sr.json +3 -0
- package/locales/sv.json +3 -0
- package/locales/zh-Hans-CN.json +3 -0
- package/package.json +1 -1
- package/views/channel.njk +1 -348
- package/views/timeline.njk +3 -274
- package/lib/controllers/reader.js +0 -1562
package/views/timeline.njk
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{% if channels.length > 0 %}
|
|
9
9
|
<details class="ms-timeline-view__filter">
|
|
10
10
|
<summary class="button button--secondary button--small">
|
|
11
|
-
|
|
11
|
+
{{ __("microsub.reader.filterChannels") }}
|
|
12
12
|
</summary>
|
|
13
13
|
<form action="{{ baseUrl }}/timeline" method="GET" class="ms-timeline-view__filter-form">
|
|
14
14
|
{% for ch in channels %}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
</label>
|
|
22
22
|
{% endif %}
|
|
23
23
|
{% endfor %}
|
|
24
|
-
<button type="submit" class="button button--primary button--small">
|
|
24
|
+
<button type="submit" class="button button--primary button--small">{{ __("microsub.reader.apply") }}</button>
|
|
25
25
|
</form>
|
|
26
26
|
</details>
|
|
27
27
|
{% endif %}
|
|
@@ -65,276 +65,5 @@
|
|
|
65
65
|
{% endif %}
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
|
-
<script type="module">
|
|
69
|
-
// CSRF token for AJAX requests
|
|
70
|
-
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
71
|
-
|
|
72
|
-
const timeline = document.getElementById('timeline');
|
|
73
|
-
if (timeline) {
|
|
74
|
-
const items = Array.from(timeline.querySelectorAll('.ms-item-card'));
|
|
75
|
-
let currentIndex = -1;
|
|
76
|
-
|
|
77
|
-
function focusItem(index) {
|
|
78
|
-
if (items[currentIndex]) items[currentIndex].classList.remove('ms-item-card--focused');
|
|
79
|
-
currentIndex = Math.max(0, Math.min(index, items.length - 1));
|
|
80
|
-
if (items[currentIndex]) {
|
|
81
|
-
items[currentIndex].classList.add('ms-item-card--focused');
|
|
82
|
-
items[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
document.addEventListener('keydown', (e) => {
|
|
87
|
-
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
88
|
-
switch(e.key) {
|
|
89
|
-
case 'j': e.preventDefault(); focusItem(currentIndex + 1); break;
|
|
90
|
-
case 'k': e.preventDefault(); focusItem(currentIndex - 1); break;
|
|
91
|
-
case 'o': case 'Enter':
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
if (items[currentIndex]) {
|
|
94
|
-
const link = items[currentIndex].querySelector('.ms-item-card__link');
|
|
95
|
-
if (link) link.click();
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Handle individual mark-read buttons
|
|
102
|
-
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader$/, '');
|
|
103
|
-
|
|
104
|
-
timeline.addEventListener('click', async (e) => {
|
|
105
|
-
const button = e.target.closest('.ms-item-actions__mark-read');
|
|
106
|
-
if (!button) return;
|
|
107
|
-
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
e.stopPropagation();
|
|
110
|
-
|
|
111
|
-
const itemId = button.dataset.itemId;
|
|
112
|
-
const channelUid = button.dataset.channelUid;
|
|
113
|
-
const channelId = button.dataset.channelId;
|
|
114
|
-
if (!itemId || (!channelUid && !channelId)) return;
|
|
115
|
-
|
|
116
|
-
button.disabled = true;
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
const formData = new URLSearchParams();
|
|
120
|
-
formData.append('action', 'timeline');
|
|
121
|
-
formData.append('method', 'mark_read');
|
|
122
|
-
formData.append('channel', channelUid || channelId);
|
|
123
|
-
formData.append('entry', itemId);
|
|
124
|
-
|
|
125
|
-
const response = await fetch(microsubApiUrl, {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrfToken },
|
|
128
|
-
body: formData.toString(),
|
|
129
|
-
credentials: 'same-origin'
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (response.ok) {
|
|
133
|
-
const card = button.closest('.ms-item-card');
|
|
134
|
-
if (card) {
|
|
135
|
-
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
136
|
-
card.style.opacity = '0';
|
|
137
|
-
card.style.transform = 'translateX(-20px)';
|
|
138
|
-
setTimeout(() => {
|
|
139
|
-
const wrapper = card.closest('.ms-timeline-view__item');
|
|
140
|
-
if (wrapper) wrapper.remove();
|
|
141
|
-
else card.remove();
|
|
142
|
-
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
|
|
143
|
-
location.reload();
|
|
144
|
-
}
|
|
145
|
-
}, 300);
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
console.error('Failed to mark item as read');
|
|
149
|
-
button.disabled = false;
|
|
150
|
-
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error('Error marking item as read:', error);
|
|
153
|
-
button.disabled = false;
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Handle caret toggle for mark-source-read popover
|
|
158
|
-
timeline.addEventListener('click', (e) => {
|
|
159
|
-
const caret = e.target.closest('.ms-item-actions__mark-read-caret');
|
|
160
|
-
if (!caret) return;
|
|
161
|
-
|
|
162
|
-
e.preventDefault();
|
|
163
|
-
e.stopPropagation();
|
|
164
|
-
|
|
165
|
-
// Close other open popovers
|
|
166
|
-
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
|
|
167
|
-
if (p !== caret.nextElementSibling) p.hidden = true;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const popover = caret.nextElementSibling;
|
|
171
|
-
if (popover) popover.hidden = !popover.hidden;
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Handle mark-source-read button
|
|
175
|
-
timeline.addEventListener('click', async (e) => {
|
|
176
|
-
const button = e.target.closest('.ms-item-actions__mark-source-read');
|
|
177
|
-
if (!button) return;
|
|
178
|
-
|
|
179
|
-
e.preventDefault();
|
|
180
|
-
e.stopPropagation();
|
|
181
|
-
|
|
182
|
-
const feedId = button.dataset.feedId;
|
|
183
|
-
const channelUid = button.dataset.channelUid;
|
|
184
|
-
const channelId = button.dataset.channelId;
|
|
185
|
-
if (!feedId || (!channelUid && !channelId)) return;
|
|
186
|
-
|
|
187
|
-
button.disabled = true;
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const formData = new URLSearchParams();
|
|
191
|
-
formData.append('action', 'timeline');
|
|
192
|
-
formData.append('method', 'mark_read_source');
|
|
193
|
-
formData.append('channel', channelUid || channelId);
|
|
194
|
-
formData.append('feed', feedId);
|
|
195
|
-
|
|
196
|
-
const response = await fetch(microsubApiUrl, {
|
|
197
|
-
method: 'POST',
|
|
198
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrfToken },
|
|
199
|
-
body: formData.toString(),
|
|
200
|
-
credentials: 'same-origin'
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
if (response.ok) {
|
|
204
|
-
// Animate out all cards from this feed
|
|
205
|
-
const cards = timeline.querySelectorAll(`.ms-item-card[data-feed-id="${feedId}"]`);
|
|
206
|
-
for (const card of cards) {
|
|
207
|
-
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
208
|
-
card.style.opacity = '0';
|
|
209
|
-
card.style.transform = 'translateX(-20px)';
|
|
210
|
-
}
|
|
211
|
-
setTimeout(() => {
|
|
212
|
-
for (const card of [...cards]) {
|
|
213
|
-
const wrapper = card.closest('.ms-timeline-view__item');
|
|
214
|
-
if (wrapper) wrapper.remove();
|
|
215
|
-
else card.remove();
|
|
216
|
-
}
|
|
217
|
-
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
|
|
218
|
-
location.reload();
|
|
219
|
-
}
|
|
220
|
-
}, 300);
|
|
221
|
-
} else {
|
|
222
|
-
button.disabled = false;
|
|
223
|
-
}
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error('Error marking source as read:', error);
|
|
226
|
-
button.disabled = false;
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// Close popovers on outside click
|
|
231
|
-
document.addEventListener('click', (e) => {
|
|
232
|
-
if (!e.target.closest('.ms-item-actions__mark-read-group')) {
|
|
233
|
-
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
|
|
234
|
-
p.hidden = true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Handle save-for-later buttons
|
|
240
|
-
timeline.addEventListener('click', async (e) => {
|
|
241
|
-
const button = e.target.closest('.ms-item-actions__save-later');
|
|
242
|
-
if (!button) return;
|
|
243
|
-
|
|
244
|
-
e.preventDefault();
|
|
245
|
-
e.stopPropagation();
|
|
246
|
-
|
|
247
|
-
const url = button.dataset.url;
|
|
248
|
-
const title = button.dataset.title;
|
|
249
|
-
if (!url) return;
|
|
250
|
-
|
|
251
|
-
button.disabled = true;
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const response = await fetch('/readlater/save', {
|
|
255
|
-
method: 'POST',
|
|
256
|
-
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
|
|
257
|
-
body: JSON.stringify({ url, title: title || url, source: 'microsub' }),
|
|
258
|
-
credentials: 'same-origin'
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
if (response.ok) {
|
|
262
|
-
button.classList.add('ms-item-actions__save-later--saved');
|
|
263
|
-
button.title = 'Saved';
|
|
264
|
-
} else {
|
|
265
|
-
button.disabled = false;
|
|
266
|
-
}
|
|
267
|
-
} catch {
|
|
268
|
-
button.disabled = false;
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// === Infinite scroll ===
|
|
273
|
-
const loader = document.getElementById('timeline-loader');
|
|
274
|
-
if (loader && timeline) {
|
|
275
|
-
const spinner = loader.querySelector('.ms-timeline__spinner');
|
|
276
|
-
const loadMoreLink = loader.querySelector('.ms-timeline__load-more');
|
|
277
|
-
const endMessage = loader.querySelector('.ms-timeline__end');
|
|
278
|
-
let cursor = loader.dataset.cursor;
|
|
279
|
-
let loading = false;
|
|
280
|
-
let hasMore = true;
|
|
281
|
-
const apiUrl = loader.dataset.apiUrl;
|
|
282
|
-
|
|
283
|
-
async function loadMore() {
|
|
284
|
-
if (loading || !hasMore) return;
|
|
285
|
-
loading = true;
|
|
286
|
-
if (spinner) spinner.style.display = '';
|
|
287
|
-
if (loadMoreLink) loadMoreLink.style.display = 'none';
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const sep = apiUrl.includes('?') ? '&' : '?';
|
|
291
|
-
const response = await fetch(`${apiUrl}${sep}after=${encodeURIComponent(cursor)}`, {
|
|
292
|
-
credentials: 'same-origin'
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
if (!response.ok) throw new Error('Failed to load');
|
|
296
|
-
|
|
297
|
-
const data = await response.json();
|
|
298
|
-
|
|
299
|
-
if (data.html && data.count > 0) {
|
|
300
|
-
timeline.insertAdjacentHTML('beforeend', data.html);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (data.paging?.after) {
|
|
304
|
-
cursor = data.paging.after;
|
|
305
|
-
if (loadMoreLink) {
|
|
306
|
-
loadMoreLink.href = `?after=${cursor}`;
|
|
307
|
-
loadMoreLink.style.display = '';
|
|
308
|
-
}
|
|
309
|
-
} else {
|
|
310
|
-
hasMore = false;
|
|
311
|
-
if (loadMoreLink) loadMoreLink.style.display = 'none';
|
|
312
|
-
if (endMessage) endMessage.style.display = '';
|
|
313
|
-
}
|
|
314
|
-
} catch (error) {
|
|
315
|
-
console.error('Infinite scroll error:', error);
|
|
316
|
-
hasMore = false;
|
|
317
|
-
if (loadMoreLink) loadMoreLink.style.display = '';
|
|
318
|
-
} finally {
|
|
319
|
-
loading = false;
|
|
320
|
-
if (spinner) spinner.style.display = 'none';
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const observer = new IntersectionObserver((entries) => {
|
|
325
|
-
if (entries[0].isIntersecting && hasMore && !loading) {
|
|
326
|
-
loadMore();
|
|
327
|
-
}
|
|
328
|
-
}, { rootMargin: '200px' });
|
|
329
|
-
observer.observe(loader);
|
|
330
|
-
|
|
331
|
-
if (loadMoreLink) {
|
|
332
|
-
loadMoreLink.addEventListener('click', (e) => {
|
|
333
|
-
e.preventDefault();
|
|
334
|
-
loadMore();
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
</script>
|
|
68
|
+
<script type="module" src="/assets/@rmdes-indiekit-endpoint-microsub/reader.js"></script>
|
|
340
69
|
{% endblock %}
|