Initial comments implementation

This commit is contained in:
Shadowfacts 2019-03-01 18:42:28 -05:00
parent 6b4ea192e1
commit 9777f72773
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
10 changed files with 170 additions and 44 deletions

View File

@ -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();
});
});
}

View File

@ -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();
}
});
});
}

View File

@ -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,

View File

@ -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"));
} }

View File

@ -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);

6
package-lock.json generated
View File

@ -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": {

View File

@ -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"
} }
} }

View File

@ -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;

63
site/js/comments.js Normal file
View File

@ -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>
`;
}

View File

@ -11,4 +11,10 @@ metadata.layout = "default.html.ejs"
<%- content %> <%- content %>
</div> </div>
</article> </article>
</div> <section id="comments-container"></section>
</div>
<script>
const permalink = "<%= metadata.permalink %>";
</script>
<script src="/js/comments.js" async></script>