2025-03-04 04:23:12 +01:00
// Based on https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/
// Attachment, card, and spoiler code is from https://github.com/cassidyjames/cassidyjames.github.io/blob/99782788a7e3ba3cc52d6803010873abd1b02b9e/_includes/comments.html#L251-L296
2025-02-06 18:05:35 +01:00
2025-03-04 04:23:12 +01:00
let lazyAsyncImage = document . getElementById ( "lazy-async-image" ) . textContent ;
let relAttributes = document . getElementById ( "rel-attributes" ) . textContent ;
2025-02-06 18:05:35 +01:00
let dateLocale = document . getElementById ( "date-locale" ) . textContent ;
let host = document . getElementById ( "host" ) . textContent ;
2025-03-04 04:23:12 +01:00
let user = document . getElementById ( "user" ) . textContent ;
2025-02-06 18:05:35 +01:00
let id = document . getElementById ( "id" ) . textContent ;
2025-03-04 04:23:12 +01:00
let articleAuthorText = document . getElementById ( "article-author-text" ) . textContent ;
2025-02-06 18:05:35 +01:00
let loadingText = document . getElementById ( "loading-text" ) . textContent ;
let noCommentsText = document . getElementById ( "no-comments-text" ) . textContent ;
let reloadText = document . getElementById ( "reload-text" ) . textContent ;
let sensitiveText = document . getElementById ( "sensitive-text" ) . textContent ;
document . getElementById ( "load-comments" ) . addEventListener ( "click" , loadComments ) ;
function escapeHtml ( unsafe ) {
return unsafe
. replace ( /&/g , "&" )
. replace ( /</g , "<" )
. replace ( />/g , ">" )
. replace ( /"/g , """ )
2025-03-04 04:23:12 +01:00
. replace ( /'/g , "'" ) ;
2025-02-06 18:05:35 +01:00
}
2025-03-04 04:23:12 +01:00
2025-02-06 18:05:35 +01:00
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 ( "title" , ` : ${ emoji . shortcode } : ` ) ;
2025-03-04 04:23:12 +01:00
img . setAttribute ( "width" , "24" ) ;
img . setAttribute ( "height" , "24" ) ;
2025-02-06 18:05:35 +01:00
if ( lazyAsyncImage == "true" ) {
img . setAttribute ( "decoding" , "async" ) ;
img . setAttribute ( "loading" , "lazy" ) ;
}
picture . appendChild ( source ) ;
picture . appendChild ( img ) ;
output = output . replace ( ` : ${ emoji . shortcode } : ` , picture . outerHTML ) ;
} ) ;
return output ;
}
function loadComments ( ) {
let commentsWrapper = document . getElementById ( "comments-wrapper" ) ;
commentsWrapper . innerHTML = "" ;
let loadCommentsButton = document . getElementById ( "load-comments" ) ;
loadCommentsButton . innerHTML = loadingText ;
loadCommentsButton . disabled = true ;
fetch ( ` https:// ${ host } /api/v1/statuses/ ${ 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 = host ;
}
const isReply = status . in _reply _to _id !== id ;
let op = false ;
if ( status . account . acct == user ) {
op = true ;
}
status . content = emojify ( status . content , status . emojis ) ;
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" ) ;
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 `
) ;
if ( lazyAsyncImage == "true" ) {
avatarImg . setAttribute ( "decoding" , "async" ) ;
avatarImg . setAttribute ( "loading" , "lazy" ) ;
}
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" , relAttributes ) ;
avatar . appendChild ( avatarPicture ) ;
comment . appendChild ( avatar ) ;
2025-03-04 04:23:12 +01:00
let display = document . createElement ( "a" ) ;
2025-02-06 18:05:35 +01:00
display . className = "display" ;
2025-03-04 04:23:12 +01:00
display . setAttribute ( "href" , status . account . url ) ;
display . setAttribute ( "rel" , relAttributes ) ;
2025-02-06 18:05:35 +01:00
display . setAttribute ( "itemprop" , "author" ) ;
display . setAttribute ( "itemtype" , "http://schema.org/Person" ) ;
display . innerHTML = status . account . display _name ;
2025-03-04 04:23:12 +01:00
let instanceBadge = document . createElement ( "span" ) ;
instanceBadge . className = "instance" ;
instanceBadge . textContent = ` @ ${ status . account . username } @ ${ instance } ` ;
2025-02-06 18:05:35 +01:00
let permalink = document . createElement ( "a" ) ;
permalink . setAttribute ( "href" , status . url ) ;
permalink . setAttribute ( "itemprop" , "url" ) ;
permalink . setAttribute ( "rel" , relAttributes ) ;
permalink . textContent = new Date (
status . created _at
) . toLocaleString ( dateLocale , {
dateStyle : "long" ,
timeStyle : "short" ,
} ) ;
let timestamp = document . createElement ( "time" ) ;
timestamp . setAttribute ( "datetime" , status . created _at ) ;
2025-03-04 04:23:12 +01:00
timestamp . classList . add ( "timestamp" ) ;
2025-02-06 18:05:35 +01:00
timestamp . appendChild ( permalink ) ;
permalink . classList . add ( "external" ) ;
2025-03-04 04:23:12 +01:00
let header = document . createElement ( "header" ) ;
header . appendChild ( display ) ;
header . appendChild ( instanceBadge ) ;
header . appendChild ( timestamp ) ;
comment . appendChild ( header ) ;
2025-02-06 18:05:35 +01:00
let main = document . createElement ( "main" ) ;
main . setAttribute ( "itemprop" , "text" ) ;
if ( status . sensitive == true || status . spoiler _text != "" ) {
let summary = document . createElement ( "summary" ) ;
if ( status . spoiler _text == "" ) {
status . spoiler _text == sensitiveText ;
}
summary . innerHTML = status . spoiler _text ;
let spoiler = document . createElement ( "details" ) ;
spoiler . appendChild ( summary ) ;
spoiler . innerHTML += status . content ;
main . appendChild ( spoiler ) ;
} else {
main . innerHTML = status . content ;
}
comment . appendChild ( main ) ;
let attachments = status . media _attachments ;
let SUPPORTED _MEDIA = [ "image" , "video" , "gifv" , "audio" ] ;
let media = document . createElement ( "div" ) ;
media . className = "attachments" ;
if (
attachments &&
Array . isArray ( attachments ) &&
attachments . length > 0
) {
attachments . forEach ( ( attachment ) => {
if ( SUPPORTED _MEDIA . includes ( attachment . type ) ) {
let mediaElement ;
switch ( attachment . type ) {
case "image" :
mediaElement = document . createElement ( "img" ) ;
mediaElement . setAttribute ( "src" , attachment . preview _url ) ;
if ( attachment . description != null ) {
mediaElement . setAttribute ( "title" , attachment . description ) ;
}
if ( lazyAsyncImage == "true" ) {
mediaElement . setAttribute ( "decoding" , "async" ) ;
mediaElement . setAttribute ( "loading" , "lazy" ) ;
}
if ( status . sensitive == true ) {
mediaElement . classList . add ( "spoiler" ) ;
}
media . appendChild ( mediaElement ) ;
break ;
case "video" :
mediaElement = document . createElement ( "video" ) ;
mediaElement . setAttribute ( "src" , attachment . url ) ;
mediaElement . setAttribute ( "controls" , "" ) ;
if ( attachment . description != null ) {
mediaElement . setAttribute ( "title" , attachment . description ) ;
}
if ( status . sensitive == true ) {
mediaElement . classList . add ( "spoiler" ) ;
}
media . appendChild ( mediaElement ) ;
break ;
case "gifv" :
mediaElement = document . createElement ( "video" ) ;
mediaElement . setAttribute ( "src" , attachment . url ) ;
mediaElement . setAttribute ( "autoplay" , "" ) ;
mediaElement . setAttribute ( "playsinline" , "" ) ;
mediaElement . setAttribute ( "loop" , "" ) ;
if ( attachment . description != null ) {
mediaElement . setAttribute ( "title" , attachment . description ) ;
}
if ( status . sensitive == true ) {
mediaElement . classList . add ( "spoiler" ) ;
}
media . appendChild ( mediaElement ) ;
break ;
case "audio" :
mediaElement = document . createElement ( "audio" ) ;
mediaElement . setAttribute ( "src" , attachment . url ) ;
mediaElement . setAttribute ( "controls" , "" ) ;
if ( attachment . description != null ) {
mediaElement . setAttribute ( "title" , attachment . description ) ;
}
media . appendChild ( mediaElement ) ;
break ;
}
let mediaLink = document . createElement ( "a" ) ;
mediaLink . setAttribute ( "href" , attachment . url ) ;
mediaLink . setAttribute ( "rel" , relAttributes ) ;
mediaLink . appendChild ( mediaElement ) ;
media . appendChild ( mediaLink ) ;
}
} ) ;
comment . appendChild ( media ) ;
}
let interactions = document . createElement ( "footer" ) ;
let boosts = document . createElement ( "a" ) ;
boosts . className = "boosts" ;
boosts . setAttribute ( "href" , ` ${ status . url } /reblogs ` ) ;
let boostsIcon = document . createElement ( "i" ) ;
2025-03-04 04:23:12 +01:00
boostsIcon . classList . add ( "ph-bold" , "ph-repeat" ) ;
2025-02-06 18:05:35 +01:00
boosts . appendChild ( boostsIcon ) ;
boosts . insertAdjacentHTML ( 'beforeend' , ` ${ status . reblogs _count } ` ) ;
interactions . appendChild ( boosts ) ;
let faves = document . createElement ( "a" ) ;
faves . className = "faves" ;
faves . setAttribute ( "href" , ` ${ status . url } /favourites ` ) ;
let favesIcon = document . createElement ( "i" ) ;
2025-03-04 04:23:12 +01:00
favesIcon . classList . add ( "ph-bold" , "ph-star" ) ;
2025-02-06 18:05:35 +01:00
faves . appendChild ( favesIcon ) ;
faves . insertAdjacentHTML ( 'beforeend' , ` ${ status . favourites _count } ` ) ;
interactions . appendChild ( faves ) ;
2025-03-04 04:23:12 +01:00
if (
status . reactions &&
Array . isArray ( status . reactions ) &&
status . reactions . length > 0
) {
let reactions = document . createElement ( "div" ) ;
reactions . classList . add ( "reactions" , "overshoot-row" ) ;
status . reactions . forEach ( reaction => {
let reactionElement = document . createElement ( "span" ) ;
reactionElement . className = "reaction" ;
if ( reaction . url ) {
// Custom emoji
let img = document . createElement ( "img" ) ;
img . className = "emoji" ;
img . setAttribute ( "src" , escapeHtml ( reaction . url ) ) ;
img . setAttribute ( "title" , ` ${ reaction . name } ` ) ;
img . setAttribute ( "width" , "24" ) ;
img . setAttribute ( "height" , "24" ) ;
reactionElement . appendChild ( img ) ;
} else {
// Unicode emoji
let emoji = document . createElement ( "span" ) ;
emoji . textContent = reaction . name ;
reactionElement . appendChild ( emoji ) ;
}
// Append the count
let count = document . createElement ( "span" ) ;
count . textContent = reaction . count ;
reactionElement . appendChild ( count ) ;
reactions . appendChild ( reactionElement ) ;
} ) ;
interactions . appendChild ( reactions ) ;
}
2025-02-06 18:05:35 +01:00
comment . appendChild ( interactions ) ;
if ( status . card != null ) {
let cardFigure = document . createElement ( "figure" ) ;
if ( status . card . image != null ) {
let cardImg = document . createElement ( "img" ) ;
cardImg . setAttribute ( "src" , status . card . image ) ;
cardImg . classList . add ( "no-hover" ) ;
cardFigure . appendChild ( cardImg ) ;
}
let cardCaption = document . createElement ( "figcaption" ) ;
let cardTitle = document . createElement ( "strong" ) ;
cardTitle . innerHTML = status . card . title ;
cardCaption . appendChild ( cardTitle ) ;
if ( status . card . description != null && status . card . description . length > 0 ) {
let cardDescription = document . createElement ( "p" ) ;
cardDescription . innerHTML = status . card . description ;
cardCaption . appendChild ( cardDescription ) ;
}
cardFigure . appendChild ( cardCaption ) ;
let card = document . createElement ( "a" ) ;
card . className = "card" ;
card . setAttribute ( "href" , status . card . url ) ;
card . setAttribute ( "rel" , relAttributes ) ;
card . appendChild ( cardFigure ) ;
comment . appendChild ( card ) ;
}
if ( op === true ) {
comment . classList . add ( "op" ) ;
instanceBadge . classList . add ( "op" ) ;
2025-03-04 04:23:12 +01:00
instanceBadge . setAttribute ( "title" , articleAuthorText ) ;
2025-02-06 18:05:35 +01:00
}
2025-03-04 04:23:12 +01:00
commentsWrapper . appendChild ( comment ) ;
2025-02-06 18:05:35 +01:00
} ) ;
}
else {
var statusText = document . createElement ( "p" ) ;
statusText . innerHTML = noCommentsText ;
statusText . setAttribute ( "id" , "comments-status" ) ;
commentsWrapper . appendChild ( statusText ) ;
}
loadCommentsButton . innerHTML = reloadText ;
} )
. catch ( function ( error ) {
console . error ( 'Error loading comments:' , error ) ;
} )
. finally ( function ( ) {
loadCommentsButton . disabled = false ;
} ) ;
}