@rmdes/indiekit-endpoint-microsub 1.0.0-beta.5 → 1.0.0-beta.7

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
@@ -78,6 +78,12 @@ export default class MicrosubEndpoint {
78
78
  "/channels/:uid/settings",
79
79
  readerController.updateSettings,
80
80
  );
81
+ readerRouter.get("/channels/:uid/feeds", readerController.feeds);
82
+ readerRouter.post("/channels/:uid/feeds", readerController.addFeed);
83
+ readerRouter.post(
84
+ "/channels/:uid/feeds/remove",
85
+ readerController.removeFeed,
86
+ );
81
87
  readerRouter.get("/item/:id", readerController.item);
82
88
  readerRouter.get("/compose", readerController.compose);
83
89
  readerRouter.post("/compose", readerController.submitCompose);
@@ -3,12 +3,18 @@
3
3
  * @module controllers/reader
4
4
  */
5
5
 
6
+ import { refreshFeedNow } from "../polling/scheduler.js";
6
7
  import {
7
8
  getChannels,
8
9
  getChannel,
9
10
  createChannel,
10
11
  updateChannelSettings,
11
12
  } from "../storage/channels.js";
13
+ import {
14
+ getFeedsForChannel,
15
+ createFeed,
16
+ deleteFeed,
17
+ } from "../storage/feeds.js";
12
18
  import { getTimelineItems, getItemById } from "../storage/items.js";
13
19
  import {
14
20
  validateChannelName,
@@ -76,6 +82,7 @@ export async function createChannelAction(request, response) {
76
82
  * View channel timeline
77
83
  * @param {object} request - Express request
78
84
  * @param {object} response - Express response
85
+ * @returns {Promise<void>}
79
86
  */
80
87
  export async function channel(request, response) {
81
88
  const { application } = request.app.locals;
@@ -107,6 +114,7 @@ export async function channel(request, response) {
107
114
  * Channel settings form
108
115
  * @param {object} request - Express request
109
116
  * @param {object} response - Express response
117
+ * @returns {Promise<void>}
110
118
  */
111
119
  export async function settings(request, response) {
112
120
  const { application } = request.app.locals;
@@ -131,6 +139,7 @@ export async function settings(request, response) {
131
139
  * Update channel settings
132
140
  * @param {object} request - Express request
133
141
  * @param {object} response - Express response
142
+ * @returns {Promise<void>}
134
143
  */
135
144
  export async function updateSettings(request, response) {
136
145
  const { application } = request.app.locals;
@@ -161,10 +170,92 @@ export async function updateSettings(request, response) {
161
170
  response.redirect(`${request.baseUrl}/channels/${uid}`);
162
171
  }
163
172
 
173
+ /**
174
+ * View feeds for a channel
175
+ * @param {object} request - Express request
176
+ * @param {object} response - Express response
177
+ * @returns {Promise<void>}
178
+ */
179
+ export async function feeds(request, response) {
180
+ const { application } = request.app.locals;
181
+ const userId = request.session?.userId;
182
+ const { uid } = request.params;
183
+
184
+ const channelDocument = await getChannel(application, uid, userId);
185
+ if (!channelDocument) {
186
+ return response.status(404).render("404");
187
+ }
188
+
189
+ const feedList = await getFeedsForChannel(application, channelDocument._id);
190
+
191
+ response.render("feeds", {
192
+ title: request.__("microsub.feeds.title"),
193
+ channel: channelDocument,
194
+ feeds: feedList,
195
+ baseUrl: request.baseUrl,
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Add feed to channel
201
+ * @param {object} request - Express request
202
+ * @param {object} response - Express response
203
+ * @returns {Promise<void>}
204
+ */
205
+ export async function addFeed(request, response) {
206
+ const { application } = request.app.locals;
207
+ const userId = request.session?.userId;
208
+ const { uid } = request.params;
209
+ const { url } = request.body;
210
+
211
+ const channelDocument = await getChannel(application, uid, userId);
212
+ if (!channelDocument) {
213
+ return response.status(404).render("404");
214
+ }
215
+
216
+ // Create feed subscription
217
+ const feed = await createFeed(application, {
218
+ channelId: channelDocument._id,
219
+ url,
220
+ title: undefined,
221
+ photo: undefined,
222
+ });
223
+
224
+ // Trigger immediate fetch in background
225
+ refreshFeedNow(application, feed._id).catch((error) => {
226
+ console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
227
+ });
228
+
229
+ response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
230
+ }
231
+
232
+ /**
233
+ * Remove feed from channel
234
+ * @param {object} request - Express request
235
+ * @param {object} response - Express response
236
+ * @returns {Promise<void>}
237
+ */
238
+ export async function removeFeed(request, response) {
239
+ const { application } = request.app.locals;
240
+ const userId = request.session?.userId;
241
+ const { uid } = request.params;
242
+ const { url } = request.body;
243
+
244
+ const channelDocument = await getChannel(application, uid, userId);
245
+ if (!channelDocument) {
246
+ return response.status(404).render("404");
247
+ }
248
+
249
+ await deleteFeed(application, channelDocument._id, url);
250
+
251
+ response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
252
+ }
253
+
164
254
  /**
165
255
  * View single item
166
256
  * @param {object} request - Express request
167
257
  * @param {object} response - Express response
258
+ * @returns {Promise<void>}
168
259
  */
169
260
  export async function item(request, response) {
170
261
  const { application } = request.app.locals;
@@ -187,6 +278,7 @@ export async function item(request, response) {
187
278
  * Compose response form
188
279
  * @param {object} request - Express request
189
280
  * @param {object} response - Express response
281
+ * @returns {Promise<void>}
190
282
  */
191
283
  export async function compose(request, response) {
192
284
  const { replyTo, likeOf, repostOf } = request.query;
@@ -218,6 +310,9 @@ export const readerController = {
218
310
  channel,
219
311
  settings,
220
312
  updateSettings,
313
+ feeds,
314
+ addFeed,
315
+ removeFeed,
221
316
  item,
222
317
  compose,
223
318
  submitCompose,
package/locales/en.json CHANGED
@@ -28,7 +28,9 @@
28
28
  "title": "Feeds",
29
29
  "follow": "Follow",
30
30
  "unfollow": "Unfollow",
31
- "empty": "No feeds followed in this channel"
31
+ "empty": "No feeds followed in this channel",
32
+ "url": "Feed URL",
33
+ "urlPlaceholder": "https://example.com/feed.xml"
32
34
  },
33
35
  "item": {
34
36
  "reply": "Reply",
@@ -47,7 +49,7 @@
47
49
  "repostOf": "Reposting"
48
50
  },
49
51
  "settings": {
50
- "title": "%{channel} settings",
52
+ "title": "{{channel}} settings",
51
53
  "excludeTypes": "Exclude interaction types",
52
54
  "excludeTypesHelp": "Select types of posts to hide from this channel",
53
55
  "excludeRegex": "Exclude pattern",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.7",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -3,7 +3,7 @@
3
3
  {% block content %}
4
4
  <div class="channel-new">
5
5
  <a href="{{ baseUrl }}/channels" class="back-link">
6
- {{ icon("arrow-left") }} {{ __("microsub.channels.title") }}
6
+ {{ icon("previous") }} {{ __("microsub.channels.title") }}
7
7
  </a>
8
8
 
9
9
  <form method="post" action="{{ baseUrl }}/channels/new">
package/views/channel.njk CHANGED
@@ -4,11 +4,14 @@
4
4
  <div class="channel">
5
5
  <header class="channel__header">
6
6
  <a href="{{ baseUrl }}/channels" class="back-link">
7
- {{ icon("arrow-left") }} {{ __("microsub.channels.title") }}
7
+ {{ icon("previous") }} {{ __("microsub.channels.title") }}
8
8
  </a>
9
9
  <div class="channel__actions">
10
+ <a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary button--small">
11
+ {{ icon("syndicate") }} {{ __("microsub.feeds.title") }}
12
+ </a>
10
13
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}/settings" class="button button--secondary button--small">
11
- {{ icon("settings") }} {{ __("microsub.channels.settings") }}
14
+ {{ icon("updatePost") }} {{ __("microsub.channels.settings") }}
12
15
  </a>
13
16
  </div>
14
17
  </header>
@@ -24,12 +27,12 @@
24
27
  <nav class="timeline__paging" aria-label="Pagination">
25
28
  {% if paging.before %}
26
29
  <a href="?before={{ paging.before }}" class="button button--secondary">
27
- {{ icon("arrow-left") }} {{ __("microsub.reader.newer") }}
30
+ {{ icon("previous") }} {{ __("microsub.reader.newer") }}
28
31
  </a>
29
32
  {% endif %}
30
33
  {% if paging.after %}
31
34
  <a href="?after={{ paging.after }}" class="button button--secondary">
32
- {{ __("microsub.reader.older") }} {{ icon("arrow-right") }}
35
+ {{ __("microsub.reader.older") }} {{ icon("next") }}
33
36
  </a>
34
37
  {% endif %}
35
38
  </nav>
package/views/compose.njk CHANGED
@@ -3,7 +3,7 @@
3
3
  {% block content %}
4
4
  <div class="compose">
5
5
  <a href="{{ baseUrl }}/channels" class="back-link">
6
- {{ icon("arrow-left") }} {{ __("Back") }}
6
+ {{ icon("previous") }} {{ __("Back") }}
7
7
  </a>
8
8
 
9
9
  {% if replyTo %}
@@ -14,7 +14,7 @@
14
14
 
15
15
  {% if likeOf %}
16
16
  <div class="compose__context">
17
- {{ icon("heart") }} {{ __("microsub.compose.likeOf") }}: <a href="{{ likeOf }}">{{ likeOf }}</a>
17
+ {{ icon("like") }} {{ __("microsub.compose.likeOf") }}: <a href="{{ likeOf }}">{{ likeOf }}</a>
18
18
  </div>
19
19
  {% endif %}
20
20
 
@@ -0,0 +1,58 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ <div class="feeds">
5
+ <header class="feeds__header">
6
+ <a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="back-link">
7
+ {{ icon("previous") }} {{ channel.name }}
8
+ </a>
9
+ </header>
10
+
11
+ <h2>{{ __("microsub.feeds.title") }}</h2>
12
+
13
+ {% if feeds.length > 0 %}
14
+ <ul class="feeds__list">
15
+ {% for feed in feeds %}
16
+ <li class="feeds__item">
17
+ <div class="feeds__info">
18
+ {% if feed.photo %}
19
+ <img src="{{ feed.photo }}" alt="" class="feeds__photo" width="32" height="32" loading="lazy">
20
+ {% endif %}
21
+ <div class="feeds__details">
22
+ <span class="feeds__name">{{ feed.title or feed.url }}</span>
23
+ <a href="{{ feed.url }}" class="feeds__url" target="_blank" rel="noopener">{{ feed.url }}</a>
24
+ </div>
25
+ </div>
26
+ <form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/remove" class="feeds__actions">
27
+ <input type="hidden" name="url" value="{{ feed.url }}">
28
+ {{ button({
29
+ text: __("microsub.feeds.unfollow"),
30
+ classes: "button--secondary button--small"
31
+ }) }}
32
+ </form>
33
+ </li>
34
+ {% endfor %}
35
+ </ul>
36
+ {% else %}
37
+ {{ prose({ text: __("microsub.feeds.empty") }) }}
38
+ {% endif %}
39
+
40
+ <div class="feeds__add">
41
+ <h3>{{ __("microsub.feeds.follow") }}</h3>
42
+ <form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="feeds__form">
43
+ {{ input({
44
+ id: "url",
45
+ name: "url",
46
+ label: __("microsub.feeds.url"),
47
+ type: "url",
48
+ required: true,
49
+ placeholder: __("microsub.feeds.urlPlaceholder"),
50
+ autocomplete: "off"
51
+ }) }}
52
+ <div class="button-group">
53
+ {{ button({ text: __("microsub.feeds.follow") }) }}
54
+ </div>
55
+ </form>
56
+ </div>
57
+ </div>
58
+ {% endblock %}
package/views/item.njk CHANGED
@@ -3,7 +3,7 @@
3
3
  {% block content %}
4
4
  <article class="item">
5
5
  <a href="{{ baseUrl }}/channels" class="back-link">
6
- {{ icon("arrow-left") }} {{ __("Back") }}
6
+ {{ icon("previous") }} {{ __("Back") }}
7
7
  </a>
8
8
 
9
9
  {% if item.author %}
@@ -56,7 +56,7 @@
56
56
  <p>{{ icon("reply") }} {{ __("Reply to") }}: <a href="{{ item.inReplyTo[0] }}">{{ item.inReplyTo[0] }}</a></p>
57
57
  {% endif %}
58
58
  {% if item.likeOf %}
59
- <p>{{ icon("heart") }} {{ __("Liked") }}: <a href="{{ item.likeOf[0] }}">{{ item.likeOf[0] }}</a></p>
59
+ <p>{{ icon("like") }} {{ __("Liked") }}: <a href="{{ item.likeOf[0] }}">{{ item.likeOf[0] }}</a></p>
60
60
  {% endif %}
61
61
  {% if item.repostOf %}
62
62
  <p>{{ icon("repost") }} {{ __("Reposted") }}: <a href="{{ item.repostOf[0] }}">{{ item.repostOf[0] }}</a></p>
@@ -72,13 +72,13 @@
72
72
  {{ icon("reply") }} {{ __("microsub.item.reply") }}
73
73
  </a>
74
74
  <a href="{{ baseUrl }}/compose?likeOf={{ item.url | urlencode }}" class="button button--secondary button--small">
75
- {{ icon("heart") }} {{ __("microsub.item.like") }}
75
+ {{ icon("like") }} {{ __("microsub.item.like") }}
76
76
  </a>
77
77
  <a href="{{ baseUrl }}/compose?repostOf={{ item.url | urlencode }}" class="button button--secondary button--small">
78
78
  {{ icon("repost") }} {{ __("microsub.item.repost") }}
79
79
  </a>
80
80
  <a href="{{ item.url }}" class="button button--secondary button--small" target="_blank" rel="noopener">
81
- {{ icon("external") }} {{ __("microsub.item.viewOriginal") }}
81
+ {{ icon("public") }} {{ __("microsub.item.viewOriginal") }}
82
82
  </a>
83
83
  </footer>
84
84
  </article>
@@ -4,12 +4,12 @@
4
4
  {{ icon("reply") }}
5
5
  </a>
6
6
  <a href="{{ baseUrl }}/compose?likeOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.like') }}">
7
- {{ icon("heart") }}
7
+ {{ icon("like") }}
8
8
  </a>
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
12
  <a href="{{ itemUrl }}" class="item-actions__button" target="_blank" rel="noopener" title="{{ __('microsub.item.viewOriginal') }}">
13
- {{ icon("external") }}
13
+ {{ icon("public") }}
14
14
  </a>
15
15
  </div>
@@ -18,7 +18,7 @@
18
18
  {% if item._type and item._type !== "entry" %}
19
19
  <div class="item-card__type">
20
20
  {% if item._type === "like" %}
21
- {{ icon("heart") }} Liked
21
+ {{ icon("like") }} Liked
22
22
  {% elif item._type === "repost" %}
23
23
  {{ icon("repost") }} Reposted
24
24
  {% elif item._type === "reply" %}
@@ -58,7 +58,7 @@
58
58
  </time>
59
59
  {% endif %}
60
60
  {% if not item._is_read %}
61
- <span class="item-card__unread">{{ icon("dot") }}</span>
61
+ <span class="item-card__unread" aria-label="Unread">●</span>
62
62
  {% endif %}
63
63
  </footer>
64
64
  </a>
package/views/reader.njk CHANGED
@@ -9,7 +9,7 @@
9
9
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="reader__channel-link">
10
10
  <span class="reader__channel-name">
11
11
  {% if channel.uid === "notifications" %}
12
- {{ icon("bell") }}
12
+ {{ icon("mention") }}
13
13
  {% endif %}
14
14
  {{ channel.name }}
15
15
  </span>
@@ -22,7 +22,7 @@
22
22
  </ul>
23
23
  <p class="reader__actions">
24
24
  <a href="{{ baseUrl }}/channels/new" class="button button--secondary">
25
- {{ icon("plus") }} {{ __("microsub.channels.new") }}
25
+ {{ icon("createPost") }} {{ __("microsub.channels.new") }}
26
26
  </a>
27
27
  </p>
28
28
  {% else %}
@@ -3,7 +3,7 @@
3
3
  {% block content %}
4
4
  <div class="settings">
5
5
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="back-link">
6
- {{ icon("arrow-left") }} {{ channel.name }}
6
+ {{ icon("previous") }} {{ channel.name }}
7
7
  </a>
8
8
 
9
9
  <form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/settings">