142 lines
5.2 KiB
JavaScript
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, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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>';
|
|
});
|
|
}
|