티스토리 수익 글 보기
/* global customElements, HTMLElement */
import DOMPurify from ‘./dependencies/dompurify/purify.js’
import { db } from ‘./dbInstance.js’
import { applyDefaults } from ‘./defaults.js’
import ‘./p2p-media.js’
import ‘./post-replies.js’
function formatDate (dateString) {
const options = { year: ‘numeric’, month: ‘short’, day: ‘numeric’ }
return new Date(dateString).toLocaleDateString(undefined, options)
}
// Helper function to calculate elapsed time (e.g., 1h, 1d, 1w)
function timeSince (dateString) {
const date = new Date(dateString)
const seconds = Math.floor((new Date() – date) / 1000)
let interval = seconds / 31536000 // 365 * 24 * 60 * 60
if (interval > 1) {
return formatDate(dateString) // Return formatted date if more than a year
}
interval = seconds / 2592000 // 30 * 24 * 60 * 60
if (interval > 1) {
return Math.floor(interval) + ‘mo’
}
interval = seconds / 604800 // 7 * 24 * 60 * 60
if (interval > 1) {
return Math.floor(interval) + ‘w’
}
interval = seconds / 86400 // 24 * 60 * 60
if (interval > 1) {
return Math.floor(interval) + ‘d’
}
interval = seconds / 3600 // 60 * 60
if (interval > 1) {
return Math.floor(interval) + ‘h’
}
interval = seconds / 60
if (interval > 1) {
return Math.floor(interval) + ‘m’
}
return Math.floor(seconds) + ‘s’
}
function insertImagesAndVideos (content) {
const parser = new DOMParser()
const contentDOM = parser.parseFromString(content, ‘text/html’)
contentDOM.querySelectorAll(‘img’).forEach(img => {
const originalSrc = img.getAttribute(‘src’)
const p2pImg = document.createElement(‘p2p-image’)
p2pImg.setAttribute(‘src’, originalSrc)
// Append the image directly to the parent node
img.parentNode.replaceChild(p2pImg, img)
})
contentDOM.querySelectorAll(‘video’).forEach(video => {
const originalSrc = video.getAttribute(‘src’)
const p2pVideo = document.createElement(‘p2p-video’)
p2pVideo.setAttribute(‘src’, originalSrc)
// Append the video directly to the parent node
video.parentNode.replaceChild(p2pVideo, video)
})
// Return the modified content as a string
return contentDOM.body.innerHTML
}
// Define a class for the web component
class DistributedPost extends HTMLElement {
static get observedAttributes () {
return [‘url’]
}
async connectedCallback () {
await applyDefaults()
this.loadAndRenderPost(this.getAttribute(‘url’))
}
async loadAndRenderPost (postUrl) {
if (!postUrl) {
this.renderErrorContent(‘No post URL provided’)
return
}
try {
const content = await db.getNote(postUrl)
if (content && content.content) {
content.content = insertImagesAndVideos(content.content) // Resolve URLs before rendering
// Assuming JSON-LD content has a “summary” field
this.renderPostContent(content)
}
} catch (error) {
console.error(error)
this.renderErrorContent(error.message)
}
}
async renderPostContent (jsonLdData) {
// Clear existing content
this.innerHTML = ”
// Check if jsonLdData is an activity instead of a note
if (‘object’ in jsonLdData) {
this.renderErrorContent(‘Expected a Note but received an Activity’)
return
}
// Create the container for the post
const postContainer = document.createElement(‘div’)
postContainer.classList.add(‘distributed-post’)
// Header for the post, which will contain actor info and published time
const postHeader = document.createElement(‘header’)
postHeader.classList.add(‘distributed-post-header’)
// Determine the source of ‘attributedTo’ based on the structure of jsonLdData
let attributedToSource = jsonLdData.attributedTo
if (‘object’ in jsonLdData && ‘attributedTo’ in jsonLdData.object) {
attributedToSource = jsonLdData.object.attributedTo
}
// Create elements for each field, using the determined source for ‘attributedTo’
if (attributedToSource) {
const actorInfo = document.createElement(‘actor-info’)
actorInfo.setAttribute(‘url’, attributedToSource)
postHeader.appendChild(actorInfo)
}
// Published time element
const publishedTime = document.createElement(‘a’)
publishedTime.href = `/post.html?url=${encodeURIComponent(db.getObjectPage(jsonLdData))}`
publishedTime.classList.add(‘time-ago’)
const elapsed = timeSince(jsonLdData.published)
publishedTime.textContent = elapsed
postHeader.appendChild(publishedTime)
// Append the header to the post container
postContainer.appendChild(postHeader)
// Main content of the post
const postContent = document.createElement(‘div’)
postContent.classList.add(‘post-content’)
// Determine content source based on structure of jsonLdData
const contentSource = jsonLdData.content || (jsonLdData.object && jsonLdData.object.content)
const sanitizedContent = DOMPurify.sanitize(contentSource)
const parser = new DOMParser()
const contentDOM = parser.parseFromString(sanitizedContent, ‘text/html’)
// Insert images and videos into the DOM
const processedContent = insertImagesAndVideos(contentSource)
// Process all anchor elements to handle actor and posts mentions
const anchors = contentDOM.querySelectorAll(‘a’)
anchors.forEach(async (anchor) => {
const href = anchor.getAttribute(‘href’)
if (href) {
const fediverseActorMatch = href.match(/^(https?|ipns|hyper):\/\/([^\/]+)\/@(\w+)$/)
const jsonldActorMatch = href.endsWith(‘about.jsonld’)
const mastodonPostMatch = href.match(/^(https?|ipns|hyper):\/\/([^\/]+)\/@(\w+)\/(\d+)$/)
const jsonldPostMatch = href.endsWith(‘.jsonld’)
if (fediverseActorMatch || jsonldActorMatch) {
anchor.setAttribute(‘href’, `/profile.html?actor=${encodeURIComponent(href)}`)
try {
const actorData = await db.getActor(href)
if (actorData) {
anchor.setAttribute(‘href’, `/profile.html?actor=${encodeURIComponent(href)}`)
} else {
console.log(‘Actor not found in DB, default redirection applied.’)
}
} catch (error) {
console.error(‘Error fetching actor data:’, error)
}
} else if (mastodonPostMatch || jsonldPostMatch) {
anchor.setAttribute(‘href’, `/post.html?url=${encodeURIComponent(href)}`)
try {
const noteData = await db.getNote(href)
if (noteData) {
anchor.setAttribute(‘href’, `/post.html?url=${encodeURIComponent(href)}`)
} else {
console.log(‘Post not found in DB, default redirection applied.’)
}
} catch (error) {
console.error(‘Error fetching note data:’, error)
}
} else {
anchor.setAttribute(‘href’, href)
}
}
})
const isSensitive = jsonLdData.sensitive || (jsonLdData.object && jsonLdData.object.sensitive)
const summary = jsonLdData.summary || (jsonLdData.object && jsonLdData.object.summary)
if (isSensitive) {
// Handle sensitive content
const details = document.createElement(‘details’)
const summaryElement = document.createElement(‘summary’)
summaryElement.classList.add(‘cw-summary’)
summaryElement.textContent = ‘Sensitive Content (click to view)’
details.appendChild(summaryElement)
const contentElement = document.createElement(‘p’)
contentElement.innerHTML = processedContent
details.appendChild(contentElement)
postContent.appendChild(details)
} else if (summary) {
// Handle content with summary
const details = document.createElement(‘details’)
const summaryElement = document.createElement(‘summary’)
summaryElement.textContent = summary // Post title goes here
details.appendChild(summaryElement)
// Adding the “Show more” and “Show less” toggle text
const toggleText = document.createElement(‘span’)
toggleText.textContent = ‘Show more’
toggleText.classList.add(‘see-more-toggle’)
summaryElement.appendChild(toggleText)
const contentElement = document.createElement(‘p’)
contentElement.innerHTML = processedContent
details.appendChild(contentElement)
postContent.appendChild(details)
// Event listener to toggle the text of the Show more/Show less element
details.addEventListener(‘toggle’, function () {
toggleText.textContent = details.open ? ‘Show less’ : ‘Show more’
})
} else {
// Regular content without summary or sensitivity
postContent.innerHTML = processedContent
}
// Append the content to the post container
postContainer.appendChild(postContent)
// Footer of the post, which will contain the full published date and platform, but only the date is clickable
const postFooter = document.createElement(‘footer’)
postFooter.classList.add(‘post-footer’)
// Create a container for the full date and additional text
const dateContainer = document.createElement(‘div’)
// Create the clickable link for the date
const fullDateLink = document.createElement(‘a’)
fullDateLink.href = `/post.html?url=${encodeURIComponent(jsonLdData.id)}`
fullDateLink.classList.add(‘full-date’)
fullDateLink.textContent = formatDate(jsonLdData.published)
dateContainer.appendChild(fullDateLink)
// Add the ‘ · reader web’ text outside of the link
const readerWebText = document.createElement(‘span’)
readerWebText.textContent = ‘ · reader web’
dateContainer.appendChild(readerWebText)
// Append the date container to the footer
postFooter.appendChild(dateContainer)
const replyFooter = document.createElement(‘div’)
replyFooter.classList.add(‘reply-footer’)
const replyCountElement = document.createElement(‘reply-count’)
replyCountElement.classList.add(‘reply-count’)
replyCountElement.setAttribute(‘url’, jsonLdData.id)
replyFooter.appendChild(replyCountElement)
postFooter.appendChild(replyFooter)
// Handle attachments of other Fedi instances
if (!isSensitive && !jsonLdData.summary && jsonLdData.attachment && jsonLdData.attachment.length > 0) {
const attachmentsContainer = document.createElement(‘div’)
attachmentsContainer.className = ‘attachments-container’
jsonLdData.attachment.forEach(attachment => {
if (attachment.mediaType.startsWith(‘image/’)) {
// If it’s an image
const img = document.createElement(‘img’)
img.src = attachment.url
img.alt = attachment.name || ‘Attached image’
img.className = ‘attachment-image’
attachmentsContainer.appendChild(img)
} else if (attachment.mediaType.startsWith(‘video/’)) {
// If it’s a video
const video = document.createElement(‘video’)
video.src = attachment.url
video.alt = attachment.name || ‘Attached video’
video.className = ‘attachment-video’
video.controls = true
attachmentsContainer.appendChild(video)
}
})
postContainer.appendChild(attachmentsContainer)
}
// Append the footer to the post container
postContainer.appendChild(postFooter)
// Append the whole post container to the custom element
this.appendChild(postContainer)
const params = new URLSearchParams(window.location.search)
if (params.get(‘view’) === ‘replies’) {
const postReplies = document.createElement(‘post-replies’)
postReplies.setAttribute(‘url’, jsonLdData.id)
this.after(postReplies)
}
}
// appendField to optionally allow HTML content
appendField (label, value, isHTML = false) {
if (value) {
const p = document.createElement(‘p’)
const strong = document.createElement(‘strong’)
strong.textContent = `${label}:`
p.appendChild(strong)
if (isHTML) {
// If the content is HTML, set innerHTML directly
const span = document.createElement(‘span’)
span.innerHTML = value
p.appendChild(span)
} else {
// If not, treat it as text
p.appendChild(document.createTextNode(` ${value}`))
}
this.appendChild(p)
}
}
renderErrorContent (errorMessage) {
// Clear existing content
this.innerHTML = ”
const errorComponent = document.createElement(‘error-message’)
errorComponent.setAttribute(‘message’, errorMessage)
this.appendChild(errorComponent)
}
}
// Register the new element with the browser
customElements.define(‘distributed-post’, DistributedPost)
// Define a class for the web component
class ActorInfo extends HTMLElement {
static get observedAttributes () {
return [‘url’]
}
constructor () {
super()
this.actorUrl = ”
}
attributeChangedCallback (name, oldValue, newValue) {
if (name === ‘url’ && newValue) {
this.actorUrl = newValue
this.fetchAndRenderActorInfo(newValue)
}
}
navigateToActorProfile () {
window.location.href = `/profile.html?actor=${encodeURIComponent(this.actorUrl)}`
}
async fetchAndRenderActorInfo (url) {
try {
const actorInfo = await db.getActor(url)
if (actorInfo) {
// Clear existing content
this.innerHTML = ”
const author = document.createElement(‘div’)
author.classList.add(‘distributed-post-author’)
const authorDetails = document.createElement(‘div’)
authorDetails.classList.add(‘actor-details’)
// Handle both single icon object and array of icons
let iconUrl = ‘./assets/profile.png’ // Default profile image path
if (actorInfo.icon) {
if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) {
iconUrl = actorInfo.icon[0].url || actorInfo.id
} else if (actorInfo.icon.url) {
iconUrl = actorInfo.icon.url || actorInfo.id
}
}
const p2pImage = document.createElement(‘p2p-image’)
p2pImage.className = ‘actor-icon’
p2pImage.setAttribute(‘src’, iconUrl)
p2pImage.alt = actorInfo.name ? actorInfo.name : ‘Actor icon’
p2pImage.addEventListener(‘click’, this.navigateToActorProfile.bind(this))
author.appendChild(p2pImage)
if (actorInfo.name) {
const pName = document.createElement(‘div’)
pName.classList.add(‘actor-name’)
pName.textContent = actorInfo.name
pName.addEventListener(‘click’, this.navigateToActorProfile.bind(this))
authorDetails.appendChild(pName)
}
if (actorInfo.preferredUsername) {
const pUserName = document.createElement(‘div’)
pUserName.classList.add(‘actor-username’)
pUserName.textContent = `@${actorInfo.preferredUsername}`
authorDetails.appendChild(pUserName)
}
// Append the authorDetails to the author div
author.appendChild(authorDetails)
// Append the author container to the actor-info component
this.appendChild(author)
}
} catch (error) {
const errorElement = renderError(error.message)
this.appendChild(errorElement)
}
}
}
// Register the new element with the browser
customElements.define(‘actor-info’, ActorInfo)