diff --git a/package.json b/package.json
index 0f441fc2..7c5de511 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"private": true,
"dependencies": {
"@hcaptcha/react-hcaptcha": "^0.3.9",
+ "@types/dompurify": "^2.4.0",
"@types/history": "^4.7.5",
"@types/react": "^16.9.20",
"@types/react-dom": "^16.9.5",
@@ -19,10 +20,12 @@
"canvas-confetti": "^1.5.1",
"connected-react-router": "^6.7.0",
"crypto-js": "^4.0.0",
+ "dompurify": "^2.4.3",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"history": "^4.10.1",
"moment": "^2.29.0",
+ "node-fetch": "^2.6.8",
"pluralize": "^8.0.0",
"podcast-index-api": "^1.1.9",
"query-string": "^6.13.5",
@@ -39,6 +42,7 @@
"redux-thunk": "^2.3.0",
"reduxsauce": "^1.1.2",
"shortid": "^2.2.15",
+ "threadcap": "^0.1.9",
"typesafe-actions": "^5.1.0",
"uuid": "^8.3.2",
"webln": "^0.3.0"
diff --git a/server/index.js b/server/index.js
index 988ee316..bc6f5f1f 100644
--- a/server/index.js
+++ b/server/index.js
@@ -2,9 +2,15 @@ const path = require('path')
const fs = require('fs');
const express = require('express')
const app = express() // create express app
+const { makeThreadcap, InMemoryCache, updateThreadcap, makeRateLimitedFetcher } = require('threadcap');
+const fetch = require('node-fetch');
+const packageJson = require('../package.json');
+
// Gets the .env variables
require('dotenv').config()
+const USER_AGENT = `Podcastindex.org-web/${packageJson.version}`;
+
// Utilizing the node repo from comster/podcast-index-api :)
// NOTE: This server will work as a reverse proxy.
const api = require('podcast-index-api')(
@@ -112,6 +118,32 @@ app.use('/api/add/byfeedurl', async (req, res) => {
res.send(response)
})
+// ------------------------------------------------
+// ------------ API to get comments for episode ---
+// ------------------------------------------------
+app.use('/api/comments/byepisodeid', async (req, res) => {
+ let episodeId = req.query.id;
+ const response = await api.episodesById(episodeId, false);
+
+ const socialInteract = response.episode.socialInteract && response.episode.socialInteract.filter((si) => si.protocol === 'activitypub');
+
+ if(!socialInteract && socialInteract.lenght >= 0) {
+ // Bad requests sounds appropriate, as the client is only expected to call this API
+ // when it validated upfront that the episode has a property socialInteract with activitypub protocol
+ res.status(400).send('The episode does not contain a socialInteract property')
+ }
+
+ const userAgent = USER_AGENT;
+ const cache = new InMemoryCache();
+ const fetcher = makeRateLimitedFetcher(fetch);
+
+ const threadcap = await makeThreadcap(socialInteract[0].uri, { userAgent, cache, fetcher });
+
+ await updateThreadcap(threadcap, { updateTime: new Date().toISOString(), userAgent, cache, fetcher });
+
+ res.send(threadcap)
+})
+
// ------------------------------------------------
// ---------- Static files for API data -----------
// ------------------------------------------------
diff --git a/ui/src/components/Comments/index.tsx b/ui/src/components/Comments/index.tsx
new file mode 100644
index 00000000..98115f52
--- /dev/null
+++ b/ui/src/components/Comments/index.tsx
@@ -0,0 +1,218 @@
+import * as React from 'react'
+import DOMPurify from 'dompurify'
+
+import './styles.scss'
+
+interface IProps {
+ id: number,
+}
+
+interface IState {
+ showComments: boolean,
+ loadingComments: boolean,
+ comments: StateComment[]
+}
+
+interface StateComment {
+ url: string,
+ publishedAt?: Date,
+ summary?: string,
+ content?: string,
+ attributedTo?: Commenter,
+ replies?: StateComment[],
+ commentError?: string,
+ repliesError?: string
+}
+
+interface Commenter {
+ name: string,
+ iconUrl: string,
+ url: string,
+ account: string
+}
+
+interface ICommentProps {
+ comment: StateComment
+}
+
+class Comment extends React.PureComponent Loading comments...
+
+
+
+ }
+ { this.props.comment.summary ?
+ Content Warning Summary
+ {
+ !this.props.comment.commentError && this.props.comment.content &&
+
+ }
+
+
+
+ Error loading this comment
+
+
+ }
+ {!this.props.comment.repliesError && this.props.comment.replies &&