From 9777f727730d7dbc97cac4e2afb72a926f257ef6 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 1 Mar 2019 18:42:28 -0500 Subject: [PATCH] Initial comments implementation --- lib/activitypub/comments.ts | 64 +++++++++++++++++++++++++++++++++ lib/activitypub/conversation.ts | 29 --------------- lib/activitypub/index.ts | 4 +-- lib/generate/copy.ts | 1 + lib/index.ts | 2 +- package-lock.json | 6 ++-- package.json | 13 +++---- site/css/main.scss | 24 +++++++++++++ site/js/comments.js | 63 ++++++++++++++++++++++++++++++++ site/layouts/article.html.ejs | 8 ++++- 10 files changed, 170 insertions(+), 44 deletions(-) create mode 100644 lib/activitypub/comments.ts delete mode 100644 lib/activitypub/conversation.ts create mode 100644 site/js/comments.js diff --git a/lib/activitypub/comments.ts b/lib/activitypub/comments.ts new file mode 100644 index 0000000..b66d79e --- /dev/null +++ b/lib/activitypub/comments.ts @@ -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 { + 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(); + }); + }); +} \ No newline at end of file diff --git a/lib/activitypub/conversation.ts b/lib/activitypub/conversation.ts deleted file mode 100644 index 349a8de..0000000 --- a/lib/activitypub/conversation.ts +++ /dev/null @@ -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(); - } - }); - }); -} \ No newline at end of file diff --git a/lib/activitypub/index.ts b/lib/activitypub/index.ts index 0cd48a1..0f7bfdf 100644 --- a/lib/activitypub/index.ts +++ b/lib/activitypub/index.ts @@ -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, diff --git a/lib/generate/copy.ts b/lib/generate/copy.ts index f2a3a96..bfaadd6 100644 --- a/lib/generate/copy.ts +++ b/lib/generate/copy.ts @@ -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")); } \ No newline at end of file diff --git a/lib/index.ts b/lib/index.ts index fee0fee..1d8a143 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -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); diff --git a/package-lock.json b/package-lock.json index 7bc61b3..09d1ff9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 1a9994c..395bab1 100644 --- a/package.json +++ b/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" } } diff --git a/site/css/main.scss b/site/css/main.scss index 674711f..2e4c066 100644 --- a/site/css/main.scss +++ b/site/css/main.scss @@ -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; diff --git a/site/js/comments.js b/site/js/comments.js new file mode 100644 index 0000000..c4f3048 --- /dev/null +++ b/site/js/comments.js @@ -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 ``; +} + +function renderComment(comment) { + const formattedDate = new Date(comment.published).toLocaleString("en-us", { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric" + }); + return ` +
+ +
+

+ ${comment.author.name} + on + ${formattedDate} +

+
+ ${comment.content} +
+
+ ${renderCommentList(comment.children)} +
+
+
+ `; +} \ No newline at end of file diff --git a/site/layouts/article.html.ejs b/site/layouts/article.html.ejs index f62a755..e3377a3 100644 --- a/site/layouts/article.html.ejs +++ b/site/layouts/article.html.ejs @@ -11,4 +11,10 @@ metadata.layout = "default.html.ejs" <%- content %> - \ No newline at end of file +
+ + + + \ No newline at end of file