+{% if MASTODON_HOST and MASTODON_USERNAME and article.metadata['mastodon-id'] %}
+<section id="comments" class="article-content">
+ <h2>Comments</h2>
+ <p>With an account on the Fediverse or Mastodon, you can respond to this <a href="https://{{ MASTODON_HOST }}/@{{ MASTODON_USERNAME }}/{{ article.metadata['mastodon-id'] }}">post</a>. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.</p>
+ <p>Learn how this is implemented <a class="link" href="https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/">here</a>.</p>
+
+ <p id="mastodon-comments-list"><button id="load-comment">Load comments</button></p>
+ <div id="comments-wrapper">
+ <noscript><p>Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit <a href="https://{{ MASTODON_HOST }}/@{{ MASTODON_USERNAME }}/{{ article.metadata['mastodon-id'] }}">the original post</a> on Mastodon.</p></noscript>
+ </div>
+ <noscript>You need JavaScript to view the comments.</noscript>
+ <script src="/assets/js/purify.min.js"></script>
+ <script type="text/javascript">
+ function escapeHtml(unsafe) {
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+ function emojify(input, emojis) {
+ let output = input;
+
+ emojis.forEach(emoji => {
+ let picture = document.createElement("picture");
+
+ let source = document.createElement("source");
+ source.setAttribute("srcset", escapeHtml(emoji.url));
+ source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
+
+ let img = document.createElement("img");
+ img.className = "emoji";
+ img.setAttribute("src", escapeHtml(emoji.static_url));
+ img.setAttribute("alt", `:${ emoji.shortcode }:`);
+ img.setAttribute("title", `:${ emoji.shortcode }:`);
+ img.setAttribute("width", "20");
+ img.setAttribute("height", "20");
+
+ picture.appendChild(source);
+ picture.appendChild(img);
+
+ output = output.replace(`:${ emoji.shortcode }:`, picture.outerHTML);
+ });
+
+ return output;
+ }
+
+ function loadComments() {
+ let commentsWrapper = document.getElementById("comments-wrapper");
+ let loadCommentsButton = document.getElementById("load-comment");
+ loadCommentsButton.innerHTML = "Loading";
+ fetch('https://{{ MASTODON_HOST }}/api/v1/statuses/{{ article.metadata['mastodon-id'] }}/context')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(data) {
+ let descendants = data['descendants'];
+ if(
+ descendants &&
+ Array.isArray(descendants) &&
+ descendants.length > 0
+ ) {
+ commentsWrapper.innerHTML = "";
+
+ descendants.forEach(function(status) {
+ console.log(descendants)
+ if( status.account.display_name.length > 0 ) {
+ status.account.display_name = escapeHtml(status.account.display_name);
+ status.account.display_name = emojify(status.account.display_name, status.account.emojis);
+ } else {
+ status.account.display_name = status.account.username;
+ };
+
+ let instance = "";
+ if( status.account.acct.includes("@") ) {
+ instance = status.account.acct.split("@")[1];
+ } else {
+ instance = "{{ MASTODON_HOST }}";
+ }
+
+ const isReply = status.in_reply_to_id !== "{{ article.metadata['mastodon-id'] }}";
+
+ let op = false;
+ if( status.account.acct == "{{ MASTODON_USERNAME }}" ) {
+ op = true;
+ }
+
+ status.content = emojify(status.content, status.emojis);
+
+ let avatarSource = document.createElement("source");
+ avatarSource.setAttribute("srcset", escapeHtml(status.account.avatar));
+ avatarSource.setAttribute("media", "(prefers-reduced-motion: no-preference)");
+
+ let avatarImg = document.createElement("img");
+ avatarImg.className = "avatar";
+ avatarImg.setAttribute("src", escapeHtml(status.account.avatar_static));
+ avatarImg.setAttribute("alt", `@${ status.account.username }@${ instance } avatar`);
+
+ let avatarPicture = document.createElement("picture");
+ avatarPicture.appendChild(avatarSource);
+ avatarPicture.appendChild(avatarImg);
+
+ let avatar = document.createElement("a");
+ avatar.className = "avatar-link";
+ avatar.setAttribute("href", status.account.url);
+ avatar.setAttribute("rel", "external nofollow");
+ avatar.setAttribute("title", `View profile at @${ status.account.username }@${ instance }`);
+ avatar.appendChild(avatarPicture);
+
+ let instanceBadge = document.createElement("a");
+ instanceBadge.className = "instance";
+ instanceBadge.setAttribute("href", status.account.url);
+ instanceBadge.setAttribute("title", `@${ status.account.username }@${ instance }`);
+ instanceBadge.setAttribute("rel", "external nofollow");
+ instanceBadge.textContent = instance;
+
+ let display = document.createElement("span");
+ display.className = "display";
+ display.setAttribute("itemprop", "author");
+ display.setAttribute("itemtype", "http://schema.org/Person");
+ display.innerHTML = status.account.display_name;
+
+ let header = document.createElement("header");
+ header.className = "author";
+ header.appendChild(display);
+ header.appendChild(instanceBadge);
+
+ let permalink = document.createElement("a");
+ permalink.setAttribute("href", status.url);
+ permalink.setAttribute("itemprop", "url");
+ permalink.setAttribute("title", `View comment at ${ instance }`);
+ permalink.setAttribute("rel", "external nofollow");
+ permalink.textContent = new Date( status.created_at ).toLocaleString('en-US', {
+ dateStyle: "long",
+ timeStyle: "short",
+ });
+
+ let timestamp = document.createElement("time");
+ timestamp.setAttribute("datetime", status.created_at);
+ timestamp.appendChild(permalink);
+
+ let main = document.createElement("main");
+ main.setAttribute("itemprop", "text");
+ main.innerHTML = status.content;
+
+ let interactions = document.createElement("footer");
+ if(status.favourites_count > 0) {
+ let faves = document.createElement("a");
+ faves.className = "faves";
+ faves.setAttribute("href", `${ status.url }/favourites`);
+ faves.setAttribute("title", `Favorites from ${ instance }`);
+ faves.textContent = status.favourites_count;
+
+ interactions.appendChild(faves);
+ }
+
+ let comment = document.createElement("article");
+ comment.id = `comment-${ status.id }`;
+ comment.className = isReply ? "comment comment-reply" : "comment";
+ comment.setAttribute("itemprop", "comment");
+ comment.setAttribute("itemtype", "http://schema.org/Comment");
+ comment.appendChild(avatar);
+ comment.appendChild(header);
+ comment.appendChild(timestamp);
+ comment.appendChild(main);
+ comment.appendChild(interactions);
+
+ if(op === true) {
+ comment.classList.add("op");
+
+ avatar.classList.add("op");
+ avatar.setAttribute(
+ "title",
+ "Blog post author; " + avatar.getAttribute("title")
+ );
+
+ instanceBadge.classList.add("op");
+ instanceBadge.setAttribute(
+ "title",
+ "Blog post author: " + instanceBadge.getAttribute("title")
+ );
+ }
+
+ commentsWrapper.innerHTML += DOMPurify.sanitize(comment.outerHTML);
+ });
+ }
+ loadCommentsButton.hidden = true;
+ });
+ }
+ document.getElementById("load-comment").addEventListener("click", loadComments);
+ </script>
+</section>
+{% endif %}
<p>Posted{% if article.authors %} by {% for author in article.authors %}<a href="{{ author.url|format_siteurl|e }}">{{ author|e }}</a>{% endfor %}{% endif %} on <time datetime="{{ article.date.isoformat() }}">{{ article.locale_date }}</time> in <a href="{{ article.category.url|format_siteurl|e }}">{{ article.category|e }}</a>.{% if article.modified %} <span class="m-label m-success">updated <time datetime="{{ article.modified.isoformat() }}">{{ article.locale_modified }}</time></span>{% endif %}{% if article.archived == 'True' %} <span class="m-label m-warning">archived</span>{% endif %}{% if article.status == 'draft' %} <span class="m-label m-dim">draft</span>{% endif %}{% if article.tags %} Tags: {% for tag in article.tags %}<a href="{{ tag.url|format_siteurl|e }}">{{ tag|e }}</a>{% if not loop.last %}, {% endif %}{% endfor %}.{% endif %}</p>