forked from shadowfacts/shadowfacts.net
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 * as articles from "./articles";
|
||||
import conversation from "./conversation";
|
||||
import comments from "./comments";
|
||||
import federate from "./federate";
|
||||
import followers from "./followers";
|
||||
import inbox from "./inbox";
|
||||
|
@ -9,7 +9,7 @@ import webfinger from "./webfinger";
|
|||
export = {
|
||||
actor,
|
||||
articles,
|
||||
conversation,
|
||||
comments,
|
||||
federate,
|
||||
followers,
|
||||
inbox,
|
||||
|
|
|
@ -5,4 +5,5 @@ export default async function copy() {
|
|||
util.write("favicon.ico", await fs.readFile("site/favicon.ico"));
|
||||
util.write("favicon-152.png", await fs.readFile("site/favicon-152.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();
|
||||
apRouter.use(validateHttpSig);
|
||||
await activitypub.actor(apRouter);
|
||||
activitypub.conversation(apRouter);
|
||||
activitypub.comments(apRouter);
|
||||
activitypub.followers(apRouter);
|
||||
activitypub.inbox(apRouter);
|
||||
activitypub.webfinger(apRouter);
|
||||
|
|
|
@ -36,9 +36,9 @@
|
|||
}
|
||||
},
|
||||
"@types/ejs": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-2.6.1.tgz",
|
||||
"integrity": "sha512-WD8015V8Y+uDfFIjiVHU7t9SgHptqTGGN8w0A2LcrL0NLtqColM15cswkHZMUfodyuTf35sup8vW0hpWRHu0dQ==",
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-2.6.3.tgz",
|
||||
"integrity": "sha512-/F+qQ0Fr0Dr1YvHjX+FCvbba4sQ27RdCPDqmP/si0e1v1GOkbQ3VRBvZPSQM7NoQ3iz3SyiJVscCP2f0vKuIhQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/express": {
|
||||
|
|
13
package.json
13
package.json
|
@ -11,10 +11,15 @@
|
|||
"dependencies": {
|
||||
"@sindresorhus/slugify": "^0.6.0",
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/ejs": "^2.6.3",
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/highlight.js": "^9.12.3",
|
||||
"@types/markdown-it": "0.0.7",
|
||||
"@types/morgan": "^1.7.35",
|
||||
"@types/node-sass": "^3.10.32",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/sanitize-html": "^1.18.2",
|
||||
"@types/sindresorhus__slugify": "^0.6.0",
|
||||
"@types/sqlite3": "^3.1.4",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"body-parser": "^1.18.3",
|
||||
|
@ -30,13 +35,5 @@
|
|||
"sqlite3": "^4.0.6",
|
||||
"typescript": "^3.2.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 {
|
||||
margin: 100px auto;
|
||||
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 %>
|
||||
</div>
|
||||
</article>
|
||||
<section id="comments-container"></section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const permalink = "<%= metadata.permalink %>";
|
||||
</script>
|
||||
<script src="/js/comments.js" async></script>
|
Loading…
Reference in New Issue