@rmdes/indiekit-endpoint-microsub 1.0.13 → 1.0.16
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/index.js +1 -0
- package/lib/controllers/reader.js +95 -6
- package/locales/en.json +4 -0
- package/package.json +1 -1
- package/views/compose.njk +17 -6
- package/views/partials/actions.njk +3 -0
package/index.js
CHANGED
|
@@ -96,6 +96,7 @@ export default class MicrosubEndpoint {
|
|
|
96
96
|
readerRouter.get("/search", readerController.searchPage);
|
|
97
97
|
readerRouter.post("/search", readerController.searchFeeds);
|
|
98
98
|
readerRouter.post("/subscribe", readerController.subscribe);
|
|
99
|
+
readerRouter.post("/api/mark-read", readerController.markAllRead);
|
|
99
100
|
router.use("/reader", readerRouter);
|
|
100
101
|
|
|
101
102
|
return router;
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
createFeed,
|
|
18
18
|
deleteFeed,
|
|
19
19
|
} from "../storage/feeds.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getTimelineItems,
|
|
22
|
+
getItemById,
|
|
23
|
+
markItemsRead,
|
|
24
|
+
} from "../storage/items.js";
|
|
21
25
|
import { getUserId } from "../utils/auth.js";
|
|
22
26
|
import {
|
|
23
27
|
validateChannelName,
|
|
@@ -315,6 +319,38 @@ function ensureString(value) {
|
|
|
315
319
|
return String(value);
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Fetch syndication targets from Micropub config
|
|
324
|
+
* @param {object} application - Indiekit application
|
|
325
|
+
* @param {string} token - Auth token
|
|
326
|
+
* @returns {Promise<Array>} Syndication targets
|
|
327
|
+
*/
|
|
328
|
+
async function getSyndicationTargets(application, token) {
|
|
329
|
+
try {
|
|
330
|
+
const micropubEndpoint = application.micropubEndpoint;
|
|
331
|
+
if (!micropubEndpoint) return [];
|
|
332
|
+
|
|
333
|
+
const micropubUrl = micropubEndpoint.startsWith("http")
|
|
334
|
+
? micropubEndpoint
|
|
335
|
+
: new URL(micropubEndpoint, application.url).href;
|
|
336
|
+
|
|
337
|
+
const configUrl = `${micropubUrl}?q=config`;
|
|
338
|
+
const configResponse = await fetch(configUrl, {
|
|
339
|
+
headers: {
|
|
340
|
+
Authorization: `Bearer ${token}`,
|
|
341
|
+
Accept: "application/json",
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (!configResponse.ok) return [];
|
|
346
|
+
|
|
347
|
+
const config = await configResponse.json();
|
|
348
|
+
return config["syndicate-to"] || [];
|
|
349
|
+
} catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
318
354
|
/**
|
|
319
355
|
* Compose response form
|
|
320
356
|
* @param {object} request - Express request
|
|
@@ -322,6 +358,8 @@ function ensureString(value) {
|
|
|
322
358
|
* @returns {Promise<void>}
|
|
323
359
|
*/
|
|
324
360
|
export async function compose(request, response) {
|
|
361
|
+
const { application } = request.app.locals;
|
|
362
|
+
|
|
325
363
|
// Support both long-form (replyTo) and short-form (reply) query params
|
|
326
364
|
const {
|
|
327
365
|
replyTo,
|
|
@@ -334,12 +372,19 @@ export async function compose(request, response) {
|
|
|
334
372
|
bookmark,
|
|
335
373
|
} = request.query;
|
|
336
374
|
|
|
375
|
+
// Fetch syndication targets if user is authenticated
|
|
376
|
+
const token = request.session?.access_token;
|
|
377
|
+
const syndicationTargets = token
|
|
378
|
+
? await getSyndicationTargets(application, token)
|
|
379
|
+
: [];
|
|
380
|
+
|
|
337
381
|
response.render("compose", {
|
|
338
382
|
title: request.__("microsub.compose.title"),
|
|
339
383
|
replyTo: ensureString(replyTo || reply),
|
|
340
384
|
likeOf: ensureString(likeOf || like),
|
|
341
385
|
repostOf: ensureString(repostOf || repost),
|
|
342
386
|
bookmarkOf: ensureString(bookmarkOf || bookmark),
|
|
387
|
+
syndicationTargets,
|
|
343
388
|
baseUrl: request.baseUrl,
|
|
344
389
|
});
|
|
345
390
|
}
|
|
@@ -357,6 +402,7 @@ export async function submitCompose(request, response) {
|
|
|
357
402
|
const likeOf = request.body["like-of"];
|
|
358
403
|
const repostOf = request.body["repost-of"];
|
|
359
404
|
const bookmarkOf = request.body["bookmark-of"];
|
|
405
|
+
const syndicateTo = request.body["mp-syndicate-to"];
|
|
360
406
|
|
|
361
407
|
// Debug logging
|
|
362
408
|
console.info(
|
|
@@ -369,6 +415,7 @@ export async function submitCompose(request, response) {
|
|
|
369
415
|
likeOf,
|
|
370
416
|
repostOf,
|
|
371
417
|
bookmarkOf,
|
|
418
|
+
syndicateTo,
|
|
372
419
|
});
|
|
373
420
|
|
|
374
421
|
// Get Micropub endpoint
|
|
@@ -396,16 +443,22 @@ export async function submitCompose(request, response) {
|
|
|
396
443
|
micropubData.append("h", "entry");
|
|
397
444
|
|
|
398
445
|
if (likeOf) {
|
|
399
|
-
// Like post
|
|
446
|
+
// Like post - content is optional comment
|
|
400
447
|
micropubData.append("like-of", likeOf);
|
|
448
|
+
if (content && content.trim()) {
|
|
449
|
+
micropubData.append("content", content.trim());
|
|
450
|
+
}
|
|
401
451
|
} else if (repostOf) {
|
|
402
|
-
// Repost
|
|
452
|
+
// Repost - content is optional comment
|
|
403
453
|
micropubData.append("repost-of", repostOf);
|
|
454
|
+
if (content && content.trim()) {
|
|
455
|
+
micropubData.append("content", content.trim());
|
|
456
|
+
}
|
|
404
457
|
} else if (bookmarkOf) {
|
|
405
|
-
// Bookmark
|
|
458
|
+
// Bookmark - content is optional comment
|
|
406
459
|
micropubData.append("bookmark-of", bookmarkOf);
|
|
407
|
-
if (content) {
|
|
408
|
-
micropubData.append("content", content);
|
|
460
|
+
if (content && content.trim()) {
|
|
461
|
+
micropubData.append("content", content.trim());
|
|
409
462
|
}
|
|
410
463
|
} else if (inReplyTo) {
|
|
411
464
|
// Reply
|
|
@@ -416,6 +469,14 @@ export async function submitCompose(request, response) {
|
|
|
416
469
|
micropubData.append("content", content || "");
|
|
417
470
|
}
|
|
418
471
|
|
|
472
|
+
// Add syndication targets
|
|
473
|
+
if (syndicateTo) {
|
|
474
|
+
const targets = Array.isArray(syndicateTo) ? syndicateTo : [syndicateTo];
|
|
475
|
+
for (const target of targets) {
|
|
476
|
+
micropubData.append("mp-syndicate-to", target);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
419
480
|
// Debug: log what we're sending
|
|
420
481
|
console.info("[Microsub] Sending to Micropub:", {
|
|
421
482
|
url: micropubUrl,
|
|
@@ -565,6 +626,33 @@ export async function subscribe(request, response) {
|
|
|
565
626
|
response.redirect(`${request.baseUrl}/channels/${channelUid}/feeds`);
|
|
566
627
|
}
|
|
567
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Mark all items in channel as read
|
|
631
|
+
* @param {object} request - Express request
|
|
632
|
+
* @param {object} response - Express response
|
|
633
|
+
* @returns {Promise<void>}
|
|
634
|
+
*/
|
|
635
|
+
export async function markAllRead(request, response) {
|
|
636
|
+
const { application } = request.app.locals;
|
|
637
|
+
const userId = getUserId(request);
|
|
638
|
+
const { channel: channelUid } = request.body;
|
|
639
|
+
|
|
640
|
+
const channelDocument = await getChannel(application, channelUid, userId);
|
|
641
|
+
if (!channelDocument) {
|
|
642
|
+
return response.status(404).render("404");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Mark all items as read using the special "last-read-entry" value
|
|
646
|
+
await markItemsRead(
|
|
647
|
+
application,
|
|
648
|
+
channelDocument._id,
|
|
649
|
+
["last-read-entry"],
|
|
650
|
+
userId,
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
response.redirect(`${request.baseUrl}/channels/${channelUid}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
568
656
|
export const readerController = {
|
|
569
657
|
index,
|
|
570
658
|
channels,
|
|
@@ -573,6 +661,7 @@ export const readerController = {
|
|
|
573
661
|
channel,
|
|
574
662
|
settings,
|
|
575
663
|
updateSettings,
|
|
664
|
+
markAllRead,
|
|
576
665
|
deleteChannel: deleteChannelAction,
|
|
577
666
|
feeds,
|
|
578
667
|
addFeed,
|
package/locales/en.json
CHANGED
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"compose": {
|
|
44
44
|
"title": "Compose",
|
|
45
45
|
"content": "What's on your mind?",
|
|
46
|
+
"comment": "Add a comment (optional)",
|
|
47
|
+
"commentHint": "Your comment will be included when this is syndicated",
|
|
48
|
+
"syndicateTo": "Syndicate to",
|
|
49
|
+
"syndicateHint": "Select where to cross-post this",
|
|
46
50
|
"submit": "Post",
|
|
47
51
|
"cancel": "Cancel",
|
|
48
52
|
"replyTo": "Replying to",
|
package/package.json
CHANGED
package/views/compose.njk
CHANGED
|
@@ -60,17 +60,30 @@
|
|
|
60
60
|
|
|
61
61
|
{% set isAction = likeOf or repostOf or bookmarkOf %}
|
|
62
62
|
|
|
63
|
-
{% if not isAction %}
|
|
64
63
|
{{ textarea({
|
|
65
|
-
label: __("microsub.compose.content"),
|
|
64
|
+
label: __("microsub.compose.content") if not isAction else __("microsub.compose.comment"),
|
|
66
65
|
id: "content",
|
|
67
66
|
name: "content",
|
|
68
|
-
rows: 5,
|
|
69
|
-
attributes: { autofocus: true }
|
|
67
|
+
rows: 5 if not isAction else 3,
|
|
68
|
+
attributes: { autofocus: true },
|
|
69
|
+
hint: __("microsub.compose.commentHint") if isAction else false
|
|
70
70
|
}) }}
|
|
71
71
|
<div class="compose__counter">
|
|
72
72
|
<span id="char-count">0</span> characters
|
|
73
73
|
</div>
|
|
74
|
+
|
|
75
|
+
{# Syndication targets #}
|
|
76
|
+
{% if syndicationTargets and syndicationTargets.length %}
|
|
77
|
+
<fieldset class="compose__syndication">
|
|
78
|
+
<legend>{{ __("microsub.compose.syndicateTo") }}</legend>
|
|
79
|
+
<p class="hint">{{ __("microsub.compose.syndicateHint") }}</p>
|
|
80
|
+
{% for target in syndicationTargets %}
|
|
81
|
+
<label class="syndication-target">
|
|
82
|
+
<input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}"{% if target.checked %} checked{% endif %}>
|
|
83
|
+
<span class="syndication-target__name">{{ target.name }}</span>
|
|
84
|
+
</label>
|
|
85
|
+
{% endfor %}
|
|
86
|
+
</fieldset>
|
|
74
87
|
{% endif %}
|
|
75
88
|
|
|
76
89
|
<div class="button-group">
|
|
@@ -84,7 +97,6 @@
|
|
|
84
97
|
</form>
|
|
85
98
|
</div>
|
|
86
99
|
|
|
87
|
-
{% if not isAction %}
|
|
88
100
|
<script type="module">
|
|
89
101
|
const textarea = document.getElementById('content');
|
|
90
102
|
const counter = document.getElementById('char-count');
|
|
@@ -94,5 +106,4 @@
|
|
|
94
106
|
});
|
|
95
107
|
}
|
|
96
108
|
</script>
|
|
97
|
-
{% endif %}
|
|
98
109
|
{% endblock %}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
<a href="{{ baseUrl }}/compose?repostOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.repost') }}">
|
|
10
10
|
{{ icon("repost") }}
|
|
11
11
|
</a>
|
|
12
|
+
<a href="{{ baseUrl }}/compose?bookmarkOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.bookmark') }}">
|
|
13
|
+
{{ icon("bookmark") }}
|
|
14
|
+
</a>
|
|
12
15
|
<a href="{{ itemUrl }}" class="item-actions__button" target="_blank" rel="noopener" title="{{ __('microsub.item.viewOriginal') }}">
|
|
13
16
|
{{ icon("public") }}
|
|
14
17
|
</a>
|