@rmdes/indiekit-endpoint-microsub 1.0.0-beta.1 → 1.0.0-beta.10
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 +18 -0
- package/lib/cache/redis.js +53 -5
- package/lib/controllers/microsub.js +6 -0
- package/lib/controllers/reader.js +188 -0
- package/lib/search/query.js +2 -1
- package/lib/storage/channels.js +4 -2
- package/lib/storage/items.js +4 -2
- package/lib/webmention/processor.js +3 -3
- package/locales/en.json +5 -2
- package/package.json +1 -1
- package/views/channel-new.njk +11 -15
- package/views/channel.njk +9 -6
- package/views/compose.njk +7 -10
- package/views/feeds.njk +58 -0
- package/views/item.njk +8 -8
- package/views/partials/actions.njk +5 -5
- package/views/partials/item-card.njk +3 -3
- package/views/reader.njk +8 -5
- package/views/search.njk +58 -0
- package/views/settings.njk +27 -49
package/index.js
CHANGED
|
@@ -78,9 +78,18 @@ 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);
|
|
90
|
+
readerRouter.get("/search", readerController.searchPage);
|
|
91
|
+
readerRouter.post("/search", readerController.searchFeeds);
|
|
92
|
+
readerRouter.post("/subscribe", readerController.subscribe);
|
|
84
93
|
router.use("/reader", readerRouter);
|
|
85
94
|
|
|
86
95
|
return router;
|
|
@@ -108,6 +117,8 @@ export default class MicrosubEndpoint {
|
|
|
108
117
|
* @param {object} indiekit - Indiekit instance
|
|
109
118
|
*/
|
|
110
119
|
init(indiekit) {
|
|
120
|
+
console.info("[Microsub] Initializing endpoint-microsub plugin");
|
|
121
|
+
|
|
111
122
|
// Register MongoDB collections
|
|
112
123
|
indiekit.addCollection("microsub_channels");
|
|
113
124
|
indiekit.addCollection("microsub_feeds");
|
|
@@ -116,6 +127,8 @@ export default class MicrosubEndpoint {
|
|
|
116
127
|
indiekit.addCollection("microsub_muted");
|
|
117
128
|
indiekit.addCollection("microsub_blocked");
|
|
118
129
|
|
|
130
|
+
console.info("[Microsub] Registered MongoDB collections");
|
|
131
|
+
|
|
119
132
|
// Register endpoint
|
|
120
133
|
indiekit.addEndpoint(this);
|
|
121
134
|
|
|
@@ -127,7 +140,12 @@ export default class MicrosubEndpoint {
|
|
|
127
140
|
// Start feed polling scheduler when server starts
|
|
128
141
|
// This will be called after the server is ready
|
|
129
142
|
if (indiekit.database) {
|
|
143
|
+
console.info("[Microsub] Database available, starting scheduler");
|
|
130
144
|
startScheduler(indiekit);
|
|
145
|
+
} else {
|
|
146
|
+
console.warn(
|
|
147
|
+
"[Microsub] Database not available at init, scheduler not started",
|
|
148
|
+
);
|
|
131
149
|
}
|
|
132
150
|
}
|
|
133
151
|
|
package/lib/cache/redis.js
CHANGED
|
@@ -3,22 +3,56 @@
|
|
|
3
3
|
* @module cache/redis
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import Redis from "ioredis";
|
|
7
|
+
|
|
8
|
+
let redisClient;
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Get Redis client from application
|
|
8
12
|
* @param {object} application - Indiekit application
|
|
9
13
|
* @returns {object|undefined} Redis client or undefined
|
|
10
14
|
*/
|
|
11
15
|
export function getRedisClient(application) {
|
|
12
|
-
// Check if Redis is
|
|
16
|
+
// Check if Redis is already initialized on the application
|
|
13
17
|
if (application.redis) {
|
|
14
18
|
return application.redis;
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
// Check if we already created a client
|
|
22
|
+
if (redisClient) {
|
|
23
|
+
return redisClient;
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
// Check for Redis URL in config
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
const redisUrl = application.config?.application?.redisUrl;
|
|
28
|
+
if (redisUrl) {
|
|
29
|
+
try {
|
|
30
|
+
redisClient = new Redis(redisUrl, {
|
|
31
|
+
maxRetriesPerRequest: 3,
|
|
32
|
+
retryStrategy(times) {
|
|
33
|
+
const delay = Math.min(times * 50, 2000);
|
|
34
|
+
return delay;
|
|
35
|
+
},
|
|
36
|
+
lazyConnect: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
redisClient.on("error", (error) => {
|
|
40
|
+
console.error("[Microsub] Redis error:", error.message);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
redisClient.on("connect", () => {
|
|
44
|
+
console.info("[Microsub] Redis connected");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Connect asynchronously
|
|
48
|
+
redisClient.connect().catch((error) => {
|
|
49
|
+
console.warn("[Microsub] Redis connection failed:", error.message);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return redisClient;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn("[Microsub] Failed to initialize Redis:", error.message);
|
|
55
|
+
}
|
|
22
56
|
}
|
|
23
57
|
}
|
|
24
58
|
|
|
@@ -131,3 +165,17 @@ export async function subscribeToChannel(redis, channel, callback) {
|
|
|
131
165
|
// Ignore subscription errors
|
|
132
166
|
}
|
|
133
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cleanup Redis connection on shutdown
|
|
171
|
+
*/
|
|
172
|
+
export async function closeRedis() {
|
|
173
|
+
if (redisClient) {
|
|
174
|
+
try {
|
|
175
|
+
await redisClient.quit();
|
|
176
|
+
redisClient = undefined;
|
|
177
|
+
} catch {
|
|
178
|
+
// Ignore cleanup errors
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -25,6 +25,12 @@ import { get as getTimeline, action as timelineAction } from "./timeline.js";
|
|
|
25
25
|
export async function get(request, response, next) {
|
|
26
26
|
try {
|
|
27
27
|
const { action } = request.query;
|
|
28
|
+
|
|
29
|
+
// If no action provided, redirect to reader UI
|
|
30
|
+
if (!action) {
|
|
31
|
+
return response.redirect(request.baseUrl + "/reader");
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
validateAction(action);
|
|
29
35
|
|
|
30
36
|
switch (action) {
|
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
* @module controllers/reader
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { discoverFeedsFromUrl } from "../feeds/fetcher.js";
|
|
7
|
+
import { refreshFeedNow } from "../polling/scheduler.js";
|
|
6
8
|
import {
|
|
7
9
|
getChannels,
|
|
8
10
|
getChannel,
|
|
9
11
|
createChannel,
|
|
10
12
|
updateChannelSettings,
|
|
11
13
|
} from "../storage/channels.js";
|
|
14
|
+
import {
|
|
15
|
+
getFeedsForChannel,
|
|
16
|
+
createFeed,
|
|
17
|
+
deleteFeed,
|
|
18
|
+
} from "../storage/feeds.js";
|
|
12
19
|
import { getTimelineItems, getItemById } from "../storage/items.js";
|
|
13
20
|
import {
|
|
14
21
|
validateChannelName,
|
|
@@ -39,6 +46,7 @@ export async function channels(request, response) {
|
|
|
39
46
|
response.render("reader", {
|
|
40
47
|
title: request.__("microsub.reader.title"),
|
|
41
48
|
channels: channelList,
|
|
49
|
+
baseUrl: request.baseUrl,
|
|
42
50
|
});
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -50,6 +58,7 @@ export async function channels(request, response) {
|
|
|
50
58
|
export async function newChannel(request, response) {
|
|
51
59
|
response.render("channel-new", {
|
|
52
60
|
title: request.__("microsub.channels.new"),
|
|
61
|
+
baseUrl: request.baseUrl,
|
|
53
62
|
});
|
|
54
63
|
}
|
|
55
64
|
|
|
@@ -74,6 +83,7 @@ export async function createChannelAction(request, response) {
|
|
|
74
83
|
* View channel timeline
|
|
75
84
|
* @param {object} request - Express request
|
|
76
85
|
* @param {object} response - Express response
|
|
86
|
+
* @returns {Promise<void>}
|
|
77
87
|
*/
|
|
78
88
|
export async function channel(request, response) {
|
|
79
89
|
const { application } = request.app.locals;
|
|
@@ -97,6 +107,7 @@ export async function channel(request, response) {
|
|
|
97
107
|
channel: channelDocument,
|
|
98
108
|
items: timeline.items,
|
|
99
109
|
paging: timeline.paging,
|
|
110
|
+
baseUrl: request.baseUrl,
|
|
100
111
|
});
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -104,6 +115,7 @@ export async function channel(request, response) {
|
|
|
104
115
|
* Channel settings form
|
|
105
116
|
* @param {object} request - Express request
|
|
106
117
|
* @param {object} response - Express response
|
|
118
|
+
* @returns {Promise<void>}
|
|
107
119
|
*/
|
|
108
120
|
export async function settings(request, response) {
|
|
109
121
|
const { application } = request.app.locals;
|
|
@@ -120,6 +132,7 @@ export async function settings(request, response) {
|
|
|
120
132
|
channel: channelDocument.name,
|
|
121
133
|
}),
|
|
122
134
|
channel: channelDocument,
|
|
135
|
+
baseUrl: request.baseUrl,
|
|
123
136
|
});
|
|
124
137
|
}
|
|
125
138
|
|
|
@@ -127,6 +140,7 @@ export async function settings(request, response) {
|
|
|
127
140
|
* Update channel settings
|
|
128
141
|
* @param {object} request - Express request
|
|
129
142
|
* @param {object} response - Express response
|
|
143
|
+
* @returns {Promise<void>}
|
|
130
144
|
*/
|
|
131
145
|
export async function updateSettings(request, response) {
|
|
132
146
|
const { application } = request.app.locals;
|
|
@@ -157,10 +171,92 @@ export async function updateSettings(request, response) {
|
|
|
157
171
|
response.redirect(`${request.baseUrl}/channels/${uid}`);
|
|
158
172
|
}
|
|
159
173
|
|
|
174
|
+
/**
|
|
175
|
+
* View feeds for a channel
|
|
176
|
+
* @param {object} request - Express request
|
|
177
|
+
* @param {object} response - Express response
|
|
178
|
+
* @returns {Promise<void>}
|
|
179
|
+
*/
|
|
180
|
+
export async function feeds(request, response) {
|
|
181
|
+
const { application } = request.app.locals;
|
|
182
|
+
const userId = request.session?.userId;
|
|
183
|
+
const { uid } = request.params;
|
|
184
|
+
|
|
185
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
186
|
+
if (!channelDocument) {
|
|
187
|
+
return response.status(404).render("404");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const feedList = await getFeedsForChannel(application, channelDocument._id);
|
|
191
|
+
|
|
192
|
+
response.render("feeds", {
|
|
193
|
+
title: request.__("microsub.feeds.title"),
|
|
194
|
+
channel: channelDocument,
|
|
195
|
+
feeds: feedList,
|
|
196
|
+
baseUrl: request.baseUrl,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Add feed to channel
|
|
202
|
+
* @param {object} request - Express request
|
|
203
|
+
* @param {object} response - Express response
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
export async function addFeed(request, response) {
|
|
207
|
+
const { application } = request.app.locals;
|
|
208
|
+
const userId = request.session?.userId;
|
|
209
|
+
const { uid } = request.params;
|
|
210
|
+
const { url } = request.body;
|
|
211
|
+
|
|
212
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
213
|
+
if (!channelDocument) {
|
|
214
|
+
return response.status(404).render("404");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Create feed subscription
|
|
218
|
+
const feed = await createFeed(application, {
|
|
219
|
+
channelId: channelDocument._id,
|
|
220
|
+
url,
|
|
221
|
+
title: undefined,
|
|
222
|
+
photo: undefined,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Trigger immediate fetch in background
|
|
226
|
+
refreshFeedNow(application, feed._id).catch((error) => {
|
|
227
|
+
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Remove feed from channel
|
|
235
|
+
* @param {object} request - Express request
|
|
236
|
+
* @param {object} response - Express response
|
|
237
|
+
* @returns {Promise<void>}
|
|
238
|
+
*/
|
|
239
|
+
export async function removeFeed(request, response) {
|
|
240
|
+
const { application } = request.app.locals;
|
|
241
|
+
const userId = request.session?.userId;
|
|
242
|
+
const { uid } = request.params;
|
|
243
|
+
const { url } = request.body;
|
|
244
|
+
|
|
245
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
246
|
+
if (!channelDocument) {
|
|
247
|
+
return response.status(404).render("404");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await deleteFeed(application, channelDocument._id, url);
|
|
251
|
+
|
|
252
|
+
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
|
253
|
+
}
|
|
254
|
+
|
|
160
255
|
/**
|
|
161
256
|
* View single item
|
|
162
257
|
* @param {object} request - Express request
|
|
163
258
|
* @param {object} response - Express response
|
|
259
|
+
* @returns {Promise<void>}
|
|
164
260
|
*/
|
|
165
261
|
export async function item(request, response) {
|
|
166
262
|
const { application } = request.app.locals;
|
|
@@ -175,6 +271,7 @@ export async function item(request, response) {
|
|
|
175
271
|
response.render("item", {
|
|
176
272
|
title: itemDocument.name || "Item",
|
|
177
273
|
item: itemDocument,
|
|
274
|
+
baseUrl: request.baseUrl,
|
|
178
275
|
});
|
|
179
276
|
}
|
|
180
277
|
|
|
@@ -182,6 +279,7 @@ export async function item(request, response) {
|
|
|
182
279
|
* Compose response form
|
|
183
280
|
* @param {object} request - Express request
|
|
184
281
|
* @param {object} response - Express response
|
|
282
|
+
* @returns {Promise<void>}
|
|
185
283
|
*/
|
|
186
284
|
export async function compose(request, response) {
|
|
187
285
|
const { replyTo, likeOf, repostOf } = request.query;
|
|
@@ -191,6 +289,7 @@ export async function compose(request, response) {
|
|
|
191
289
|
replyTo,
|
|
192
290
|
likeOf,
|
|
193
291
|
repostOf,
|
|
292
|
+
baseUrl: request.baseUrl,
|
|
194
293
|
});
|
|
195
294
|
}
|
|
196
295
|
|
|
@@ -204,6 +303,89 @@ export async function submitCompose(request, response) {
|
|
|
204
303
|
response.redirect(`${request.baseUrl}/channels`);
|
|
205
304
|
}
|
|
206
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
|
+
|
|
207
389
|
export const readerController = {
|
|
208
390
|
index,
|
|
209
391
|
channels,
|
|
@@ -212,7 +394,13 @@ export const readerController = {
|
|
|
212
394
|
channel,
|
|
213
395
|
settings,
|
|
214
396
|
updateSettings,
|
|
397
|
+
feeds,
|
|
398
|
+
addFeed,
|
|
399
|
+
removeFeed,
|
|
215
400
|
item,
|
|
216
401
|
compose,
|
|
217
402
|
submitCompose,
|
|
403
|
+
searchPage,
|
|
404
|
+
searchFeeds,
|
|
405
|
+
subscribe,
|
|
218
406
|
};
|
package/lib/search/query.js
CHANGED
|
@@ -94,7 +94,8 @@ export async function searchItemsRegex(
|
|
|
94
94
|
{ "author.name": regex },
|
|
95
95
|
],
|
|
96
96
|
})
|
|
97
|
-
|
|
97
|
+
// eslint-disable-next-line unicorn/no-array-sort -- MongoDB cursor method, not Array#sort
|
|
98
|
+
.sort({ published: -1 })
|
|
98
99
|
.limit(limit)
|
|
99
100
|
.toArray();
|
|
100
101
|
|
package/lib/storage/channels.js
CHANGED
|
@@ -55,7 +55,8 @@ export async function createChannel(application, { name, userId }) {
|
|
|
55
55
|
// Get max order for user
|
|
56
56
|
const maxOrderResult = await collection
|
|
57
57
|
.find({ userId })
|
|
58
|
-
|
|
58
|
+
// eslint-disable-next-line unicorn/no-array-sort -- MongoDB cursor method, not Array#sort
|
|
59
|
+
.sort({ order: -1 })
|
|
59
60
|
.limit(1)
|
|
60
61
|
.toArray();
|
|
61
62
|
|
|
@@ -93,7 +94,8 @@ export async function getChannels(application, userId) {
|
|
|
93
94
|
const channels = await collection
|
|
94
95
|
// eslint-disable-next-line unicorn/no-array-callback-reference -- filter is MongoDB query object
|
|
95
96
|
.find(filter)
|
|
96
|
-
|
|
97
|
+
// eslint-disable-next-line unicorn/no-array-sort -- MongoDB cursor method, not Array#sort
|
|
98
|
+
.sort({ order: 1 })
|
|
97
99
|
.toArray();
|
|
98
100
|
|
|
99
101
|
// Get unread counts for each channel
|
package/lib/storage/items.js
CHANGED
|
@@ -99,7 +99,8 @@ export async function getTimelineItems(application, channelId, options = {}) {
|
|
|
99
99
|
const items = await collection
|
|
100
100
|
// eslint-disable-next-line unicorn/no-array-callback-reference -- query is MongoDB query object
|
|
101
101
|
.find(query)
|
|
102
|
-
|
|
102
|
+
// eslint-disable-next-line unicorn/no-array-sort -- MongoDB cursor method, not Array#sort
|
|
103
|
+
.sort(sort)
|
|
103
104
|
.limit(limit + 1)
|
|
104
105
|
.toArray();
|
|
105
106
|
|
|
@@ -370,7 +371,8 @@ export async function searchItems(application, channelId, query, limit = 20) {
|
|
|
370
371
|
{ summary: regex },
|
|
371
372
|
],
|
|
372
373
|
})
|
|
373
|
-
|
|
374
|
+
// eslint-disable-next-line unicorn/no-array-sort -- MongoDB cursor method, not Array#sort
|
|
375
|
+
.sort({ published: -1 })
|
|
374
376
|
.limit(limit)
|
|
375
377
|
.toArray();
|
|
376
378
|
|
|
@@ -127,13 +127,13 @@ export async function getNotifications(application, userId, options = {}) {
|
|
|
127
127
|
query.readBy = { $ne: userId };
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/* eslint-disable unicorn/no-array-callback-reference --
|
|
130
|
+
/* eslint-disable unicorn/no-array-callback-reference, unicorn/no-array-sort -- MongoDB cursor methods */
|
|
131
131
|
const notifications = await collection
|
|
132
132
|
.find(query)
|
|
133
|
-
.
|
|
133
|
+
.sort({ published: -1 })
|
|
134
134
|
.limit(limit)
|
|
135
135
|
.toArray();
|
|
136
|
-
/* eslint-enable unicorn/no-array-callback-reference */
|
|
136
|
+
/* eslint-enable unicorn/no-array-callback-reference, unicorn/no-array-sort */
|
|
137
137
|
|
|
138
138
|
return notifications.map((n) => transformNotification(n, userId));
|
|
139
139
|
}
|
package/locales/en.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"channels": {
|
|
11
11
|
"title": "Channels",
|
|
12
|
+
"name": "Channel name",
|
|
12
13
|
"new": "New channel",
|
|
13
14
|
"create": "Create channel",
|
|
14
15
|
"delete": "Delete channel",
|
|
@@ -27,7 +28,9 @@
|
|
|
27
28
|
"title": "Feeds",
|
|
28
29
|
"follow": "Follow",
|
|
29
30
|
"unfollow": "Unfollow",
|
|
30
|
-
"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"
|
|
31
34
|
},
|
|
32
35
|
"item": {
|
|
33
36
|
"reply": "Reply",
|
|
@@ -46,7 +49,7 @@
|
|
|
46
49
|
"repostOf": "Reposting"
|
|
47
50
|
},
|
|
48
51
|
"settings": {
|
|
49
|
-
"title": "
|
|
52
|
+
"title": "{{channel}} settings",
|
|
50
53
|
"excludeTypes": "Exclude interaction types",
|
|
51
54
|
"excludeTypesHelp": "Select types of posts to hide from this channel",
|
|
52
55
|
"excludeRegex": "Exclude pattern",
|
package/package.json
CHANGED
package/views/channel-new.njk
CHANGED
|
@@ -2,29 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
{% block content %}
|
|
4
4
|
<div class="channel-new">
|
|
5
|
-
<a href="{{
|
|
6
|
-
{{ icon("
|
|
5
|
+
<a href="{{ baseUrl }}/channels" class="back-link">
|
|
6
|
+
{{ icon("previous") }} {{ __("microsub.channels.title") }}
|
|
7
7
|
</a>
|
|
8
8
|
|
|
9
|
-
<form method="post" action="{{
|
|
10
|
-
{{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
required: true,
|
|
18
|
-
autocomplete: "off",
|
|
19
|
-
autofocus: true
|
|
20
|
-
}
|
|
9
|
+
<form method="post" action="{{ baseUrl }}/channels/new">
|
|
10
|
+
{{ input({
|
|
11
|
+
id: "name",
|
|
12
|
+
name: "name",
|
|
13
|
+
label: __("microsub.channels.name"),
|
|
14
|
+
required: true,
|
|
15
|
+
autocomplete: "off",
|
|
16
|
+
attributes: { autofocus: true }
|
|
21
17
|
}) }}
|
|
22
18
|
|
|
23
19
|
<div class="button-group">
|
|
24
20
|
{{ button({
|
|
25
21
|
text: __("microsub.channels.create")
|
|
26
22
|
}) }}
|
|
27
|
-
<a href="{{
|
|
23
|
+
<a href="{{ baseUrl }}/channels" class="button button--secondary">
|
|
28
24
|
{{ __("Cancel") }}
|
|
29
25
|
</a>
|
|
30
26
|
</div>
|
package/views/channel.njk
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
{% block content %}
|
|
4
4
|
<div class="channel">
|
|
5
5
|
<header class="channel__header">
|
|
6
|
-
<a href="{{
|
|
7
|
-
{{ icon("
|
|
6
|
+
<a href="{{ baseUrl }}/channels" class="back-link">
|
|
7
|
+
{{ icon("previous") }} {{ __("microsub.channels.title") }}
|
|
8
8
|
</a>
|
|
9
9
|
<div class="channel__actions">
|
|
10
|
-
<a href="{{
|
|
11
|
-
{{ icon("
|
|
10
|
+
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary button--small">
|
|
11
|
+
{{ icon("syndicate") }} {{ __("microsub.feeds.title") }}
|
|
12
|
+
</a>
|
|
13
|
+
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/settings" class="button button--secondary button--small">
|
|
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
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
{% block content %}
|
|
4
4
|
<div class="compose">
|
|
5
|
-
<a href="{{
|
|
6
|
-
{{ icon("
|
|
5
|
+
<a href="{{ baseUrl }}/channels" class="back-link">
|
|
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
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
</div>
|
|
25
25
|
{% endif %}
|
|
26
26
|
|
|
27
|
-
<form method="post" action="{{
|
|
27
|
+
<form method="post" action="{{ baseUrl }}/compose">
|
|
28
28
|
{% if replyTo %}
|
|
29
29
|
<input type="hidden" name="in-reply-to" value="{{ replyTo }}">
|
|
30
30
|
{% endif %}
|
|
@@ -37,14 +37,11 @@
|
|
|
37
37
|
|
|
38
38
|
{% if not likeOf and not repostOf %}
|
|
39
39
|
{{ textarea({
|
|
40
|
-
label:
|
|
41
|
-
text: __("microsub.compose.content"),
|
|
42
|
-
classes: "label--visuallyhidden"
|
|
43
|
-
},
|
|
40
|
+
label: __("microsub.compose.content"),
|
|
44
41
|
id: "content",
|
|
45
42
|
name: "content",
|
|
46
43
|
rows: 5,
|
|
47
|
-
autofocus: true
|
|
44
|
+
attributes: { autofocus: true }
|
|
48
45
|
}) }}
|
|
49
46
|
{% endif %}
|
|
50
47
|
|
|
@@ -52,7 +49,7 @@
|
|
|
52
49
|
{{ button({
|
|
53
50
|
text: __("microsub.compose.submit")
|
|
54
51
|
}) }}
|
|
55
|
-
<a href="{{
|
|
52
|
+
<a href="{{ baseUrl }}/channels" class="button button--secondary">
|
|
56
53
|
{{ __("microsub.compose.cancel") }}
|
|
57
54
|
</a>
|
|
58
55
|
</div>
|
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
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
{% block content %}
|
|
4
4
|
<article class="item">
|
|
5
|
-
<a href="{{
|
|
6
|
-
{{ icon("
|
|
5
|
+
<a href="{{ baseUrl }}/channels" class="back-link">
|
|
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>
|
|
@@ -68,17 +68,17 @@
|
|
|
68
68
|
{% endif %}
|
|
69
69
|
|
|
70
70
|
<footer class="item__actions">
|
|
71
|
-
<a href="{{
|
|
71
|
+
<a href="{{ baseUrl }}/compose?replyTo={{ item.url | urlencode }}" class="button button--secondary button--small">
|
|
72
72
|
{{ icon("reply") }} {{ __("microsub.item.reply") }}
|
|
73
73
|
</a>
|
|
74
|
-
<a href="{{
|
|
75
|
-
{{ icon("
|
|
74
|
+
<a href="{{ baseUrl }}/compose?likeOf={{ item.url | urlencode }}" class="button button--secondary button--small">
|
|
75
|
+
{{ icon("like") }} {{ __("microsub.item.like") }}
|
|
76
76
|
</a>
|
|
77
|
-
<a href="{{
|
|
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>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{# Item action buttons #}
|
|
2
2
|
<div class="item-actions">
|
|
3
|
-
<a href="{{
|
|
3
|
+
<a href="{{ baseUrl }}/compose?replyTo={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.reply') }}">
|
|
4
4
|
{{ icon("reply") }}
|
|
5
5
|
</a>
|
|
6
|
-
<a href="{{
|
|
7
|
-
{{ icon("
|
|
6
|
+
<a href="{{ baseUrl }}/compose?likeOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.like') }}">
|
|
7
|
+
{{ icon("like") }}
|
|
8
8
|
</a>
|
|
9
|
-
<a href="{{
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{# Item card for timeline display #}
|
|
2
2
|
<article class="item-card{% if item._is_read %} item-card--read{% endif %}">
|
|
3
|
-
<a href="{{
|
|
3
|
+
<a href="{{ baseUrl }}/item/{{ item.uid }}" class="item-card__link">
|
|
4
4
|
{% if item.author %}
|
|
5
5
|
<div class="item-card__author">
|
|
6
6
|
{% if item.author.photo %}
|
|
@@ -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
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<ul class="reader__channels">
|
|
7
7
|
{% for channel in channels %}
|
|
8
8
|
<li class="reader__channel">
|
|
9
|
-
<a href="{{
|
|
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>
|
|
@@ -21,14 +21,17 @@
|
|
|
21
21
|
{% endfor %}
|
|
22
22
|
</ul>
|
|
23
23
|
<p class="reader__actions">
|
|
24
|
-
<a href="{{
|
|
25
|
-
{{ icon("
|
|
24
|
+
<a href="{{ baseUrl }}/search" class="button button--primary">
|
|
25
|
+
{{ icon("syndicate") }} {{ __("microsub.feeds.follow") }}
|
|
26
|
+
</a>
|
|
27
|
+
<a href="{{ baseUrl }}/channels/new" class="button button--secondary">
|
|
28
|
+
{{ icon("createPost") }} {{ __("microsub.channels.new") }}
|
|
26
29
|
</a>
|
|
27
30
|
</p>
|
|
28
31
|
{% else %}
|
|
29
32
|
{{ prose({ text: __("microsub.channels.empty") }) }}
|
|
30
33
|
<p>
|
|
31
|
-
<a href="{{
|
|
34
|
+
<a href="{{ baseUrl }}/channels/new" class="button button--primary">
|
|
32
35
|
{{ __("microsub.channels.new") }}
|
|
33
36
|
</a>
|
|
34
37
|
</p>
|
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 %}
|
package/views/settings.njk
CHANGED
|
@@ -2,77 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
{% block content %}
|
|
4
4
|
<div class="settings">
|
|
5
|
-
<a href="{{
|
|
6
|
-
{{ icon("
|
|
5
|
+
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="back-link">
|
|
6
|
+
{{ icon("previous") }} {{ channel.name }}
|
|
7
7
|
</a>
|
|
8
8
|
|
|
9
|
-
<form method="post" action="{{
|
|
10
|
-
{{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
hint: {
|
|
16
|
-
text: __("microsub.settings.excludeTypesHelp")
|
|
9
|
+
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/settings">
|
|
10
|
+
{{ checkboxes({
|
|
11
|
+
name: "excludeTypes",
|
|
12
|
+
values: channel.settings.excludeTypes,
|
|
13
|
+
fieldset: {
|
|
14
|
+
legend: __("microsub.settings.excludeTypes")
|
|
17
15
|
},
|
|
16
|
+
hint: __("microsub.settings.excludeTypesHelp"),
|
|
18
17
|
items: [
|
|
19
18
|
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
value: "like",
|
|
23
|
-
text: __("microsub.settings.types.like"),
|
|
24
|
-
checked: channel.settings.excludeTypes | includes("like")
|
|
19
|
+
label: __("microsub.settings.types.like"),
|
|
20
|
+
value: "like"
|
|
25
21
|
},
|
|
26
22
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
value: "repost",
|
|
30
|
-
text: __("microsub.settings.types.repost"),
|
|
31
|
-
checked: channel.settings.excludeTypes | includes("repost")
|
|
23
|
+
label: __("microsub.settings.types.repost"),
|
|
24
|
+
value: "repost"
|
|
32
25
|
},
|
|
33
26
|
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
value: "bookmark",
|
|
37
|
-
text: __("microsub.settings.types.bookmark"),
|
|
38
|
-
checked: channel.settings.excludeTypes | includes("bookmark")
|
|
27
|
+
label: __("microsub.settings.types.bookmark"),
|
|
28
|
+
value: "bookmark"
|
|
39
29
|
},
|
|
40
30
|
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
value: "reply",
|
|
44
|
-
text: __("microsub.settings.types.reply"),
|
|
45
|
-
checked: channel.settings.excludeTypes | includes("reply")
|
|
31
|
+
label: __("microsub.settings.types.reply"),
|
|
32
|
+
value: "reply"
|
|
46
33
|
},
|
|
47
34
|
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
value: "checkin",
|
|
51
|
-
text: __("microsub.settings.types.checkin"),
|
|
52
|
-
checked: channel.settings.excludeTypes | includes("checkin")
|
|
35
|
+
label: __("microsub.settings.types.checkin"),
|
|
36
|
+
value: "checkin"
|
|
53
37
|
}
|
|
54
38
|
]
|
|
55
|
-
})
|
|
39
|
+
}) }}
|
|
56
40
|
|
|
57
|
-
{{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
hint:
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
input: {
|
|
65
|
-
id: "excludeRegex",
|
|
66
|
-
name: "excludeRegex",
|
|
67
|
-
value: channel.settings.excludeRegex
|
|
68
|
-
}
|
|
41
|
+
{{ input({
|
|
42
|
+
id: "excludeRegex",
|
|
43
|
+
name: "excludeRegex",
|
|
44
|
+
label: __("microsub.settings.excludeRegex"),
|
|
45
|
+
hint: __("microsub.settings.excludeRegexHelp"),
|
|
46
|
+
value: channel.settings.excludeRegex
|
|
69
47
|
}) }}
|
|
70
48
|
|
|
71
49
|
<div class="button-group">
|
|
72
50
|
{{ button({
|
|
73
51
|
text: __("microsub.settings.save")
|
|
74
52
|
}) }}
|
|
75
|
-
<a href="{{
|
|
53
|
+
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="button button--secondary">
|
|
76
54
|
{{ __("Cancel") }}
|
|
77
55
|
</a>
|
|
78
56
|
</div>
|