@rmdes/indiekit-endpoint-microsub 1.0.55 → 1.0.57
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 +61 -49
- package/lib/activitypub/outbox-fetcher.js +14 -2
- package/lib/cache/redis.js +26 -7
- package/lib/controllers/channels.js +2 -2
- 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 +250 -0
- package/lib/controllers/search.js +6 -0
- package/lib/controllers/timeline.js +6 -4
- package/lib/feeds/atom.js +1 -1
- package/lib/feeds/capabilities.js +5 -0
- package/lib/feeds/fetcher.js +5 -28
- 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 +22 -614
- package/lib/feeds/rss.js +1 -1
- package/lib/media/proxy.js +82 -27
- package/lib/polling/processor.js +30 -21
- package/lib/polling/scheduler.js +2 -0
- package/lib/realtime/broker.js +6 -1
- package/lib/storage/channels.js +53 -42
- package/lib/storage/feeds.js +3 -1
- 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 +113 -610
- package/lib/storage/read-state.js +1 -1
- package/lib/utils/async-handler.js +7 -0
- package/lib/utils/constants.js +7 -0
- package/lib/utils/csrf.js +51 -0
- package/lib/utils/html.js +25 -0
- package/lib/utils/sanitize.js +61 -0
- package/lib/utils/source-type.js +28 -0
- package/lib/utils/validation.js +8 -2
- package/lib/webmention/processor.js +1 -1
- package/lib/webmention/verifier.js +10 -21
- package/lib/websub/subscriber.js +12 -0
- 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 +3 -1
- package/views/actor.njk +2 -0
- package/views/channel-new.njk +1 -0
- package/views/channel.njk +3 -344
- package/views/compose.njk +1 -0
- package/views/deck-settings.njk +1 -0
- package/views/feed-edit.njk +3 -0
- package/views/feeds.njk +4 -0
- package/views/layouts/reader.njk +1 -0
- package/views/search.njk +2 -0
- package/views/settings.njk +2 -0
- package/views/timeline.njk +3 -271
- package/lib/controllers/reader.js +0 -1580
package/views/channel.njk
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<div class="ms-channel__actions">
|
|
8
8
|
{% if not showRead and items.length > 0 %}
|
|
9
9
|
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
|
|
10
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
10
11
|
<input type="hidden" name="channel" value="{{ channel.uid }}">
|
|
11
12
|
<input type="hidden" name="entry" value="last-read-entry">
|
|
12
13
|
<button type="submit" class="button button--secondary button--small">
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
|
|
41
42
|
{% if items.length > 0 %}
|
|
42
43
|
<form method="POST" action="{{ baseUrl }}/api/mark-view-read" id="mark-view-form">
|
|
44
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
43
45
|
<input type="hidden" name="channel" value="{{ channel.uid }}">
|
|
44
46
|
{% for item in items %}
|
|
45
47
|
{% if not item._is_read %}
|
|
@@ -94,348 +96,5 @@
|
|
|
94
96
|
{% endif %}
|
|
95
97
|
</div>
|
|
96
98
|
|
|
97
|
-
<script type="module">
|
|
98
|
-
// Keyboard navigation (j/k for items, o to open)
|
|
99
|
-
const timeline = document.getElementById('timeline');
|
|
100
|
-
if (timeline) {
|
|
101
|
-
const items = Array.from(timeline.querySelectorAll('.ms-item-card'));
|
|
102
|
-
let currentIndex = -1;
|
|
103
|
-
|
|
104
|
-
function focusItem(index) {
|
|
105
|
-
if (items[currentIndex]) {
|
|
106
|
-
items[currentIndex].classList.remove('ms-item-card--focused');
|
|
107
|
-
}
|
|
108
|
-
currentIndex = Math.max(0, Math.min(index, items.length - 1));
|
|
109
|
-
if (items[currentIndex]) {
|
|
110
|
-
items[currentIndex].classList.add('ms-item-card--focused');
|
|
111
|
-
items[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
document.addEventListener('keydown', (e) => {
|
|
116
|
-
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
117
|
-
|
|
118
|
-
switch(e.key) {
|
|
119
|
-
case 'j':
|
|
120
|
-
e.preventDefault();
|
|
121
|
-
focusItem(currentIndex + 1);
|
|
122
|
-
break;
|
|
123
|
-
case 'k':
|
|
124
|
-
e.preventDefault();
|
|
125
|
-
focusItem(currentIndex - 1);
|
|
126
|
-
break;
|
|
127
|
-
case 'o':
|
|
128
|
-
case 'Enter':
|
|
129
|
-
e.preventDefault();
|
|
130
|
-
if (items[currentIndex]) {
|
|
131
|
-
const link = items[currentIndex].querySelector('.ms-item-card__link');
|
|
132
|
-
if (link) link.click();
|
|
133
|
-
}
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Handle individual mark-read buttons
|
|
139
|
-
const channelUid = timeline.dataset.channel;
|
|
140
|
-
// Microsub API is at the parent of /reader (e.g., /microsub not /microsub/reader)
|
|
141
|
-
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader$/, '');
|
|
142
|
-
|
|
143
|
-
timeline.addEventListener('click', async (e) => {
|
|
144
|
-
const button = e.target.closest('.ms-item-actions__mark-read');
|
|
145
|
-
if (!button) return;
|
|
146
|
-
|
|
147
|
-
e.preventDefault();
|
|
148
|
-
e.stopPropagation();
|
|
149
|
-
|
|
150
|
-
const itemId = button.dataset.itemId;
|
|
151
|
-
if (!itemId) return;
|
|
152
|
-
|
|
153
|
-
// Disable button while processing
|
|
154
|
-
button.disabled = true;
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
const formData = new URLSearchParams();
|
|
158
|
-
formData.append('action', 'timeline');
|
|
159
|
-
formData.append('method', 'mark_read');
|
|
160
|
-
formData.append('channel', channelUid);
|
|
161
|
-
formData.append('entry', itemId);
|
|
162
|
-
|
|
163
|
-
const response = await fetch(microsubApiUrl, {
|
|
164
|
-
method: 'POST',
|
|
165
|
-
headers: {
|
|
166
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
167
|
-
},
|
|
168
|
-
body: formData.toString(),
|
|
169
|
-
credentials: 'same-origin'
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (response.ok) {
|
|
173
|
-
// Hide the item with animation
|
|
174
|
-
const card = button.closest('.ms-item-card');
|
|
175
|
-
if (card) {
|
|
176
|
-
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
177
|
-
card.style.opacity = '0';
|
|
178
|
-
card.style.transform = 'translateX(-20px)';
|
|
179
|
-
setTimeout(() => {
|
|
180
|
-
card.remove();
|
|
181
|
-
// Check if timeline is now empty
|
|
182
|
-
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
|
|
183
|
-
location.reload();
|
|
184
|
-
}
|
|
185
|
-
}, 300);
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
console.error('Failed to mark item as read');
|
|
189
|
-
button.disabled = false;
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
console.error('Error marking item as read:', error);
|
|
193
|
-
button.disabled = false;
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Handle caret toggle for mark-source-read popover
|
|
198
|
-
timeline.addEventListener('click', (e) => {
|
|
199
|
-
const caret = e.target.closest('.ms-item-actions__mark-read-caret');
|
|
200
|
-
if (!caret) return;
|
|
201
|
-
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
e.stopPropagation();
|
|
204
|
-
|
|
205
|
-
// Close other open popovers
|
|
206
|
-
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
|
|
207
|
-
if (p !== caret.nextElementSibling) p.hidden = true;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const popover = caret.nextElementSibling;
|
|
211
|
-
if (popover) popover.hidden = !popover.hidden;
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Handle mark-source-read button
|
|
215
|
-
timeline.addEventListener('click', async (e) => {
|
|
216
|
-
const button = e.target.closest('.ms-item-actions__mark-source-read');
|
|
217
|
-
if (!button) return;
|
|
218
|
-
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
e.stopPropagation();
|
|
221
|
-
|
|
222
|
-
const feedId = button.dataset.feedId;
|
|
223
|
-
if (!feedId) return;
|
|
224
|
-
|
|
225
|
-
button.disabled = true;
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const formData = new URLSearchParams();
|
|
229
|
-
formData.append('action', 'timeline');
|
|
230
|
-
formData.append('method', 'mark_read_source');
|
|
231
|
-
formData.append('channel', channelUid);
|
|
232
|
-
formData.append('feed', feedId);
|
|
233
|
-
|
|
234
|
-
const response = await fetch(microsubApiUrl, {
|
|
235
|
-
method: 'POST',
|
|
236
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
237
|
-
body: formData.toString(),
|
|
238
|
-
credentials: 'same-origin'
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
if (response.ok) {
|
|
242
|
-
// Animate out all cards from this feed
|
|
243
|
-
const cards = timeline.querySelectorAll(`.ms-item-card[data-feed-id="${feedId}"]`);
|
|
244
|
-
for (const card of cards) {
|
|
245
|
-
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
246
|
-
card.style.opacity = '0';
|
|
247
|
-
card.style.transform = 'translateX(-20px)';
|
|
248
|
-
}
|
|
249
|
-
setTimeout(() => {
|
|
250
|
-
for (const card of [...cards]) {
|
|
251
|
-
card.remove();
|
|
252
|
-
}
|
|
253
|
-
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
|
|
254
|
-
location.reload();
|
|
255
|
-
}
|
|
256
|
-
}, 300);
|
|
257
|
-
} else {
|
|
258
|
-
button.disabled = false;
|
|
259
|
-
}
|
|
260
|
-
} catch (error) {
|
|
261
|
-
console.error('Error marking source as read:', error);
|
|
262
|
-
button.disabled = false;
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Close popovers on outside click
|
|
267
|
-
document.addEventListener('click', (e) => {
|
|
268
|
-
if (!e.target.closest('.ms-item-actions__mark-read-group')) {
|
|
269
|
-
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
|
|
270
|
-
p.hidden = true;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Handle save-for-later buttons
|
|
276
|
-
timeline.addEventListener('click', async (e) => {
|
|
277
|
-
const button = e.target.closest('.ms-item-actions__save-later');
|
|
278
|
-
if (!button) return;
|
|
279
|
-
|
|
280
|
-
e.preventDefault();
|
|
281
|
-
e.stopPropagation();
|
|
282
|
-
|
|
283
|
-
const url = button.dataset.url;
|
|
284
|
-
const title = button.dataset.title;
|
|
285
|
-
if (!url) return;
|
|
286
|
-
|
|
287
|
-
button.disabled = true;
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const response = await fetch('/readlater/save', {
|
|
291
|
-
method: 'POST',
|
|
292
|
-
headers: { 'Content-Type': 'application/json' },
|
|
293
|
-
body: JSON.stringify({ url, title: title || url, source: 'microsub' }),
|
|
294
|
-
credentials: 'same-origin'
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
if (response.ok) {
|
|
298
|
-
button.classList.add('ms-item-actions__save-later--saved');
|
|
299
|
-
button.title = 'Saved';
|
|
300
|
-
} else {
|
|
301
|
-
button.disabled = false;
|
|
302
|
-
}
|
|
303
|
-
} catch {
|
|
304
|
-
button.disabled = false;
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// === Infinite scroll ===
|
|
310
|
-
const loader = document.getElementById('timeline-loader');
|
|
311
|
-
if (loader && timeline) {
|
|
312
|
-
const spinner = loader.querySelector('.ms-timeline__spinner');
|
|
313
|
-
const loadMoreLink = loader.querySelector('.ms-timeline__load-more');
|
|
314
|
-
const endMessage = loader.querySelector('.ms-timeline__end');
|
|
315
|
-
let cursor = loader.dataset.cursor;
|
|
316
|
-
let loading = false;
|
|
317
|
-
let hasMore = true;
|
|
318
|
-
const apiUrl = loader.dataset.apiUrl;
|
|
319
|
-
const showReadParam = loader.dataset.showRead;
|
|
320
|
-
|
|
321
|
-
async function loadMore() {
|
|
322
|
-
if (loading || !hasMore) return;
|
|
323
|
-
loading = true;
|
|
324
|
-
if (spinner) spinner.style.display = '';
|
|
325
|
-
if (loadMoreLink) loadMoreLink.style.display = 'none';
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
const params = new URLSearchParams({ after: cursor });
|
|
329
|
-
if (showReadParam === 'true') params.append('showRead', 'true');
|
|
330
|
-
|
|
331
|
-
const response = await fetch(`${apiUrl}?${params}`, {
|
|
332
|
-
credentials: 'same-origin'
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
if (!response.ok) throw new Error('Failed to load');
|
|
336
|
-
|
|
337
|
-
const data = await response.json();
|
|
338
|
-
|
|
339
|
-
if (data.html && data.count > 0) {
|
|
340
|
-
timeline.insertAdjacentHTML('beforeend', data.html);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (data.paging?.after) {
|
|
344
|
-
cursor = data.paging.after;
|
|
345
|
-
if (loadMoreLink) {
|
|
346
|
-
loadMoreLink.href = `?after=${cursor}${showReadParam === 'true' ? '&showRead=true' : ''}`;
|
|
347
|
-
loadMoreLink.style.display = '';
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
hasMore = false;
|
|
351
|
-
if (loadMoreLink) loadMoreLink.style.display = 'none';
|
|
352
|
-
if (endMessage) endMessage.style.display = '';
|
|
353
|
-
}
|
|
354
|
-
} catch (error) {
|
|
355
|
-
console.error('Infinite scroll error:', error);
|
|
356
|
-
hasMore = false;
|
|
357
|
-
if (loadMoreLink) loadMoreLink.style.display = '';
|
|
358
|
-
} finally {
|
|
359
|
-
loading = false;
|
|
360
|
-
if (spinner) spinner.style.display = 'none';
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// IntersectionObserver auto-loads when sentinel is visible
|
|
365
|
-
const observer = new IntersectionObserver((entries) => {
|
|
366
|
-
if (entries[0].isIntersecting && hasMore && !loading) {
|
|
367
|
-
loadMore();
|
|
368
|
-
}
|
|
369
|
-
}, { rootMargin: '200px' });
|
|
370
|
-
observer.observe(loader);
|
|
371
|
-
|
|
372
|
-
// Click handler for fallback link
|
|
373
|
-
if (loadMoreLink) {
|
|
374
|
-
loadMoreLink.addEventListener('click', (e) => {
|
|
375
|
-
e.preventDefault();
|
|
376
|
-
loadMore();
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Expose loadMore for mark-view-as-read to trigger
|
|
381
|
-
window.__microsubLoadMore = loadMore;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// === Mark current view as read button ===
|
|
385
|
-
const markViewBtn = document.querySelector('.js-mark-view-read');
|
|
386
|
-
if (markViewBtn && timeline) {
|
|
387
|
-
markViewBtn.style.display = '';
|
|
388
|
-
const markViewApiUrl = '{{ baseUrl }}'.replace(/\/reader$/, '');
|
|
389
|
-
|
|
390
|
-
markViewBtn.addEventListener('click', async () => {
|
|
391
|
-
const unreadCards = timeline.querySelectorAll('.ms-item-card:not(.ms-item-card--read)');
|
|
392
|
-
const itemIds = [...unreadCards].map((card) => card.dataset.itemId).filter(Boolean);
|
|
393
|
-
|
|
394
|
-
if (itemIds.length === 0) return;
|
|
395
|
-
|
|
396
|
-
markViewBtn.disabled = true;
|
|
397
|
-
|
|
398
|
-
const formData = new URLSearchParams();
|
|
399
|
-
formData.append('action', 'timeline');
|
|
400
|
-
formData.append('method', 'mark_read');
|
|
401
|
-
formData.append('channel', markViewBtn.dataset.channel);
|
|
402
|
-
for (const id of itemIds) {
|
|
403
|
-
formData.append('entry', id);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
const response = await fetch(markViewApiUrl, {
|
|
408
|
-
method: 'POST',
|
|
409
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
410
|
-
body: formData.toString(),
|
|
411
|
-
credentials: 'same-origin'
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
if (response.ok) {
|
|
415
|
-
for (const card of unreadCards) {
|
|
416
|
-
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
417
|
-
card.style.opacity = '0';
|
|
418
|
-
card.style.transform = 'translateX(-20px)';
|
|
419
|
-
}
|
|
420
|
-
setTimeout(() => {
|
|
421
|
-
for (const card of [...unreadCards]) card.remove();
|
|
422
|
-
markViewBtn.disabled = false;
|
|
423
|
-
|
|
424
|
-
// Trigger infinite scroll to load next batch
|
|
425
|
-
if (typeof window.__microsubLoadMore === 'function') {
|
|
426
|
-
window.__microsubLoadMore();
|
|
427
|
-
} else if (timeline.querySelectorAll('.ms-item-card').length === 0) {
|
|
428
|
-
location.reload();
|
|
429
|
-
}
|
|
430
|
-
}, 300);
|
|
431
|
-
} else {
|
|
432
|
-
markViewBtn.disabled = false;
|
|
433
|
-
}
|
|
434
|
-
} catch (error) {
|
|
435
|
-
console.error('Error marking current view as read:', error);
|
|
436
|
-
markViewBtn.disabled = false;
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
</script>
|
|
99
|
+
<script type="module" src="/assets/@rmdes-indiekit-endpoint-microsub/reader.js"></script>
|
|
441
100
|
{% endblock %}
|
package/views/compose.njk
CHANGED
package/views/deck-settings.njk
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
</header>
|
|
11
11
|
|
|
12
12
|
<form action="{{ baseUrl }}/deck/settings" method="POST">
|
|
13
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
13
14
|
<p>Select which channels appear as columns in your deck, and their order.</p>
|
|
14
15
|
|
|
15
16
|
<div class="ms-deck-settings__channels">
|
package/views/feed-edit.njk
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
37
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/edit" class="ms-feed-edit__form">
|
|
38
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
38
39
|
{{ input({
|
|
39
40
|
id: "url",
|
|
40
41
|
name: "url",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
<h3>Other Actions</h3>
|
|
65
66
|
|
|
66
67
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/rediscover" class="ms-feed-edit__action">
|
|
68
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
67
69
|
<p>Run feed discovery on the current URL to find the actual RSS/Atom feed.</p>
|
|
68
70
|
{{ button({
|
|
69
71
|
text: "Rediscover Feed",
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
</form>
|
|
73
75
|
|
|
74
76
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/refresh" class="ms-feed-edit__action">
|
|
77
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
75
78
|
<p>Force refresh this feed now.</p>
|
|
76
79
|
{{ button({
|
|
77
80
|
text: "Refresh Now",
|
package/views/feeds.njk
CHANGED
|
@@ -62,16 +62,19 @@
|
|
|
62
62
|
{{ icon("updatePost") }}
|
|
63
63
|
</a>
|
|
64
64
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/rediscover" style="display:inline;">
|
|
65
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
65
66
|
<button type="submit" class="button button--secondary button--small" title="Rediscover feed">
|
|
66
67
|
{{ icon("syndicate") }}
|
|
67
68
|
</button>
|
|
68
69
|
</form>
|
|
69
70
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/refresh" style="display:inline;">
|
|
71
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
70
72
|
<button type="submit" class="button button--secondary button--small" title="Refresh now">
|
|
71
73
|
{{ icon("repost") }}
|
|
72
74
|
</button>
|
|
73
75
|
</form>
|
|
74
76
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/remove" style="display:inline;">
|
|
77
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
75
78
|
<input type="hidden" name="url" value="{{ feed.url }}">
|
|
76
79
|
<button type="submit" class="button button--warning button--small" title="Unfollow">
|
|
77
80
|
{{ icon("delete") }}
|
|
@@ -91,6 +94,7 @@
|
|
|
91
94
|
<div class="ms-feeds__add">
|
|
92
95
|
<h3>{{ __("microsub.feeds.follow") }}</h3>
|
|
93
96
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="ms-feeds__form">
|
|
97
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
94
98
|
{{ input({
|
|
95
99
|
id: "url",
|
|
96
100
|
name: "url",
|
package/views/layouts/reader.njk
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
{% block content %}
|
|
8
8
|
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-microsub/styles.css">
|
|
9
|
+
<meta name="csrf-token" content="{{ csrfToken }}">
|
|
9
10
|
{% include "partials/breadcrumbs.njk" %}
|
|
10
11
|
{% include "partials/view-switcher.njk" %}
|
|
11
12
|
{% block reader %}{% endblock %}
|
package/views/search.njk
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<h2>{{ __("microsub.search.title") }}</h2>
|
|
10
10
|
|
|
11
11
|
<form method="post" action="{{ baseUrl }}/search" class="ms-search__form">
|
|
12
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
12
13
|
{{ input({
|
|
13
14
|
id: "query",
|
|
14
15
|
name: "query",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
</div>
|
|
61
62
|
{% if result.valid %}
|
|
62
63
|
<form method="post" action="{{ baseUrl }}/subscribe" class="ms-search__subscribe">
|
|
64
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
63
65
|
<input type="hidden" name="url" value="{{ result.url }}">
|
|
64
66
|
<label for="channel-{{ loop.index }}" class="-!-visually-hidden">{{ __("microsub.channels.title") }}</label>
|
|
65
67
|
<select name="channel" id="channel-{{ loop.index }}" class="select select--small">
|
package/views/settings.njk
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<h2>{{ __("microsub.settings.title", { channel: channel.name }) }}</h2>
|
|
10
10
|
|
|
11
11
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/settings">
|
|
12
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
12
13
|
{{ checkboxes({
|
|
13
14
|
name: "excludeTypes",
|
|
14
15
|
values: channel.settings.excludeTypes,
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
<h3>{{ __("microsub.settings.dangerZone") }}</h3>
|
|
65
66
|
<p class="hint">{{ __("microsub.settings.deleteWarning") }}</p>
|
|
66
67
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/delete" onsubmit="return confirm('{{ __("microsub.settings.deleteConfirm") }}');">
|
|
68
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
67
69
|
{{ button({
|
|
68
70
|
text: __("microsub.settings.delete"),
|
|
69
71
|
classes: "button--danger"
|