Add comments

This commit is contained in:
Simon Detheridge 2023-10-19 14:51:43 +01:00
parent 9d6cc301e8
commit a28cc7cd39
Signed by: simon
GPG Key ID: 38640971DA1E704E
2 changed files with 29 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
const MASTODON_ACCOUNT_ID = '109285376472065471' const MASTODON_ACCOUNT_ID = '109285376472065471'
const MASTODON_HOST = 'social.sd.ai' const MASTODON_HOST = 'social.sd.ai'
// Copies the text from an element to the clipboard, and flashes the element to provide visual feedback
async function copyElementTextToClipboard(e) async function copyElementTextToClipboard(e)
{ {
const text = e.textContent const text = e.textContent
@ -12,6 +13,7 @@ async function copyElementTextToClipboard(e)
}, 600); }, 600);
} }
// sanitise text content for display
function escapeHtml(unsafe) { function escapeHtml(unsafe) {
return unsafe return unsafe
.replace(/&/g, "&") .replace(/&/g, "&")
@ -21,16 +23,24 @@ function escapeHtml(unsafe) {
.replace(/'/g, "'"); .replace(/'/g, "'");
} }
// renders the content - provides an array of toots, the element to add the content to and whether to show a copyable link for replies
function renderMastodonContent(toots, parentElement, showLink) { function renderMastodonContent(toots, parentElement, showLink) {
// clear the parent element so that we can add the new content
parentElement.innerHTML = '' parentElement.innerHTML = ''
// add a simple "no comments" message if there are no toots
if (!Array.isArray(toots) || toots.length === 0) { if (!Array.isArray(toots) || toots.length === 0) {
document.getElementById('mastodon-comments-list').innerHTML = "<div class='mastodon-comment'>No comments (yet)!</div>" document.getElementById('mastodon-comments-list').innerHTML = "<div class='mastodon-comment'>No comments (yet)!</div>"
return return
} }
// render each toot
for (const toot of toots) { for (const toot of toots) {
if (toot.sensitive) { if (toot.sensitive) {
// don't display toots marked as sensitive
continue continue
} }
// sanitise the toot content for display, including correctly rendering custom emojis
toot.account.display_name = escapeHtml(toot.account.display_name) toot.account.display_name = escapeHtml(toot.account.display_name)
toot.account.emojis.forEach(emoji => { toot.account.emojis.forEach(emoji => {
toot.account.display_name = toot.account.display_name.replace(`:${emoji.shortcode}:`, toot.account.display_name = toot.account.display_name.replace(`:${emoji.shortcode}:`,
@ -40,6 +50,8 @@ function renderMastodonContent(toots, parentElement, showLink) {
toot.content = toot.content.replace(`:${emoji.shortcode}:`, toot.content = toot.content.replace(`:${emoji.shortcode}:`,
`<img src="${escapeHtml(emoji.static_url)}" alt="Emoji ${emoji.shortcode}" class="mastodon-emoji" />`); `<img src="${escapeHtml(emoji.static_url)}" alt="Emoji ${emoji.shortcode}" class="mastodon-emoji" />`);
}) })
// create a block of HTML content including the toot data
const comment = const comment =
`<div class="mastodon-comment"> `<div class="mastodon-comment">
<div class="mastodon-avatar"> <div class="mastodon-avatar">
@ -69,30 +81,44 @@ function renderMastodonContent(toots, parentElement, showLink) {
</div> </div>
</div> </div>
</div>` </div>`
// Use DOMPurify to create a sanitised element for the toot
const child = DOMPurify.sanitize(comment, {'RETURN_DOM_FRAGMENT': true}); const child = DOMPurify.sanitize(comment, {'RETURN_DOM_FRAGMENT': true});
// make all toot links clickable
const links = child.querySelectorAll('.tootlink'); const links = child.querySelectorAll('.tootlink');
for (const link of links) { for (const link of links) {
link.onclick = function() { return copyElementTextToClipboard(this); } link.onclick = function() { return copyElementTextToClipboard(this); }
} }
// insert the toot into the DOM
parentElement.appendChild(child); parentElement.appendChild(child);
} }
} }
// We set this in the "code injection" footer for any page for which we want to enable comments
let MASTODON_POST_ID let MASTODON_POST_ID
// when the page has finished loading, send a request for the toots
document.addEventListener("DOMContentLoaded", async (event) => { document.addEventListener("DOMContentLoaded", async (event) => {
let url, isComments let url, isComments
// if we're being crawled, don't render comments - may help against spam
const isBot = /bot|google|baidu|bing|msn|teoma|slurp|yandex/i const isBot = /bot|google|baidu|bing|msn|teoma|slurp|yandex/i
.test(navigator.userAgent) .test(navigator.userAgent)
// if there is a sidebar, we're expecting to load the toots from the main account
if (document.getElementsByClassName('gh-sidebar').length > 0) { if (document.getElementsByClassName('gh-sidebar').length > 0) {
url = `https://${MASTODON_HOST}/api/v1/accounts/${MASTODON_ACCOUNT_ID}/statuses?exclude_replies=true&exclude_reblogs=true` url = `https://${MASTODON_HOST}/api/v1/accounts/${MASTODON_ACCOUNT_ID}/statuses?exclude_replies=true&exclude_reblogs=true`
} }
// if there's a post ID and we're not a bot, we're expecting to load the replies from a specific toot
if (MASTODON_POST_ID && !isBot) { if (MASTODON_POST_ID && !isBot) {
url = `https://${MASTODON_HOST}/api/v1/statuses/${MASTODON_POST_ID}/context` url = `https://${MASTODON_HOST}/api/v1/statuses/${MASTODON_POST_ID}/context`
isComments = true isComments = true
} }
// find the element to append the content to - if there isn't one, we don't need to query
const element = document.getElementById('mastodon-comments-list') const element = document.getElementById('mastodon-comments-list')
if (url && element) { if (url && element) {
// populate the link to the source toot, if necessary (for replies)
const linkElement = document.getElementById('toot-link-top') const linkElement = document.getElementById('toot-link-top')
const clipElement = document.getElementById('toot-link-clip') const clipElement = document.getElementById('toot-link-clip')
const tootUrl = `https://${MASTODON_HOST}/@s/${MASTODON_POST_ID}` const tootUrl = `https://${MASTODON_HOST}/@s/${MASTODON_POST_ID}`
@ -102,11 +128,13 @@ document.addEventListener("DOMContentLoaded", async (event) => {
if (clipElement) { if (clipElement) {
clipElement.innerText = tootUrl clipElement.innerText = tootUrl
} }
// fetch the data from Mastodon
const response = await fetch(url) const response = await fetch(url)
let content = await response.json() let content = await response.json()
if (isComments) { if (isComments) {
content = content.descendants content = content.descendants
} }
// render the content into the page
const header = document.getElementById('mastodon-comments-header') const header = document.getElementById('mastodon-comments-header')
if (header) { if (header) {
header.style.display = '' header.style.display = ''