Hendrik Langer
7 months ago
3 changed files with 401 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
title: Mastodon-comments |
|||
author: Andreas Varotsis |
|||
version: 1.0.1 |
|||
quarto-required: ">=1.3.0" |
|||
contributes: |
|||
filters: |
|||
- mastodon-comments.lua |
|||
|
@ -0,0 +1,347 @@ |
|||
const styles = ` |
|||
:root { |
|||
--font-color: #5d686f; |
|||
--font-size: 1.0rem; |
|||
|
|||
--block-border-width: 1px; |
|||
--block-border-radius: 3px; |
|||
--block-border-color: #ededf0; |
|||
--block-background-color: #f7f8f8; |
|||
|
|||
--comment-indent: 40px; |
|||
} |
|||
|
|||
#mastodon-stats { |
|||
text-align: center; |
|||
font-size: calc(var(--font-size) * 2) |
|||
} |
|||
|
|||
#mastodon-comments-list { |
|||
margin: 0 auto; |
|||
margin-top: 1rem; |
|||
} |
|||
|
|||
.mastodon-comment { |
|||
background-color: var(--block-background-color); |
|||
border-radius: var(--block-border-radius); |
|||
border: var(--block-border-width) var(--block-border-color) solid; |
|||
padding: 20px; |
|||
margin-bottom: 1.5rem; |
|||
display: flex; |
|||
flex-direction: column; |
|||
color: var(--font-color); |
|||
font-size: var(--font-size); |
|||
} |
|||
|
|||
.mastodon-comment p { |
|||
margin-bottom: 0px; |
|||
} |
|||
|
|||
.mastodon-comment .author { |
|||
padding-top:0; |
|||
display:flex; |
|||
} |
|||
|
|||
.mastodon-comment .author a { |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.mastodon-comment .author .avatar img { |
|||
margin-right:1rem; |
|||
min-width:60px; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.mastodon-comment .author .details { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.mastodon-comment .author .details .name { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.mastodon-comment .author .details .user { |
|||
color: #5d686f; |
|||
font-size: medium; |
|||
} |
|||
|
|||
.mastodon-comment .author .date { |
|||
margin-left: auto; |
|||
font-size: small; |
|||
} |
|||
|
|||
.mastodon-comment .content { |
|||
margin: 15px 20px; |
|||
} |
|||
|
|||
.mastodon-comment .attachments { |
|||
margin: 0px 10px; |
|||
} |
|||
|
|||
.mastodon-comment .attachments > * { |
|||
margin: 0px 10px; |
|||
} |
|||
|
|||
.mastodon-comment .attachments img { |
|||
max-width: 100%; |
|||
} |
|||
|
|||
.mastodon-comment .content p:first-child { |
|||
margin-top:0; |
|||
margin-bottom:0; |
|||
} |
|||
|
|||
.mastodon-comment .status > div, #mastodon-stats > div { |
|||
display: inline-block; |
|||
margin-right: 15px; |
|||
} |
|||
|
|||
.mastodon-comment .status a, #mastodon-stats a { |
|||
color: #5d686f; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.mastodon-comment .status .replies.active a, #mastodon-stats .replies.active a { |
|||
color: #003eaa; |
|||
} |
|||
|
|||
.mastodon-comment .status .reblogs.active a, #mastodon-stats .reblogs.active a { |
|||
color: #8c8dff; |
|||
} |
|||
|
|||
.mastodon-comment .status .favourites.active a, #mastodon-stats .favourites.active a { |
|||
color: #ca8f04; |
|||
} |
|||
`;
|
|||
|
|||
class MastodonComments extends HTMLElement { |
|||
constructor() { |
|||
super(); |
|||
|
|||
// Retrieve the host, user, and tootId from global variables
|
|||
this.host = mastodonHost; // Previously this.getAttribute("host")
|
|||
this.user = mastodonUser; // Previously this.getAttribute("user")
|
|||
this.tootId = mastodonTootId; // Previously this.getAttribute("tootId")
|
|||
|
|||
this.commentsLoaded = false; |
|||
|
|||
const styleElem = document.createElement("style"); |
|||
styleElem.innerHTML = styles; |
|||
document.head.appendChild(styleElem); |
|||
} |
|||
|
|||
|
|||
connectedCallback() { |
|||
this.innerHTML = ` |
|||
|
|||
<h2>Comments</h2> |
|||
|
|||
<noscript> |
|||
<div id="error"> |
|||
Please enable JavaScript to view the comments powered by the Fediverse. |
|||
</div> |
|||
</noscript> |
|||
<p>You can use your Fediverse (i.e. Mastodon, among many others) account to reply to this <a class="link" |
|||
href="https://${this.host}/@${this.user}/${this.tootId}">post</a>. |
|||
</p> |
|||
<div id="mastodon-stats"></div> |
|||
<p id="mastodon-comments-list"></p> |
|||
`;
|
|||
|
|||
const comments = document.getElementById("mastodon-comments-list"); |
|||
const rootStyle = this.getAttribute("style"); |
|||
if (rootStyle) { |
|||
comments.setAttribute("style", rootStyle); |
|||
} |
|||
this.respondToVisibility(comments, this.loadComments.bind(this)); |
|||
} |
|||
|
|||
escapeHtml(unsafe) { |
|||
return (unsafe || "") |
|||
.replace(/&/g, "&") |
|||
.replace(/</g, "<") |
|||
.replace(/>/g, ">") |
|||
.replace(/"/g, """) |
|||
.replace(/'/g, "'"); |
|||
} |
|||
|
|||
toot_active(toot, what) { |
|||
var count = toot[what + "_count"]; |
|||
return count > 0 ? "active" : ""; |
|||
} |
|||
|
|||
toot_count(toot, what) { |
|||
var count = toot[what + "_count"]; |
|||
return count > 0 ? count : ""; |
|||
} |
|||
|
|||
toot_stats(toot) { |
|||
return ` |
|||
<div class="replies ${this.toot_active(toot, "replies")}"> |
|||
<a href="${ |
|||
toot.url |
|||
}" rel="nofollow"><i class="fa fa-reply fa-fw"></i>${this.toot_count( |
|||
toot, |
|||
"replies", |
|||
)}</a> |
|||
</div> |
|||
<div class="reblogs ${this.toot_active(toot, "reblogs")}"> |
|||
<a href="${ |
|||
toot.url |
|||
}" rel="nofollow"><i class="fa fa-retweet fa-fw"></i>${this.toot_count( |
|||
toot, |
|||
"reblogs", |
|||
)}</a> |
|||
</div> |
|||
<div class="favourites ${this.toot_active(toot, "favourites")}"> |
|||
<a href="${ |
|||
toot.url |
|||
}" rel="nofollow"><i class="fa fa-star fa-fw"></i>${this.toot_count( |
|||
toot, |
|||
"favourites", |
|||
)}</a> |
|||
</div> |
|||
`;
|
|||
} |
|||
|
|||
user_account(account) { |
|||
var result = `@${account.acct}`; |
|||
if (account.acct.indexOf("@") === -1) { |
|||
var domain = new URL(account.url); |
|||
result += `@${domain.hostname}`; |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
render_toots(toots, in_reply_to, depth) { |
|||
var tootsToRender = toots |
|||
.filter((toot) => toot.in_reply_to_id === in_reply_to) |
|||
.sort((a, b) => a.created_at.localeCompare(b.created_at)); |
|||
tootsToRender.forEach((toot) => this.render_toot(toots, toot, depth)); |
|||
} |
|||
|
|||
render_toot(toots, toot, depth) { |
|||
toot.account.display_name = this.escapeHtml(toot.account.display_name); |
|||
toot.account.emojis.forEach((emoji) => { |
|||
toot.account.display_name = toot.account.display_name.replace( |
|||
`:${emoji.shortcode}:`, |
|||
`<img src="${this.escapeHtml(emoji.static_url)}" alt="Emoji ${ |
|||
emoji.shortcode |
|||
}" height="20" width="20" />`,
|
|||
); |
|||
}); |
|||
|
|||
const mastodonComment = `<div class="mastodon-comment" style="margin-left: calc(var(--comment-indent) * ${depth})">
|
|||
<div class="author"> |
|||
<div class="avatar"> |
|||
<img src="${this.escapeHtml( |
|||
toot.account.avatar_static, |
|||
)}" height=60 width=60 alt=""> |
|||
</div> |
|||
<div class="details"> |
|||
<a class="name" href="${toot.account.url}" rel="nofollow">${ |
|||
toot.account.display_name |
|||
}</a> |
|||
<a class="user" href="${ |
|||
toot.account.url |
|||
}" rel="nofollow">${this.user_account(toot.account)}</a> |
|||
</div> |
|||
<a class="date" href="${ |
|||
toot.url |
|||
}" rel="nofollow">${toot.created_at.substr( |
|||
0, |
|||
10, |
|||
)} ${toot.created_at.substr(11, 8)}</a> |
|||
</div> |
|||
<div class="content">${toot.content}</div> |
|||
<div class="attachments"> |
|||
${toot.media_attachments |
|||
.map((attachment) => { |
|||
if (attachment.type === "image") { |
|||
return `<a href="${attachment.url}" rel="nofollow"><img src="${ |
|||
attachment.preview_url |
|||
}" alt="${this.escapeHtml(attachment.description)}" /></a>`; |
|||
} else if (attachment.type === "video") { |
|||
return `<video controls><source src="${attachment.url}" type="${attachment.mime_type}"></video>`; |
|||
} else if (attachment.type === "gifv") { |
|||
return `<video autoplay loop muted playsinline><source src="${attachment.url}" type="${attachment.mime_type}"></video>`; |
|||
} else if (attachment.type === "audio") { |
|||
return `<audio controls><source src="${attachment.url}" type="${attachment.mime_type}"></audio>`; |
|||
} else { |
|||
return `<a href="${attachment.url}" rel="nofollow">${attachment.type}</a>`; |
|||
} |
|||
}) |
|||
.join("")} |
|||
</div> |
|||
<div class="status"> |
|||
${this.toot_stats(toot)} |
|||
</div> |
|||
</div>`; |
|||
|
|||
var div = document.createElement("div"); |
|||
div.innerHTML = |
|||
typeof DOMPurify !== "undefined" |
|||
? DOMPurify.sanitize(mastodonComment.trim()) |
|||
: mastodonComment.trim(); |
|||
document |
|||
.getElementById("mastodon-comments-list") |
|||
.appendChild(div.firstChild); |
|||
|
|||
this.render_toots(toots, toot.id, depth + 1); |
|||
} |
|||
|
|||
loadComments() { |
|||
if (this.commentsLoaded) return; |
|||
|
|||
document.getElementById("mastodon-comments-list").innerHTML = |
|||
"Loading comments from the Fediverse..."; |
|||
|
|||
let _this = this; |
|||
|
|||
fetch("https://" + this.host + "/api/v1/statuses/" + this.tootId) |
|||
.then((response) => response.json()) |
|||
.then((toot) => { |
|||
document.getElementById("mastodon-stats").innerHTML = |
|||
this.toot_stats(toot); |
|||
}); |
|||
|
|||
fetch( |
|||
"https://" + this.host + "/api/v1/statuses/" + this.tootId + "/context", |
|||
) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
if ( |
|||
data["descendants"] && |
|||
Array.isArray(data["descendants"]) && |
|||
data["descendants"].length > 0 |
|||
) { |
|||
document.getElementById("mastodon-comments-list").innerHTML = ""; |
|||
_this.render_toots(data["descendants"], _this.tootId, 0); |
|||
} else { |
|||
document.getElementById("mastodon-comments-list").innerHTML = |
|||
"<p>No comments found</p>"; |
|||
} |
|||
|
|||
_this.commentsLoaded = true; |
|||
}); |
|||
} |
|||
|
|||
respondToVisibility(element, callback) { |
|||
var options = { |
|||
root: null, |
|||
}; |
|||
|
|||
var observer = new IntersectionObserver((entries, observer) => { |
|||
entries.forEach((entry) => { |
|||
if (entry.intersectionRatio > 0) { |
|||
callback(); |
|||
} |
|||
}); |
|||
}, options); |
|||
|
|||
observer.observe(element); |
|||
} |
|||
} |
|||
|
|||
customElements.define("mastodon-comments", MastodonComments); |
@ -0,0 +1,46 @@ |
|||
local function ensureHtmlDeps() |
|||
quarto.doc.addHtmlDependency({ |
|||
name = 'mastodon-comments', |
|||
version = '1.0.0', |
|||
scripts = {"mastodon-comments.js"}, |
|||
}) |
|||
end |
|||
|
|||
function Meta(m) |
|||
ensureHtmlDeps() |
|||
if m.mastodon_comments and m.mastodon_comments.user and m.mastodon_comments.toot_id and m.mastodon_comments.host then |
|||
local user = pandoc.utils.stringify(m.mastodon_comments.user) |
|||
local toot_id = pandoc.utils.stringify(m.mastodon_comments.toot_id) |
|||
local host = pandoc.utils.stringify(m.mastodon_comments.host) |
|||
local mastodon_html = '<mastodon-comments host="' .. host .. '" user="' .. user .. '" tootId="' .. toot_id .. '"></mastodon-comments>' |
|||
|
|||
-- JavaScript to inject Mastodon comments into a specific div |
|||
local inject_script = [[ |
|||
<script type="text/javascript"> |
|||
document.addEventListener('DOMContentLoaded', function() { |
|||
var div = document.getElementById('quarto-content'); |
|||
if(div) { |
|||
div.innerHTML += `]] .. mastodon_html .. [[`; |
|||
} |
|||
}); |
|||
</script> |
|||
]] |
|||
|
|||
-- Include external scripts and styles directly |
|||
local script_html = '<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js" integrity="sha512-uHOKtSfJWScGmyyFr2O2+efpDx2nhwHU2v7MVeptzZoiC7bdF6Ny/CmZhN2AwIK1oCFiVQQ5DA/L9FSzyPNu6Q==" crossorigin="anonymous"></script>' |
|||
local css_html = '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">' |
|||
|
|||
-- Insert these elements in the document's head |
|||
quarto.doc.includeText("in-header", script_html .. css_html .. inject_script) |
|||
|
|||
-- JavaScript variable definitions |
|||
local js_vars = '<script type="text/javascript">\n' .. |
|||
'var mastodonHost = "' .. host .. '";\n' .. |
|||
'var mastodonUser = "' .. user .. '";\n' .. |
|||
'var mastodonTootId = "' .. toot_id .. '";\n' .. |
|||
'</script>' |
|||
|
|||
-- Include JavaScript variables in the header |
|||
quarto.doc.includeText("in-header", js_vars) |
|||
end |
|||
end |
Loading…
Reference in new issue