@rmdes/indiekit-endpoint-microsub 1.0.0-beta.7 → 1.0.0-beta.8
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 +3 -0
- package/lib/controllers/reader.js +87 -0
- package/package.json +1 -1
- package/views/reader.njk +3 -0
- package/views/search.njk +58 -0
package/index.js
CHANGED
|
@@ -87,6 +87,9 @@ export default class MicrosubEndpoint {
|
|
|
87
87
|
readerRouter.get("/item/:id", readerController.item);
|
|
88
88
|
readerRouter.get("/compose", readerController.compose);
|
|
89
89
|
readerRouter.post("/compose", readerController.submitCompose);
|
|
90
|
+
readerRouter.get("/search", readerController.searchPage);
|
|
91
|
+
readerRouter.post("/search", readerController.searchFeeds);
|
|
92
|
+
readerRouter.post("/subscribe", readerController.subscribe);
|
|
90
93
|
router.use("/reader", readerRouter);
|
|
91
94
|
|
|
92
95
|
return router;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @module controllers/reader
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { discoverFeedsFromUrl } from "../feeds/fetcher.js";
|
|
6
7
|
import { refreshFeedNow } from "../polling/scheduler.js";
|
|
7
8
|
import {
|
|
8
9
|
getChannels,
|
|
@@ -302,6 +303,89 @@ export async function submitCompose(request, response) {
|
|
|
302
303
|
response.redirect(`${request.baseUrl}/channels`);
|
|
303
304
|
}
|
|
304
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Search/discover feeds page
|
|
308
|
+
* @param {object} request - Express request
|
|
309
|
+
* @param {object} response - Express response
|
|
310
|
+
* @returns {Promise<void>}
|
|
311
|
+
*/
|
|
312
|
+
export async function searchPage(request, response) {
|
|
313
|
+
const { application } = request.app.locals;
|
|
314
|
+
const userId = request.session?.userId;
|
|
315
|
+
|
|
316
|
+
const channelList = await getChannels(application, userId);
|
|
317
|
+
|
|
318
|
+
response.render("search", {
|
|
319
|
+
title: request.__("microsub.search.title"),
|
|
320
|
+
channels: channelList,
|
|
321
|
+
baseUrl: request.baseUrl,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Search for feeds from URL
|
|
327
|
+
* @param {object} request - Express request
|
|
328
|
+
* @param {object} response - Express response
|
|
329
|
+
* @returns {Promise<void>}
|
|
330
|
+
*/
|
|
331
|
+
export async function searchFeeds(request, response) {
|
|
332
|
+
const { application } = request.app.locals;
|
|
333
|
+
const userId = request.session?.userId;
|
|
334
|
+
const { query } = request.body;
|
|
335
|
+
|
|
336
|
+
const channelList = await getChannels(application, userId);
|
|
337
|
+
|
|
338
|
+
let results = [];
|
|
339
|
+
if (query) {
|
|
340
|
+
try {
|
|
341
|
+
results = await discoverFeedsFromUrl(query);
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore discovery errors
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
response.render("search", {
|
|
348
|
+
title: request.__("microsub.search.title"),
|
|
349
|
+
channels: channelList,
|
|
350
|
+
query,
|
|
351
|
+
results,
|
|
352
|
+
searched: true,
|
|
353
|
+
baseUrl: request.baseUrl,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Subscribe to a feed from search results
|
|
359
|
+
* @param {object} request - Express request
|
|
360
|
+
* @param {object} response - Express response
|
|
361
|
+
* @returns {Promise<void>}
|
|
362
|
+
*/
|
|
363
|
+
export async function subscribe(request, response) {
|
|
364
|
+
const { application } = request.app.locals;
|
|
365
|
+
const userId = request.session?.userId;
|
|
366
|
+
const { url, channel: channelUid } = request.body;
|
|
367
|
+
|
|
368
|
+
const channelDocument = await getChannel(application, channelUid, userId);
|
|
369
|
+
if (!channelDocument) {
|
|
370
|
+
return response.status(404).render("404");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Create feed subscription
|
|
374
|
+
const feed = await createFeed(application, {
|
|
375
|
+
channelId: channelDocument._id,
|
|
376
|
+
url,
|
|
377
|
+
title: undefined,
|
|
378
|
+
photo: undefined,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Trigger immediate fetch in background
|
|
382
|
+
refreshFeedNow(application, feed._id).catch((error) => {
|
|
383
|
+
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
response.redirect(`${request.baseUrl}/channels/${channelUid}/feeds`);
|
|
387
|
+
}
|
|
388
|
+
|
|
305
389
|
export const readerController = {
|
|
306
390
|
index,
|
|
307
391
|
channels,
|
|
@@ -316,4 +400,7 @@ export const readerController = {
|
|
|
316
400
|
item,
|
|
317
401
|
compose,
|
|
318
402
|
submitCompose,
|
|
403
|
+
searchPage,
|
|
404
|
+
searchFeeds,
|
|
405
|
+
subscribe,
|
|
319
406
|
};
|
package/package.json
CHANGED
package/views/reader.njk
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
{% endfor %}
|
|
22
22
|
</ul>
|
|
23
23
|
<p class="reader__actions">
|
|
24
|
+
<a href="{{ baseUrl }}/search" class="button button--primary">
|
|
25
|
+
{{ icon("syndicate") }} {{ __("microsub.feeds.follow") }}
|
|
26
|
+
</a>
|
|
24
27
|
<a href="{{ baseUrl }}/channels/new" class="button button--secondary">
|
|
25
28
|
{{ icon("createPost") }} {{ __("microsub.channels.new") }}
|
|
26
29
|
</a>
|
package/views/search.njk
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="search">
|
|
5
|
+
<a href="{{ baseUrl }}/channels" class="back-link">
|
|
6
|
+
{{ icon("previous") }} {{ __("microsub.channels.title") }}
|
|
7
|
+
</a>
|
|
8
|
+
|
|
9
|
+
<h2>{{ __("microsub.search.title") }}</h2>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="{{ baseUrl }}/search" class="search__form">
|
|
12
|
+
{{ input({
|
|
13
|
+
id: "query",
|
|
14
|
+
name: "query",
|
|
15
|
+
label: __("microsub.search.placeholder"),
|
|
16
|
+
type: "url",
|
|
17
|
+
required: true,
|
|
18
|
+
placeholder: "https://example.com",
|
|
19
|
+
autocomplete: "off",
|
|
20
|
+
value: query,
|
|
21
|
+
attributes: { autofocus: true }
|
|
22
|
+
}) }}
|
|
23
|
+
<div class="button-group">
|
|
24
|
+
{{ button({ text: __("microsub.search.submit") }) }}
|
|
25
|
+
</div>
|
|
26
|
+
</form>
|
|
27
|
+
|
|
28
|
+
{% if results and results.length > 0 %}
|
|
29
|
+
<div class="search__results">
|
|
30
|
+
<h3>{{ __("microsub.search.title") }}</h3>
|
|
31
|
+
<ul class="search__list">
|
|
32
|
+
{% for result in results %}
|
|
33
|
+
<li class="search__item">
|
|
34
|
+
<div class="search__feed">
|
|
35
|
+
<span class="search__url">{{ result.url }}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<form method="post" action="{{ baseUrl }}/subscribe" class="search__subscribe">
|
|
38
|
+
<input type="hidden" name="url" value="{{ result.url }}">
|
|
39
|
+
<label for="channel-{{ loop.index }}" class="visually-hidden">{{ __("microsub.channels.title") }}</label>
|
|
40
|
+
<select name="channel" id="channel-{{ loop.index }}" class="select select--small">
|
|
41
|
+
{% for channel in channels %}
|
|
42
|
+
<option value="{{ channel.uid }}">{{ channel.name }}</option>
|
|
43
|
+
{% endfor %}
|
|
44
|
+
</select>
|
|
45
|
+
{{ button({
|
|
46
|
+
text: __("microsub.feeds.follow"),
|
|
47
|
+
classes: "button--small"
|
|
48
|
+
}) }}
|
|
49
|
+
</form>
|
|
50
|
+
</li>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</ul>
|
|
53
|
+
</div>
|
|
54
|
+
{% elif searched %}
|
|
55
|
+
{{ prose({ text: __("microsub.search.noResults") }) }}
|
|
56
|
+
{% endif %}
|
|
57
|
+
</div>
|
|
58
|
+
{% endblock %}
|