@geekity/cms 0.29.0 → 0.30.0
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/README.md +84 -32
- package/dist/admin/store.d.ts +42 -15
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +71 -31
- package/dist/admin/store.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -3
- package/dist/cli.js.map +1 -1
- package/dist/comments/index.d.ts +2 -2
- package/dist/comments/index.d.ts.map +1 -1
- package/dist/comments/index.js +1 -1
- package/dist/comments/index.js.map +1 -1
- package/dist/comments/records.d.ts +32 -4
- package/dist/comments/records.d.ts.map +1 -1
- package/dist/comments/records.js +182 -9
- package/dist/comments/records.js.map +1 -1
- package/dist/comments/routes.js +3 -3
- package/dist/comments/routes.js.map +1 -1
- package/dist/federation/backfill.js +6 -4
- package/dist/federation/backfill.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +28 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +10 -2
- package/dist/plugin.js.map +1 -1
- package/dist/plugins/site.d.ts.map +1 -1
- package/dist/plugins/site.js +7 -1
- package/dist/plugins/site.js.map +1 -1
- package/dist/web/conversation.d.ts +47 -23
- package/dist/web/conversation.d.ts.map +1 -1
- package/dist/web/conversation.js +151 -120
- package/dist/web/conversation.js.map +1 -1
- package/dist/web/feed-source.d.ts +10 -7
- package/dist/web/feed-source.d.ts.map +1 -1
- package/dist/web/feed-source.js +21 -13
- package/dist/web/feed-source.js.map +1 -1
- package/dist/web/guids.d.ts +4 -16
- package/dist/web/guids.d.ts.map +1 -1
- package/dist/web/guids.js +5 -17
- package/dist/web/guids.js.map +1 -1
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +63 -53
- package/dist/web/routes.js.map +1 -1
- package/dist/webmention/receive.js +5 -5
- package/dist/webmention/receive.js.map +1 -1
- package/package.json +1 -1
- package/themes/default/README.md +41 -31
- package/themes/default/layouts/comment.njk +3 -3
- package/themes/default/partials/comment.njk +5 -5
package/dist/web/conversation.js
CHANGED
|
@@ -5,10 +5,10 @@ import { postLabel, postTypeOf } from '../content/post-type.js';
|
|
|
5
5
|
import { RSVP_LABELS } from '../content/rsvp.js';
|
|
6
6
|
import { readAllQuoteAuthorizations } from '../federation/quotes.js';
|
|
7
7
|
import { actorHandle, guessedName, quoteFrom, replyFrom, REPLY_ACTIVITY_TYPE, } from '../federation/replies.js';
|
|
8
|
-
import { authorContext, authorFeedHref, siteAuthorContext } from './authors.js';
|
|
8
|
+
import { authorContext, authorFeedHref, profileContext, siteAuthorContext } from './authors.js';
|
|
9
9
|
import { activityStreamsId, isListed, isServed, permalinkOfObjectId } from './documents.js';
|
|
10
|
-
import {
|
|
11
|
-
import { COMMENT_PAGE_PREFIX, commentPageHref, feedGuid
|
|
10
|
+
import { isKey, repliesFeedPath, repliesKey } from './feed-source.js';
|
|
11
|
+
import { COMMENT_PAGE_PREFIX, commentPageHref, feedGuid } from './guids.js';
|
|
12
12
|
import { absoluteUrl } from './negotiate.js';
|
|
13
13
|
import { sanitizeCommentHtml } from './sanitize.js';
|
|
14
14
|
const RSVP_GROUP_ORDER = ['yes', 'maybe', 'interested', 'no'];
|
|
@@ -76,7 +76,12 @@ function postConversation(context, document) {
|
|
|
76
76
|
function gather(context, document, seen = new Set([document.path])) {
|
|
77
77
|
const objectId = activityStreamsId(document, context.baseUrl);
|
|
78
78
|
const root = rootOf(context, document);
|
|
79
|
-
const
|
|
79
|
+
const stored = context.admin.listCommentsFor(document.slug);
|
|
80
|
+
const users = context.users();
|
|
81
|
+
// What was imported from the fediverse is walked from too: an answer to an
|
|
82
|
+
// imported note names the note, which no logged activity may carry.
|
|
83
|
+
const origins = stored.flatMap((comment) => comment.source === 'activitypub' && comment.origin !== undefined ? [comment.origin] : []);
|
|
84
|
+
const activities = objectId === undefined ? [] : activitiesAround(context.admin, [objectId, ...origins]);
|
|
80
85
|
const naming = authorNaming(context.admin);
|
|
81
86
|
const quotes = objectId === undefined
|
|
82
87
|
? []
|
|
@@ -88,7 +93,11 @@ function gather(context, document, seen = new Set([document.path])) {
|
|
|
88
93
|
const mentions = [...quotes];
|
|
89
94
|
const answers = [];
|
|
90
95
|
const held = new Map();
|
|
91
|
-
|
|
96
|
+
// One like per actor, so a like imported from the site this one replaced
|
|
97
|
+
// and the same person's like delivered here are one.
|
|
98
|
+
const reacted = new Set(stored.flatMap(({ kind, actor }) => actor === undefined || (kind !== 'like' && kind !== 'boost' && kind !== 'repost')
|
|
99
|
+
? []
|
|
100
|
+
: [`${kind === 'like' ? 'like' : 'boost'}\0${actor}`]));
|
|
92
101
|
const isEvent = postTypeOf(document) === 'event';
|
|
93
102
|
for (const activity of activities) {
|
|
94
103
|
const answer = isEvent ? RSVP_ACTIVITIES[activity.activityType] : undefined;
|
|
@@ -99,7 +108,7 @@ function gather(context, document, seen = new Set([document.path])) {
|
|
|
99
108
|
continue;
|
|
100
109
|
const author = naming(activity.actorId);
|
|
101
110
|
answers.push({
|
|
102
|
-
|
|
111
|
+
...unstored(activity.activityId ?? `${activity.actorId}#rsvp`),
|
|
103
112
|
source: 'activitypub',
|
|
104
113
|
kind: 'reply',
|
|
105
114
|
author,
|
|
@@ -119,17 +128,21 @@ function gather(context, document, seen = new Set([document.path])) {
|
|
|
119
128
|
// belongs to that reply's own conversation, wherever it is shown.
|
|
120
129
|
if (activity.objectId !== objectId)
|
|
121
130
|
continue;
|
|
122
|
-
// One like per actor, however many times it was delivered, and none
|
|
123
|
-
// all once its actor has taken it back.
|
|
131
|
+
// One like per actor, however many times it was delivered, and none
|
|
132
|
+
// at all once its actor has taken it back.
|
|
124
133
|
const key = `${kind}\0${activity.actorId}`;
|
|
125
134
|
if (reacted.has(key))
|
|
126
135
|
continue;
|
|
127
136
|
if (activity.activityId !== null && withdrawn.has(activity.activityId))
|
|
128
137
|
continue;
|
|
138
|
+
if (activity.activityId !== null &&
|
|
139
|
+
heldComment(context.admin, activity.activityId) !== undefined) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
129
142
|
reacted.add(key);
|
|
130
143
|
const author = naming(activity.actorId);
|
|
131
144
|
(kind === 'like' ? likes : boosts).push({
|
|
132
|
-
|
|
145
|
+
...unstored(activity.activityId ?? `${activity.actorId}#${kind}`),
|
|
133
146
|
source: 'activitypub',
|
|
134
147
|
kind,
|
|
135
148
|
author,
|
|
@@ -143,13 +156,14 @@ function gather(context, document, seen = new Set([document.path])) {
|
|
|
143
156
|
continue;
|
|
144
157
|
}
|
|
145
158
|
const reply = replyFrom(activity);
|
|
146
|
-
if (reply === undefined)
|
|
159
|
+
if (reply === undefined || heldComment(context.admin, reply.id) !== undefined)
|
|
147
160
|
continue;
|
|
148
|
-
|
|
161
|
+
const parent = heldComment(context.admin, reply.inReplyTo);
|
|
162
|
+
written.push(federatedInteraction(parent === undefined ? reply : { ...reply, inReplyTo: parent.id }, naming(activity.actorId)));
|
|
149
163
|
}
|
|
150
|
-
for (const
|
|
151
|
-
const comment = interactionOf(
|
|
152
|
-
if (
|
|
164
|
+
for (const record of stored) {
|
|
165
|
+
const comment = interactionOf(record, document.permalink, context.baseUrl, users);
|
|
166
|
+
if (record.status !== 'approved') {
|
|
153
167
|
held.set(comment.id, comment);
|
|
154
168
|
continue;
|
|
155
169
|
}
|
|
@@ -219,7 +233,7 @@ function commentThread(context, id) {
|
|
|
219
233
|
};
|
|
220
234
|
}
|
|
221
235
|
function replyNamed(context, document, url) {
|
|
222
|
-
return namedIn(context, document, gather(context, document), url)
|
|
236
|
+
return namedIn(context, document, gather(context, document), url);
|
|
223
237
|
}
|
|
224
238
|
/** The entry of a gathered conversation a URL names, held ones included. */
|
|
225
239
|
function namedIn(context, document, said, url) {
|
|
@@ -230,8 +244,8 @@ function namedIn(context, document, said, url) {
|
|
|
230
244
|
catch {
|
|
231
245
|
return undefined;
|
|
232
246
|
}
|
|
233
|
-
const
|
|
234
|
-
return [...said.written, ...said.held.values()].find((reply) => reply.
|
|
247
|
+
const key = keyNamedBy(named, document, context.baseUrl);
|
|
248
|
+
return [...said.written, ...said.held.values()].find((reply) => reply.key === key ||
|
|
235
249
|
reply.id === named.href ||
|
|
236
250
|
(reply.url !== null && absoluteUrl(reply.url, context.baseUrl) === named.href));
|
|
237
251
|
}
|
|
@@ -249,35 +263,30 @@ function heldAt(context, url) {
|
|
|
249
263
|
}
|
|
250
264
|
return { kind: 'reply', post, reply };
|
|
251
265
|
}
|
|
252
|
-
function repliesTo(context,
|
|
253
|
-
if (!
|
|
254
|
-
|
|
255
|
-
if (stored?.source !== 'comment')
|
|
256
|
-
return undefined;
|
|
257
|
-
const post = context.store.getBySlug(stored.slug);
|
|
258
|
-
return post === undefined ? undefined : repliesIn(context, post, segment);
|
|
259
|
-
}
|
|
266
|
+
function repliesTo(context, key) {
|
|
267
|
+
if (!isKey(key))
|
|
268
|
+
return undefined;
|
|
260
269
|
// Each source's index is asked in turn, so a key naming nothing costs three
|
|
261
270
|
// lookups and no more (TASK-327).
|
|
262
|
-
const [document] = context.store.listByRepliesKey(
|
|
271
|
+
const [document] = context.store.listByRepliesKey(key);
|
|
263
272
|
if (document !== undefined) {
|
|
264
|
-
if (repliesKey(feedGuid(document, context.baseUrl)) ===
|
|
273
|
+
if (repliesKey(feedGuid(document, context.baseUrl)) === key) {
|
|
265
274
|
return repliesIn(context, document, null);
|
|
266
275
|
}
|
|
267
276
|
const root = threadRootOf(context, document);
|
|
268
|
-
return root === undefined ? undefined : repliesIn(context, root,
|
|
277
|
+
return root === undefined ? undefined : repliesIn(context, root, key);
|
|
269
278
|
}
|
|
270
|
-
const
|
|
279
|
+
const comment = context.admin.getComment(key);
|
|
271
280
|
if (comment !== undefined) {
|
|
272
281
|
const post = context.store.getBySlug(comment.slug);
|
|
273
|
-
return post === undefined ? undefined : repliesIn(context, post,
|
|
282
|
+
return post === undefined ? undefined : repliesIn(context, post, key);
|
|
274
283
|
}
|
|
275
|
-
for (const activity of context.admin.listRepliesByKey(
|
|
284
|
+
for (const activity of context.admin.listRepliesByKey(key)) {
|
|
276
285
|
const reply = replyFrom(activity);
|
|
277
286
|
if (reply === undefined)
|
|
278
287
|
continue;
|
|
279
288
|
const post = documentNamed(context, reply.id);
|
|
280
|
-
return post === undefined ? undefined : repliesIn(context, post,
|
|
289
|
+
return post === undefined ? undefined : repliesIn(context, post, key);
|
|
281
290
|
}
|
|
282
291
|
return undefined;
|
|
283
292
|
}
|
|
@@ -285,17 +294,17 @@ function repliesTo(context, segment) {
|
|
|
285
294
|
* The direct replies of one reply in a document's thread, or of the document
|
|
286
295
|
* itself, while the document is served and the reply is one a reader sees.
|
|
287
296
|
*/
|
|
288
|
-
function repliesIn(context, post,
|
|
297
|
+
function repliesIn(context, post, key) {
|
|
289
298
|
if (!isServed(post, context.store.now()))
|
|
290
299
|
return undefined;
|
|
291
300
|
const thread = postConversation(context, post).replies;
|
|
292
|
-
const reply =
|
|
301
|
+
const reply = key === null ? null : visibleReply(thread, key);
|
|
293
302
|
if (reply === undefined)
|
|
294
303
|
return undefined;
|
|
295
304
|
return {
|
|
296
305
|
post,
|
|
297
306
|
reply,
|
|
298
|
-
replies:
|
|
307
|
+
replies: repliesFed(reply?.replies ?? thread).map((entry) => ({ ...entry, parent: reply })),
|
|
299
308
|
};
|
|
300
309
|
}
|
|
301
310
|
function upstreamsOf(context, document) {
|
|
@@ -367,16 +376,16 @@ function documentNamed(context, url, depth = 0) {
|
|
|
367
376
|
return undefined;
|
|
368
377
|
}
|
|
369
378
|
if (named.origin === new URL(context.baseUrl).origin) {
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
const comment = context.admin.getComment(
|
|
379
|
+
const key = commentKeyAt(named.pathname);
|
|
380
|
+
if (key !== undefined) {
|
|
381
|
+
const comment = context.admin.getComment(key);
|
|
373
382
|
return comment === undefined ? undefined : context.store.getBySlug(comment.slug);
|
|
374
383
|
}
|
|
375
384
|
const permalink = permalinkOfObjectId(`${named.origin}${named.pathname}`, context.baseUrl);
|
|
376
385
|
const found = permalink === undefined ? undefined : context.store.getByPermalink(permalink);
|
|
377
386
|
return found ?? context.store.getByStoredObjectId(named.href);
|
|
378
387
|
}
|
|
379
|
-
const
|
|
388
|
+
const comment = context.admin.listCommentsAt(named.href)[0] ?? heldComment(context.admin, named.href);
|
|
380
389
|
if (comment !== undefined)
|
|
381
390
|
return context.store.getBySlug(comment.slug);
|
|
382
391
|
for (const activity of context.admin.listActivitiesAbout([named.href])) {
|
|
@@ -447,7 +456,7 @@ export function commentNames(id, permalink, baseUrl) {
|
|
|
447
456
|
/** A reply post as the thread's own shape, answering `inReplyTo`. */
|
|
448
457
|
function replyPostInteraction(context, post, id, inReplyTo) {
|
|
449
458
|
return {
|
|
450
|
-
id,
|
|
459
|
+
...unstored(id),
|
|
451
460
|
source: 'post',
|
|
452
461
|
kind: 'reply',
|
|
453
462
|
author: documentAuthor(context, post),
|
|
@@ -464,27 +473,32 @@ function documentAuthor(context, post) {
|
|
|
464
473
|
const users = context.users();
|
|
465
474
|
const site = readSiteSettings(context.contentDir);
|
|
466
475
|
const author = authorContext(users, post.author) ?? siteAuthorContext(users, site.author);
|
|
476
|
+
return bylineAuthor(author, site.title);
|
|
477
|
+
}
|
|
478
|
+
/** A byline as the thread names its author, `fallback` when it names nobody. */
|
|
479
|
+
function bylineAuthor(author, fallback) {
|
|
467
480
|
return {
|
|
468
|
-
name: author?.name ??
|
|
481
|
+
name: author?.name ?? fallback,
|
|
469
482
|
handle: null,
|
|
470
483
|
url: author?.url ?? null,
|
|
471
484
|
avatar: author?.avatar ?? null,
|
|
472
485
|
actorId: null,
|
|
473
486
|
feed: author?.username === undefined ? null : authorFeedHref(author.username, 'rss'),
|
|
487
|
+
username: author?.username ?? null,
|
|
474
488
|
};
|
|
475
489
|
}
|
|
476
|
-
/** The
|
|
477
|
-
function
|
|
490
|
+
/** The key a URL on this site names by a comment's page or by an anchor on the post. */
|
|
491
|
+
function keyNamedBy(url, document, baseUrl) {
|
|
478
492
|
const post = new URL(absoluteUrl(document.permalink, baseUrl));
|
|
479
493
|
if (url.origin !== post.origin)
|
|
480
494
|
return undefined;
|
|
481
|
-
const onItsPage =
|
|
495
|
+
const onItsPage = commentKeyAt(url.pathname);
|
|
482
496
|
if (onItsPage !== undefined)
|
|
483
497
|
return onItsPage;
|
|
484
498
|
const anchor = `#${commentAnchor('')}`;
|
|
485
499
|
if (url.pathname !== post.pathname || !url.hash.startsWith(anchor))
|
|
486
500
|
return undefined;
|
|
487
|
-
return
|
|
501
|
+
return url.hash.slice(anchor.length);
|
|
488
502
|
}
|
|
489
503
|
/**
|
|
490
504
|
* What a reply answers, from the top of the thread down to its parent.
|
|
@@ -554,7 +568,7 @@ function siteConversation(context, limit) {
|
|
|
554
568
|
offset += page.length;
|
|
555
569
|
for (const activity of page) {
|
|
556
570
|
const reply = replyFrom(activity);
|
|
557
|
-
if (reply === undefined)
|
|
571
|
+
if (reply === undefined || heldComment(context.admin, reply.id) !== undefined)
|
|
558
572
|
continue;
|
|
559
573
|
// A note answering another note is as much this post's as one answering
|
|
560
574
|
// the post, so the post is found the way a reply post finds its thread.
|
|
@@ -576,26 +590,31 @@ function siteConversation(context, limit) {
|
|
|
576
590
|
said.push({ ...quote, post });
|
|
577
591
|
}
|
|
578
592
|
}
|
|
579
|
-
for (const stored of context.admin.listComments({
|
|
580
|
-
status: 'approved',
|
|
581
|
-
limit: limit * OVERSCAN,
|
|
582
|
-
})) {
|
|
583
|
-
const post = context.store.getBySlug(stored.slug);
|
|
584
|
-
if (!shown(post))
|
|
585
|
-
continue;
|
|
586
|
-
said.push({ ...interactionOf(stored, post.permalink), post });
|
|
587
|
-
}
|
|
588
593
|
// Each post's thread, read once: where each reply sits and what it carries
|
|
589
594
|
// is the thread's to say, so the site's feed and the post's agree.
|
|
590
595
|
const threads = new Map();
|
|
591
596
|
const placedOn = (post) => {
|
|
592
597
|
let found = threads.get(post.path);
|
|
593
598
|
if (found === undefined) {
|
|
594
|
-
found = new Map(
|
|
599
|
+
found = new Map(saidIn(postConversation(context, post)).map((reply) => [reply.id, reply]));
|
|
595
600
|
threads.set(post.path, found);
|
|
596
601
|
}
|
|
597
602
|
return found;
|
|
598
603
|
};
|
|
604
|
+
// A stored comment is taken from its post's thread rather than read on its
|
|
605
|
+
// own, so a reaction or an event's RSVP, which the thread holds apart, is
|
|
606
|
+
// left out here as it is from the post's feed.
|
|
607
|
+
for (const stored of context.admin.listComments({
|
|
608
|
+
status: 'approved',
|
|
609
|
+
limit: limit * OVERSCAN,
|
|
610
|
+
})) {
|
|
611
|
+
const post = context.store.getBySlug(stored.slug);
|
|
612
|
+
if (!shown(post))
|
|
613
|
+
continue;
|
|
614
|
+
const entry = placedOn(post).get(stored.id);
|
|
615
|
+
if (entry !== undefined)
|
|
616
|
+
said.push({ ...entry, post });
|
|
617
|
+
}
|
|
599
618
|
// A reply post is in the feed of the thread it is shown in, unlisted or
|
|
600
619
|
// not: the thread is listed, and the reply post is one of its answers.
|
|
601
620
|
for (const answer of context.store.listReplyPosts({ limit: limit * OVERSCAN })) {
|
|
@@ -606,7 +625,7 @@ function siteConversation(context, limit) {
|
|
|
606
625
|
if (reply !== undefined)
|
|
607
626
|
said.push({ ...reply, post });
|
|
608
627
|
}
|
|
609
|
-
return newestFirst(said, limit).map((entry) => {
|
|
628
|
+
return newestFirst(said.filter(conversational), limit).map((entry) => {
|
|
610
629
|
const thread = placedOn(entry.post);
|
|
611
630
|
return {
|
|
612
631
|
...(thread.get(entry.id) ?? { ...entry, parent: null }),
|
|
@@ -619,7 +638,7 @@ function siteConversation(context, limit) {
|
|
|
619
638
|
function directCounts(context, documents) {
|
|
620
639
|
return new Map(documents.map((document) => [
|
|
621
640
|
document.permalink,
|
|
622
|
-
|
|
641
|
+
repliesFed(postConversation(context, document).replies).length,
|
|
623
642
|
]));
|
|
624
643
|
}
|
|
625
644
|
/**
|
|
@@ -653,7 +672,7 @@ function quotesOf(admin, approvals, objectId, naming) {
|
|
|
653
672
|
if (quote?.id !== approval.quote || quote.quoted !== objectId)
|
|
654
673
|
continue;
|
|
655
674
|
quotes.push({
|
|
656
|
-
|
|
675
|
+
...unstored(quote.id),
|
|
657
676
|
source: 'activitypub',
|
|
658
677
|
kind: 'mention',
|
|
659
678
|
author: naming(quote.actorId),
|
|
@@ -674,7 +693,7 @@ function quotesOf(admin, approvals, objectId, naming) {
|
|
|
674
693
|
/** One logged fediverse reply as the thread's own shape. */
|
|
675
694
|
function federatedInteraction(reply, author) {
|
|
676
695
|
return {
|
|
677
|
-
|
|
696
|
+
...unstored(reply.id),
|
|
678
697
|
source: 'activitypub',
|
|
679
698
|
kind: 'reply',
|
|
680
699
|
author,
|
|
@@ -688,24 +707,33 @@ function federatedInteraction(reply, author) {
|
|
|
688
707
|
replies: [],
|
|
689
708
|
};
|
|
690
709
|
}
|
|
710
|
+
function unstored(id) {
|
|
711
|
+
return { id, key: repliesKey(id), guid: id, migrated: false };
|
|
712
|
+
}
|
|
691
713
|
/** One stored comment as the thread's own shape. */
|
|
692
|
-
function interactionOf(comment, permalink) {
|
|
714
|
+
function interactionOf(comment, permalink, baseUrl, users) {
|
|
715
|
+
// A user the site no longer has is named as the comment was signed.
|
|
716
|
+
const user = users.find((candidate) => candidate.username === comment.user);
|
|
693
717
|
return {
|
|
694
718
|
id: comment.id,
|
|
719
|
+
key: comment.id,
|
|
720
|
+
guid: comment.source === 'comment'
|
|
721
|
+
? absoluteUrl(commentPageHref(comment.id), baseUrl)
|
|
722
|
+
: (comment.origin ?? comment.url ?? comment.id),
|
|
723
|
+
migrated: comment.migrated === true,
|
|
695
724
|
source: comment.source,
|
|
696
725
|
kind: comment.kind,
|
|
697
|
-
author:
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
},
|
|
726
|
+
author: user === undefined
|
|
727
|
+
? {
|
|
728
|
+
name: comment.author.name,
|
|
729
|
+
handle: comment.actor === undefined ? null : (actorHandle(comment.actor) ?? null),
|
|
730
|
+
url: comment.author.url,
|
|
731
|
+
avatar: comment.author.avatar === null ? null : avatarHref(comment.author.avatar),
|
|
732
|
+
actorId: comment.actor ?? null,
|
|
733
|
+
feed: comment.source === 'webmention' ? comment.author.url : null,
|
|
734
|
+
username: null,
|
|
735
|
+
}
|
|
736
|
+
: bylineAuthor(profileContext(user), comment.author.name),
|
|
709
737
|
// Where it can be read. A comment written here has a page of its own
|
|
710
738
|
// (TASK-318); a webmention lives on the page it was sent from, and its
|
|
711
739
|
// `url` says so. A theme that prints `url` for a fediverse reply prints a
|
|
@@ -725,46 +753,53 @@ function interactionOf(comment, permalink) {
|
|
|
725
753
|
};
|
|
726
754
|
}
|
|
727
755
|
/**
|
|
728
|
-
* The
|
|
756
|
+
* The stored comment holding a fediverse note or activity id as its origin:
|
|
757
|
+
* one imported from the site this one replaced, keyed by that id as it would
|
|
758
|
+
* have been had it arrived here (TASK-336). A delivery or a fetch of the same
|
|
759
|
+
* id is that comment again, and the stored one wins, since it is the one the
|
|
760
|
+
* moderator decided on and the thread anchors.
|
|
761
|
+
*/
|
|
762
|
+
export function heldComment(admin, id) {
|
|
763
|
+
const comment = admin.getComment(repliesKey(id));
|
|
764
|
+
return comment?.origin === id ? comment : undefined;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* The `id` a thread entry is anchored at on the page, by its key.
|
|
729
768
|
*
|
|
730
769
|
* Prefixed rather than bare so a comment can never collide with a heading
|
|
731
770
|
* anchor the post's own Markdown produced.
|
|
732
771
|
*/
|
|
733
|
-
export function commentAnchor(
|
|
734
|
-
return `comment-${
|
|
772
|
+
export function commentAnchor(key) {
|
|
773
|
+
return `comment-${key}`;
|
|
735
774
|
}
|
|
736
|
-
/** The
|
|
737
|
-
export function
|
|
775
|
+
/** The key of the comment whose page a path is, or `undefined` (TASK-319). */
|
|
776
|
+
export function commentKeyAt(pathname) {
|
|
738
777
|
if (!pathname.startsWith(COMMENT_PAGE_PREFIX) || !pathname.endsWith('/'))
|
|
739
778
|
return undefined;
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
return undefined;
|
|
743
|
-
return decoded(encoded);
|
|
744
|
-
}
|
|
745
|
-
function decoded(value) {
|
|
746
|
-
try {
|
|
747
|
-
return decodeURIComponent(value);
|
|
748
|
-
}
|
|
749
|
-
catch {
|
|
750
|
-
return undefined;
|
|
751
|
-
}
|
|
779
|
+
const key = pathname.slice(COMMENT_PAGE_PREFIX.length, -1);
|
|
780
|
+
return isKey(key) ? key : undefined;
|
|
752
781
|
}
|
|
753
|
-
/**
|
|
754
|
-
* Everything said in a conversation, at every depth and in no order: the
|
|
755
|
-
* replies and the mentions.
|
|
756
|
-
*
|
|
757
|
-
* The likes and the boosts are left out because they say nothing. A feed item
|
|
758
|
-
* is somebody's words, and "@ada liked this" has none to carry — a reader
|
|
759
|
-
* polling a comments feed would get an entry with an empty body and no way to
|
|
760
|
-
* tell what it was for.
|
|
761
|
-
*/
|
|
782
|
+
/** What a post's comments feed carries, at every depth and in no order. */
|
|
762
783
|
export function spokenIn(conversation) {
|
|
784
|
+
return saidIn(conversation).filter(conversational);
|
|
785
|
+
}
|
|
786
|
+
/** The mentions and every visible reply of a conversation, each with its parent. */
|
|
787
|
+
function saidIn(conversation) {
|
|
763
788
|
return [
|
|
764
789
|
...conversation.mentions.map((mention) => ({ ...mention, parent: null })),
|
|
765
790
|
...placed(conversation.replies),
|
|
766
791
|
];
|
|
767
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Whether a feed carries it and a reply count counts it (TASK-335): somebody's
|
|
795
|
+
* words, a reply or a mention, said here. A like, a boost or a repost says
|
|
796
|
+
* nothing — "@ada liked this" would be an item with an empty body — and a
|
|
797
|
+
* migrated entry was published before the site came here, so a reader must
|
|
798
|
+
* not see it as new.
|
|
799
|
+
*/
|
|
800
|
+
function conversational(entry) {
|
|
801
|
+
return !entry.migrated && (entry.kind === 'reply' || entry.kind === 'mention');
|
|
802
|
+
}
|
|
768
803
|
/** Every visible reply of a thread at any depth, each with the reply it sits under. */
|
|
769
804
|
function placed(replies, parent = null) {
|
|
770
805
|
return directReplies(replies).flatMap((reply) => [
|
|
@@ -780,15 +815,9 @@ function placed(replies, parent = null) {
|
|
|
780
815
|
function directReplies(replies) {
|
|
781
816
|
return replies.flatMap((reply) => (isWithheld(reply) ? directReplies(reply.replies) : [reply]));
|
|
782
817
|
}
|
|
783
|
-
/**
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
* as a key.
|
|
787
|
-
*/
|
|
788
|
-
function repliesHrefOf(reply, baseUrl) {
|
|
789
|
-
return repliesFeedPath(reply.source === 'comment' && !isRepliesKey(reply.id)
|
|
790
|
-
? reply.id
|
|
791
|
-
: repliesKey(replyGuid(reply, baseUrl)));
|
|
818
|
+
/** The direct replies a feed carries and a count advertises. */
|
|
819
|
+
function repliesFed(replies) {
|
|
820
|
+
return directReplies(replies).filter(conversational);
|
|
792
821
|
}
|
|
793
822
|
/**
|
|
794
823
|
* Interactions as a comments feed carries them: newest first, at most `limit`,
|
|
@@ -807,10 +836,10 @@ export function feedComments(said, options) {
|
|
|
807
836
|
? thread === undefined
|
|
808
837
|
? undefined
|
|
809
838
|
: feedGuid(thread, baseUrl)
|
|
810
|
-
:
|
|
811
|
-
const replies =
|
|
839
|
+
: entry.parent.guid;
|
|
840
|
+
const replies = repliesFed(entry.replies);
|
|
812
841
|
return {
|
|
813
|
-
id:
|
|
842
|
+
id: entry.guid,
|
|
814
843
|
url: absoluteUrl(entry.url ?? '/', baseUrl),
|
|
815
844
|
author: entry.author.name,
|
|
816
845
|
published: entry.published,
|
|
@@ -821,7 +850,7 @@ export function feedComments(said, options) {
|
|
|
821
850
|
: {
|
|
822
851
|
replies: {
|
|
823
852
|
count: replies.length,
|
|
824
|
-
feed: absoluteUrl(
|
|
853
|
+
feed: absoluteUrl(repliesFeedPath(entry.key), baseUrl),
|
|
825
854
|
},
|
|
826
855
|
}),
|
|
827
856
|
...(entry.author.feed === null
|
|
@@ -955,7 +984,8 @@ function threadOf(said, under = said.root) {
|
|
|
955
984
|
const list = siblings(said.held.get(id)?.inReplyTo ?? said.root, id, true);
|
|
956
985
|
if (list === undefined)
|
|
957
986
|
return undefined;
|
|
958
|
-
const
|
|
987
|
+
const key = isKey(id) ? id : repliesKey(id);
|
|
988
|
+
const entry = { id, key, withheld: true, replies: [] };
|
|
959
989
|
list.push(entry);
|
|
960
990
|
withheld.set(id, entry);
|
|
961
991
|
return entry;
|
|
@@ -965,12 +995,12 @@ function threadOf(said, under = said.root) {
|
|
|
965
995
|
}
|
|
966
996
|
return top;
|
|
967
997
|
}
|
|
968
|
-
/** The entry of a thread with this
|
|
969
|
-
export function visibleReply(replies,
|
|
998
|
+
/** The entry of a thread with this key that a reader can see, at any depth. */
|
|
999
|
+
export function visibleReply(replies, key) {
|
|
970
1000
|
for (const reply of replies) {
|
|
971
|
-
if (!isWithheld(reply) && reply.
|
|
1001
|
+
if (!isWithheld(reply) && reply.key === key)
|
|
972
1002
|
return reply;
|
|
973
|
-
const found = visibleReply(reply.replies,
|
|
1003
|
+
const found = visibleReply(reply.replies, key);
|
|
974
1004
|
if (found !== undefined)
|
|
975
1005
|
return found;
|
|
976
1006
|
}
|
|
@@ -1021,10 +1051,10 @@ const REACTIONS = {
|
|
|
1021
1051
|
* stops when a round finds nothing new, which it must, because a row is
|
|
1022
1052
|
* collected once and there are finitely many.
|
|
1023
1053
|
*/
|
|
1024
|
-
function activitiesAround(admin,
|
|
1054
|
+
function activitiesAround(admin, objectIds) {
|
|
1025
1055
|
const collected = new Map();
|
|
1026
|
-
const asked = new Set(
|
|
1027
|
-
let frontier = [
|
|
1056
|
+
const asked = new Set(objectIds);
|
|
1057
|
+
let frontier = [...asked];
|
|
1028
1058
|
while (frontier.length > 0) {
|
|
1029
1059
|
const next = [];
|
|
1030
1060
|
for (const activity of admin.listActivitiesAbout(frontier)) {
|
|
@@ -1106,6 +1136,7 @@ function authorNaming(admin) {
|
|
|
1106
1136
|
avatar: icon === null ? null : avatarHref(icon),
|
|
1107
1137
|
actorId,
|
|
1108
1138
|
feed: null,
|
|
1139
|
+
username: null,
|
|
1109
1140
|
};
|
|
1110
1141
|
known.set(actorId, author);
|
|
1111
1142
|
return author;
|