@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 +6 -0
- package/lib/controllers/reader.js +95 -0
- package/locales/en.json +4 -2
- package/package.json +1 -1
- package/views/channel-new.njk +1 -1
- package/views/channel.njk +7 -4
- package/views/compose.njk +2 -2
- package/views/feeds.njk +58 -0
- package/views/item.njk +4 -4
- package/views/partials/actions.njk +2 -2
- package/views/partials/item-card.njk +2 -2
- package/views/reader.njk +2 -2
- package/views/settings.njk +1 -1
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": "
|
|
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
package/views/channel-new.njk
CHANGED
|
@@ -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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
17
|
+
{{ icon("like") }} {{ __("microsub.compose.likeOf") }}: <a href="{{ likeOf }}">{{ likeOf }}</a>
|
|
18
18
|
</div>
|
|
19
19
|
{% endif %}
|
|
20
20
|
|
package/views/feeds.njk
ADDED
|
@@ -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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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"
|
|
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("
|
|
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("
|
|
25
|
+
{{ icon("createPost") }} {{ __("microsub.channels.new") }}
|
|
26
26
|
</a>
|
|
27
27
|
</p>
|
|
28
28
|
{% else %}
|
package/views/settings.njk
CHANGED
|
@@ -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("
|
|
6
|
+
{{ icon("previous") }} {{ channel.name }}
|
|
7
7
|
</a>
|
|
8
8
|
|
|
9
9
|
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/settings">
|