Skip to content

Instantly share code, notes, and snippets.

@lylejantzi3rd
Last active April 21, 2024 21:18
Show Gist options
  • Save lylejantzi3rd/a11afd2d3d6c7370156cec8576f53536 to your computer and use it in GitHub Desktop.
Save lylejantzi3rd/a11afd2d3d6c7370156cec8576f53536 to your computer and use it in GitHub Desktop.
Flatten Substack Comments
const postId = window._preloads.post.id;
const url = window._preloads.base_url;
async function getComments() {
const response = await fetch(`${url}/api/v1/post/${postId}/comments?token=&all_comments=true&sort=most_recent_first`);
const result = await response.json();
return result?.comments;
}
function getAllChildren(children, acc = []) {
for(const child of children) {
if (child?.children?.length) {
acc = [...acc, ...getAllChildren(child.children)];
}
acc.push(child);
}
return acc;
}
function sortAsc(a, b) {
let dateA = new Date(a.edited_at || a.date);
let dateB = new Date(b.edited_at || b.date);
return dateA - dateB;
}
function formatDate(date) {
let seconds = Math.floor((new Date() - date) / 1000);
let interval = 0;
interval = seconds / 31536000;
if (interval > 1) {
const month = date.toLocaleString('default', { month: 'long' });
return `${month} ${date.getDate()}, ${date.getFullYear()}`;
}
interval = seconds / 86400;
if (interval > 1) {
const month = date.toLocaleString('default', { month: 'long' });
return `${month} ${date.getDate()}`;
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hrs ago";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes ago";
}
return Math.floor(seconds) + " seconds ago";
}
function printCommentBody(body) {
const paragraphs = body.split(/\r\n|\r|\n/).filter(Boolean);
return paragraphs.map(p => `<p><span>${p}</p></span>`).join("\n");
}
function printPublication(metadata) {
const pub = metadata?.author_on_other_pub;
if (pub) {
return `
<div class="_publicationHoverCardTarget_1ypz6_51">
<a class="commenter-publication" href="${pub.base_url}" rel="nofollow" target="_blank">
<span>${pub.name}</span>
</a>
</div>`;
}
return '';
}
function printComment(comment) {
let profileUrl = '';
if(comment.user_banned && comment.name) {
comment.name = comment.name + ' (Banned)';
profileUrl = `https://substack.com/profile/${comment.user_id}-${comment.name.replaceAll(' ', '-').toLowerCase()}`;
} else if(comment.user_banned && !comment.name) {
comment.name = 'Removed (Banned)';
comment.body = 'Comment removed';
profileUrl = 'https://substack.com/profile/null'
comment.photo_url = 'https%3A%2F%2Fsubstack.com%2Fimg%2Favatars%2Flogged-out.png';
} else if ('deleted' === comment.status) {
comment.name = 'deleted';
comment.body = 'Comment deleted';
profileUrl = 'https://substack.com/profile/null'
comment.photo_url = 'https%3A%2F%2Fsubstack.com%2Fimg%2Favatars%2Flogged-out.png';
} else if ('moderator_removed' === comment.status) {
comment.name = 'Removed';
comment.body = 'Comment removed';
profileUrl = 'https://substack.com/profile/null'
comment.photo_url = 'https%3A%2F%2Fsubstack.com%2Fimg%2Favatars%2Flogged-out.png';
} else {
profileUrl = `https://substack.com/profile/${comment.user_id}-${comment.name.replaceAll(' ', '-').toLowerCase()}`;
}
const profilePhotoUrl = comment.photo_url ? encodeURIComponent(comment.photo_url) : 'https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe3f930-3206-4dd0-a1e4-138ce0b0b743_144x144.png';
return `
<div class="comment">
<div id="comment-${comment.id}" class="comment-anchor"></div>
<div id="comment-${comment.id}-reply" class="comment-anchor"></div>
<table class="comment-content">
<tr>
<td class="comment-head">
<div class="profile-hover-card-target _profileHoverCardTarget_1ypz6_50">
<div class="user-head">
<a href="${profileUrl}">
<div class="profile-img-wrap">
<img
class="_img_16u6n_1 pencraft pc-reset"
src="https://substackcdn.com/image/fetch/w_66,h_66,c_fill,f_auto,q_auto:good,fl_progressive:steep/${profilePhotoUrl}"
sizes="100vw"
alt=""
width="66"
height="66"
/>
${comment?.user_banned ? `
<svg role="img" width="100" height="100" viewBox="0 0 100 100" fill="none" stroke-width="1.8" stroke="#000" xmlns="http://www.w3.org/2000/svg">
<g>
<title></title>
<circle cx="50" cy="50" stroke="#000000" r="46" stroke-width="8"></circle>
<line x1="20" y1="20" x2="80" y2="80" stroke="#000000" stroke-width="8"></line>
</g>
</svg>
`: ''}
</div>
</a>
</div>
</div>
</td>
<td class="comment-rest">
<div class="comment-meta">
<span class="commenter-name">
<div class="pencraft pc-display-flex pc-gap-4 pc-alignItems-center pc-reset pc-display-inline-flex">
<div class="profile-hover-card-target _profileHoverCardTarget_1ypz6_50">
<a href="${profileUrl}">
<div class="pencraft pc-reset _color-pub-primary-text_1k90y_204 _line-height-20_1k90y_95 _font-text_1k90y_121 _size-13_1k90y_45 _weight-semibold_1k90y_165 _reset_1k90y_1">${comment.name}</div>
</a>
</div>
</div>
</span>
${printPublication(comment.metadata)}
<a class="comment-timestamp" href="${url}/comment/${comment.id}" rel="nofollow" native="true">${formatDate(new Date(comment.date))}</a>
</div>
<div class="comment-body">${printCommentBody(comment.body)}</div>
</td>
</tr>
</table>
<div class="comment-list-collapser">
<div class="comment-list-collapser-line"></div>
</div>
</div>
`;
}
function printComments(comments) {
return `
<div class="comment-list">
<div class="comment-list-items">
${comments.map(printComment).join("\n")}
</div>
</div>
<hr/>
`;
}
async function replaceComments() {
let comments = await getComments();
let flattenedComments = getAllChildren(comments);
flattenedComments.sort(sortAsc);
document.querySelector('.comment-list-container').innerHTML = printComments(flattenedComments);
}
replaceComments();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment