Initial comments implementation
This commit is contained in:
parent
6b4ea192e1
commit
9777f72773
|
@ -0,0 +1,64 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
import { Database } from "sqlite3";
|
||||||
|
import { Note, Actor } from "./activity";
|
||||||
|
import { getCachedActor } from "./federate";
|
||||||
|
|
||||||
|
const domain = process.env.DOMAIN;
|
||||||
|
|
||||||
|
interface Comment extends Note {
|
||||||
|
author: Actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getConversationComments(conversation: string, db: Database): Promise<Comment[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all("SELECT notes.id AS comment_id, notes.content, notes.published, notes.in_reply_to, actors.id AS actor_id, actors.display_name, actors.icon_url FROM notes INNER JOIN actors ON actors.id = notes.attributed_to WHERE notes.conversation = $conversation", {
|
||||||
|
$conversation: conversation
|
||||||
|
}, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
const comments = rows.map(row => {
|
||||||
|
return {
|
||||||
|
id: row.comment_id,
|
||||||
|
content: row.content,
|
||||||
|
published: row.published,
|
||||||
|
inReplyTo: row.in_reply_to,
|
||||||
|
author: {
|
||||||
|
id: row.actor_id,
|
||||||
|
name: row.display_name,
|
||||||
|
icon: row.icon_url
|
||||||
|
} as Actor
|
||||||
|
} as Comment;
|
||||||
|
});
|
||||||
|
resolve(comments);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function comments(router: Router) {
|
||||||
|
router.get("/ap/conversation/:id", async (req, res) => {
|
||||||
|
const db = req.app.get("db") as Database;
|
||||||
|
const comments = await getConversationComments(`https://${domain}/ap/conversation/${req.params.id}`, db);
|
||||||
|
res.json(comments).end();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/comments", (req, res) => {
|
||||||
|
const id = req.query.id;
|
||||||
|
if (!id) {
|
||||||
|
res.sendStatus(400).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const db = req.app.get("db") as Database;
|
||||||
|
db.get("SELECT conversation FROM articles WHERE id = $id", {
|
||||||
|
$id: id
|
||||||
|
}, async (err, result) => {
|
||||||
|
if (!result || !result.conversation) {
|
||||||
|
res.json([]).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const comments = await getConversationComments(result.conversation, db);
|
||||||
|
res.json(comments).end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
import { Router } from "express";
|
|
||||||
import { Database } from "sqlite3";
|
|
||||||
|
|
||||||
const domain = process.env.DOMAIN;
|
|
||||||
|
|
||||||
export default function conversation(router: Router) {
|
|
||||||
router.get("/ap/conversation/:id", (req, res) => {
|
|
||||||
const db = req.app.get("db") as Database;
|
|
||||||
db.all("SELECT * FROM notes WHERE conversation = $conversation", {
|
|
||||||
$conversation: `https://${domain}/ap/conversation/${req.params.id}`
|
|
||||||
}, (err, rows) => {
|
|
||||||
if (err) {
|
|
||||||
res.status(500).end(err);
|
|
||||||
} else {
|
|
||||||
const notes = rows.map(row => {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
attributedTo: row.attributed_to,
|
|
||||||
content: row.content,
|
|
||||||
published: row.published,
|
|
||||||
inReplyTo: row.in_reply_to,
|
|
||||||
conversation: row.conversation
|
|
||||||
};
|
|
||||||
});
|
|
||||||
res.json(notes).end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import actor from "./actor";
|
import actor from "./actor";
|
||||||
import * as articles from "./articles";
|
import * as articles from "./articles";
|
||||||
import conversation from "./conversation";
|
import comments from "./comments";
|
||||||
import federate from "./federate";
|
import federate from "./federate";
|
||||||
import followers from "./followers";
|
import followers from "./followers";
|
||||||
import inbox from "./inbox";
|
import inbox from "./inbox";
|
||||||
|
@ -9,7 +9,7 @@ import webfinger from "./webfinger";
|
||||||
export = {
|
export = {
|
||||||
actor,
|
actor,
|
||||||
articles,
|
articles,
|
||||||
conversation,
|
comments,
|
||||||
federate,
|
federate,
|
||||||
followers,
|
followers,
|
||||||
inbox,
|
inbox,
|
||||||
|
|
|
@ -5,4 +5,5 @@ export default async function copy() {
|
||||||
util.write("favicon.ico", await fs.readFile("site/favicon.ico"));
|
util.write("favicon.ico", await fs.readFile("site/favicon.ico"));
|
||||||
util.write("favicon-152.png", await fs.readFile("site/favicon-152.png"));
|
util.write("favicon-152.png", await fs.readFile("site/favicon-152.png"));
|
||||||
util.write("shadowfacts.png", await fs.readFile("site/shadowfacts.png"));
|
util.write("shadowfacts.png", await fs.readFile("site/shadowfacts.png"));
|
||||||
|
util.write("js/comments.js", await fs.readFile("site/js/comments.js"));
|
||||||
}
|
}
|
|
@ -48,7 +48,7 @@ app.use(bodyParser.json({ type: "application/activity+json" }));
|
||||||
const apRouter = Router();
|
const apRouter = Router();
|
||||||
apRouter.use(validateHttpSig);
|
apRouter.use(validateHttpSig);
|
||||||
await activitypub.actor(apRouter);
|
await activitypub.actor(apRouter);
|
||||||
activitypub.conversation(apRouter);
|
activitypub.comments(apRouter);
|
||||||
activitypub.followers(apRouter);
|
activitypub.followers(apRouter);
|
||||||
activitypub.inbox(apRouter);
|
activitypub.inbox(apRouter);
|
||||||
activitypub.webfinger(apRouter);
|
activitypub.webfinger(apRouter);
|
||||||
|
|
|
@ -36,9 +36,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/ejs": {
|
"@types/ejs": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-2.6.3.tgz",
|
||||||
"integrity": "sha512-WD8015V8Y+uDfFIjiVHU7t9SgHptqTGGN8w0A2LcrL0NLtqColM15cswkHZMUfodyuTf35sup8vW0hpWRHu0dQ==",
|
"integrity": "sha512-/F+qQ0Fr0Dr1YvHjX+FCvbba4sQ27RdCPDqmP/si0e1v1GOkbQ3VRBvZPSQM7NoQ3iz3SyiJVscCP2f0vKuIhQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/express": {
|
"@types/express": {
|
||||||
|
|
13
package.json
13
package.json
|
@ -11,10 +11,15 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/slugify": "^0.6.0",
|
"@sindresorhus/slugify": "^0.6.0",
|
||||||
"@types/body-parser": "^1.17.0",
|
"@types/body-parser": "^1.17.0",
|
||||||
|
"@types/ejs": "^2.6.3",
|
||||||
"@types/express": "^4.16.1",
|
"@types/express": "^4.16.1",
|
||||||
|
"@types/highlight.js": "^9.12.3",
|
||||||
|
"@types/markdown-it": "0.0.7",
|
||||||
"@types/morgan": "^1.7.35",
|
"@types/morgan": "^1.7.35",
|
||||||
|
"@types/node-sass": "^3.10.32",
|
||||||
"@types/request": "^2.48.1",
|
"@types/request": "^2.48.1",
|
||||||
"@types/sanitize-html": "^1.18.2",
|
"@types/sanitize-html": "^1.18.2",
|
||||||
|
"@types/sindresorhus__slugify": "^0.6.0",
|
||||||
"@types/sqlite3": "^3.1.4",
|
"@types/sqlite3": "^3.1.4",
|
||||||
"@types/uuid": "^3.4.4",
|
"@types/uuid": "^3.4.4",
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
|
@ -30,13 +35,5 @@
|
||||||
"sqlite3": "^4.0.6",
|
"sqlite3": "^4.0.6",
|
||||||
"typescript": "^3.2.2",
|
"typescript": "^3.2.2",
|
||||||
"uuid": "^3.3.2"
|
"uuid": "^3.3.2"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/ejs": "^2.6.1",
|
|
||||||
"@types/highlight.js": "^9.12.3",
|
|
||||||
"@types/markdown-it": "0.0.7",
|
|
||||||
"@types/node-sass": "^3.10.32",
|
|
||||||
"@types/sindresorhus__slugify": "^0.6.0",
|
|
||||||
"http-server": "^0.11.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,30 @@ article {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#comments-container {
|
||||||
|
.comments-list {
|
||||||
|
margin-top: 0px;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-user-avatar {
|
||||||
|
width: 50px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-info {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-children {
|
||||||
|
margin-left: 50px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
margin: 100px auto;
|
margin: 100px auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
fetchComments();
|
||||||
|
|
||||||
|
async function fetchComments() {
|
||||||
|
const res = await fetch(`/comments?id=${permalink}`);
|
||||||
|
const comments = await res.json();
|
||||||
|
const rootId = new URL(permalink, document.location).toString();
|
||||||
|
const tree = buildCommentsTree(comments, rootId)[0];
|
||||||
|
const html = renderCommentList(tree);
|
||||||
|
document.getElementById("comments-container").innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCommentsTree(comments, parent) {
|
||||||
|
console.log(`Building tree for ${parent}`);
|
||||||
|
let [children, rem] = partition(comments, it => it.inReplyTo === parent);
|
||||||
|
for (const child of children) {
|
||||||
|
const [subChildren, subRem] = buildCommentsTree(rem, child.id);
|
||||||
|
rem = subRem;
|
||||||
|
child.children = subChildren;
|
||||||
|
}
|
||||||
|
return [children, rem]
|
||||||
|
}
|
||||||
|
|
||||||
|
function partition(array, fn) {
|
||||||
|
const trueArr = [];
|
||||||
|
const falseArr = [];
|
||||||
|
for (const el of array) {
|
||||||
|
(fn(el) ? trueArr : falseArr).push(el);
|
||||||
|
}
|
||||||
|
return [trueArr, falseArr];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCommentList(comments) {
|
||||||
|
const rendered = comments.map(renderComment).join("\n");
|
||||||
|
return `<ul class="comments-list">` + rendered + `</ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComment(comment) {
|
||||||
|
const formattedDate = new Date(comment.published).toLocaleString("en-us", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric"
|
||||||
|
});
|
||||||
|
return `
|
||||||
|
<div class="comment">
|
||||||
|
<img class="comment-user-avatar" src="${comment.author.icon}">
|
||||||
|
<div class="comment-main">
|
||||||
|
<p class="comment-info">
|
||||||
|
<a class="comment-user-name" href="${comment.author.id}">${comment.author.name}</a>
|
||||||
|
on
|
||||||
|
<a class="comment-date" href="${comment.id}">${formattedDate}</a>
|
||||||
|
</p>
|
||||||
|
<div class="comment-body">
|
||||||
|
${comment.content}
|
||||||
|
</div>
|
||||||
|
<div class="comment-children">
|
||||||
|
${renderCommentList(comment.children)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -11,4 +11,10 @@ metadata.layout = "default.html.ejs"
|
||||||
<%- content %>
|
<%- content %>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
<section id="comments-container"></section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const permalink = "<%= metadata.permalink %>";
|
||||||
|
</script>
|
||||||
|
<script src="/js/comments.js" async></script>
|
Loading…
Reference in New Issue