Files
marcus-web/static/js/mastodon-comments.js
T

142 lines
5.2 KiB
JavaScript

/**
* Mastodon Comments Loader
*
* Fetches and displays replies to a Mastodon post.
* Uses DOMPurify for HTML sanitization.
*
* Inspired by Andreas Scherbaum's implementation.
* Aesthetic inspired by I Saw the TV Glow.
*/
var commentsLoaded = false;
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function formatDate(dateStr) {
var d = new Date(dateStr);
var year = d.getFullYear();
var month = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
var hours = String(d.getHours()).padStart(2, '0');
var mins = String(d.getMinutes()).padStart(2, '0');
return year + '-' + month + '-' + day + ' ' + hours + ':' + mins;
}
function getUserHandle(account) {
var handle = '@' + account.acct;
if (account.acct.indexOf('@') === -1) {
var domain = new URL(account.url);
handle += '@' + domain.hostname;
}
return handle;
}
function renderComment(toot, depth) {
// Skip blocked toots
if (blockedToots.includes(toot.url)) {
return '';
}
// Process display name with custom emojis
var displayName = escapeHtml(toot.account.display_name || toot.account.username);
toot.account.emojis.forEach(function(emoji) {
var emojiImg = '<img src="' + escapeHtml(emoji.static_url) + '" alt=":' + emoji.shortcode + ':" class="emoji" height="18" width="18">';
displayName = displayName.replace(':' + emoji.shortcode + ':', emojiImg);
});
var indent = depth > 0 ? ' style="margin-left: ' + (depth * 20) + 'px; border-left: 2px dashed #666;"' : '';
var html = '<div class="mastodon-comment"' + indent + '>';
html += '<div class="comment-header">';
html += '<pre>';
html += '/* ---------------------------------------- */\n';
html += '/* FROM: ' + getUserHandle(toot.account).padEnd(32) + ' */\n';
html += '/* DATE: ' + formatDate(toot.created_at).padEnd(32) + ' */\n';
html += '/* ---------------------------------------- */</pre>';
html += '</div>';
html += '<div class="comment-author">';
html += '<img src="' + escapeHtml(toot.account.avatar_static) + '" alt="" class="avatar">';
html += '<div class="author-info">';
html += '<a href="' + escapeHtml(toot.account.url) + '" rel="nofollow" class="display-name">' + displayName + '</a>';
html += '<span class="handle">' + escapeHtml(getUserHandle(toot.account)) + '</span>';
html += '</div>';
html += '</div>';
html += '<div class="comment-content">' + toot.content + '</div>';
// Media attachments
if (toot.media_attachments && toot.media_attachments.length > 0) {
html += '<div class="comment-attachments">';
toot.media_attachments.forEach(function(attachment) {
if (attachment.type === 'image') {
html += '<a href="' + escapeHtml(attachment.url) + '" rel="nofollow"><img src="' + escapeHtml(attachment.preview_url) + '" alt="' + escapeHtml(attachment.description || 'attachment') + '"></a>';
} else if (attachment.type === 'video' || attachment.type === 'gifv') {
html += '<video controls ' + (attachment.type === 'gifv' ? 'autoplay loop muted' : '') + '><source src="' + escapeHtml(attachment.url) + '"></video>';
}
});
html += '</div>';
}
html += '<div class="comment-meta">';
html += '<a href="' + escapeHtml(toot.url) + '" rel="nofollow" class="comment-link">[VIEW ORIGINAL]</a>';
if (toot.replies_count > 0) {
html += ' <span class="replies">[' + toot.replies_count + ' REPLIES]</span>';
}
html += '</div>';
html += '</div>';
return html;
}
function renderComments(toots, parentId, depth) {
var html = '';
var replies = toots
.filter(function(toot) { return toot.in_reply_to_id === parentId; })
.sort(function(a, b) { return a.created_at.localeCompare(b.created_at); });
replies.forEach(function(toot) {
html += renderComment(toot, depth);
html += renderComments(toots, toot.id, depth + 1);
});
return html;
}
function loadMastodonComments() {
if (commentsLoaded) return;
var container = document.getElementById('mastodon-comments-list');
container.innerHTML = '<pre class="loading">\n++ ESTABLISHING CONNECTION TO ' + mastodonHost.toUpperCase() + ' ++\n++ PLEASE STAND BY ++\n</pre>';
var apiUrl = 'https://' + mastodonHost + '/api/v1/statuses/' + mastodonId + '/context';
fetch(apiUrl)
.then(function(response) {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(function(data) {
if (data.descendants && data.descendants.length > 0) {
var html = '<pre class="comments-received">\n++ TRANSMISSION COMPLETE ++\n++ ' + data.descendants.length + ' COMMENT(S) RECEIVED ++\n</pre>';
html += renderComments(data.descendants, mastodonId, 0);
container.innerHTML = DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
} else {
container.innerHTML = '<pre class="no-comments">\n++ NO COMMENTS RECEIVED ++\n++ BE THE FIRST TO RESPOND ++\n</pre>';
}
commentsLoaded = true;
})
.catch(function(error) {
container.innerHTML = '<pre class="comments-error">\n++ TRANSMISSION ERROR ++\n++ FAILED TO LOAD COMMENTS ++\n++ ERROR: ' + escapeHtml(error.message) + ' ++\n</pre>';
});
}