@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 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 { getTimelineItems, getItemById } from "../storage/items.js";
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 (no content needed)
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 (no content needed)
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 (content optional)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.13",
3
+ "version": "1.0.16",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",
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>