From 2cd352fc3ff36c26caee92a244c1d383d618b796 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 16 Jan 2024 00:30:39 +0530 Subject: [PATCH 001/149] feat: Implement IPFS post loading and JSON-LD rendering --- index.html | 9 +++-- post.html | 14 ++----- post.js | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 post.js diff --git a/index.html b/index.html index febeba1..77ac7d8 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,12 @@ Distributed Press Reader - +Posts

- Work in progress, check back later or follow us on GitHub. + Work in progress, check back later or + follow us on GitHub.

diff --git a/post.html b/post.html index 28ae837..4fd6ffd 100644 --- a/post.html +++ b/post.html @@ -1,13 +1,7 @@ Distributed Press Reader - + -- TODO: -- Load post from `window.location.searchParams.get("url")` -- Fetch json -- render content -- fetch actor info and render it -- refactor into reusable web component that takes the url as an attribute. -- factor out the avatar/profile name rendering into a separate component that gets nested +
+ + diff --git a/post.js b/post.js new file mode 100644 index 0000000..572d7d0 --- /dev/null +++ b/post.js @@ -0,0 +1,116 @@ +async function loadPost(url) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return await response.text(); + } catch (error) { + console.error("Error fetching post:", error); + } +} + +async function parsePostHtml(htmlContent) { + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlContent, "text/html"); + const alternateLink = doc.querySelector('link[rel="alternate"]'); + return alternateLink ? alternateLink.href : null; +} + +async function fetchJsonLd(jsonLdUrl) { + try { + const response = await fetch(jsonLdUrl); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Error fetching JSON-LD:", error); + } +} + +function renderPost(jsonLdData) { + const postContainer = document.getElementById("post-container"); + let contentHtml = ""; + + // Render the main fields + contentHtml += jsonLdData.summary + ? `

Summary: ${jsonLdData.summary}

` + : ""; + contentHtml += jsonLdData.published + ? `

Published: ${jsonLdData.published}

` + : ""; + contentHtml += jsonLdData.attributedTo + ? `

Author: ${jsonLdData.attributedTo}

` + : ""; + contentHtml += jsonLdData.content + ? `

Content: ${jsonLdData.content}

` + : ""; + + // Handle sensitive content + if (jsonLdData.sensitive) { + contentHtml += ` +
+ Sensitive Content (click to view) +

${jsonLdData.sensitive}

+
+ `; + } + + postContainer.innerHTML = contentHtml; + + // TODO: Fetch actor info and render it + // TODO: Refactor into reusable web component that takes the URL as an attribute + // TODO: Factor out the avatar/profile name rendering into a separate component +} + +async function init() { + const urlParams = new URLSearchParams(window.location.search); + let postUrl = urlParams.get("url"); + + if (!postUrl) { + // console.error('No post URL provided'); + postUrl = + "ipfs://bafybeifslnipwp5uanmhkckokwuse7h5gfrrjzqq4jg5oxewxbzrdcdawu"; + } + + let htmlContent; + if (postUrl.startsWith("ipfs://")) { + htmlContent = await loadPostFromIpfs(postUrl); + } else { + htmlContent = await loadPost(postUrl); + } + + const jsonLdUrl = await parsePostHtml(htmlContent); + if (jsonLdUrl) { + const jsonLdData = await fetchJsonLd(jsonLdUrl); + renderPost(jsonLdData); + } else { + console.error("JSON-LD URL not found in the post"); + } +} +init(); + +async function loadPostFromIpfs(ipfsUrl) { + try { + // Try loading content using native IPFS URLs + const nativeResponse = await fetch(ipfsUrl); + if (nativeResponse.ok) { + return await nativeResponse.text(); + } + } catch (error) { + console.log("Native IPFS loading failed, trying HTTP gateway:", error); + } + + // Fallback to loading content via an HTTP IPFS gateway + const gatewayUrl = ipfsUrl.replace("ipfs://", "https://dweb.link/ipfs/"); + try { + const gatewayResponse = await fetch(gatewayUrl); + if (!gatewayResponse.ok) { + throw new Error(`HTTP error! Status: ${gatewayResponse.status}`); + } + return await gatewayResponse.text(); + } catch (error) { + console.error("Error fetching IPFS content via HTTP gateway:", error); + } +} From 9ecce626f7668a1df20f9840fc35df2c5a953d14 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 24 Jan 2024 02:03:59 +0530 Subject: [PATCH 002/149] feat: fetch actorInfo and implement web components --- post.html | 9 ++++- post.js | 109 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 85 insertions(+), 33 deletions(-) diff --git a/post.html b/post.html index 4fd6ffd..db69c39 100644 --- a/post.html +++ b/post.html @@ -1,7 +1,12 @@ Distributed Press Reader - -
+
+ + + +
diff --git a/post.js b/post.js index 572d7d0..387f334 100644 --- a/post.js +++ b/post.js @@ -33,6 +33,12 @@ function renderPost(jsonLdData) { const postContainer = document.getElementById("post-container"); let contentHtml = ""; + // Display actorInfo + if (jsonLdData.attributedTo) { + // Use the actor-info web component + contentHtml += ``; + } + // Render the main fields contentHtml += jsonLdData.summary ? `

Summary: ${jsonLdData.summary}

` @@ -58,38 +64,7 @@ function renderPost(jsonLdData) { } postContainer.innerHTML = contentHtml; - - // TODO: Fetch actor info and render it - // TODO: Refactor into reusable web component that takes the URL as an attribute - // TODO: Factor out the avatar/profile name rendering into a separate component -} - -async function init() { - const urlParams = new URLSearchParams(window.location.search); - let postUrl = urlParams.get("url"); - - if (!postUrl) { - // console.error('No post URL provided'); - postUrl = - "ipfs://bafybeifslnipwp5uanmhkckokwuse7h5gfrrjzqq4jg5oxewxbzrdcdawu"; - } - - let htmlContent; - if (postUrl.startsWith("ipfs://")) { - htmlContent = await loadPostFromIpfs(postUrl); - } else { - htmlContent = await loadPost(postUrl); - } - - const jsonLdUrl = await parsePostHtml(htmlContent); - if (jsonLdUrl) { - const jsonLdData = await fetchJsonLd(jsonLdUrl); - renderPost(jsonLdData); - } else { - console.error("JSON-LD URL not found in the post"); - } } -init(); async function loadPostFromIpfs(ipfsUrl) { try { @@ -114,3 +89,75 @@ async function loadPostFromIpfs(ipfsUrl) { console.error("Error fetching IPFS content via HTTP gateway:", error); } } + +async function fetchActorInfo(actorUrl) { + try { + const response = await fetch(actorUrl); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Error fetching actor info:", error); + } +} + +// Define a class for the web component +class DistributedPost extends HTMLElement { + static get observedAttributes() { + return ["url"]; + } + + connectedCallback() { + this.loadAndRenderPost(this.getAttribute("url")); + } + + async loadAndRenderPost(postUrl) { + if (!postUrl) { + console.error("No post URL provided"); + return; + } + + let htmlContent; + if (postUrl.startsWith("ipfs://")) { + htmlContent = await loadPostFromIpfs(postUrl); + } else { + htmlContent = await loadPost(postUrl); + } + + const jsonLdUrl = await parsePostHtml(htmlContent); + if (jsonLdUrl) { + const jsonLdData = await fetchJsonLd(jsonLdUrl); + this.innerHTML = renderPost(jsonLdData); + } else { + console.error("JSON-LD URL not found in the post"); + } + } +} + +// 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"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === "url" && newValue) { + this.fetchAndRenderActorInfo(newValue); + } + } + + async fetchAndRenderActorInfo(url) { + const actorInfo = await fetchActorInfo(url); + if (actorInfo) { + // Render the actor's avatar and name + this.innerHTML = `

${actorInfo.name}

${actorInfo.name}

${actorInfo.summary}

`; + } + } +} + +// Register the new element with the browser +customElements.define("actor-info", ActorInfo); From 8e7e871d701ebf1d98439b2d0698716e3c7dee99 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 29 Jan 2024 21:16:14 +0530 Subject: [PATCH 003/149] feat: add Accept header for JSON-LD fetch requests --- post.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/post.js b/post.js index 387f334..306e213 100644 --- a/post.js +++ b/post.js @@ -19,7 +19,11 @@ async function parsePostHtml(htmlContent) { async function fetchJsonLd(jsonLdUrl) { try { - const response = await fetch(jsonLdUrl); + const headers = new Headers({ + "Accept": "application/ld+json" + }); + + const response = await fetch(jsonLdUrl, { headers }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } From dd26b631d68b4eb76573f26a05c533111a57a5ee Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 29 Jan 2024 22:41:29 +0530 Subject: [PATCH 004/149] feat: render error messages within post and actor components --- post.js | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/post.js b/post.js index 306e213..9f1c5a2 100644 --- a/post.js +++ b/post.js @@ -20,7 +20,7 @@ async function parsePostHtml(htmlContent) { async function fetchJsonLd(jsonLdUrl) { try { const headers = new Headers({ - "Accept": "application/ld+json" + Accept: "application/ld+json", }); const response = await fetch(jsonLdUrl, { headers }); @@ -106,6 +106,10 @@ async function fetchActorInfo(actorUrl) { } } +function renderError(message) { + return `

${message}

`; +} + // Define a class for the web component class DistributedPost extends HTMLElement { static get observedAttributes() { @@ -118,24 +122,32 @@ class DistributedPost extends HTMLElement { async loadAndRenderPost(postUrl) { if (!postUrl) { - console.error("No post URL provided"); + this.renderErrorContent("No post URL provided"); return; } let htmlContent; - if (postUrl.startsWith("ipfs://")) { - htmlContent = await loadPostFromIpfs(postUrl); - } else { - htmlContent = await loadPost(postUrl); + try { + if (postUrl.startsWith("ipfs://")) { + htmlContent = await loadPostFromIpfs(postUrl); + } else { + htmlContent = await loadPost(postUrl); + } + + const jsonLdUrl = await parsePostHtml(htmlContent); + if (jsonLdUrl) { + const jsonLdData = await fetchJsonLd(jsonLdUrl); + this.innerHTML = renderPost(jsonLdData); + } else { + this.renderErrorContent("JSON-LD URL not found in the post"); + } + } catch (error) { + this.renderErrorContent(error.message); } + } - const jsonLdUrl = await parsePostHtml(htmlContent); - if (jsonLdUrl) { - const jsonLdData = await fetchJsonLd(jsonLdUrl); - this.innerHTML = renderPost(jsonLdData); - } else { - console.error("JSON-LD URL not found in the post"); - } + renderErrorContent(errorMessage) { + this.innerHTML = renderError(errorMessage); } } @@ -155,10 +167,14 @@ class ActorInfo extends HTMLElement { } async fetchAndRenderActorInfo(url) { - const actorInfo = await fetchActorInfo(url); - if (actorInfo) { - // Render the actor's avatar and name - this.innerHTML = `

${actorInfo.name}

${actorInfo.name}

${actorInfo.summary}

`; + try { + const actorInfo = await fetchActorInfo(url); + if (actorInfo) { + // Render the actor's avatar and name + this.innerHTML = `

${actorInfo.name}

${actorInfo.name}

${actorInfo.summary}

`; + } + } catch (error) { + this.innerHTML = renderError(error.message); } } } From 6be50f351b5c156fecbb6b47a0d1edb286ee245e Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 29 Jan 2024 23:04:22 +0530 Subject: [PATCH 005/149] feat: add Accept header for multiple MIME types in loadPost --- post.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/post.js b/post.js index 9f1c5a2..f079f1d 100644 --- a/post.js +++ b/post.js @@ -1,10 +1,25 @@ async function loadPost(url) { try { - const response = await fetch(url); + const headers = new Headers({ + Accept: "application/activity+json, application/ld+json, text/html", + }); + + const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } - return await response.text(); + + const contentType = response.headers.get("content-type"); + if ( + contentType.includes("application/ld+json") || + contentType.includes("application/activity+json") + ) { + // Directly return JSON-LD if the response is JSON-LD or ActivityPub type + return await response.json(); + } else { + // Return HTML content for further processing if the response is HTML + return await response.text(); + } } catch (error) { console.error("Error fetching post:", error); } From 667e94f6b5bc801b8ccb74a4da84b20ce9b55eaf Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 29 Jan 2024 23:35:42 +0530 Subject: [PATCH 006/149] refactor: integrate post rendering into DistributedPost component --- post.js | 83 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/post.js b/post.js index f079f1d..0b92b3a 100644 --- a/post.js +++ b/post.js @@ -48,43 +48,6 @@ async function fetchJsonLd(jsonLdUrl) { } } -function renderPost(jsonLdData) { - const postContainer = document.getElementById("post-container"); - let contentHtml = ""; - - // Display actorInfo - if (jsonLdData.attributedTo) { - // Use the actor-info web component - contentHtml += ``; - } - - // Render the main fields - contentHtml += jsonLdData.summary - ? `

Summary: ${jsonLdData.summary}

` - : ""; - contentHtml += jsonLdData.published - ? `

Published: ${jsonLdData.published}

` - : ""; - contentHtml += jsonLdData.attributedTo - ? `

Author: ${jsonLdData.attributedTo}

` - : ""; - contentHtml += jsonLdData.content - ? `

Content: ${jsonLdData.content}

` - : ""; - - // Handle sensitive content - if (jsonLdData.sensitive) { - contentHtml += ` -
- Sensitive Content (click to view) -

${jsonLdData.sensitive}

-
- `; - } - - postContainer.innerHTML = contentHtml; -} - async function loadPostFromIpfs(ipfsUrl) { try { // Try loading content using native IPFS URLs @@ -152,7 +115,7 @@ class DistributedPost extends HTMLElement { const jsonLdUrl = await parsePostHtml(htmlContent); if (jsonLdUrl) { const jsonLdData = await fetchJsonLd(jsonLdUrl); - this.innerHTML = renderPost(jsonLdData); + this.renderPostContent(jsonLdData); } else { this.renderErrorContent("JSON-LD URL not found in the post"); } @@ -161,8 +124,50 @@ class DistributedPost extends HTMLElement { } } + renderPostContent(jsonLdData) { + // Clear existing content + this.innerHTML = ""; + + // Create elements for each field + if (jsonLdData.attributedTo) { + const actorInfo = document.createElement("actor-info"); + actorInfo.setAttribute("url", jsonLdData.attributedTo); + this.appendChild(actorInfo); + } + + this.appendField("Summary", jsonLdData.summary); + this.appendField("Published", jsonLdData.published); + this.appendField("Author", jsonLdData.attributedTo); + this.appendField("Content", jsonLdData.content); + + if (jsonLdData.sensitive) { + const details = document.createElement("details"); + const summary = document.createElement("summary"); + summary.textContent = "Sensitive Content (click to view)"; + details.appendChild(summary); + const content = document.createElement("p"); + content.textContent = jsonLdData.sensitive; + details.appendChild(content); + this.appendChild(details); + } + } + + appendField(label, value) { + if (value) { + const p = document.createElement("p"); + p.innerHTML = `${label}: ${value}`; + this.appendChild(p); + } + } + renderErrorContent(errorMessage) { - this.innerHTML = renderError(errorMessage); + // Clear existing content + this.innerHTML = ""; + + const errorElement = document.createElement("p"); + errorElement.className = "error"; + errorElement.textContent = errorMessage; + this.appendChild(errorElement); } } From dab5fbbfb6e23d59ee42bb6c83b0a3bb06df9a5b Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 1 Feb 2024 22:47:01 +0530 Subject: [PATCH 007/149] refactor: replace innerHTML with secure DOM manipulation methods --- post.js | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/post.js b/post.js index 0b92b3a..2b08006 100644 --- a/post.js +++ b/post.js @@ -85,7 +85,10 @@ async function fetchActorInfo(actorUrl) { } function renderError(message) { - return `

${message}

`; + const errorElement = document.createElement("p"); + errorElement.classList.add("error"); + errorElement.textContent = message; + return errorElement; } // Define a class for the web component @@ -126,7 +129,9 @@ class DistributedPost extends HTMLElement { renderPostContent(jsonLdData) { // Clear existing content - this.innerHTML = ""; + while (this.firstChild) { + this.removeChild(this.firstChild); + } // Create elements for each field if (jsonLdData.attributedTo) { @@ -155,14 +160,19 @@ class DistributedPost extends HTMLElement { appendField(label, value) { if (value) { const p = document.createElement("p"); - p.innerHTML = `${label}: ${value}`; + const strong = document.createElement("strong"); + strong.textContent = `${label}:`; + p.appendChild(strong); + p.appendChild(document.createTextNode(` ${value}`)); this.appendChild(p); } } renderErrorContent(errorMessage) { // Clear existing content - this.innerHTML = ""; + while (this.firstChild) { + this.removeChild(this.firstChild); + } const errorElement = document.createElement("p"); errorElement.className = "error"; @@ -191,10 +201,25 @@ class ActorInfo extends HTMLElement { const actorInfo = await fetchActorInfo(url); if (actorInfo) { // Render the actor's avatar and name - this.innerHTML = `

${actorInfo.name}

${actorInfo.name}

${actorInfo.summary}

`; + // Clear existing content + while (this.firstChild) { + this.removeChild(this.firstChild); + } + const pName = document.createElement("p"); + pName.textContent = actorInfo.name; + const img = document.createElement("img"); + img.src = actorInfo.icon[0].url; + img.width = 69; + img.alt = actorInfo.name; + const pSummary = document.createElement("p"); + pSummary.textContent = actorInfo.summary; + this.appendChild(pName); + this.appendChild(img); + this.appendChild(pSummary); } } catch (error) { - this.innerHTML = renderError(error.message); + const errorElement = renderError(error.message); + this.appendChild(errorElement); } } } From a0153deda0e26b7a796477eda077329b86f652e0 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 1 Feb 2024 22:52:50 +0530 Subject: [PATCH 008/149] feat: add support for direct loading of IPNS and Hyper protocols --- post.html | 5 ++--- post.js | 58 +++++++++++++++++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/post.html b/post.html index db69c39..f8ed099 100644 --- a/post.html +++ b/post.html @@ -3,9 +3,8 @@
- + +
diff --git a/post.js b/post.js index 2b08006..2766547 100644 --- a/post.js +++ b/post.js @@ -48,29 +48,29 @@ async function fetchJsonLd(jsonLdUrl) { } } -async function loadPostFromIpfs(ipfsUrl) { - try { - // Try loading content using native IPFS URLs - const nativeResponse = await fetch(ipfsUrl); - if (nativeResponse.ok) { - return await nativeResponse.text(); - } - } catch (error) { - console.log("Native IPFS loading failed, trying HTTP gateway:", error); - } - - // Fallback to loading content via an HTTP IPFS gateway - const gatewayUrl = ipfsUrl.replace("ipfs://", "https://dweb.link/ipfs/"); - try { - const gatewayResponse = await fetch(gatewayUrl); - if (!gatewayResponse.ok) { - throw new Error(`HTTP error! Status: ${gatewayResponse.status}`); - } - return await gatewayResponse.text(); - } catch (error) { - console.error("Error fetching IPFS content via HTTP gateway:", error); - } -} +// async function loadPostFromIpfs(ipfsUrl) { +// try { +// // Try loading content using native IPFS URLs +// const nativeResponse = await fetch(ipfsUrl); +// if (nativeResponse.ok) { +// return await nativeResponse.text(); +// } +// } catch (error) { +// console.log("Native IPFS loading failed, trying HTTP gateway:", error); +// } + +// // Fallback to loading content via an HTTP IPFS gateway +// const gatewayUrl = ipfsUrl.replace("ipfs://", "https://dweb.link/ipfs/"); +// try { +// const gatewayResponse = await fetch(gatewayUrl); +// if (!gatewayResponse.ok) { +// throw new Error(`HTTP error! Status: ${gatewayResponse.status}`); +// } +// return await gatewayResponse.text(); +// } catch (error) { +// console.error("Error fetching IPFS content via HTTP gateway:", error); +// } +// } async function fetchActorInfo(actorUrl) { try { @@ -109,8 +109,16 @@ class DistributedPost extends HTMLElement { let htmlContent; try { - if (postUrl.startsWith("ipfs://")) { - htmlContent = await loadPostFromIpfs(postUrl); + // if (postUrl.startsWith("ipfs://")) { + // htmlContent = await loadPostFromIpfs(postUrl); + // } + if (postUrl.startsWith("ipns://") || postUrl.startsWith("hyper://")) { + // Directly load content for ipns and hyper URLs + const nativeResponse = await fetch(postUrl); + if (!nativeResponse.ok) { + throw new Error(`HTTP error! Status: ${nativeResponse.status}`); + } + htmlContent = await nativeResponse.text(); } else { htmlContent = await loadPost(postUrl); } From 6d0f5e1e121bc4ce41734656569dfbcdf3f89e27 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 1 Feb 2024 22:56:24 +0530 Subject: [PATCH 009/149] refactor: enhance content negotiation with application/json in Accept header --- post.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/post.js b/post.js index 2766547..bb8d8bd 100644 --- a/post.js +++ b/post.js @@ -1,8 +1,9 @@ +const ACCEPT_HEADER = + "application/activity+json, application/ld+json, application/json, text/html"; + async function loadPost(url) { try { - const headers = new Headers({ - Accept: "application/activity+json, application/ld+json, text/html", - }); + const headers = new Headers({ Accept: ACCEPT_HEADER }); const response = await fetch(url, { headers }); if (!response.ok) { @@ -12,7 +13,8 @@ async function loadPost(url) { const contentType = response.headers.get("content-type"); if ( contentType.includes("application/ld+json") || - contentType.includes("application/activity+json") + contentType.includes("application/activity+json") || + contentType.includes("application/json") ) { // Directly return JSON-LD if the response is JSON-LD or ActivityPub type return await response.json(); From 7c8fd4b9a097be872d3e49d07ca33df062ec0ef2 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 2 Feb 2024 01:58:57 +0530 Subject: [PATCH 010/149] feat: Enhance content loading with header link parsing --- post.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/post.js b/post.js index bb8d8bd..d1c8037 100644 --- a/post.js +++ b/post.js @@ -18,9 +18,27 @@ async function loadPost(url) { ) { // Directly return JSON-LD if the response is JSON-LD or ActivityPub type return await response.json(); - } else { - // Return HTML content for further processing if the response is HTML - return await response.text(); + } else if (contentType.includes("text/html")) { + // For HTML responses, look for the link in the HTTP headers + const linkHeader = response.headers.get("Link"); + if (linkHeader) { + const matches = linkHeader.match( + /<([^>]+)>;\s*rel="alternate";\s*type="application\/ld\+json"/ + ); + if (matches && matches[1]) { + // Found JSON-LD link in headers, fetch that URL + return fetchJsonLd(matches[1]); + } + } + // If no link header or alternate JSON-LD link is found, or response is HTML without JSON-LD link, process as HTML + const htmlContent = await response.text(); + const jsonLdUrl = await parsePostHtml(htmlContent); + if (jsonLdUrl) { + // Found JSON-LD link in HTML, fetch that URL + return fetchJsonLd(jsonLdUrl); + } + // No JSON-LD link found in HTML + throw new Error("No JSON-LD link found in the response"); } } catch (error) { console.error("Error fetching post:", error); From 3deb01090dabac4c7c0130b1bb4f3b17375f662b Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 9 Feb 2024 02:17:03 +0530 Subject: [PATCH 011/149] feat: add direct support for .jsonld URLs and add hypha gateway --- post.html | 3 ++- post.js | 52 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/post.html b/post.html index f8ed099..305cd4f 100644 --- a/post.html +++ b/post.html @@ -4,7 +4,8 @@
- + +
diff --git a/post.js b/post.js index d1c8037..809d395 100644 --- a/post.js +++ b/post.js @@ -80,7 +80,7 @@ async function fetchJsonLd(jsonLdUrl) { // } // // Fallback to loading content via an HTTP IPFS gateway -// const gatewayUrl = ipfsUrl.replace("ipfs://", "https://dweb.link/ipfs/"); +// const gatewayUrl = ipfsUrl.replace("ipfs://", "https://ipfs.hypha.coop/ipfs/"); // try { // const gatewayResponse = await fetch(gatewayUrl); // if (!gatewayResponse.ok) { @@ -127,28 +127,44 @@ class DistributedPost extends HTMLElement { return; } - let htmlContent; try { + // Check if the URL directly points to a JSON-LD document + if (postUrl.endsWith(".jsonld")) { + const jsonLdData = await fetchJsonLd(postUrl); + this.renderPostContent(jsonLdData); + return; + } + + // Handle different URL schemes and HTML content + let content; // if (postUrl.startsWith("ipfs://")) { - // htmlContent = await loadPostFromIpfs(postUrl); + // content = await loadPostFromIpfs(postUrl); // } - if (postUrl.startsWith("ipns://") || postUrl.startsWith("hyper://")) { - // Directly load content for ipns and hyper URLs - const nativeResponse = await fetch(postUrl); - if (!nativeResponse.ok) { - throw new Error(`HTTP error! Status: ${nativeResponse.status}`); - } - htmlContent = await nativeResponse.text(); + if ( + postUrl.startsWith("ipns://") || + postUrl.startsWith("hyper://") || + postUrl.startsWith("https://") + ) { + content = await loadPost(postUrl); } else { - htmlContent = await loadPost(postUrl); + this.renderErrorContent("Unsupported URL scheme"); + return; } - const jsonLdUrl = await parsePostHtml(htmlContent); - if (jsonLdUrl) { - const jsonLdData = await fetchJsonLd(jsonLdUrl); - this.renderPostContent(jsonLdData); + // For HTML content, attempt to find and fetch JSON-LD link within the content + if (typeof content === "object" && !content.summary) { + // Assuming JSON-LD content has a "summary" field + this.renderPostContent(content); + } else if (typeof content === "string") { + const jsonLdUrl = await parsePostHtml(content); + if (jsonLdUrl) { + const jsonLdData = await fetchJsonLd(jsonLdUrl); + this.renderPostContent(jsonLdData); + } else { + this.renderErrorContent("JSON-LD URL not found in the post"); + } } else { - this.renderErrorContent("JSON-LD URL not found in the post"); + this.renderErrorContent("Invalid content type"); } } catch (error) { this.renderErrorContent(error.message); @@ -168,7 +184,6 @@ class DistributedPost extends HTMLElement { this.appendChild(actorInfo); } - this.appendField("Summary", jsonLdData.summary); this.appendField("Published", jsonLdData.published); this.appendField("Author", jsonLdData.attributedTo); this.appendField("Content", jsonLdData.content); @@ -239,11 +254,8 @@ class ActorInfo extends HTMLElement { img.src = actorInfo.icon[0].url; img.width = 69; img.alt = actorInfo.name; - const pSummary = document.createElement("p"); - pSummary.textContent = actorInfo.summary; this.appendChild(pName); this.appendChild(img); - this.appendChild(pSummary); } } catch (error) { const errorElement = renderError(error.message); From b68589b7a4c7692d21cf70e4030b002a12c083ac Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Sat, 10 Feb 2024 22:20:42 +0530 Subject: [PATCH 012/149] refactor: DOM manipulation to use innerHTML for cleaner implementation --- post.html | 2 +- post.js | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/post.html b/post.html index 305cd4f..470d38d 100644 --- a/post.html +++ b/post.html @@ -5,7 +5,7 @@ - + diff --git a/post.js b/post.js index 809d395..ccd3ef9 100644 --- a/post.js +++ b/post.js @@ -173,9 +173,7 @@ class DistributedPost extends HTMLElement { renderPostContent(jsonLdData) { // Clear existing content - while (this.firstChild) { - this.removeChild(this.firstChild); - } + this.innerHTML = ""; // Create elements for each field if (jsonLdData.attributedTo) { @@ -213,9 +211,7 @@ class DistributedPost extends HTMLElement { renderErrorContent(errorMessage) { // Clear existing content - while (this.firstChild) { - this.removeChild(this.firstChild); - } + this.innerHTML = ""; const errorElement = document.createElement("p"); errorElement.className = "error"; @@ -245,9 +241,8 @@ class ActorInfo extends HTMLElement { if (actorInfo) { // Render the actor's avatar and name // Clear existing content - while (this.firstChild) { - this.removeChild(this.firstChild); - } + this.innerHTML = ""; + const pName = document.createElement("p"); pName.textContent = actorInfo.name; const img = document.createElement("img"); From 833302d8b8066834ac48ace967ec4fc3c89014b6 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Tue, 20 Feb 2024 23:38:05 -0500 Subject: [PATCH 013/149] Initial sketch of db with dependency --- db.js | 111 +++++ dependencies/idb/LICENSE | 6 + dependencies/idb/async-iterators.d.ts | 1 + dependencies/idb/database-extras.d.ts | 1 + dependencies/idb/entry.d.ts | 627 ++++++++++++++++++++++++++ dependencies/idb/index.d.ts | 3 + dependencies/idb/index.js | 292 ++++++++++++ dependencies/idb/util.d.ts | 3 + dependencies/idb/wrap-idb-value.d.ts | 34 ++ 9 files changed, 1078 insertions(+) create mode 100644 db.js create mode 100644 dependencies/idb/LICENSE create mode 100644 dependencies/idb/async-iterators.d.ts create mode 100644 dependencies/idb/database-extras.d.ts create mode 100644 dependencies/idb/entry.d.ts create mode 100644 dependencies/idb/index.d.ts create mode 100644 dependencies/idb/index.js create mode 100644 dependencies/idb/util.d.ts create mode 100644 dependencies/idb/wrap-idb-value.d.ts diff --git a/db.js b/db.js new file mode 100644 index 0000000..2f40f8f --- /dev/null +++ b/db.js @@ -0,0 +1,111 @@ +import { openDB } from './dependencies/idb/index.js' + +export const ACTORS_STORE = 'actors' +export const NOTES_STORE = 'notes' +export const ID_FIELD = 'id' +export const URL_FIELD = 'url' +export const CREATED_FIELD = 'created' +export const UPDATED_FIELD = 'updated' +export const PUBLISHED_FIELD = 'published' +export const TO_FIELD = 'to' +export const CC_FIELD = 'cc' +export const IN_REPLY_TO_FIELD = 'inReplyTo' +export const TAG_NAMES_FIELD = 'tag_names' +export const ATTRIBUTED_TO_FIELD = 'attributedTo' +export const CONVERSATION_FIELD = 'conversation' + +// TODO: When ingesting notes and actors, wrap any dates in `new Date()` +// TODO: When ingesting notes add a "tag_names" field which is just the names of the tag +// TODO: When ingesting notes, also load their replies +// TODO: Detect P2P URLs and use gateways (wrap `fetch` with it?) + +export class ActivityPubDB { + constructor (db, fetch = globalThis.fetch) { + this.db = db + this.fetch = fetch + } + + static async load (name, fetch = globalThis.fetch) { + const db = await openDB(name, 1, { + upgrade + }) + + return new ActivityPubDB(db, fetch) + } + + async getActor (url) { + // Try to load from db + // else try to ingest then load from db + const request = await this.fetch(url, { headers: { Accept: 'application/ld+json' } }) + // Handle 404 + const actor = await request.json() + this.db.add(ACTORS_STORE, actor) + return this.db.get(ACTORS_STORE, actor.id) + } + + async getNote (url) { + // Try to load from db + // Else try to ingest then load from db + } + + async * searchNotes (query) { + // Query can search by any of the indexed fields. + // Everything gets sorted by the `published` time. + // We should find a way to allow for arrays of values and open multiple iterators + // Kinda like this: https://git.sr.ht/~breatheoutbreathein/ushin-db/tree/master/item/USHINBase.js#L509 + } + + async ingestActor (url) { + // Load actor and save to actors store + // Ingest outbox as collection + } + + async ingestCollection (url, verifyAttributed = '') { + // Load outbox as async iterator, handle extra pages + // Go through each activity and if it's a note, ingest or delete it + } + + async ingestNote (url) { + // Load by url + // Convert needed fields to date + // Add tag_names field + // Add to notes store + // Loop through replies (if possible) and ingest them + } + + async deleteNote (url) { + // delete note using the url as the `id` from the notes store + } +} + +function upgrade (db) { + const actors = db.createObjectStore(ACTORS_STORE, { + keyPath: 'id', + autoIncrement: false + }) + + actors.createIndex(CREATED_FIELD, CREATED_FIELD) + actors.createIndex(UPDATED_FIELD, UPDATED_FIELD) + actors.createIndex(URL_FIELD, URL_FIELD) + + const notes = db.createObjectStore(NOTES_STORE, { + keyPath: 'id', + autoIncrement: false + }) + + addRegularIndex(PUBLISHED_FIELD) + addRegularIndex(TO_FIELD) + addRegularIndex(URL_FIELD) + addSortedIndex(TAG_NAMES_FIELD, { multiEntry: true }) + addSortedIndex(IN_REPLY_TO_FIELD) + addSortedIndex(ATTRIBUTED_TO_FIELD) + addSortedIndex(CONVERSATION_FIELD) + addSortedIndex(TO_FIELD) + + function addRegularIndex (field, options = {}) { + notes.createIndex(field, field, options) + } + function addSortedIndex (field, options = {}) { + notes.createIndex(field, [field, PUBLISHED_FIELD], options) + } +} diff --git a/dependencies/idb/LICENSE b/dependencies/idb/LICENSE new file mode 100644 index 0000000..f8b22ce --- /dev/null +++ b/dependencies/idb/LICENSE @@ -0,0 +1,6 @@ +ISC License (ISC) +Copyright (c) 2016, Jake Archibald + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/dependencies/idb/async-iterators.d.ts b/dependencies/idb/async-iterators.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dependencies/idb/async-iterators.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dependencies/idb/database-extras.d.ts b/dependencies/idb/database-extras.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dependencies/idb/database-extras.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dependencies/idb/entry.d.ts b/dependencies/idb/entry.d.ts new file mode 100644 index 0000000..0bc1f04 --- /dev/null +++ b/dependencies/idb/entry.d.ts @@ -0,0 +1,627 @@ +export interface OpenDBCallbacks { + /** + * Called if this version of the database has never been opened before. Use it to specify the + * schema for the database. + * + * @param database A database instance that you can use to add/remove stores and indexes. + * @param oldVersion Last version of the database opened by the user. + * @param newVersion Whatever new version you provided. + * @param transaction The transaction for this upgrade. + * This is useful if you need to get data from other stores as part of a migration. + * @param event The event object for the associated 'upgradeneeded' event. + */ + upgrade?(database: IDBPDatabase, oldVersion: number, newVersion: number | null, transaction: IDBPTransaction[], 'versionchange'>, event: IDBVersionChangeEvent): void; + /** + * Called if there are older versions of the database open on the origin, so this version cannot + * open. + * + * @param currentVersion Version of the database that's blocking this one. + * @param blockedVersion The version of the database being blocked (whatever version you provided to `openDB`). + * @param event The event object for the associated `blocked` event. + */ + blocked?(currentVersion: number, blockedVersion: number | null, event: IDBVersionChangeEvent): void; + /** + * Called if this connection is blocking a future version of the database from opening. + * + * @param currentVersion Version of the open database (whatever version you provided to `openDB`). + * @param blockedVersion The version of the database that's being blocked. + * @param event The event object for the associated `versionchange` event. + */ + blocking?(currentVersion: number, blockedVersion: number | null, event: IDBVersionChangeEvent): void; + /** + * Called if the browser abnormally terminates the connection. + * This is not called when `db.close()` is called. + */ + terminated?(): void; +} +/** + * Open a database. + * + * @param name Name of the database. + * @param version Schema version. + * @param callbacks Additional callbacks. + */ +export declare function openDB(name: string, version?: number, { blocked, upgrade, blocking, terminated }?: OpenDBCallbacks): Promise>; +export interface DeleteDBCallbacks { + /** + * Called if there are connections to this database open, so it cannot be deleted. + * + * @param currentVersion Version of the database that's blocking the delete operation. + * @param event The event object for the associated `blocked` event. + */ + blocked?(currentVersion: number, event: IDBVersionChangeEvent): void; +} +/** + * Delete a database. + * + * @param name Name of the database. + */ +export declare function deleteDB(name: string, { blocked }?: DeleteDBCallbacks): Promise; +export { unwrap, wrap } from './wrap-idb-value.js'; +type KeyToKeyNoIndex = { + [K in keyof T]: string extends K ? never : number extends K ? never : K; +}; +type ValuesOf = T extends { + [K in keyof T]: infer U; +} ? U : never; +type KnownKeys = ValuesOf>; +type Omit = Pick>; +export interface DBSchema { + [s: string]: DBSchemaValue; +} +interface IndexKeys { + [s: string]: IDBValidKey; +} +interface DBSchemaValue { + key: IDBValidKey; + value: any; + indexes?: IndexKeys; +} +/** + * Extract known object store names from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + */ +export type StoreNames = DBTypes extends DBSchema ? KnownKeys : string; +/** + * Extract database value types from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + */ +export type StoreValue> = DBTypes extends DBSchema ? DBTypes[StoreName]['value'] : any; +/** + * Extract database key types from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + */ +export type StoreKey> = DBTypes extends DBSchema ? DBTypes[StoreName]['key'] : IDBValidKey; +/** + * Extract the names of indexes in certain object stores from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + */ +export type IndexNames> = DBTypes extends DBSchema ? keyof DBTypes[StoreName]['indexes'] : string; +/** + * Extract the types of indexes in certain object stores from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + * @template IndexName Names of the indexes to get the types of. + */ +export type IndexKey, IndexName extends IndexNames> = DBTypes extends DBSchema ? IndexName extends keyof DBTypes[StoreName]['indexes'] ? DBTypes[StoreName]['indexes'][IndexName] : IDBValidKey : IDBValidKey; +type CursorSource>, StoreName extends StoreNames, IndexName extends IndexNames | unknown, Mode extends IDBTransactionMode = 'readonly'> = IndexName extends IndexNames ? IDBPIndex : IDBPObjectStore; +type CursorKey, IndexName extends IndexNames | unknown> = IndexName extends IndexNames ? IndexKey : StoreKey; +type IDBPDatabaseExtends = Omit; +/** + * A variation of DOMStringList with precise string types + */ +export interface TypedDOMStringList extends DOMStringList { + contains(string: T): boolean; + item(index: number): T | null; + [index: number]: T; + [Symbol.iterator](): IterableIterator; +} +interface IDBTransactionOptions { + /** + * The durability of the transaction. + * + * The default is "default". Using "relaxed" provides better performance, but with fewer + * guarantees. Web applications are encouraged to use "relaxed" for ephemeral data such as caches + * or quickly changing records, and "strict" in cases where reducing the risk of data loss + * outweighs the impact to performance and power. + */ + durability?: 'default' | 'strict' | 'relaxed'; +} +export interface IDBPDatabase extends IDBPDatabaseExtends { + /** + * The names of stores in the database. + */ + readonly objectStoreNames: TypedDOMStringList>; + /** + * Creates a new object store. + * + * Throws a "InvalidStateError" DOMException if not called within an upgrade transaction. + */ + createObjectStore>(name: Name, optionalParameters?: IDBObjectStoreParameters): IDBPObjectStore>, Name, 'versionchange'>; + /** + * Deletes the object store with the given name. + * + * Throws a "InvalidStateError" DOMException if not called within an upgrade transaction. + */ + deleteObjectStore(name: StoreNames): void; + /** + * Start a new transaction. + * + * @param storeNames The object store(s) this transaction needs. + * @param mode + * @param options + */ + transaction, Mode extends IDBTransactionMode = 'readonly'>(storeNames: Name, mode?: Mode, options?: IDBTransactionOptions): IDBPTransaction; + transaction>, Mode extends IDBTransactionMode = 'readonly'>(storeNames: Names, mode?: Mode, options?: IDBTransactionOptions): IDBPTransaction; + /** + * Add a value to a store. + * + * Rejects if an item of a given key already exists in the store. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param value + * @param key + */ + add>(storeName: Name, value: StoreValue, key?: StoreKey | IDBKeyRange): Promise>; + /** + * Deletes all records in a store. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + */ + clear(name: StoreNames): Promise; + /** + * Retrieves the number of records matching the given query in a store. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param key + */ + count>(storeName: Name, key?: StoreKey | IDBKeyRange | null): Promise; + /** + * Retrieves the number of records matching the given query in an index. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param key + */ + countFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, key?: IndexKey | IDBKeyRange | null): Promise; + /** + * Deletes records in a store matching the given query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param key + */ + delete>(storeName: Name, key: StoreKey | IDBKeyRange): Promise; + /** + * Retrieves the value of the first record in a store matching the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + */ + get>(storeName: Name, query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves the value of the first record in an index matching the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + */ + getFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves all values in a store that match the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + * @param count Maximum number of values to return. + */ + getAll>(storeName: Name, query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves all values in an index that match the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + * @param count Maximum number of values to return. + */ + getAllFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records in a store matching the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeys>(storeName: Name, query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records in an index matching the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeysFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the key of the first record in a store that matches the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + */ + getKey>(storeName: Name, query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves the key of the first record in an index that matches the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + */ + getKeyFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Put an item in the database. + * + * Replaces any item with the same key. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param value + * @param key + */ + put>(storeName: Name, value: StoreValue, key?: StoreKey | IDBKeyRange): Promise>; +} +type IDBPTransactionExtends = Omit; +export interface IDBPTransaction> = ArrayLike>, Mode extends IDBTransactionMode = 'readonly'> extends IDBPTransactionExtends { + /** + * The transaction's mode. + */ + readonly mode: Mode; + /** + * The names of stores in scope for this transaction. + */ + readonly objectStoreNames: TypedDOMStringList; + /** + * The transaction's connection. + */ + readonly db: IDBPDatabase; + /** + * Promise for the completion of this transaction. + */ + readonly done: Promise; + /** + * The associated object store, if the transaction covers a single store, otherwise undefined. + */ + readonly store: TxStores[1] extends undefined ? IDBPObjectStore : undefined; + /** + * Returns an IDBObjectStore in the transaction's scope. + */ + objectStore(name: StoreName): IDBPObjectStore; +} +type IDBPObjectStoreExtends = Omit; +export interface IDBPObjectStore> = ArrayLike>, StoreName extends StoreNames = StoreNames, Mode extends IDBTransactionMode = 'readonly'> extends IDBPObjectStoreExtends { + /** + * The names of indexes in the store. + */ + readonly indexNames: TypedDOMStringList>; + /** + * The associated transaction. + */ + readonly transaction: IDBPTransaction; + /** + * Add a value to the store. + * + * Rejects if an item of a given key already exists in the store. + */ + add: Mode extends 'readonly' ? undefined : (value: StoreValue, key?: StoreKey | IDBKeyRange) => Promise>; + /** + * Deletes all records in store. + */ + clear: Mode extends 'readonly' ? undefined : () => Promise; + /** + * Retrieves the number of records matching the given query. + */ + count(key?: StoreKey | IDBKeyRange | null): Promise; + /** + * Creates a new index in store. + * + * Throws an "InvalidStateError" DOMException if not called within an upgrade transaction. + */ + createIndex: Mode extends 'versionchange' ? >(name: IndexName, keyPath: string | string[], options?: IDBIndexParameters) => IDBPIndex : undefined; + /** + * Deletes records in store matching the given query. + */ + delete: Mode extends 'readonly' ? undefined : (key: StoreKey | IDBKeyRange) => Promise; + /** + * Retrieves the value of the first record matching the query. + * + * Resolves with undefined if no match is found. + */ + get(query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves all values that match the query. + * + * @param query + * @param count Maximum number of values to return. + */ + getAll(query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records matching the query. + * + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeys(query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the key of the first record that matches the query. + * + * Resolves with undefined if no match is found. + */ + getKey(query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Get a query of a given name. + */ + index>(name: IndexName): IDBPIndex; + /** + * Opens a cursor over the records matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openCursor(query?: StoreKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Opens a cursor over the keys matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openKeyCursor(query?: StoreKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Put an item in the store. + * + * Replaces any item with the same key. + */ + put: Mode extends 'readonly' ? undefined : (value: StoreValue, key?: StoreKey | IDBKeyRange) => Promise>; + /** + * Iterate over the store. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; + /** + * Iterate over the records matching the query. + * + * @param query If null, all records match. + * @param direction + */ + iterate(query?: StoreKey | IDBKeyRange | null, direction?: IDBCursorDirection): AsyncIterableIterator>; +} +type IDBPIndexExtends = Omit; +export interface IDBPIndex> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames = IndexNames, Mode extends IDBTransactionMode = 'readonly'> extends IDBPIndexExtends { + /** + * The IDBObjectStore the index belongs to. + */ + readonly objectStore: IDBPObjectStore; + /** + * Retrieves the number of records matching the given query. + */ + count(key?: IndexKey | IDBKeyRange | null): Promise; + /** + * Retrieves the value of the first record matching the query. + * + * Resolves with undefined if no match is found. + */ + get(query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves all values that match the query. + * + * @param query + * @param count Maximum number of values to return. + */ + getAll(query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records matching the query. + * + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeys(query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the key of the first record that matches the query. + * + * Resolves with undefined if no match is found. + */ + getKey(query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Opens a cursor over the records matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openCursor(query?: IndexKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Opens a cursor over the keys matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openKeyCursor(query?: IndexKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Iterate over the index. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; + /** + * Iterate over the records matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + iterate(query?: IndexKey | IDBKeyRange | null, direction?: IDBCursorDirection): AsyncIterableIterator>; +} +type IDBPCursorExtends = Omit; +export interface IDBPCursor> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursorExtends { + /** + * The key of the current index or object store item. + */ + readonly key: CursorKey; + /** + * The key of the current object store item. + */ + readonly primaryKey: StoreKey; + /** + * Returns the IDBObjectStore or IDBIndex the cursor was opened from. + */ + readonly source: CursorSource; + /** + * Advances the cursor a given number of records. + * + * Resolves to null if no matching records remain. + */ + advance(this: T, count: number): Promise; + /** + * Advance the cursor by one record (unless 'key' is provided). + * + * Resolves to null if no matching records remain. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + */ + continue(this: T, key?: CursorKey): Promise; + /** + * Advance the cursor by given keys. + * + * The operation is 'and' – both keys must be satisfied. + * + * Resolves to null if no matching records remain. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + * @param primaryKey and where the object store has a key equal to or greater than this value. + */ + continuePrimaryKey(this: T, key: CursorKey, primaryKey: StoreKey): Promise; + /** + * Delete the current record. + */ + delete: Mode extends 'readonly' ? undefined : () => Promise; + /** + * Updated the current record. + */ + update: Mode extends 'readonly' ? undefined : (value: StoreValue) => Promise>; + /** + * Iterate over the cursor. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; +} +type IDBPCursorIteratorValueExtends> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> = Omit, 'advance' | 'continue' | 'continuePrimaryKey'>; +export interface IDBPCursorIteratorValue> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursorIteratorValueExtends { + /** + * Advances the cursor a given number of records. + */ + advance(this: T, count: number): void; + /** + * Advance the cursor by one record (unless 'key' is provided). + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + */ + continue(this: T, key?: CursorKey): void; + /** + * Advance the cursor by given keys. + * + * The operation is 'and' – both keys must be satisfied. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + * @param primaryKey and where the object store has a key equal to or greater than this value. + */ + continuePrimaryKey(this: T, key: CursorKey, primaryKey: StoreKey): void; +} +export interface IDBPCursorWithValue> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursor { + /** + * The value of the current item. + */ + readonly value: StoreValue; + /** + * Iterate over the cursor. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; +} +type IDBPCursorWithValueIteratorValueExtends> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> = Omit, 'advance' | 'continue' | 'continuePrimaryKey'>; +export interface IDBPCursorWithValueIteratorValue> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursorWithValueIteratorValueExtends { + /** + * Advances the cursor a given number of records. + */ + advance(this: T, count: number): void; + /** + * Advance the cursor by one record (unless 'key' is provided). + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + */ + continue(this: T, key?: CursorKey): void; + /** + * Advance the cursor by given keys. + * + * The operation is 'and' – both keys must be satisfied. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + * @param primaryKey and where the object store has a key equal to or greater than this value. + */ + continuePrimaryKey(this: T, key: CursorKey, primaryKey: StoreKey): void; +} diff --git a/dependencies/idb/index.d.ts b/dependencies/idb/index.d.ts new file mode 100644 index 0000000..02bc7a4 --- /dev/null +++ b/dependencies/idb/index.d.ts @@ -0,0 +1,3 @@ +export * from './entry.js'; +import './database-extras.js'; +import './async-iterators.js'; diff --git a/dependencies/idb/index.js b/dependencies/idb/index.js new file mode 100644 index 0000000..9e4feb5 --- /dev/null +++ b/dependencies/idb/index.js @@ -0,0 +1,292 @@ +const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c) + +let idbProxyableTypes +let cursorAdvanceMethods +// This is a function to prevent it throwing up in node environments. +function getIdbProxyableTypes () { + return (idbProxyableTypes || + (idbProxyableTypes = [ + IDBDatabase, + IDBObjectStore, + IDBIndex, + IDBCursor, + IDBTransaction + ])) +} +// This is a function to prevent it throwing up in node environments. +function getCursorAdvanceMethods () { + return (cursorAdvanceMethods || + (cursorAdvanceMethods = [ + IDBCursor.prototype.advance, + IDBCursor.prototype.continue, + IDBCursor.prototype.continuePrimaryKey + ])) +} +const transactionDoneMap = new WeakMap() +const transformCache = new WeakMap() +const reverseTransformCache = new WeakMap() +function promisifyRequest (request) { + const promise = new Promise((resolve, reject) => { + const unlisten = () => { + request.removeEventListener('success', success) + request.removeEventListener('error', error) + } + const success = () => { + resolve(wrap(request.result)) + unlisten() + } + const error = () => { + reject(request.error) + unlisten() + } + request.addEventListener('success', success) + request.addEventListener('error', error) + }) + // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This + // is because we create many promises from a single IDBRequest. + reverseTransformCache.set(promise, request) + return promise +} +function cacheDonePromiseForTransaction (tx) { + // Early bail if we've already created a done promise for this transaction. + if (transactionDoneMap.has(tx)) { return } + const done = new Promise((resolve, reject) => { + const unlisten = () => { + tx.removeEventListener('complete', complete) + tx.removeEventListener('error', error) + tx.removeEventListener('abort', error) + } + const complete = () => { + resolve() + unlisten() + } + const error = () => { + reject(tx.error || new DOMException('AbortError', 'AbortError')) + unlisten() + } + tx.addEventListener('complete', complete) + tx.addEventListener('error', error) + tx.addEventListener('abort', error) + }) + // Cache it for later retrieval. + transactionDoneMap.set(tx, done) +} +let idbProxyTraps = { + get (target, prop, receiver) { + if (target instanceof IDBTransaction) { + // Special handling for transaction.done. + if (prop === 'done') { return transactionDoneMap.get(target) } + // Make tx.store return the only store in the transaction, or undefined if there are many. + if (prop === 'store') { + return receiver.objectStoreNames[1] + ? undefined + : receiver.objectStore(receiver.objectStoreNames[0]) + } + } + // Else transform whatever we get back. + return wrap(target[prop]) + }, + set (target, prop, value) { + target[prop] = value + return true + }, + has (target, prop) { + if (target instanceof IDBTransaction && + (prop === 'done' || prop === 'store')) { + return true + } + return prop in target + } +} +function replaceTraps (callback) { + idbProxyTraps = callback(idbProxyTraps) +} +function wrapFunction (func) { + // Due to expected object equality (which is enforced by the caching in `wrap`), we + // only create one new func per func. + // Cursor methods are special, as the behaviour is a little more different to standard IDB. In + // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the + // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense + // with real promises, so each advance methods returns a new promise for the cursor object, or + // undefined if the end of the cursor has been reached. + if (getCursorAdvanceMethods().includes(func)) { + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + func.apply(unwrap(this), args) + return wrap(this.request) + } + } + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + return wrap(func.apply(unwrap(this), args)) + } +} +function transformCachableValue (value) { + if (typeof value === 'function') { return wrapFunction(value) } + // This doesn't return, it just creates a 'done' promise for the transaction, + // which is later returned for transaction.done (see idbObjectHandler). + if (value instanceof IDBTransaction) { cacheDonePromiseForTransaction(value) } + if (instanceOfAny(value, getIdbProxyableTypes())) { return new Proxy(value, idbProxyTraps) } + // Return the same value back if we're not going to transform it. + return value +} +function wrap (value) { + // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because + // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. + if (value instanceof IDBRequest) { return promisifyRequest(value) } + // If we've already transformed this value before, reuse the transformed value. + // This is faster, but it also provides object equality. + if (transformCache.has(value)) { return transformCache.get(value) } + const newValue = transformCachableValue(value) + // Not all types are transformed. + // These may be primitive types, so they can't be WeakMap keys. + if (newValue !== value) { + transformCache.set(value, newValue) + reverseTransformCache.set(newValue, value) + } + return newValue +} +const unwrap = (value) => reverseTransformCache.get(value) + +/** + * Open a database. + * + * @param name Name of the database. + * @param version Schema version. + * @param callbacks Additional callbacks. + */ +function openDB (name, version, { blocked, upgrade, blocking, terminated } = {}) { + const request = indexedDB.open(name, version) + const openPromise = wrap(request) + if (upgrade) { + request.addEventListener('upgradeneeded', (event) => { + upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event) + }) + } + if (blocked) { + request.addEventListener('blocked', (event) => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event.newVersion, event)) + } + openPromise + .then((db) => { + if (terminated) { db.addEventListener('close', () => terminated()) } + if (blocking) { + db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event)) + } + }) + .catch(() => { }) + return openPromise +} +/** + * Delete a database. + * + * @param name Name of the database. + */ +function deleteDB (name, { blocked } = {}) { + const request = indexedDB.deleteDatabase(name) + if (blocked) { + request.addEventListener('blocked', (event) => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event)) + } + return wrap(request).then(() => undefined) +} + +const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'] +const writeMethods = ['put', 'add', 'delete', 'clear'] +const cachedMethods = new Map() +function getMethod (target, prop) { + if (!(target instanceof IDBDatabase && + !(prop in target) && + typeof prop === 'string')) { + return + } + if (cachedMethods.get(prop)) { return cachedMethods.get(prop) } + const targetFuncName = prop.replace(/FromIndex$/, '') + const useIndex = prop !== targetFuncName + const isWrite = writeMethods.includes(targetFuncName) + if ( + // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. + !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || + !(isWrite || readMethods.includes(targetFuncName))) { + return + } + const method = async function (storeName, ...args) { + // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( + const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly') + let target = tx.store + if (useIndex) { target = target.index(args.shift()) } + // Must reject if op rejects. + // If it's a write operation, must reject if tx.done rejects. + // Must reject with op rejection first. + // Must resolve with op value. + // Must handle both promises (no unhandled rejections) + return (await Promise.all([ + target[targetFuncName](...args), + isWrite && tx.done + ]))[0] + } + cachedMethods.set(prop, method) + return method +} +replaceTraps((oldTraps) => ({ + ...oldTraps, + get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), + has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop) +})) + +const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance'] +const methodMap = {} +const advanceResults = new WeakMap() +const ittrProxiedCursorToOriginalProxy = new WeakMap() +const cursorIteratorTraps = { + get (target, prop) { + if (!advanceMethodProps.includes(prop)) { return target[prop] } + let cachedFunc = methodMap[prop] + if (!cachedFunc) { + cachedFunc = methodMap[prop] = function (...args) { + advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args)) + } + } + return cachedFunc + } +} +async function * iterate (...args) { + // tslint:disable-next-line:no-this-assignment + let cursor = this + if (!(cursor instanceof IDBCursor)) { + cursor = await cursor.openCursor(...args) + } + if (!cursor) { return } + cursor = cursor + const proxiedCursor = new Proxy(cursor, cursorIteratorTraps) + ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor) + // Map this double-proxy back to the original, so other cursor methods work. + reverseTransformCache.set(proxiedCursor, unwrap(cursor)) + while (cursor) { + yield proxiedCursor + // If one of the advancing methods was not called, call continue(). + cursor = await (advanceResults.get(proxiedCursor) || cursor.continue()) + advanceResults.delete(proxiedCursor) + } +} +function isIteratorProp (target, prop) { + return ((prop === Symbol.asyncIterator && + instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) || + (prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore]))) +} +replaceTraps((oldTraps) => ({ + ...oldTraps, + get (target, prop, receiver) { + if (isIteratorProp(target, prop)) { return iterate } + return oldTraps.get(target, prop, receiver) + }, + has (target, prop) { + return isIteratorProp(target, prop) || oldTraps.has(target, prop) + } +})) + +export { deleteDB, openDB, unwrap, wrap } diff --git a/dependencies/idb/util.d.ts b/dependencies/idb/util.d.ts new file mode 100644 index 0000000..8bb5539 --- /dev/null +++ b/dependencies/idb/util.d.ts @@ -0,0 +1,3 @@ +export type Constructor = new (...args: any[]) => any; +export type Func = (...args: any[]) => any; +export declare const instanceOfAny: (object: any, constructors: Constructor[]) => boolean; diff --git a/dependencies/idb/wrap-idb-value.d.ts b/dependencies/idb/wrap-idb-value.d.ts new file mode 100644 index 0000000..a8d8cd4 --- /dev/null +++ b/dependencies/idb/wrap-idb-value.d.ts @@ -0,0 +1,34 @@ +import { IDBPCursor, IDBPCursorWithValue, IDBPDatabase, IDBPIndex, IDBPObjectStore, IDBPTransaction } from './entry.js'; +export declare const reverseTransformCache: WeakMap; +export declare function replaceTraps(callback: (currentTraps: ProxyHandler) => ProxyHandler): void; +/** + * Enhance an IDB object with helpers. + * + * @param value The thing to enhance. + */ +export declare function wrap(value: IDBDatabase): IDBPDatabase; +export declare function wrap(value: IDBIndex): IDBPIndex; +export declare function wrap(value: IDBObjectStore): IDBPObjectStore; +export declare function wrap(value: IDBTransaction): IDBPTransaction; +export declare function wrap(value: IDBOpenDBRequest): Promise; +export declare function wrap(value: IDBRequest): Promise; +/** + * Revert an enhanced IDB object to a plain old miserable IDB one. + * + * Will also revert a promise back to an IDBRequest. + * + * @param value The enhanced object to revert. + */ +interface Unwrap { + (value: IDBPCursorWithValue): IDBCursorWithValue; + (value: IDBPCursor): IDBCursor; + (value: IDBPDatabase): IDBDatabase; + (value: IDBPIndex): IDBIndex; + (value: IDBPObjectStore): IDBObjectStore; + (value: IDBPTransaction): IDBTransaction; + (value: Promise>): IDBOpenDBRequest; + (value: Promise): IDBOpenDBRequest; + (value: Promise): IDBRequest; +} +export declare const unwrap: Unwrap; +export {}; From ec15c1bd20e4f8a90efcfd27f67fc0498fb27b52 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 21 Feb 2024 21:24:49 +0530 Subject: [PATCH 014/149] feat: dynamically set post URL from query string parameter --- post.html | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/post.html b/post.html index 470d38d..58052da 100644 --- a/post.html +++ b/post.html @@ -5,8 +5,21 @@ - - + + + From 7bb6c9973223e630b16b438f537ffb291540adac Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 21 Feb 2024 21:25:41 +0530 Subject: [PATCH 015/149] fix: correct handling of sensitive content in posts --- post.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/post.js b/post.js index ccd3ef9..dca2a9f 100644 --- a/post.js +++ b/post.js @@ -184,17 +184,20 @@ class DistributedPost extends HTMLElement { this.appendField("Published", jsonLdData.published); this.appendField("Author", jsonLdData.attributedTo); - this.appendField("Content", jsonLdData.content); + // Handle sensitive content if (jsonLdData.sensitive) { const details = document.createElement("details"); const summary = document.createElement("summary"); summary.textContent = "Sensitive Content (click to view)"; details.appendChild(summary); const content = document.createElement("p"); - content.textContent = jsonLdData.sensitive; + content.textContent = jsonLdData.content; details.appendChild(content); this.appendChild(details); + } else { + // If not sensitive, display content as usual + this.appendField("Content", jsonLdData.content); } } From f3df7132d5fd6c694a5f2bf86262478c5877f5f1 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 19:57:24 +0530 Subject: [PATCH 016/149] refactor: remove redundant DOMContentLoaded listener --- post.html | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/post.html b/post.html index 58052da..32a557a 100644 --- a/post.html +++ b/post.html @@ -11,15 +11,13 @@ From 9c3177f9004864cd21b39e1653e4b5e2967aa031 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 20:00:20 +0530 Subject: [PATCH 017/149] chore: add TODO for sanitizing HTML in post content --- post.js | 1 + 1 file changed, 1 insertion(+) diff --git a/post.js b/post.js index dca2a9f..e9a4b2c 100644 --- a/post.js +++ b/post.js @@ -192,6 +192,7 @@ class DistributedPost extends HTMLElement { summary.textContent = "Sensitive Content (click to view)"; details.appendChild(summary); const content = document.createElement("p"); + // TODO: Sanitize jsonLdData.content to remove or escape any harmful HTML content before displaying content.textContent = jsonLdData.content; details.appendChild(content); this.appendChild(details); From ae115583089a077651a456fdc3283afffc9b754e Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Sat, 10 Feb 2024 22:27:15 +0530 Subject: [PATCH 018/149] feat: implement outbox --- index.html | 3 +- outbox.html | 16 ++++ outbox.js | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 outbox.html create mode 100644 outbox.js diff --git a/index.html b/index.html index 77ac7d8..b032c62 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,8 @@ -Posts +Post +Outbox

Work in progress, check back later or +Distributed Press Reader + +

+
+ + +
+ + diff --git a/outbox.js b/outbox.js new file mode 100644 index 0000000..f5f161c --- /dev/null +++ b/outbox.js @@ -0,0 +1,205 @@ +class DistributedOutbox extends HTMLElement { + constructor() { + super(); + this.renderedItems = new Set(); // Tracks rendered items + this.numPosts = 1; // Default value + this.page = 1; // Default value + this.totalPages = 0; // Keep track of total pages + } + + static get observedAttributes() { + return ["url", "num-posts", "page"]; + } + + connectedCallback() { + // Use attributes or default values + this.numPosts = parseInt(this.getAttribute("num-posts")) || this.numPosts; + this.page = parseInt(this.getAttribute("page")) || this.page; + this.loadOutbox(this.getAttribute("url")); + } + + async loadOutbox(outboxUrl) { + this.clearContent(); + for await (const item of this.fetchOutboxItems(outboxUrl)) { + // Check if the item hasn't been rendered + if (!this.renderedItems.has(item.object)) { + this.renderItem(item); + // Mark as rendered + this.renderedItems.add(item.object); + } + } + } + + async *fetchOutboxItems(outboxUrl) { + if (!outboxUrl) { + console.error("No outbox URL provided"); + return; + } + + try { + const response = await fetch(outboxUrl); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const outboxData = await response.json(); + + this.totalPages = Math.ceil( + outboxData.orderedItems.length / this.numPosts + ); + // Prevent page number from going beyond total pages + this.page = Math.min(this.page, this.totalPages); + + // Simulate pagination by slicing the items array + const startIndex = (this.page - 1) * this.numPosts; + const endIndex = startIndex + this.numPosts; + const paginatedItems = outboxData.orderedItems.slice( + startIndex, + endIndex + ); + + for (const item of paginatedItems) { + yield item; + } + } catch (error) { + console.error("Error fetching outbox:", error); + } + } + + renderItem(item) { + const activityElement = document.createElement("distributed-activity"); + activityElement.setAttribute("type", item.type); + activityElement.setAttribute("data", JSON.stringify(item)); + this.appendChild(activityElement); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === "url") { + this.clearContent(); + this.loadOutbox(newValue); + } else if (name === "num-posts" || name === "page") { + // Convert attribute name from kebab-case to camelCase + const propName = name.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); + this[propName] = parseInt(newValue, 10); + this.clearContent(); + this.loadOutbox(this.getAttribute("url")); + } + } + clearContent() { + // Clear existing content + this.innerHTML = ""; + + this.renderedItems.clear(); + } +} + +// Register the new element with the browser +customElements.define("distributed-outbox", DistributedOutbox); + +class DistributedActivity extends HTMLElement { + constructor() { + super(); + this.activityType = ""; + this.activityData = {}; + } + + static get observedAttributes() { + return ["type", "data"]; + } + + connectedCallback() { + this.activityType = this.getAttribute("type"); + this.activityData = JSON.parse(this.getAttribute("data")); + this.renderActivity(); + } + + async fetchAndDisplayPost(postUrl) { + try { + const response = await fetch(postUrl); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const postData = await response.json(); + this.displayPostContent(postData.content); + } catch (error) { + console.error("Error fetching post content:", error); + } + } + + displayPostContent(content) { + // Clear existing content + this.innerHTML = ""; + + const postUrl = this.activityData.object; + // Create and append the distributed-post component + const distributedPostElement = document.createElement("distributed-post"); + distributedPostElement.setAttribute("url", postUrl); + this.appendChild(distributedPostElement); + } + + renderActivity() { + // Clear existing content + this.innerHTML = ""; + + switch (this.activityType) { + case "Create": + this.fetchAndDisplayPost(this.activityData.object); + break; + case "Follow": + this.displayFollowActivity(); + break; + case "Like": + this.displayLikeActivity(); + break; + default: + const message = `Activity type ${this.activityType} is not implemented yet.`; + const messageElement = document.createElement("p"); + messageElement.textContent = message; + this.appendChild(messageElement); + break; + } + } + + displayFollowActivity() { + const message = `New follow request from ${this.activityData.actor}`; + const messageElement = document.createElement("p"); + messageElement.textContent = message; + this.appendChild(messageElement); + } + + displayLikeActivity() { + const message = `New like on ${this.activityData.object}`; + const messageElement = document.createElement("p"); + messageElement.textContent = message; + this.appendChild(messageElement); + } + + attributeChangedCallback(name, oldValue, newValue) { + if ((name === "type" || name === "data") && newValue !== oldValue) { + this.activityType = this.getAttribute("type"); + this.activityData = JSON.parse(this.getAttribute("data")); + this.renderActivity(); + } + } +} + +// Register the new element with the browser +customElements.define("distributed-activity", DistributedActivity); + +// Functions to navigate to the next or previous page +document.addEventListener("DOMContentLoaded", () => { + const outbox = document.querySelector("distributed-outbox"); + window.nextPage = () => { + const currentPage = parseInt(outbox.getAttribute("page"), 10) || 1; + // Access the totalPages property + const totalPages = outbox.totalPages; + if (currentPage < totalPages) { + // Ensure the page number doesn't go below 1 + outbox.setAttribute("page", currentPage + 1); + } + }; + window.prevPage = () => { + const currentPage = parseInt(outbox.getAttribute("page"), 10) || 1; + // Ensure the page number doesn't go below 1 + outbox.setAttribute("page", Math.max(1, currentPage - 1)); + }; +}); From 427397d0eaad163a1d62000f44a026a96b66611b Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 02:58:13 +0530 Subject: [PATCH 019/149] refactor: outbox component pagination --- outbox.html | 4 ++-- outbox.js | 46 ++++++++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/outbox.html b/outbox.html index f670af3..668516b 100644 --- a/outbox.html +++ b/outbox.html @@ -9,8 +9,8 @@ >
- - + +
diff --git a/outbox.js b/outbox.js index f5f161c..f5f8aa9 100644 --- a/outbox.js +++ b/outbox.js @@ -16,6 +16,7 @@ class DistributedOutbox extends HTMLElement { this.numPosts = parseInt(this.getAttribute("num-posts")) || this.numPosts; this.page = parseInt(this.getAttribute("page")) || this.page; this.loadOutbox(this.getAttribute("url")); + this.paginationControls(); } async loadOutbox(outboxUrl) { @@ -72,6 +73,30 @@ class DistributedOutbox extends HTMLElement { this.appendChild(activityElement); } + paginationControls() { + // Attach event listeners for pagination + const prevButton = document.getElementById("prevPage"); + const nextButton = document.getElementById("nextPage"); + if (prevButton) { + prevButton.addEventListener("click", () => this.prevPage()); + } + if (nextButton) { + nextButton.addEventListener("click", () => this.nextPage()); + } + } + + nextPage() { + const currentPage = this.page; + if (currentPage < this.totalPages) { + this.setAttribute("page", currentPage + 1); + } + } + + prevPage() { + const currentPage = this.page; + this.setAttribute("page", Math.max(1, currentPage - 1)); + } + attributeChangedCallback(name, oldValue, newValue) { if (name === "url") { this.clearContent(); @@ -84,10 +109,10 @@ class DistributedOutbox extends HTMLElement { this.loadOutbox(this.getAttribute("url")); } } + clearContent() { // Clear existing content this.innerHTML = ""; - this.renderedItems.clear(); } } @@ -184,22 +209,3 @@ class DistributedActivity extends HTMLElement { // Register the new element with the browser customElements.define("distributed-activity", DistributedActivity); - -// Functions to navigate to the next or previous page -document.addEventListener("DOMContentLoaded", () => { - const outbox = document.querySelector("distributed-outbox"); - window.nextPage = () => { - const currentPage = parseInt(outbox.getAttribute("page"), 10) || 1; - // Access the totalPages property - const totalPages = outbox.totalPages; - if (currentPage < totalPages) { - // Ensure the page number doesn't go below 1 - outbox.setAttribute("page", currentPage + 1); - } - }; - window.prevPage = () => { - const currentPage = parseInt(outbox.getAttribute("page"), 10) || 1; - // Ensure the page number doesn't go below 1 - outbox.setAttribute("page", Math.max(1, currentPage - 1)); - }; -}); From 79316054cbb94f15fb67b4ad08bcd34386ab3d4a Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 03:01:31 +0530 Subject: [PATCH 020/149] feat: implement deduplication using object property as key --- outbox.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/outbox.js b/outbox.js index f5f8aa9..10dab46 100644 --- a/outbox.js +++ b/outbox.js @@ -1,7 +1,7 @@ class DistributedOutbox extends HTMLElement { constructor() { super(); - this.renderedItems = new Set(); // Tracks rendered items + this.renderedItems = new Map(); // Tracks rendered items by ID this.numPosts = 1; // Default value this.page = 1; // Default value this.totalPages = 0; // Keep track of total pages @@ -22,12 +22,20 @@ class DistributedOutbox extends HTMLElement { async loadOutbox(outboxUrl) { this.clearContent(); for await (const item of this.fetchOutboxItems(outboxUrl)) { - // Check if the item hasn't been rendered - if (!this.renderedItems.has(item.object)) { + const itemKey = item.object; + if (itemKey === undefined) { + console.error("Item key (object property) is undefined, item:", item); + continue; // Skip this item + } + if (!this.renderedItems.has(itemKey)) { this.renderItem(item); - // Mark as rendered - this.renderedItems.add(item.object); + // Mark as rendered by adding to the Map + this.renderedItems.set(itemKey, item); + // console.log(`Rendered item with key: ${itemKey}`); } + // else { + // console.log(`Duplicate item with key: ${itemKey} skipped`); + // } } } From d18c583bee8d2a436ba31352463ec3ea3707c7fc Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 03:03:25 +0530 Subject: [PATCH 021/149] refactor: use properties instead of attributes for complex data in web components --- outbox.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/outbox.js b/outbox.js index 10dab46..2a403c8 100644 --- a/outbox.js +++ b/outbox.js @@ -76,8 +76,8 @@ class DistributedOutbox extends HTMLElement { renderItem(item) { const activityElement = document.createElement("distributed-activity"); - activityElement.setAttribute("type", item.type); - activityElement.setAttribute("data", JSON.stringify(item)); + activityElement.type = item.type; + activityElement.data = item; this.appendChild(activityElement); } @@ -140,8 +140,8 @@ class DistributedActivity extends HTMLElement { } connectedCallback() { - this.activityType = this.getAttribute("type"); - this.activityData = JSON.parse(this.getAttribute("data")); + this.activityType = this.type; + this.activityData = this.data; this.renderActivity(); } From 283c4f55d63ffafc497bd18ec40e872e109c6087 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 03:05:44 +0530 Subject: [PATCH 022/149] refactor: specify radix in parseInt calls for numPosts and page --- outbox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outbox.js b/outbox.js index 2a403c8..af4967b 100644 --- a/outbox.js +++ b/outbox.js @@ -13,8 +13,8 @@ class DistributedOutbox extends HTMLElement { connectedCallback() { // Use attributes or default values - this.numPosts = parseInt(this.getAttribute("num-posts")) || this.numPosts; - this.page = parseInt(this.getAttribute("page")) || this.page; + this.numPosts = parseInt(this.getAttribute("num-posts"), 10) || this.numPosts; + this.page = parseInt(this.getAttribute("page"), 10) || this.page; this.loadOutbox(this.getAttribute("url")); this.paginationControls(); } From c0c6cef64cf9a9c4ae8d4f41b3dc2a3977883c2b Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 03:07:33 +0530 Subject: [PATCH 023/149] feat: increase default numPosts value in DistributedOutbox constructor --- outbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outbox.js b/outbox.js index af4967b..a8bf60b 100644 --- a/outbox.js +++ b/outbox.js @@ -2,7 +2,7 @@ class DistributedOutbox extends HTMLElement { constructor() { super(); this.renderedItems = new Map(); // Tracks rendered items by ID - this.numPosts = 1; // Default value + this.numPosts = 32; // Default value this.page = 1; // Default value this.totalPages = 0; // Keep track of total pages } From 1a1144bd1054757a4b284b0f2f9acd2c16d37d0e Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 22 Feb 2024 22:09:44 +0530 Subject: [PATCH 024/149] refactor: move pagination listeners to outbox.html --- outbox.html | 12 ++++++++++++ outbox.js | 13 ------------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/outbox.html b/outbox.html index 668516b..e3029eb 100644 --- a/outbox.html +++ b/outbox.html @@ -14,3 +14,15 @@ + diff --git a/outbox.js b/outbox.js index a8bf60b..1b90502 100644 --- a/outbox.js +++ b/outbox.js @@ -16,7 +16,6 @@ class DistributedOutbox extends HTMLElement { this.numPosts = parseInt(this.getAttribute("num-posts"), 10) || this.numPosts; this.page = parseInt(this.getAttribute("page"), 10) || this.page; this.loadOutbox(this.getAttribute("url")); - this.paginationControls(); } async loadOutbox(outboxUrl) { @@ -81,18 +80,6 @@ class DistributedOutbox extends HTMLElement { this.appendChild(activityElement); } - paginationControls() { - // Attach event listeners for pagination - const prevButton = document.getElementById("prevPage"); - const nextButton = document.getElementById("nextPage"); - if (prevButton) { - prevButton.addEventListener("click", () => this.prevPage()); - } - if (nextButton) { - nextButton.addEventListener("click", () => this.nextPage()); - } - } - nextPage() { const currentPage = this.page; if (currentPage < this.totalPages) { From ab062d980f287e5facca7ad82c05343946e4f5b6 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 23 Feb 2024 02:00:17 +0530 Subject: [PATCH 025/149] feat: enable dynamic data loading from URL --- outbox.html | 3 ++- outbox.js | 58 ++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/outbox.html b/outbox.html index e3029eb..89ad83e 100644 --- a/outbox.html +++ b/outbox.html @@ -4,8 +4,9 @@
+
diff --git a/outbox.js b/outbox.js index 1b90502..0433828 100644 --- a/outbox.js +++ b/outbox.js @@ -13,7 +13,8 @@ class DistributedOutbox extends HTMLElement { connectedCallback() { // Use attributes or default values - this.numPosts = parseInt(this.getAttribute("num-posts"), 10) || this.numPosts; + this.numPosts = + parseInt(this.getAttribute("num-posts"), 10) || this.numPosts; this.page = parseInt(this.getAttribute("page"), 10) || this.page; this.loadOutbox(this.getAttribute("url")); } @@ -120,16 +121,41 @@ class DistributedActivity extends HTMLElement { super(); this.activityType = ""; this.activityData = {}; + this.activityUrl = null; } static get observedAttributes() { - return ["type", "data"]; + return ["type", "data", "url"]; } - connectedCallback() { - this.activityType = this.type; - this.activityData = this.data; - this.renderActivity(); + async connectedCallback() { + // Check if the component already has type and data set as properties + if (this.type && this.data) { + this.activityType = this.type; + this.activityData = this.data; + this.renderActivity(); + } + // Load from URL if type and data are not set + else if (this.activityUrl) { + await this.loadDataFromUrl(this.activityUrl); + } else { + console.error("Activity data is not provided and no URL is specified."); + } + } + + async loadDataFromUrl(activityUrl) { + try { + const response = await fetch(activityUrl); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const activityData = await response.json(); + this.type = activityData.type; + this.data = activityData; + this.connectedCallback(); + } catch (error) { + console.error("Error loading activity data from URL:", error); + } } async fetchAndDisplayPost(postUrl) { @@ -194,10 +220,22 @@ class DistributedActivity extends HTMLElement { } attributeChangedCallback(name, oldValue, newValue) { - if ((name === "type" || name === "data") && newValue !== oldValue) { - this.activityType = this.getAttribute("type"); - this.activityData = JSON.parse(this.getAttribute("data")); - this.renderActivity(); + if (newValue !== oldValue) { + if (name === "type") { + this.activityType = newValue; + this.renderActivity(); + } else if (name === "data") { + this.activityData = JSON.parse(newValue); + this.renderActivity(); + } else if (name === "url") { + this.loadDataFromUrl(newValue) + .then(() => { + this.renderActivity(); + }) + .catch((error) => { + console.error("Error loading activity data from URL:", error); + }); + } } } } From f3f1f49eaefe0f1cec21896fd976eba8bf088048 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 26 Feb 2024 00:36:04 +0530 Subject: [PATCH 026/149] feat: enhance outbox handling to support direct and URL-referenced post activities --- outbox.html | 4 +-- outbox.js | 94 +++++++++++++++++++++++++++++++++-------------------- post.js | 60 +++++++++++++++++++++++++--------- 3 files changed, 105 insertions(+), 53 deletions(-) diff --git a/outbox.html b/outbox.html index 89ad83e..193de64 100644 --- a/outbox.html +++ b/outbox.html @@ -4,9 +4,9 @@
- +
diff --git a/outbox.js b/outbox.js index 0433828..037d9eb 100644 --- a/outbox.js +++ b/outbox.js @@ -2,7 +2,7 @@ class DistributedOutbox extends HTMLElement { constructor() { super(); this.renderedItems = new Map(); // Tracks rendered items by ID - this.numPosts = 32; // Default value + this.numPosts = 1; // Default value this.page = 1; // Default value this.totalPages = 0; // Keep track of total pages } @@ -22,20 +22,20 @@ class DistributedOutbox extends HTMLElement { async loadOutbox(outboxUrl) { this.clearContent(); for await (const item of this.fetchOutboxItems(outboxUrl)) { - const itemKey = item.object; - if (itemKey === undefined) { - console.error("Item key (object property) is undefined, item:", item); - continue; // Skip this item - } - if (!this.renderedItems.has(itemKey)) { - this.renderItem(item); - // Mark as rendered by adding to the Map - this.renderedItems.set(itemKey, item); - // console.log(`Rendered item with key: ${itemKey}`); - } - // else { - // console.log(`Duplicate item with key: ${itemKey} skipped`); - // } + this.processItem(item); + } + } + + + processItem(item) { + const itemKey = item.id || item.object; + if (!itemKey) { + console.error("Item key is undefined, item:", item); + return; + } + if (!this.renderedItems.has(itemKey)) { + this.renderItem(item); + this.renderedItems.set(itemKey, true); } } @@ -44,35 +44,44 @@ class DistributedOutbox extends HTMLElement { console.error("No outbox URL provided"); return; } - + try { const response = await fetch(outboxUrl); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } - const outboxData = await response.json(); - - this.totalPages = Math.ceil( - outboxData.orderedItems.length / this.numPosts - ); - // Prevent page number from going beyond total pages + const outbox = await response.json(); + + // Adjust for both direct items and items loaded via URLs + const items = []; + for (const itemOrUrl of outbox.orderedItems) { + if (typeof itemOrUrl === 'string') { // URL to an activity + const itemResponse = await fetch(itemOrUrl); + if (itemResponse.ok) { + const item = await itemResponse.json(); + items.push(item); + } + } else { + items.push(itemOrUrl); // Directly included activity + } + } + + this.totalPages = Math.ceil(items.length / this.numPosts); this.page = Math.min(this.page, this.totalPages); - - // Simulate pagination by slicing the items array + + // Calculate the range of items to be loaded based on the current page and numPosts const startIndex = (this.page - 1) * this.numPosts; const endIndex = startIndex + this.numPosts; - const paginatedItems = outboxData.orderedItems.slice( - startIndex, - endIndex - ); - - for (const item of paginatedItems) { + const itemsToLoad = items.slice(startIndex, endIndex); + + for (const item of itemsToLoad) { yield item; } } catch (error) { console.error("Error fetching outbox:", error); } } + renderItem(item) { const activityElement = document.createElement("distributed-activity"); @@ -158,27 +167,42 @@ class DistributedActivity extends HTMLElement { } } - async fetchAndDisplayPost(postUrl) { + async fetchAndDisplayPost() { + let postUrl; + // Determine the source of the post (direct activity or URL pointing to the activity) + const isDirectPost = + typeof this.activityData.object === "string" || + this.activityData.object instanceof String; + + if (isDirectPost) { + postUrl = this.activityData.object; + } else if (this.activityData.object && this.activityData.object.id) { + postUrl = this.activityData.id; + } else { + postUrl = this.activityData.object; + } + try { const response = await fetch(postUrl); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const postData = await response.json(); - this.displayPostContent(postData.content); + // Determine how to extract content based on the post source + const content = isDirectPost ? postData.content : postData.object.content; + this.displayPostContent(content, postUrl); } catch (error) { console.error("Error fetching post content:", error); } } - displayPostContent(content) { + displayPostContent(content, url) { // Clear existing content this.innerHTML = ""; - const postUrl = this.activityData.object; // Create and append the distributed-post component const distributedPostElement = document.createElement("distributed-post"); - distributedPostElement.setAttribute("url", postUrl); + distributedPostElement.setAttribute("url", url); this.appendChild(distributedPostElement); } diff --git a/post.js b/post.js index e9a4b2c..2d8f669 100644 --- a/post.js +++ b/post.js @@ -175,15 +175,27 @@ class DistributedPost extends HTMLElement { // Clear existing content this.innerHTML = ""; - // Create elements for each field - if (jsonLdData.attributedTo) { + // 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", jsonLdData.attributedTo); + actorInfo.setAttribute("url", attributedToSource); this.appendChild(actorInfo); } this.appendField("Published", jsonLdData.published); - this.appendField("Author", jsonLdData.attributedTo); + this.appendField("Author", attributedToSource); + + // Determine content source based on structure of jsonLdData + let contentSource = jsonLdData.content; + if ("object" in jsonLdData && "content" in jsonLdData.object) { + contentSource = jsonLdData.object.content; + } // Handle sensitive content if (jsonLdData.sensitive) { @@ -192,13 +204,13 @@ class DistributedPost extends HTMLElement { summary.textContent = "Sensitive Content (click to view)"; details.appendChild(summary); const content = document.createElement("p"); - // TODO: Sanitize jsonLdData.content to remove or escape any harmful HTML content before displaying - content.textContent = jsonLdData.content; + // TODO: Sanitize contentSource to remove or escape any harmful HTML content before displaying + content.textContent = contentSource; details.appendChild(content); this.appendChild(details); } else { // If not sensitive, display content as usual - this.appendField("Content", jsonLdData.content); + this.appendField("Content", contentSource); } } @@ -243,18 +255,34 @@ class ActorInfo extends HTMLElement { try { const actorInfo = await fetchActorInfo(url); if (actorInfo) { - // Render the actor's avatar and name // Clear existing content this.innerHTML = ""; - const pName = document.createElement("p"); - pName.textContent = actorInfo.name; - const img = document.createElement("img"); - img.src = actorInfo.icon[0].url; - img.width = 69; - img.alt = actorInfo.name; - this.appendChild(pName); - this.appendChild(img); + if (actorInfo.name) { + const pName = document.createElement("p"); + pName.textContent = actorInfo.name; + this.appendChild(pName); + } + + // Handle both single icon object and array of icons + let iconUrl = null; + if (actorInfo.icon) { + if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) { + // Assume first icon if array + iconUrl = actorInfo.icon[0].url; + } else if (actorInfo.icon.url) { + // Directly use the URL if object + iconUrl = actorInfo.icon.url; + } + + if (iconUrl) { + const img = document.createElement("img"); + img.src = iconUrl; + img.width = 69; + img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + this.appendChild(img); + } + } } } catch (error) { const errorElement = renderError(error.message); From d36d796161b76ca05965a0441fb69e05f293cfcf Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 26 Feb 2024 03:08:18 +0530 Subject: [PATCH 027/149] feat: sanitize HTML content in DistributedPost component using DOMPurify --- dependencies/dompurify/purify.js | 1514 ++++++++++++++++++++++++++ dependencies/dompurify/purify.js.map | 1 + post.js | 26 +- 3 files changed, 1535 insertions(+), 6 deletions(-) create mode 100644 dependencies/dompurify/purify.js create mode 100644 dependencies/dompurify/purify.js.map diff --git a/dependencies/dompurify/purify.js b/dependencies/dompurify/purify.js new file mode 100644 index 0000000..da0df8a --- /dev/null +++ b/dependencies/dompurify/purify.js @@ -0,0 +1,1514 @@ +/*! @license DOMPurify 3.0.9 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.9/LICENSE */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.DOMPurify = factory()); +})(this, (function () { 'use strict'; + + const { + entries, + setPrototypeOf, + isFrozen, + getPrototypeOf, + getOwnPropertyDescriptor + } = Object; + let { + freeze, + seal, + create + } = Object; // eslint-disable-line import/no-mutable-exports + let { + apply, + construct + } = typeof Reflect !== 'undefined' && Reflect; + if (!freeze) { + freeze = function freeze(x) { + return x; + }; + } + if (!seal) { + seal = function seal(x) { + return x; + }; + } + if (!apply) { + apply = function apply(fun, thisValue, args) { + return fun.apply(thisValue, args); + }; + } + if (!construct) { + construct = function construct(Func, args) { + return new Func(...args); + }; + } + const arrayForEach = unapply(Array.prototype.forEach); + const arrayPop = unapply(Array.prototype.pop); + const arrayPush = unapply(Array.prototype.push); + const stringToLowerCase = unapply(String.prototype.toLowerCase); + const stringToString = unapply(String.prototype.toString); + const stringMatch = unapply(String.prototype.match); + const stringReplace = unapply(String.prototype.replace); + const stringIndexOf = unapply(String.prototype.indexOf); + const stringTrim = unapply(String.prototype.trim); + const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty); + const regExpTest = unapply(RegExp.prototype.test); + const typeErrorCreate = unconstruct(TypeError); + + /** + * Creates a new function that calls the given function with a specified thisArg and arguments. + * + * @param {Function} func - The function to be wrapped and called. + * @returns {Function} A new function that calls the given function with a specified thisArg and arguments. + */ + function unapply(func) { + return function (thisArg) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + return apply(func, thisArg, args); + }; + } + + /** + * Creates a new function that constructs an instance of the given constructor function with the provided arguments. + * + * @param {Function} func - The constructor function to be wrapped and called. + * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments. + */ + function unconstruct(func) { + return function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + return construct(func, args); + }; + } + + /** + * Add properties to a lookup table + * + * @param {Object} set - The set to which elements will be added. + * @param {Array} array - The array containing elements to be added to the set. + * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set. + * @returns {Object} The modified set with added elements. + */ + function addToSet(set, array) { + let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; + if (setPrototypeOf) { + // Make 'in' and truthy checks like Boolean(set.constructor) + // independent of any properties defined on Object.prototype. + // Prevent prototype setters from intercepting set as a this value. + setPrototypeOf(set, null); + } + let l = array.length; + while (l--) { + let element = array[l]; + if (typeof element === 'string') { + const lcElement = transformCaseFunc(element); + if (lcElement !== element) { + // Config presets (e.g. tags.js, attrs.js) are immutable. + if (!isFrozen(array)) { + array[l] = lcElement; + } + element = lcElement; + } + } + set[element] = true; + } + return set; + } + + /** + * Clean up an array to harden against CSPP + * + * @param {Array} array - The array to be cleaned. + * @returns {Array} The cleaned version of the array + */ + function cleanArray(array) { + for (let index = 0; index < array.length; index++) { + const isPropertyExist = objectHasOwnProperty(array, index); + if (!isPropertyExist) { + array[index] = null; + } + } + return array; + } + + /** + * Shallow clone an object + * + * @param {Object} object - The object to be cloned. + * @returns {Object} A new object that copies the original. + */ + function clone(object) { + const newObject = create(null); + for (const [property, value] of entries(object)) { + const isPropertyExist = objectHasOwnProperty(object, property); + if (isPropertyExist) { + if (Array.isArray(value)) { + newObject[property] = cleanArray(value); + } else if (value && typeof value === 'object' && value.constructor === Object) { + newObject[property] = clone(value); + } else { + newObject[property] = value; + } + } + } + return newObject; + } + + /** + * This method automatically checks if the prop is function or getter and behaves accordingly. + * + * @param {Object} object - The object to look up the getter function in its prototype chain. + * @param {String} prop - The property name for which to find the getter function. + * @returns {Function} The getter function found in the prototype chain or a fallback function. + */ + function lookupGetter(object, prop) { + while (object !== null) { + const desc = getOwnPropertyDescriptor(object, prop); + if (desc) { + if (desc.get) { + return unapply(desc.get); + } + if (typeof desc.value === 'function') { + return unapply(desc.value); + } + } + object = getPrototypeOf(object); + } + function fallbackValue() { + return null; + } + return fallbackValue; + } + + const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); + + // SVG + const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); + const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); + + // List of SVG elements that are disallowed by default. + // We still need to know them so that we can do namespace + // checks properly in case one wants to add them to + // allow-list. + const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); + const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']); + + // Similarly to SVG, we want to know all MathML elements, + // even those that we disallow by default. + const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); + const text = freeze(['#text']); + + const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns', 'slot']); + const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); + const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); + const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); + + // eslint-disable-next-line unicorn/better-regex + const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode + const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); + const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm); + const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape + const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape + const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape + ); + + const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); + const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex + ); + + const DOCTYPE_NAME = seal(/^html$/i); + + var EXPRESSIONS = /*#__PURE__*/Object.freeze({ + __proto__: null, + MUSTACHE_EXPR: MUSTACHE_EXPR, + ERB_EXPR: ERB_EXPR, + TMPLIT_EXPR: TMPLIT_EXPR, + DATA_ATTR: DATA_ATTR, + ARIA_ATTR: ARIA_ATTR, + IS_ALLOWED_URI: IS_ALLOWED_URI, + IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA, + ATTR_WHITESPACE: ATTR_WHITESPACE, + DOCTYPE_NAME: DOCTYPE_NAME + }); + + const getGlobal = function getGlobal() { + return typeof window === 'undefined' ? null : window; + }; + + /** + * Creates a no-op policy for internal use only. + * Don't export this function outside this module! + * @param {TrustedTypePolicyFactory} trustedTypes The policy factory. + * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). + * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types + * are not supported or creating the policy failed). + */ + const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { + if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { + return null; + } + + // Allow the callers to control the unique policy name + // by adding a data-tt-policy-suffix to the script element with the DOMPurify. + // Policy creation with duplicate names throws in Trusted Types. + let suffix = null; + const ATTR_NAME = 'data-tt-policy-suffix'; + if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { + suffix = purifyHostElement.getAttribute(ATTR_NAME); + } + const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); + try { + return trustedTypes.createPolicy(policyName, { + createHTML(html) { + return html; + }, + createScriptURL(scriptUrl) { + return scriptUrl; + } + }); + } catch (_) { + // Policy creation failed (most likely another DOMPurify script has + // already run). Skip creating the policy, as this will only cause errors + // if TT are enforced. + console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); + return null; + } + }; + function createDOMPurify() { + let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); + const DOMPurify = root => createDOMPurify(root); + + /** + * Version label, exposed for easier checks + * if DOMPurify is up to date or not + */ + DOMPurify.version = '3.0.9'; + + /** + * Array of elements that DOMPurify removed during sanitation. + * Empty if nothing was removed. + */ + DOMPurify.removed = []; + if (!window || !window.document || window.document.nodeType !== 9) { + // Not running in a browser, provide a factory function + // so that you can pass your own Window + DOMPurify.isSupported = false; + return DOMPurify; + } + let { + document + } = window; + const originalDocument = document; + const currentScript = originalDocument.currentScript; + const { + DocumentFragment, + HTMLTemplateElement, + Node, + Element, + NodeFilter, + NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap, + HTMLFormElement, + DOMParser, + trustedTypes + } = window; + const ElementPrototype = Element.prototype; + const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); + const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); + const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); + const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); + + // As per issue #47, the web-components registry is inherited by a + // new document created via createHTMLDocument. As per the spec + // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) + // a new empty registry is used when creating a template contents owner + // document, so we use that as our parent document to ensure nothing + // is inherited. + if (typeof HTMLTemplateElement === 'function') { + const template = document.createElement('template'); + if (template.content && template.content.ownerDocument) { + document = template.content.ownerDocument; + } + } + let trustedTypesPolicy; + let emptyHTML = ''; + const { + implementation, + createNodeIterator, + createDocumentFragment, + getElementsByTagName + } = document; + const { + importNode + } = originalDocument; + let hooks = {}; + + /** + * Expose whether this browser supports running the full DOMPurify. + */ + DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; + const { + MUSTACHE_EXPR, + ERB_EXPR, + TMPLIT_EXPR, + DATA_ATTR, + ARIA_ATTR, + IS_SCRIPT_OR_DATA, + ATTR_WHITESPACE + } = EXPRESSIONS; + let { + IS_ALLOWED_URI: IS_ALLOWED_URI$1 + } = EXPRESSIONS; + + /** + * We consider the elements and attributes below to be safe. Ideally + * don't add any new ones but feel free to remove unwanted ones. + */ + + /* allowed element names */ + let ALLOWED_TAGS = null; + const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); + + /* Allowed attribute names */ + let ALLOWED_ATTR = null; + const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); + + /* + * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements. + * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) + * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) + * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. + */ + let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { + tagNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + attributeNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + allowCustomizedBuiltInElements: { + writable: true, + configurable: false, + enumerable: true, + value: false + } + })); + + /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ + let FORBID_TAGS = null; + + /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ + let FORBID_ATTR = null; + + /* Decide if ARIA attributes are okay */ + let ALLOW_ARIA_ATTR = true; + + /* Decide if custom data attributes are okay */ + let ALLOW_DATA_ATTR = true; + + /* Decide if unknown protocols are okay */ + let ALLOW_UNKNOWN_PROTOCOLS = false; + + /* Decide if self-closing tags in attributes are allowed. + * Usually removed due to a mXSS issue in jQuery 3.0 */ + let ALLOW_SELF_CLOSE_IN_ATTR = true; + + /* Output should be safe for common template engines. + * This means, DOMPurify removes data attributes, mustaches and ERB + */ + let SAFE_FOR_TEMPLATES = false; + + /* Decide if document with ... should be returned */ + let WHOLE_DOCUMENT = false; + + /* Track whether config is already set on this instance of DOMPurify. */ + let SET_CONFIG = false; + + /* Decide if all elements (e.g. style, script) must be children of + * document.body. By default, browsers might move them to document.head */ + let FORCE_BODY = false; + + /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported). + * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead + */ + let RETURN_DOM = false; + + /* Decide if a DOM `DocumentFragment` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported) */ + let RETURN_DOM_FRAGMENT = false; + + /* Try to return a Trusted Type object instead of a string, return a string in + * case Trusted Types are not supported */ + let RETURN_TRUSTED_TYPE = false; + + /* Output should be free from DOM clobbering attacks? + * This sanitizes markups named with colliding, clobberable built-in DOM APIs. + */ + let SANITIZE_DOM = true; + + /* Achieve full DOM Clobbering protection by isolating the namespace of named + * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. + * + * HTML/DOM spec rules that enable DOM Clobbering: + * - Named Access on Window (§7.3.3) + * - DOM Tree Accessors (§3.1.5) + * - Form Element Parent-Child Relations (§4.10.3) + * - Iframe srcdoc / Nested WindowProxies (§4.8.5) + * - HTMLCollection (§4.2.10.2) + * + * Namespace isolation is implemented by prefixing `id` and `name` attributes + * with a constant string, i.e., `user-content-` + */ + let SANITIZE_NAMED_PROPS = false; + const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; + + /* Keep element content when removing element? */ + let KEEP_CONTENT = true; + + /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead + * of importing it into a new Document and returning a sanitized copy */ + let IN_PLACE = false; + + /* Allow usage of profiles like html, svg and mathMl */ + let USE_PROFILES = {}; + + /* Tags to ignore content of when KEEP_CONTENT is true */ + let FORBID_CONTENTS = null; + const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); + + /* Tags that are safe for data: URIs */ + let DATA_URI_TAGS = null; + const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); + + /* Attributes safe for values like "javascript:" */ + let URI_SAFE_ATTRIBUTES = null; + const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); + const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; + const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; + /* Document namespace */ + let NAMESPACE = HTML_NAMESPACE; + let IS_EMPTY_INPUT = false; + + /* Allowed XHTML+XML namespaces */ + let ALLOWED_NAMESPACES = null; + const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); + + /* Parsing of strict XHTML documents */ + let PARSER_MEDIA_TYPE = null; + const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; + const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; + let transformCaseFunc = null; + + /* Keep a reference to config to pass to hooks */ + let CONFIG = null; + + /* Ideally, do not touch anything below this line */ + /* ______________________________________________ */ + + const formElement = document.createElement('form'); + const isRegexOrFunction = function isRegexOrFunction(testValue) { + return testValue instanceof RegExp || testValue instanceof Function; + }; + + /** + * _parseConfig + * + * @param {Object} cfg optional config literal + */ + // eslint-disable-next-line complexity + const _parseConfig = function _parseConfig() { + let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + if (CONFIG && CONFIG === cfg) { + return; + } + + /* Shield configuration object from tampering */ + if (!cfg || typeof cfg !== 'object') { + cfg = {}; + } + + /* Shield configuration object from prototype pollution */ + cfg = clone(cfg); + PARSER_MEDIA_TYPE = + // eslint-disable-next-line unicorn/prefer-includes + SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; + + // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. + transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; + + /* Set configuration parameters */ + ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; + ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; + ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; + URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), + // eslint-disable-line indent + cfg.ADD_URI_SAFE_ATTR, + // eslint-disable-line indent + transformCaseFunc // eslint-disable-line indent + ) // eslint-disable-line indent + : DEFAULT_URI_SAFE_ATTRIBUTES; + DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), + // eslint-disable-line indent + cfg.ADD_DATA_URI_TAGS, + // eslint-disable-line indent + transformCaseFunc // eslint-disable-line indent + ) // eslint-disable-line indent + : DEFAULT_DATA_URI_TAGS; + FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; + FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {}; + FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {}; + USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false; + ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true + ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true + ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false + ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true + SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false + WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false + RETURN_DOM = cfg.RETURN_DOM || false; // Default false + RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false + RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false + FORCE_BODY = cfg.FORCE_BODY || false; // Default false + SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true + SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false + KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true + IN_PLACE = cfg.IN_PLACE || false; // Default false + IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI; + NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; + CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {}; + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { + CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; + } + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { + CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; + } + if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { + CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; + } + if (SAFE_FOR_TEMPLATES) { + ALLOW_DATA_ATTR = false; + } + if (RETURN_DOM_FRAGMENT) { + RETURN_DOM = true; + } + + /* Parse profile info */ + if (USE_PROFILES) { + ALLOWED_TAGS = addToSet({}, text); + ALLOWED_ATTR = []; + if (USE_PROFILES.html === true) { + addToSet(ALLOWED_TAGS, html$1); + addToSet(ALLOWED_ATTR, html); + } + if (USE_PROFILES.svg === true) { + addToSet(ALLOWED_TAGS, svg$1); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.svgFilters === true) { + addToSet(ALLOWED_TAGS, svgFilters); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.mathMl === true) { + addToSet(ALLOWED_TAGS, mathMl$1); + addToSet(ALLOWED_ATTR, mathMl); + addToSet(ALLOWED_ATTR, xml); + } + } + + /* Merge configuration parameters */ + if (cfg.ADD_TAGS) { + if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { + ALLOWED_TAGS = clone(ALLOWED_TAGS); + } + addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); + } + if (cfg.ADD_ATTR) { + if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { + ALLOWED_ATTR = clone(ALLOWED_ATTR); + } + addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); + } + if (cfg.ADD_URI_SAFE_ATTR) { + addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); + } + if (cfg.FORBID_CONTENTS) { + if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { + FORBID_CONTENTS = clone(FORBID_CONTENTS); + } + addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); + } + + /* Add #text in case KEEP_CONTENT is set to true */ + if (KEEP_CONTENT) { + ALLOWED_TAGS['#text'] = true; + } + + /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ + if (WHOLE_DOCUMENT) { + addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); + } + + /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ + if (ALLOWED_TAGS.table) { + addToSet(ALLOWED_TAGS, ['tbody']); + delete FORBID_TAGS.tbody; + } + if (cfg.TRUSTED_TYPES_POLICY) { + if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); + } + if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); + } + + // Overwrite existing TrustedTypes policy. + trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; + + // Sign local variables required by `sanitize`. + emptyHTML = trustedTypesPolicy.createHTML(''); + } else { + // Uninitialized policy, attempt to initialize the internal dompurify policy. + if (trustedTypesPolicy === undefined) { + trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); + } + + // If creating the internal policy succeeded sign internal variables. + if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') { + emptyHTML = trustedTypesPolicy.createHTML(''); + } + } + + // Prevent further manipulation of configuration. + // Not available in IE8, Safari 5, etc. + if (freeze) { + freeze(cfg); + } + CONFIG = cfg; + }; + const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); + const HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']); + + // Certain elements are allowed in both SVG and HTML + // namespace. We need to specify them explicitly + // so that they don't get erroneously deleted from + // HTML namespace. + const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); + + /* Keep track of all possible SVG and MathML tags + * so that we can perform the namespace checks + * correctly. */ + const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); + const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); + + /** + * @param {Element} element a DOM element whose namespace is being checked + * @returns {boolean} Return false if the element has a + * namespace that a spec-compliant parser would never + * return. Return true otherwise. + */ + const _checkValidNamespace = function _checkValidNamespace(element) { + let parent = getParentNode(element); + + // In JSDOM, if we're inside shadow DOM, then parentNode + // can be null. We just simulate parent in this case. + if (!parent || !parent.tagName) { + parent = { + namespaceURI: NAMESPACE, + tagName: 'template' + }; + } + const tagName = stringToLowerCase(element.tagName); + const parentTagName = stringToLowerCase(parent.tagName); + if (!ALLOWED_NAMESPACES[element.namespaceURI]) { + return false; + } + if (element.namespaceURI === SVG_NAMESPACE) { + // The only way to switch from HTML namespace to SVG + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'svg'; + } + + // The only way to switch from MathML to SVG is via` + // svg if parent is either or MathML + // text integration points. + if (parent.namespaceURI === MATHML_NAMESPACE) { + return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); + } + + // We only allow elements that are defined in SVG + // spec. All others are disallowed in SVG namespace. + return Boolean(ALL_SVG_TAGS[tagName]); + } + if (element.namespaceURI === MATHML_NAMESPACE) { + // The only way to switch from HTML namespace to MathML + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'math'; + } + + // The only way to switch from SVG to MathML is via + // and HTML integration points + if (parent.namespaceURI === SVG_NAMESPACE) { + return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; + } + + // We only allow elements that are defined in MathML + // spec. All others are disallowed in MathML namespace. + return Boolean(ALL_MATHML_TAGS[tagName]); + } + if (element.namespaceURI === HTML_NAMESPACE) { + // The only way to switch from SVG to HTML is via + // HTML integration points, and from MathML to HTML + // is via MathML text integration points + if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { + return false; + } + if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { + return false; + } + + // We disallow tags that are specific for MathML + // or SVG and should never appear in HTML namespace + return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); + } + + // For XHTML and XML documents that support custom namespaces + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { + return true; + } + + // The code should never reach this place (this means + // that the element somehow got namespace that is not + // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). + // Return false just in case. + return false; + }; + + /** + * _forceRemove + * + * @param {Node} node a DOM node + */ + const _forceRemove = function _forceRemove(node) { + arrayPush(DOMPurify.removed, { + element: node + }); + try { + // eslint-disable-next-line unicorn/prefer-dom-node-remove + node.parentNode.removeChild(node); + } catch (_) { + node.remove(); + } + }; + + /** + * _removeAttribute + * + * @param {String} name an Attribute name + * @param {Node} node a DOM node + */ + const _removeAttribute = function _removeAttribute(name, node) { + try { + arrayPush(DOMPurify.removed, { + attribute: node.getAttributeNode(name), + from: node + }); + } catch (_) { + arrayPush(DOMPurify.removed, { + attribute: null, + from: node + }); + } + node.removeAttribute(name); + + // We void attribute values for unremovable "is"" attributes + if (name === 'is' && !ALLOWED_ATTR[name]) { + if (RETURN_DOM || RETURN_DOM_FRAGMENT) { + try { + _forceRemove(node); + } catch (_) {} + } else { + try { + node.setAttribute(name, ''); + } catch (_) {} + } + } + }; + + /** + * _initDocument + * + * @param {String} dirty a string of dirty markup + * @return {Document} a DOM, filled with the dirty markup + */ + const _initDocument = function _initDocument(dirty) { + /* Create a HTML document */ + let doc = null; + let leadingWhitespace = null; + if (FORCE_BODY) { + dirty = '' + dirty; + } else { + /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ + const matches = stringMatch(dirty, /^[\r\n\t ]+/); + leadingWhitespace = matches && matches[0]; + } + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { + // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) + dirty = '' + dirty + ''; + } + const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; + /* + * Use the DOMParser API by default, fallback later if needs be + * DOMParser not work for svg when has multiple root element. + */ + if (NAMESPACE === HTML_NAMESPACE) { + try { + doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); + } catch (_) {} + } + + /* Use createHTMLDocument in case DOMParser is not available */ + if (!doc || !doc.documentElement) { + doc = implementation.createDocument(NAMESPACE, 'template', null); + try { + doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; + } catch (_) { + // Syntax error if dirtyPayload is invalid xml + } + } + const body = doc.body || doc.documentElement; + if (dirty && leadingWhitespace) { + body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); + } + + /* Work on whole document or just its body */ + if (NAMESPACE === HTML_NAMESPACE) { + return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; + } + return WHOLE_DOCUMENT ? doc.documentElement : body; + }; + + /** + * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. + * + * @param {Node} root The root element or node to start traversing on. + * @return {NodeIterator} The created NodeIterator + */ + const _createNodeIterator = function _createNodeIterator(root) { + return createNodeIterator.call(root.ownerDocument || root, root, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, null); + }; + + /** + * _isClobbered + * + * @param {Node} elm element to check for clobbering attacks + * @return {Boolean} true if clobbered, false if safe + */ + const _isClobbered = function _isClobbered(elm) { + return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function'); + }; + + /** + * Checks whether the given object is a DOM node. + * + * @param {Node} object object to check whether it's a DOM node + * @return {Boolean} true is object is a DOM node + */ + const _isNode = function _isNode(object) { + return typeof Node === 'function' && object instanceof Node; + }; + + /** + * _executeHook + * Execute user configurable hooks + * + * @param {String} entryPoint Name of the hook's entry point + * @param {Node} currentNode node to work on with the hook + * @param {Object} data additional hook parameters + */ + const _executeHook = function _executeHook(entryPoint, currentNode, data) { + if (!hooks[entryPoint]) { + return; + } + arrayForEach(hooks[entryPoint], hook => { + hook.call(DOMPurify, currentNode, data, CONFIG); + }); + }; + + /** + * _sanitizeElements + * + * @protect nodeName + * @protect textContent + * @protect removeChild + * + * @param {Node} currentNode to check for permission to exist + * @return {Boolean} true if node was killed, false if left alive + */ + const _sanitizeElements = function _sanitizeElements(currentNode) { + let content = null; + + /* Execute a hook if present */ + _executeHook('beforeSanitizeElements', currentNode, null); + + /* Check if element is clobbered or can clobber */ + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + return true; + } + + /* Now let's check the element's type and name */ + const tagName = transformCaseFunc(currentNode.nodeName); + + /* Execute a hook if present */ + _executeHook('uponSanitizeElement', currentNode, { + tagName, + allowedTags: ALLOWED_TAGS + }); + + /* Detect mXSS attempts abusing namespace confusion */ + if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { + _forceRemove(currentNode); + return true; + } + + /* Remove element if anything forbids its presence */ + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + /* Check if we have a custom element to handle */ + if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { + return false; + } + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { + return false; + } + } + + /* Keep content except for bad-listed elements */ + if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { + const parentNode = getParentNode(currentNode) || currentNode.parentNode; + const childNodes = getChildNodes(currentNode) || currentNode.childNodes; + if (childNodes && parentNode) { + const childCount = childNodes.length; + for (let i = childCount - 1; i >= 0; --i) { + parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode)); + } + } + } + _forceRemove(currentNode); + return true; + } + + /* Check whether element has a valid namespace */ + if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { + _forceRemove(currentNode); + return true; + } + + /* Make sure that older browsers don't get fallback-tag mXSS */ + if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { + _forceRemove(currentNode); + return true; + } + + /* Sanitize element content to be template-safe */ + if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) { + /* Get the element's text content */ + content = currentNode.textContent; + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + content = stringReplace(content, expr, ' '); + }); + if (currentNode.textContent !== content) { + arrayPush(DOMPurify.removed, { + element: currentNode.cloneNode() + }); + currentNode.textContent = content; + } + } + + /* Execute a hook if present */ + _executeHook('afterSanitizeElements', currentNode, null); + return false; + }; + + /** + * _isValidAttribute + * + * @param {string} lcTag Lowercase tag name of containing element. + * @param {string} lcName Lowercase attribute name. + * @param {string} value Attribute value. + * @return {Boolean} Returns true if `value` is valid, otherwise false. + */ + // eslint-disable-next-line complexity + const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { + /* Make sure attribute cannot clobber */ + if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { + return false; + } + + /* Allow valid data-* attributes: At least one character after "-" + (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) + XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) + We don't need to check the value; it's always URI safe. */ + if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { + if ( + // First condition does a very basic check if a) it's basically a valid custom element tagname AND + // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck + _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) || + // Alternative, second condition checks if it's an `is`-attribute, AND + // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else { + return false; + } + /* Check value is safe. First, is attr inert? If so, is safe */ + } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) { + return false; + } else ; + return true; + }; + + /** + * _isBasicCustomElement + * checks if at least one dash is included in tagName, and it's not the first char + * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name + * + * @param {string} tagName name of the tag of the node to sanitize + * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false. + */ + const _isBasicCustomElement = function _isBasicCustomElement(tagName) { + return tagName !== 'annotation-xml' && tagName.indexOf('-') > 0; + }; + + /** + * _sanitizeAttributes + * + * @protect attributes + * @protect nodeName + * @protect removeAttribute + * @protect setAttribute + * + * @param {Node} currentNode to sanitize + */ + const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { + /* Execute a hook if present */ + _executeHook('beforeSanitizeAttributes', currentNode, null); + const { + attributes + } = currentNode; + + /* Check if we have attributes; if not we might have a text node */ + if (!attributes) { + return; + } + const hookEvent = { + attrName: '', + attrValue: '', + keepAttr: true, + allowedAttributes: ALLOWED_ATTR + }; + let l = attributes.length; + + /* Go backwards over all attributes; safely remove bad ones */ + while (l--) { + const attr = attributes[l]; + const { + name, + namespaceURI, + value: attrValue + } = attr; + const lcName = transformCaseFunc(name); + let value = name === 'value' ? attrValue : stringTrim(attrValue); + + /* Execute a hook if present */ + hookEvent.attrName = lcName; + hookEvent.attrValue = value; + hookEvent.keepAttr = true; + hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set + _executeHook('uponSanitizeAttribute', currentNode, hookEvent); + value = hookEvent.attrValue; + /* Did the hooks approve of the attribute? */ + if (hookEvent.forceKeepAttr) { + continue; + } + + /* Remove attribute */ + _removeAttribute(name, currentNode); + + /* Did the hooks approve of the attribute? */ + if (!hookEvent.keepAttr) { + continue; + } + + /* Work around a security issue in jQuery 3.0 */ + if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { + _removeAttribute(name, currentNode); + continue; + } + + /* Sanitize attribute content to be template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + value = stringReplace(value, expr, ' '); + }); + } + + /* Is `value` valid for this attribute? */ + const lcTag = transformCaseFunc(currentNode.nodeName); + if (!_isValidAttribute(lcTag, lcName, value)) { + continue; + } + + /* Full DOM Clobbering protection via namespace isolation, + * Prefix id and name attributes with `user-content-` + */ + if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) { + // Remove the attribute with this value + _removeAttribute(name, currentNode); + + // Prefix the value and later re-create the attribute with the sanitized value + value = SANITIZE_NAMED_PROPS_PREFIX + value; + } + + /* Handle attributes that require Trusted Types */ + if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') { + if (namespaceURI) ; else { + switch (trustedTypes.getAttributeType(lcTag, lcName)) { + case 'TrustedHTML': + { + value = trustedTypesPolicy.createHTML(value); + break; + } + case 'TrustedScriptURL': + { + value = trustedTypesPolicy.createScriptURL(value); + break; + } + } + } + } + + /* Handle invalid data-* attribute set by try-catching it */ + try { + if (namespaceURI) { + currentNode.setAttributeNS(namespaceURI, name, value); + } else { + /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ + currentNode.setAttribute(name, value); + } + arrayPop(DOMPurify.removed); + } catch (_) {} + } + + /* Execute a hook if present */ + _executeHook('afterSanitizeAttributes', currentNode, null); + }; + + /** + * _sanitizeShadowDOM + * + * @param {DocumentFragment} fragment to iterate over recursively + */ + const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) { + let shadowNode = null; + const shadowIterator = _createNodeIterator(fragment); + + /* Execute a hook if present */ + _executeHook('beforeSanitizeShadowDOM', fragment, null); + while (shadowNode = shadowIterator.nextNode()) { + /* Execute a hook if present */ + _executeHook('uponSanitizeShadowNode', shadowNode, null); + + /* Sanitize tags and elements */ + if (_sanitizeElements(shadowNode)) { + continue; + } + + /* Deep shadow DOM detected */ + if (shadowNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(shadowNode.content); + } + + /* Check attributes, sanitize if necessary */ + _sanitizeAttributes(shadowNode); + } + + /* Execute a hook if present */ + _executeHook('afterSanitizeShadowDOM', fragment, null); + }; + + /** + * Sanitize + * Public method providing core sanitation functionality + * + * @param {String|Node} dirty string or DOM node + * @param {Object} cfg object + */ + // eslint-disable-next-line complexity + DOMPurify.sanitize = function (dirty) { + let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + let body = null; + let importedNode = null; + let currentNode = null; + let returnNode = null; + /* Make sure we have a string to sanitize. + DO NOT return early, as this will return the wrong type if + the user has requested a DOM object rather than a string */ + IS_EMPTY_INPUT = !dirty; + if (IS_EMPTY_INPUT) { + dirty = ''; + } + + /* Stringify, in case dirty is an object */ + if (typeof dirty !== 'string' && !_isNode(dirty)) { + if (typeof dirty.toString === 'function') { + dirty = dirty.toString(); + if (typeof dirty !== 'string') { + throw typeErrorCreate('dirty is not a string, aborting'); + } + } else { + throw typeErrorCreate('toString is not a function'); + } + } + + /* Return dirty HTML if DOMPurify cannot run */ + if (!DOMPurify.isSupported) { + return dirty; + } + + /* Assign config vars */ + if (!SET_CONFIG) { + _parseConfig(cfg); + } + + /* Clean up removed elements */ + DOMPurify.removed = []; + + /* Check if dirty is correctly typed for IN_PLACE */ + if (typeof dirty === 'string') { + IN_PLACE = false; + } + if (IN_PLACE) { + /* Do some early pre-sanitization to avoid unsafe root nodes */ + if (dirty.nodeName) { + const tagName = transformCaseFunc(dirty.nodeName); + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); + } + } + } else if (dirty instanceof Node) { + /* If dirty is a DOM element, append to an empty document to avoid + elements being stripped by the parser */ + body = _initDocument(''); + importedNode = body.ownerDocument.importNode(dirty, true); + if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') { + /* Node is already a body, use as is */ + body = importedNode; + } else if (importedNode.nodeName === 'HTML') { + body = importedNode; + } else { + // eslint-disable-next-line unicorn/prefer-dom-node-append + body.appendChild(importedNode); + } + } else { + /* Exit directly if we have nothing to do */ + if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && + // eslint-disable-next-line unicorn/prefer-includes + dirty.indexOf('<') === -1) { + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; + } + + /* Initialize the document to work on */ + body = _initDocument(dirty); + + /* Check we have a DOM node from the data */ + if (!body) { + return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; + } + } + + /* Remove first element node (ours) if FORCE_BODY is set */ + if (body && FORCE_BODY) { + _forceRemove(body.firstChild); + } + + /* Get node iterator */ + const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body); + + /* Now start iterating over the created document */ + while (currentNode = nodeIterator.nextNode()) { + /* Sanitize tags and elements */ + if (_sanitizeElements(currentNode)) { + continue; + } + + /* Shadow DOM detected, sanitize it */ + if (currentNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(currentNode.content); + } + + /* Check attributes, sanitize if necessary */ + _sanitizeAttributes(currentNode); + } + + /* If we sanitized `dirty` in-place, return it. */ + if (IN_PLACE) { + return dirty; + } + + /* Return sanitized string or DOM */ + if (RETURN_DOM) { + if (RETURN_DOM_FRAGMENT) { + returnNode = createDocumentFragment.call(body.ownerDocument); + while (body.firstChild) { + // eslint-disable-next-line unicorn/prefer-dom-node-append + returnNode.appendChild(body.firstChild); + } + } else { + returnNode = body; + } + if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { + /* + AdoptNode() is not used because internal state is not reset + (e.g. the past names map of a HTMLFormElement), this is safe + in theory but we would rather not risk another attack vector. + The state that is cloned by importNode() is explicitly defined + by the specs. + */ + returnNode = importNode.call(originalDocument, returnNode, true); + } + return returnNode; + } + let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; + + /* Serialize doctype if allowed */ + if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { + serializedHTML = '\n' + serializedHTML; + } + + /* Sanitize final string template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + serializedHTML = stringReplace(serializedHTML, expr, ' '); + }); + } + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; + }; + + /** + * Public method to set the configuration once + * setConfig + * + * @param {Object} cfg configuration object + */ + DOMPurify.setConfig = function () { + let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + _parseConfig(cfg); + SET_CONFIG = true; + }; + + /** + * Public method to remove the configuration + * clearConfig + * + */ + DOMPurify.clearConfig = function () { + CONFIG = null; + SET_CONFIG = false; + }; + + /** + * Public method to check if an attribute value is valid. + * Uses last set config, if any. Otherwise, uses config defaults. + * isValidAttribute + * + * @param {String} tag Tag name of containing element. + * @param {String} attr Attribute name. + * @param {String} value Attribute value. + * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false. + */ + DOMPurify.isValidAttribute = function (tag, attr, value) { + /* Initialize shared config vars if necessary. */ + if (!CONFIG) { + _parseConfig({}); + } + const lcTag = transformCaseFunc(tag); + const lcName = transformCaseFunc(attr); + return _isValidAttribute(lcTag, lcName, value); + }; + + /** + * AddHook + * Public method to add DOMPurify hooks + * + * @param {String} entryPoint entry point for the hook to add + * @param {Function} hookFunction function to execute + */ + DOMPurify.addHook = function (entryPoint, hookFunction) { + if (typeof hookFunction !== 'function') { + return; + } + hooks[entryPoint] = hooks[entryPoint] || []; + arrayPush(hooks[entryPoint], hookFunction); + }; + + /** + * RemoveHook + * Public method to remove a DOMPurify hook at a given entryPoint + * (pops it from the stack of hooks if more are present) + * + * @param {String} entryPoint entry point for the hook to remove + * @return {Function} removed(popped) hook + */ + DOMPurify.removeHook = function (entryPoint) { + if (hooks[entryPoint]) { + return arrayPop(hooks[entryPoint]); + } + }; + + /** + * RemoveHooks + * Public method to remove all DOMPurify hooks at a given entryPoint + * + * @param {String} entryPoint entry point for the hooks to remove + */ + DOMPurify.removeHooks = function (entryPoint) { + if (hooks[entryPoint]) { + hooks[entryPoint] = []; + } + }; + + /** + * RemoveAllHooks + * Public method to remove all DOMPurify hooks + */ + DOMPurify.removeAllHooks = function () { + hooks = {}; + }; + return DOMPurify; + } + var purify = createDOMPurify(); + + return purify; + +})); +//# sourceMappingURL=purify.js.map + +export default DOMPurify; diff --git a/dependencies/dompurify/purify.js.map b/dependencies/dompurify/purify.js.map new file mode 100644 index 0000000..42e764c --- /dev/null +++ b/dependencies/dompurify/purify.js.map @@ -0,0 +1 @@ +{"version":3,"file":"purify.js","sources":["../src/utils.js","../src/tags.js","../src/attrs.js","../src/regexp.js","../src/purify.js"],"sourcesContent":["const {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n} = Object;\n\nlet { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports\nlet { apply, construct } = typeof Reflect !== 'undefined' && Reflect;\n\nif (!freeze) {\n freeze = function (x) {\n return x;\n };\n}\n\nif (!seal) {\n seal = function (x) {\n return x;\n };\n}\n\nif (!apply) {\n apply = function (fun, thisValue, args) {\n return fun.apply(thisValue, args);\n };\n}\n\nif (!construct) {\n construct = function (Func, args) {\n return new Func(...args);\n };\n}\n\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayIndexOf = unapply(Array.prototype.indexOf);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst arraySlice = unapply(Array.prototype.slice);\n\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\n\nconst objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\n\nconst regExpTest = unapply(RegExp.prototype.test);\n\nconst typeErrorCreate = unconstruct(TypeError);\n\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param {Function} func - The function to be wrapped and called.\n * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(func) {\n return (thisArg, ...args) => apply(func, thisArg, args);\n}\n\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param {Function} func - The constructor function to be wrapped and called.\n * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(func) {\n return (...args) => construct(func, args);\n}\n\n/**\n * Add properties to a lookup table\n *\n * @param {Object} set - The set to which elements will be added.\n * @param {Array} array - The array containing elements to be added to the set.\n * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns {Object} The modified set with added elements.\n */\nfunction addToSet(set, array, transformCaseFunc = stringToLowerCase) {\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n\n let l = array.length;\n while (l--) {\n let element = array[l];\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n array[l] = lcElement;\n }\n\n element = lcElement;\n }\n }\n\n set[element] = true;\n }\n\n return set;\n}\n\n/**\n * Clean up an array to harden against CSPP\n *\n * @param {Array} array - The array to be cleaned.\n * @returns {Array} The cleaned version of the array\n */\nfunction cleanArray(array) {\n for (let index = 0; index < array.length; index++) {\n const isPropertyExist = objectHasOwnProperty(array, index);\n\n if (!isPropertyExist) {\n array[index] = null;\n }\n }\n\n return array;\n}\n\n/**\n * Shallow clone an object\n *\n * @param {Object} object - The object to be cloned.\n * @returns {Object} A new object that copies the original.\n */\nfunction clone(object) {\n const newObject = create(null);\n\n for (const [property, value] of entries(object)) {\n const isPropertyExist = objectHasOwnProperty(object, property);\n\n if (isPropertyExist) {\n if (Array.isArray(value)) {\n newObject[property] = cleanArray(value);\n } else if (\n value &&\n typeof value === 'object' &&\n value.constructor === Object\n ) {\n newObject[property] = clone(value);\n } else {\n newObject[property] = value;\n }\n }\n }\n\n return newObject;\n}\n\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param {Object} object - The object to look up the getter function in its prototype chain.\n * @param {String} prop - The property name for which to find the getter function.\n * @returns {Function} The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter(object, prop) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n\n object = getPrototypeOf(object);\n }\n\n function fallbackValue() {\n return null;\n }\n\n return fallbackValue;\n}\n\nexport {\n // Array\n arrayForEach,\n arrayIndexOf,\n arrayPop,\n arrayPush,\n arraySlice,\n // Object\n entries,\n freeze,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n isFrozen,\n setPrototypeOf,\n seal,\n clone,\n create,\n objectHasOwnProperty,\n // RegExp\n regExpTest,\n // String\n stringIndexOf,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringTrim,\n // Errors\n typeErrorCreate,\n // Other\n lookupGetter,\n addToSet,\n // Reflect\n unapply,\n unconstruct,\n};\n","import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'a',\n 'abbr',\n 'acronym',\n 'address',\n 'area',\n 'article',\n 'aside',\n 'audio',\n 'b',\n 'bdi',\n 'bdo',\n 'big',\n 'blink',\n 'blockquote',\n 'body',\n 'br',\n 'button',\n 'canvas',\n 'caption',\n 'center',\n 'cite',\n 'code',\n 'col',\n 'colgroup',\n 'content',\n 'data',\n 'datalist',\n 'dd',\n 'decorator',\n 'del',\n 'details',\n 'dfn',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'element',\n 'em',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'font',\n 'footer',\n 'form',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hgroup',\n 'hr',\n 'html',\n 'i',\n 'img',\n 'input',\n 'ins',\n 'kbd',\n 'label',\n 'legend',\n 'li',\n 'main',\n 'map',\n 'mark',\n 'marquee',\n 'menu',\n 'menuitem',\n 'meter',\n 'nav',\n 'nobr',\n 'ol',\n 'optgroup',\n 'option',\n 'output',\n 'p',\n 'picture',\n 'pre',\n 'progress',\n 'q',\n 'rp',\n 'rt',\n 'ruby',\n 's',\n 'samp',\n 'section',\n 'select',\n 'shadow',\n 'small',\n 'source',\n 'spacer',\n 'span',\n 'strike',\n 'strong',\n 'style',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'template',\n 'textarea',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'track',\n 'tt',\n 'u',\n 'ul',\n 'var',\n 'video',\n 'wbr',\n]);\n\n// SVG\nexport const svg = freeze([\n 'svg',\n 'a',\n 'altglyph',\n 'altglyphdef',\n 'altglyphitem',\n 'animatecolor',\n 'animatemotion',\n 'animatetransform',\n 'circle',\n 'clippath',\n 'defs',\n 'desc',\n 'ellipse',\n 'filter',\n 'font',\n 'g',\n 'glyph',\n 'glyphref',\n 'hkern',\n 'image',\n 'line',\n 'lineargradient',\n 'marker',\n 'mask',\n 'metadata',\n 'mpath',\n 'path',\n 'pattern',\n 'polygon',\n 'polyline',\n 'radialgradient',\n 'rect',\n 'stop',\n 'style',\n 'switch',\n 'symbol',\n 'text',\n 'textpath',\n 'title',\n 'tref',\n 'tspan',\n 'view',\n 'vkern',\n]);\n\nexport const svgFilters = freeze([\n 'feBlend',\n 'feColorMatrix',\n 'feComponentTransfer',\n 'feComposite',\n 'feConvolveMatrix',\n 'feDiffuseLighting',\n 'feDisplacementMap',\n 'feDistantLight',\n 'feDropShadow',\n 'feFlood',\n 'feFuncA',\n 'feFuncB',\n 'feFuncG',\n 'feFuncR',\n 'feGaussianBlur',\n 'feImage',\n 'feMerge',\n 'feMergeNode',\n 'feMorphology',\n 'feOffset',\n 'fePointLight',\n 'feSpecularLighting',\n 'feSpotLight',\n 'feTile',\n 'feTurbulence',\n]);\n\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nexport const svgDisallowed = freeze([\n 'animate',\n 'color-profile',\n 'cursor',\n 'discard',\n 'font-face',\n 'font-face-format',\n 'font-face-name',\n 'font-face-src',\n 'font-face-uri',\n 'foreignobject',\n 'hatch',\n 'hatchpath',\n 'mesh',\n 'meshgradient',\n 'meshpatch',\n 'meshrow',\n 'missing-glyph',\n 'script',\n 'set',\n 'solidcolor',\n 'unknown',\n 'use',\n]);\n\nexport const mathMl = freeze([\n 'math',\n 'menclose',\n 'merror',\n 'mfenced',\n 'mfrac',\n 'mglyph',\n 'mi',\n 'mlabeledtr',\n 'mmultiscripts',\n 'mn',\n 'mo',\n 'mover',\n 'mpadded',\n 'mphantom',\n 'mroot',\n 'mrow',\n 'ms',\n 'mspace',\n 'msqrt',\n 'mstyle',\n 'msub',\n 'msup',\n 'msubsup',\n 'mtable',\n 'mtd',\n 'mtext',\n 'mtr',\n 'munder',\n 'munderover',\n 'mprescripts',\n]);\n\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nexport const mathMlDisallowed = freeze([\n 'maction',\n 'maligngroup',\n 'malignmark',\n 'mlongdiv',\n 'mscarries',\n 'mscarry',\n 'msgroup',\n 'mstack',\n 'msline',\n 'msrow',\n 'semantics',\n 'annotation',\n 'annotation-xml',\n 'mprescripts',\n 'none',\n]);\n\nexport const text = freeze(['#text']);\n","import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'accept',\n 'action',\n 'align',\n 'alt',\n 'autocapitalize',\n 'autocomplete',\n 'autopictureinpicture',\n 'autoplay',\n 'background',\n 'bgcolor',\n 'border',\n 'capture',\n 'cellpadding',\n 'cellspacing',\n 'checked',\n 'cite',\n 'class',\n 'clear',\n 'color',\n 'cols',\n 'colspan',\n 'controls',\n 'controlslist',\n 'coords',\n 'crossorigin',\n 'datetime',\n 'decoding',\n 'default',\n 'dir',\n 'disabled',\n 'disablepictureinpicture',\n 'disableremoteplayback',\n 'download',\n 'draggable',\n 'enctype',\n 'enterkeyhint',\n 'face',\n 'for',\n 'headers',\n 'height',\n 'hidden',\n 'high',\n 'href',\n 'hreflang',\n 'id',\n 'inputmode',\n 'integrity',\n 'ismap',\n 'kind',\n 'label',\n 'lang',\n 'list',\n 'loading',\n 'loop',\n 'low',\n 'max',\n 'maxlength',\n 'media',\n 'method',\n 'min',\n 'minlength',\n 'multiple',\n 'muted',\n 'name',\n 'nonce',\n 'noshade',\n 'novalidate',\n 'nowrap',\n 'open',\n 'optimum',\n 'pattern',\n 'placeholder',\n 'playsinline',\n 'poster',\n 'preload',\n 'pubdate',\n 'radiogroup',\n 'readonly',\n 'rel',\n 'required',\n 'rev',\n 'reversed',\n 'role',\n 'rows',\n 'rowspan',\n 'spellcheck',\n 'scope',\n 'selected',\n 'shape',\n 'size',\n 'sizes',\n 'span',\n 'srclang',\n 'start',\n 'src',\n 'srcset',\n 'step',\n 'style',\n 'summary',\n 'tabindex',\n 'title',\n 'translate',\n 'type',\n 'usemap',\n 'valign',\n 'value',\n 'width',\n 'xmlns',\n 'slot',\n]);\n\nexport const svg = freeze([\n 'accent-height',\n 'accumulate',\n 'additive',\n 'alignment-baseline',\n 'ascent',\n 'attributename',\n 'attributetype',\n 'azimuth',\n 'basefrequency',\n 'baseline-shift',\n 'begin',\n 'bias',\n 'by',\n 'class',\n 'clip',\n 'clippathunits',\n 'clip-path',\n 'clip-rule',\n 'color',\n 'color-interpolation',\n 'color-interpolation-filters',\n 'color-profile',\n 'color-rendering',\n 'cx',\n 'cy',\n 'd',\n 'dx',\n 'dy',\n 'diffuseconstant',\n 'direction',\n 'display',\n 'divisor',\n 'dur',\n 'edgemode',\n 'elevation',\n 'end',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'filter',\n 'filterunits',\n 'flood-color',\n 'flood-opacity',\n 'font-family',\n 'font-size',\n 'font-size-adjust',\n 'font-stretch',\n 'font-style',\n 'font-variant',\n 'font-weight',\n 'fx',\n 'fy',\n 'g1',\n 'g2',\n 'glyph-name',\n 'glyphref',\n 'gradientunits',\n 'gradienttransform',\n 'height',\n 'href',\n 'id',\n 'image-rendering',\n 'in',\n 'in2',\n 'k',\n 'k1',\n 'k2',\n 'k3',\n 'k4',\n 'kerning',\n 'keypoints',\n 'keysplines',\n 'keytimes',\n 'lang',\n 'lengthadjust',\n 'letter-spacing',\n 'kernelmatrix',\n 'kernelunitlength',\n 'lighting-color',\n 'local',\n 'marker-end',\n 'marker-mid',\n 'marker-start',\n 'markerheight',\n 'markerunits',\n 'markerwidth',\n 'maskcontentunits',\n 'maskunits',\n 'max',\n 'mask',\n 'media',\n 'method',\n 'mode',\n 'min',\n 'name',\n 'numoctaves',\n 'offset',\n 'operator',\n 'opacity',\n 'order',\n 'orient',\n 'orientation',\n 'origin',\n 'overflow',\n 'paint-order',\n 'path',\n 'pathlength',\n 'patterncontentunits',\n 'patterntransform',\n 'patternunits',\n 'points',\n 'preservealpha',\n 'preserveaspectratio',\n 'primitiveunits',\n 'r',\n 'rx',\n 'ry',\n 'radius',\n 'refx',\n 'refy',\n 'repeatcount',\n 'repeatdur',\n 'restart',\n 'result',\n 'rotate',\n 'scale',\n 'seed',\n 'shape-rendering',\n 'specularconstant',\n 'specularexponent',\n 'spreadmethod',\n 'startoffset',\n 'stddeviation',\n 'stitchtiles',\n 'stop-color',\n 'stop-opacity',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-miterlimit',\n 'stroke-opacity',\n 'stroke',\n 'stroke-width',\n 'style',\n 'surfacescale',\n 'systemlanguage',\n 'tabindex',\n 'targetx',\n 'targety',\n 'transform',\n 'transform-origin',\n 'text-anchor',\n 'text-decoration',\n 'text-rendering',\n 'textlength',\n 'type',\n 'u1',\n 'u2',\n 'unicode',\n 'values',\n 'viewbox',\n 'visibility',\n 'version',\n 'vert-adv-y',\n 'vert-origin-x',\n 'vert-origin-y',\n 'width',\n 'word-spacing',\n 'wrap',\n 'writing-mode',\n 'xchannelselector',\n 'ychannelselector',\n 'x',\n 'x1',\n 'x2',\n 'xmlns',\n 'y',\n 'y1',\n 'y2',\n 'z',\n 'zoomandpan',\n]);\n\nexport const mathMl = freeze([\n 'accent',\n 'accentunder',\n 'align',\n 'bevelled',\n 'close',\n 'columnsalign',\n 'columnlines',\n 'columnspan',\n 'denomalign',\n 'depth',\n 'dir',\n 'display',\n 'displaystyle',\n 'encoding',\n 'fence',\n 'frame',\n 'height',\n 'href',\n 'id',\n 'largeop',\n 'length',\n 'linethickness',\n 'lspace',\n 'lquote',\n 'mathbackground',\n 'mathcolor',\n 'mathsize',\n 'mathvariant',\n 'maxsize',\n 'minsize',\n 'movablelimits',\n 'notation',\n 'numalign',\n 'open',\n 'rowalign',\n 'rowlines',\n 'rowspacing',\n 'rowspan',\n 'rspace',\n 'rquote',\n 'scriptlevel',\n 'scriptminsize',\n 'scriptsizemultiplier',\n 'selection',\n 'separator',\n 'separators',\n 'stretchy',\n 'subscriptshift',\n 'supscriptshift',\n 'symmetric',\n 'voffset',\n 'width',\n 'xmlns',\n]);\n\nexport const xml = freeze([\n 'xlink:href',\n 'xml:id',\n 'xlink:title',\n 'xml:space',\n 'xmlns:xlink',\n]);\n","import { seal } from './utils.js';\n\n// eslint-disable-next-line unicorn/better-regex\nexport const MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\nexport const ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\nexport const TMPLIT_EXPR = seal(/\\${[\\w\\W]*}/gm);\nexport const DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]/); // eslint-disable-line no-useless-escape\nexport const ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nexport const IS_ALLOWED_URI = seal(\n /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\nexport const IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nexport const ATTR_WHITESPACE = seal(\n /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\nexport const DOCTYPE_NAME = seal(/^html$/i);\n","import * as TAGS from './tags.js';\nimport * as ATTRS from './attrs.js';\nimport * as EXPRESSIONS from './regexp.js';\nimport {\n addToSet,\n clone,\n entries,\n freeze,\n arrayForEach,\n arrayPop,\n arrayPush,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringIndexOf,\n stringTrim,\n regExpTest,\n typeErrorCreate,\n lookupGetter,\n create,\n objectHasOwnProperty,\n} from './utils.js';\n\nconst getGlobal = function () {\n return typeof window === 'undefined' ? null : window;\n};\n\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param {TrustedTypePolicyFactory} trustedTypes The policy factory.\n * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function (trustedTypes, purifyHostElement) {\n if (\n typeof trustedTypes !== 'object' ||\n typeof trustedTypes.createPolicy !== 'function'\n ) {\n return null;\n }\n\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n },\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn(\n 'TrustedTypes policy ' + policyName + ' could not be created.'\n );\n return null;\n }\n};\n\nfunction createDOMPurify(window = getGlobal()) {\n const DOMPurify = (root) => createDOMPurify(root);\n\n /**\n * Version label, exposed for easier checks\n * if DOMPurify is up to date or not\n */\n DOMPurify.version = VERSION;\n\n /**\n * Array of elements that DOMPurify removed during sanitation.\n * Empty if nothing was removed.\n */\n DOMPurify.removed = [];\n\n if (!window || !window.document || window.document.nodeType !== 9) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n\n return DOMPurify;\n }\n\n let { document } = window;\n\n const originalDocument = document;\n const currentScript = originalDocument.currentScript;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes,\n } = window;\n\n const ElementPrototype = Element.prototype;\n\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n\n let trustedTypesPolicy;\n let emptyHTML = '';\n\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName,\n } = document;\n const { importNode } = originalDocument;\n\n let hooks = {};\n\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported =\n typeof entries === 'function' &&\n typeof getParentNode === 'function' &&\n implementation &&\n implementation.createHTMLDocument !== undefined;\n\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n } = EXPRESSIONS;\n\n let { IS_ALLOWED_URI } = EXPRESSIONS;\n\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [\n ...TAGS.html,\n ...TAGS.svg,\n ...TAGS.svgFilters,\n ...TAGS.mathMl,\n ...TAGS.text,\n ]);\n\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [\n ...ATTRS.html,\n ...ATTRS.svg,\n ...ATTRS.mathMl,\n ...ATTRS.xml,\n ]);\n\n /*\n * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(\n create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false,\n },\n })\n );\n\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (§7.3.3)\n * - DOM Tree Accessors (§3.1.5)\n * - Form Element Parent-Child Relations (§4.10.3)\n * - Iframe srcdoc / Nested WindowProxies (§4.8.5)\n * - HTMLCollection (§4.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES = {};\n\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, [\n 'annotation-xml',\n 'audio',\n 'colgroup',\n 'desc',\n 'foreignobject',\n 'head',\n 'iframe',\n 'math',\n 'mi',\n 'mn',\n 'mo',\n 'ms',\n 'mtext',\n 'noembed',\n 'noframes',\n 'noscript',\n 'plaintext',\n 'script',\n 'style',\n 'svg',\n 'template',\n 'thead',\n 'title',\n 'video',\n 'xmp',\n ]);\n\n /* Tags that are safe for data: URIs */\n let DATA_URI_TAGS = null;\n const DEFAULT_DATA_URI_TAGS = addToSet({}, [\n 'audio',\n 'video',\n 'img',\n 'source',\n 'image',\n 'track',\n ]);\n\n /* Attributes safe for values like \"javascript:\" */\n let URI_SAFE_ATTRIBUTES = null;\n const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, [\n 'alt',\n 'class',\n 'for',\n 'id',\n 'label',\n 'name',\n 'pattern',\n 'placeholder',\n 'role',\n 'summary',\n 'title',\n 'value',\n 'style',\n 'xmlns',\n ]);\n\n const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n /* Document namespace */\n let NAMESPACE = HTML_NAMESPACE;\n let IS_EMPTY_INPUT = false;\n\n /* Allowed XHTML+XML namespaces */\n let ALLOWED_NAMESPACES = null;\n const DEFAULT_ALLOWED_NAMESPACES = addToSet(\n {},\n [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE],\n stringToString\n );\n\n /* Parsing of strict XHTML documents */\n let PARSER_MEDIA_TYPE = null;\n const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n let transformCaseFunc = null;\n\n /* Keep a reference to config to pass to hooks */\n let CONFIG = null;\n\n /* Ideally, do not touch anything below this line */\n /* ______________________________________________ */\n\n const formElement = document.createElement('form');\n\n const isRegexOrFunction = function (testValue) {\n return testValue instanceof RegExp || testValue instanceof Function;\n };\n\n /**\n * _parseConfig\n *\n * @param {Object} cfg optional config literal\n */\n // eslint-disable-next-line complexity\n const _parseConfig = function (cfg = {}) {\n if (CONFIG && CONFIG === cfg) {\n return;\n }\n\n /* Shield configuration object from tampering */\n if (!cfg || typeof cfg !== 'object') {\n cfg = {};\n }\n\n /* Shield configuration object from prototype pollution */\n cfg = clone(cfg);\n\n PARSER_MEDIA_TYPE =\n // eslint-disable-next-line unicorn/prefer-includes\n SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1\n ? DEFAULT_PARSER_MEDIA_TYPE\n : cfg.PARSER_MEDIA_TYPE;\n\n // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n transformCaseFunc =\n PARSER_MEDIA_TYPE === 'application/xhtml+xml'\n ? stringToString\n : stringToLowerCase;\n\n /* Set configuration parameters */\n ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS')\n ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc)\n : DEFAULT_ALLOWED_TAGS;\n ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR')\n ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc)\n : DEFAULT_ALLOWED_ATTR;\n ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES')\n ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString)\n : DEFAULT_ALLOWED_NAMESPACES;\n URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR')\n ? addToSet(\n clone(DEFAULT_URI_SAFE_ATTRIBUTES), // eslint-disable-line indent\n cfg.ADD_URI_SAFE_ATTR, // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_URI_SAFE_ATTRIBUTES;\n DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS')\n ? addToSet(\n clone(DEFAULT_DATA_URI_TAGS), // eslint-disable-line indent\n cfg.ADD_DATA_URI_TAGS, // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_DATA_URI_TAGS;\n FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS')\n ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc)\n : DEFAULT_FORBID_CONTENTS;\n FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS')\n ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc)\n : {};\n FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR')\n ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc)\n : {};\n USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES')\n ? cfg.USE_PROFILES\n : false;\n ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n IN_PLACE = cfg.IN_PLACE || false; // Default false\n IS_ALLOWED_URI = cfg.ALLOWED_URI_REGEXP || EXPRESSIONS.IS_ALLOWED_URI;\n NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)\n ) {\n CUSTOM_ELEMENT_HANDLING.tagNameCheck =\n cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n }\n\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)\n ) {\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck =\n cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n }\n\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements ===\n 'boolean'\n ) {\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements =\n cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n }\n\n if (SAFE_FOR_TEMPLATES) {\n ALLOW_DATA_ATTR = false;\n }\n\n if (RETURN_DOM_FRAGMENT) {\n RETURN_DOM = true;\n }\n\n /* Parse profile info */\n if (USE_PROFILES) {\n ALLOWED_TAGS = addToSet({}, TAGS.text);\n ALLOWED_ATTR = [];\n if (USE_PROFILES.html === true) {\n addToSet(ALLOWED_TAGS, TAGS.html);\n addToSet(ALLOWED_ATTR, ATTRS.html);\n }\n\n if (USE_PROFILES.svg === true) {\n addToSet(ALLOWED_TAGS, TAGS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n\n if (USE_PROFILES.svgFilters === true) {\n addToSet(ALLOWED_TAGS, TAGS.svgFilters);\n addToSet(ALLOWED_ATTR, ATTRS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n\n if (USE_PROFILES.mathMl === true) {\n addToSet(ALLOWED_TAGS, TAGS.mathMl);\n addToSet(ALLOWED_ATTR, ATTRS.mathMl);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n }\n\n /* Merge configuration parameters */\n if (cfg.ADD_TAGS) {\n if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n ALLOWED_TAGS = clone(ALLOWED_TAGS);\n }\n\n addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n }\n\n if (cfg.ADD_ATTR) {\n if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n ALLOWED_ATTR = clone(ALLOWED_ATTR);\n }\n\n addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n }\n\n if (cfg.ADD_URI_SAFE_ATTR) {\n addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n }\n\n if (cfg.FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n\n addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n }\n\n /* Add #text in case KEEP_CONTENT is set to true */\n if (KEEP_CONTENT) {\n ALLOWED_TAGS['#text'] = true;\n }\n\n /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n if (WHOLE_DOCUMENT) {\n addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n }\n\n /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n if (ALLOWED_TAGS.table) {\n addToSet(ALLOWED_TAGS, ['tbody']);\n delete FORBID_TAGS.tbody;\n }\n\n if (cfg.TRUSTED_TYPES_POLICY) {\n if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n throw typeErrorCreate(\n 'TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.'\n );\n }\n\n if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n throw typeErrorCreate(\n 'TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.'\n );\n }\n\n // Overwrite existing TrustedTypes policy.\n trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n\n // Sign local variables required by `sanitize`.\n emptyHTML = trustedTypesPolicy.createHTML('');\n } else {\n // Uninitialized policy, attempt to initialize the internal dompurify policy.\n if (trustedTypesPolicy === undefined) {\n trustedTypesPolicy = _createTrustedTypesPolicy(\n trustedTypes,\n currentScript\n );\n }\n\n // If creating the internal policy succeeded sign internal variables.\n if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n emptyHTML = trustedTypesPolicy.createHTML('');\n }\n }\n\n // Prevent further manipulation of configuration.\n // Not available in IE8, Safari 5, etc.\n if (freeze) {\n freeze(cfg);\n }\n\n CONFIG = cfg;\n };\n\n const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, [\n 'mi',\n 'mo',\n 'mn',\n 'ms',\n 'mtext',\n ]);\n\n const HTML_INTEGRATION_POINTS = addToSet({}, [\n 'foreignobject',\n 'desc',\n 'title',\n 'annotation-xml',\n ]);\n\n // Certain elements are allowed in both SVG and HTML\n // namespace. We need to specify them explicitly\n // so that they don't get erroneously deleted from\n // HTML namespace.\n const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, [\n 'title',\n 'style',\n 'font',\n 'a',\n 'script',\n ]);\n\n /* Keep track of all possible SVG and MathML tags\n * so that we can perform the namespace checks\n * correctly. */\n const ALL_SVG_TAGS = addToSet({}, [\n ...TAGS.svg,\n ...TAGS.svgFilters,\n ...TAGS.svgDisallowed,\n ]);\n const ALL_MATHML_TAGS = addToSet({}, [\n ...TAGS.mathMl,\n ...TAGS.mathMlDisallowed,\n ]);\n\n /**\n * @param {Element} element a DOM element whose namespace is being checked\n * @returns {boolean} Return false if the element has a\n * namespace that a spec-compliant parser would never\n * return. Return true otherwise.\n */\n const _checkValidNamespace = function (element) {\n let parent = getParentNode(element);\n\n // In JSDOM, if we're inside shadow DOM, then parentNode\n // can be null. We just simulate parent in this case.\n if (!parent || !parent.tagName) {\n parent = {\n namespaceURI: NAMESPACE,\n tagName: 'template',\n };\n }\n\n const tagName = stringToLowerCase(element.tagName);\n const parentTagName = stringToLowerCase(parent.tagName);\n\n if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n return false;\n }\n\n if (element.namespaceURI === SVG_NAMESPACE) {\n // The only way to switch from HTML namespace to SVG\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'svg';\n }\n\n // The only way to switch from MathML to SVG is via`\n // svg if parent is either or MathML\n // text integration points.\n if (parent.namespaceURI === MATHML_NAMESPACE) {\n return (\n tagName === 'svg' &&\n (parentTagName === 'annotation-xml' ||\n MATHML_TEXT_INTEGRATION_POINTS[parentTagName])\n );\n }\n\n // We only allow elements that are defined in SVG\n // spec. All others are disallowed in SVG namespace.\n return Boolean(ALL_SVG_TAGS[tagName]);\n }\n\n if (element.namespaceURI === MATHML_NAMESPACE) {\n // The only way to switch from HTML namespace to MathML\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'math';\n }\n\n // The only way to switch from SVG to MathML is via\n // and HTML integration points\n if (parent.namespaceURI === SVG_NAMESPACE) {\n return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];\n }\n\n // We only allow elements that are defined in MathML\n // spec. All others are disallowed in MathML namespace.\n return Boolean(ALL_MATHML_TAGS[tagName]);\n }\n\n if (element.namespaceURI === HTML_NAMESPACE) {\n // The only way to switch from SVG to HTML is via\n // HTML integration points, and from MathML to HTML\n // is via MathML text integration points\n if (\n parent.namespaceURI === SVG_NAMESPACE &&\n !HTML_INTEGRATION_POINTS[parentTagName]\n ) {\n return false;\n }\n\n if (\n parent.namespaceURI === MATHML_NAMESPACE &&\n !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]\n ) {\n return false;\n }\n\n // We disallow tags that are specific for MathML\n // or SVG and should never appear in HTML namespace\n return (\n !ALL_MATHML_TAGS[tagName] &&\n (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])\n );\n }\n\n // For XHTML and XML documents that support custom namespaces\n if (\n PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&\n ALLOWED_NAMESPACES[element.namespaceURI]\n ) {\n return true;\n }\n\n // The code should never reach this place (this means\n // that the element somehow got namespace that is not\n // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).\n // Return false just in case.\n return false;\n };\n\n /**\n * _forceRemove\n *\n * @param {Node} node a DOM node\n */\n const _forceRemove = function (node) {\n arrayPush(DOMPurify.removed, { element: node });\n\n try {\n // eslint-disable-next-line unicorn/prefer-dom-node-remove\n node.parentNode.removeChild(node);\n } catch (_) {\n node.remove();\n }\n };\n\n /**\n * _removeAttribute\n *\n * @param {String} name an Attribute name\n * @param {Node} node a DOM node\n */\n const _removeAttribute = function (name, node) {\n try {\n arrayPush(DOMPurify.removed, {\n attribute: node.getAttributeNode(name),\n from: node,\n });\n } catch (_) {\n arrayPush(DOMPurify.removed, {\n attribute: null,\n from: node,\n });\n }\n\n node.removeAttribute(name);\n\n // We void attribute values for unremovable \"is\"\" attributes\n if (name === 'is' && !ALLOWED_ATTR[name]) {\n if (RETURN_DOM || RETURN_DOM_FRAGMENT) {\n try {\n _forceRemove(node);\n } catch (_) {}\n } else {\n try {\n node.setAttribute(name, '');\n } catch (_) {}\n }\n }\n };\n\n /**\n * _initDocument\n *\n * @param {String} dirty a string of dirty markup\n * @return {Document} a DOM, filled with the dirty markup\n */\n const _initDocument = function (dirty) {\n /* Create a HTML document */\n let doc = null;\n let leadingWhitespace = null;\n\n if (FORCE_BODY) {\n dirty = '' + dirty;\n } else {\n /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */\n const matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n leadingWhitespace = matches && matches[0];\n }\n\n if (\n PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&\n NAMESPACE === HTML_NAMESPACE\n ) {\n // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)\n dirty =\n '' +\n dirty +\n '';\n }\n\n const dirtyPayload = trustedTypesPolicy\n ? trustedTypesPolicy.createHTML(dirty)\n : dirty;\n /*\n * Use the DOMParser API by default, fallback later if needs be\n * DOMParser not work for svg when has multiple root element.\n */\n if (NAMESPACE === HTML_NAMESPACE) {\n try {\n doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);\n } catch (_) {}\n }\n\n /* Use createHTMLDocument in case DOMParser is not available */\n if (!doc || !doc.documentElement) {\n doc = implementation.createDocument(NAMESPACE, 'template', null);\n try {\n doc.documentElement.innerHTML = IS_EMPTY_INPUT\n ? emptyHTML\n : dirtyPayload;\n } catch (_) {\n // Syntax error if dirtyPayload is invalid xml\n }\n }\n\n const body = doc.body || doc.documentElement;\n\n if (dirty && leadingWhitespace) {\n body.insertBefore(\n document.createTextNode(leadingWhitespace),\n body.childNodes[0] || null\n );\n }\n\n /* Work on whole document or just its body */\n if (NAMESPACE === HTML_NAMESPACE) {\n return getElementsByTagName.call(\n doc,\n WHOLE_DOCUMENT ? 'html' : 'body'\n )[0];\n }\n\n return WHOLE_DOCUMENT ? doc.documentElement : body;\n };\n\n /**\n * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.\n *\n * @param {Node} root The root element or node to start traversing on.\n * @return {NodeIterator} The created NodeIterator\n */\n const _createNodeIterator = function (root) {\n return createNodeIterator.call(\n root.ownerDocument || root,\n root,\n // eslint-disable-next-line no-bitwise\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT,\n null\n );\n };\n\n /**\n * _isClobbered\n *\n * @param {Node} elm element to check for clobbering attacks\n * @return {Boolean} true if clobbered, false if safe\n */\n const _isClobbered = function (elm) {\n return (\n elm instanceof HTMLFormElement &&\n (typeof elm.nodeName !== 'string' ||\n typeof elm.textContent !== 'string' ||\n typeof elm.removeChild !== 'function' ||\n !(elm.attributes instanceof NamedNodeMap) ||\n typeof elm.removeAttribute !== 'function' ||\n typeof elm.setAttribute !== 'function' ||\n typeof elm.namespaceURI !== 'string' ||\n typeof elm.insertBefore !== 'function' ||\n typeof elm.hasChildNodes !== 'function')\n );\n };\n\n /**\n * Checks whether the given object is a DOM node.\n *\n * @param {Node} object object to check whether it's a DOM node\n * @return {Boolean} true is object is a DOM node\n */\n const _isNode = function (object) {\n return typeof Node === 'function' && object instanceof Node;\n };\n\n /**\n * _executeHook\n * Execute user configurable hooks\n *\n * @param {String} entryPoint Name of the hook's entry point\n * @param {Node} currentNode node to work on with the hook\n * @param {Object} data additional hook parameters\n */\n const _executeHook = function (entryPoint, currentNode, data) {\n if (!hooks[entryPoint]) {\n return;\n }\n\n arrayForEach(hooks[entryPoint], (hook) => {\n hook.call(DOMPurify, currentNode, data, CONFIG);\n });\n };\n\n /**\n * _sanitizeElements\n *\n * @protect nodeName\n * @protect textContent\n * @protect removeChild\n *\n * @param {Node} currentNode to check for permission to exist\n * @return {Boolean} true if node was killed, false if left alive\n */\n const _sanitizeElements = function (currentNode) {\n let content = null;\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeElements', currentNode, null);\n\n /* Check if element is clobbered or can clobber */\n if (_isClobbered(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Now let's check the element's type and name */\n const tagName = transformCaseFunc(currentNode.nodeName);\n\n /* Execute a hook if present */\n _executeHook('uponSanitizeElement', currentNode, {\n tagName,\n allowedTags: ALLOWED_TAGS,\n });\n\n /* Detect mXSS attempts abusing namespace confusion */\n if (\n currentNode.hasChildNodes() &&\n !_isNode(currentNode.firstElementChild) &&\n regExpTest(/<[/\\w]/g, currentNode.innerHTML) &&\n regExpTest(/<[/\\w]/g, currentNode.textContent)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Remove element if anything forbids its presence */\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n ) {\n return false;\n }\n\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n ) {\n return false;\n }\n }\n\n /* Keep content except for bad-listed elements */\n if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n const parentNode = getParentNode(currentNode) || currentNode.parentNode;\n const childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n\n if (childNodes && parentNode) {\n const childCount = childNodes.length;\n\n for (let i = childCount - 1; i >= 0; --i) {\n parentNode.insertBefore(\n cloneNode(childNodes[i], true),\n getNextSibling(currentNode)\n );\n }\n }\n }\n\n _forceRemove(currentNode);\n return true;\n }\n\n /* Check whether element has a valid namespace */\n if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Make sure that older browsers don't get fallback-tag mXSS */\n if (\n (tagName === 'noscript' ||\n tagName === 'noembed' ||\n tagName === 'noframes') &&\n regExpTest(/<\\/no(script|embed|frames)/i, currentNode.innerHTML)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Sanitize element content to be template-safe */\n if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {\n /* Get the element's text content */\n content = currentNode.textContent;\n\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n content = stringReplace(content, expr, ' ');\n });\n\n if (currentNode.textContent !== content) {\n arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });\n currentNode.textContent = content;\n }\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeElements', currentNode, null);\n\n return false;\n };\n\n /**\n * _isValidAttribute\n *\n * @param {string} lcTag Lowercase tag name of containing element.\n * @param {string} lcName Lowercase attribute name.\n * @param {string} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid, otherwise false.\n */\n // eslint-disable-next-line complexity\n const _isValidAttribute = function (lcTag, lcName, value) {\n /* Make sure attribute cannot clobber */\n if (\n SANITIZE_DOM &&\n (lcName === 'id' || lcName === 'name') &&\n (value in document || value in formElement)\n ) {\n return false;\n }\n\n /* Allow valid data-* attributes: At least one character after \"-\"\n (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)\n XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)\n We don't need to check the value; it's always URI safe. */\n if (\n ALLOW_DATA_ATTR &&\n !FORBID_ATTR[lcName] &&\n regExpTest(DATA_ATTR, lcName)\n ) {\n // This attribute is safe\n } else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) {\n // This attribute is safe\n /* Otherwise, check the name is permitted */\n } else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\n if (\n // First condition does a very basic check if a) it's basically a valid custom element tagname AND\n // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck\n (_isBasicCustomElement(lcTag) &&\n ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag)) ||\n (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag))) &&\n ((CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName)) ||\n (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)))) ||\n // Alternative, second condition checks if it's an `is`-attribute, AND\n // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n (lcName === 'is' &&\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements &&\n ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value)) ||\n (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))))\n ) {\n // If user has supplied a regexp or function in CUSTOM_ELEMENT_HANDLING.tagNameCheck, we need to also allow derived custom elements using the same tagName test.\n // Additionally, we need to allow attributes passing the CUSTOM_ELEMENT_HANDLING.attributeNameCheck user has configured, as custom elements can define these at their own discretion.\n } else {\n return false;\n }\n /* Check value is safe. First, is attr inert? If so, is safe */\n } else if (URI_SAFE_ATTRIBUTES[lcName]) {\n // This attribute is safe\n /* Check no script, data or unknown possibly unsafe URI\n unless we know URI values are safe for that attribute */\n } else if (\n regExpTest(IS_ALLOWED_URI, stringReplace(value, ATTR_WHITESPACE, ''))\n ) {\n // This attribute is safe\n /* Keep image data URIs alive if src/xlink:href is allowed */\n /* Further prevent gadget XSS for dynamically built script tags */\n } else if (\n (lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') &&\n lcTag !== 'script' &&\n stringIndexOf(value, 'data:') === 0 &&\n DATA_URI_TAGS[lcTag]\n ) {\n // This attribute is safe\n /* Allow unknown protocols: This provides support for links that\n are handled by protocol handlers which may be unknown ahead of\n time, e.g. fb:, spotify: */\n } else if (\n ALLOW_UNKNOWN_PROTOCOLS &&\n !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))\n ) {\n // This attribute is safe\n /* Check for binary attributes */\n } else if (value) {\n return false;\n } else {\n // Binary attributes are safe at this point\n /* Anything else, presume unsafe, do not add it back */\n }\n\n return true;\n };\n\n /**\n * _isBasicCustomElement\n * checks if at least one dash is included in tagName, and it's not the first char\n * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name\n *\n * @param {string} tagName name of the tag of the node to sanitize\n * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.\n */\n const _isBasicCustomElement = function (tagName) {\n return tagName !== 'annotation-xml' && tagName.indexOf('-') > 0;\n };\n\n /**\n * _sanitizeAttributes\n *\n * @protect attributes\n * @protect nodeName\n * @protect removeAttribute\n * @protect setAttribute\n *\n * @param {Node} currentNode to sanitize\n */\n const _sanitizeAttributes = function (currentNode) {\n /* Execute a hook if present */\n _executeHook('beforeSanitizeAttributes', currentNode, null);\n\n const { attributes } = currentNode;\n\n /* Check if we have attributes; if not we might have a text node */\n if (!attributes) {\n return;\n }\n\n const hookEvent = {\n attrName: '',\n attrValue: '',\n keepAttr: true,\n allowedAttributes: ALLOWED_ATTR,\n };\n let l = attributes.length;\n\n /* Go backwards over all attributes; safely remove bad ones */\n while (l--) {\n const attr = attributes[l];\n const { name, namespaceURI, value: attrValue } = attr;\n const lcName = transformCaseFunc(name);\n\n let value = name === 'value' ? attrValue : stringTrim(attrValue);\n\n /* Execute a hook if present */\n hookEvent.attrName = lcName;\n hookEvent.attrValue = value;\n hookEvent.keepAttr = true;\n hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set\n _executeHook('uponSanitizeAttribute', currentNode, hookEvent);\n value = hookEvent.attrValue;\n /* Did the hooks approve of the attribute? */\n if (hookEvent.forceKeepAttr) {\n continue;\n }\n\n /* Remove attribute */\n _removeAttribute(name, currentNode);\n\n /* Did the hooks approve of the attribute? */\n if (!hookEvent.keepAttr) {\n continue;\n }\n\n /* Work around a security issue in jQuery 3.0 */\n if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\\/>/i, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n\n /* Sanitize attribute content to be template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n value = stringReplace(value, expr, ' ');\n });\n }\n\n /* Is `value` valid for this attribute? */\n const lcTag = transformCaseFunc(currentNode.nodeName);\n if (!_isValidAttribute(lcTag, lcName, value)) {\n continue;\n }\n\n /* Full DOM Clobbering protection via namespace isolation,\n * Prefix id and name attributes with `user-content-`\n */\n if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {\n // Remove the attribute with this value\n _removeAttribute(name, currentNode);\n\n // Prefix the value and later re-create the attribute with the sanitized value\n value = SANITIZE_NAMED_PROPS_PREFIX + value;\n }\n\n /* Handle attributes that require Trusted Types */\n if (\n trustedTypesPolicy &&\n typeof trustedTypes === 'object' &&\n typeof trustedTypes.getAttributeType === 'function'\n ) {\n if (namespaceURI) {\n /* Namespaces are not yet supported, see https://bugs.chromium.org/p/chromium/issues/detail?id=1305293 */\n } else {\n switch (trustedTypes.getAttributeType(lcTag, lcName)) {\n case 'TrustedHTML': {\n value = trustedTypesPolicy.createHTML(value);\n break;\n }\n\n case 'TrustedScriptURL': {\n value = trustedTypesPolicy.createScriptURL(value);\n break;\n }\n\n default: {\n break;\n }\n }\n }\n }\n\n /* Handle invalid data-* attribute set by try-catching it */\n try {\n if (namespaceURI) {\n currentNode.setAttributeNS(namespaceURI, name, value);\n } else {\n /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. \"x-schema\". */\n currentNode.setAttribute(name, value);\n }\n\n arrayPop(DOMPurify.removed);\n } catch (_) {}\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeAttributes', currentNode, null);\n };\n\n /**\n * _sanitizeShadowDOM\n *\n * @param {DocumentFragment} fragment to iterate over recursively\n */\n const _sanitizeShadowDOM = function (fragment) {\n let shadowNode = null;\n const shadowIterator = _createNodeIterator(fragment);\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeShadowDOM', fragment, null);\n\n while ((shadowNode = shadowIterator.nextNode())) {\n /* Execute a hook if present */\n _executeHook('uponSanitizeShadowNode', shadowNode, null);\n\n /* Sanitize tags and elements */\n if (_sanitizeElements(shadowNode)) {\n continue;\n }\n\n /* Deep shadow DOM detected */\n if (shadowNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(shadowNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(shadowNode);\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeShadowDOM', fragment, null);\n };\n\n /**\n * Sanitize\n * Public method providing core sanitation functionality\n *\n * @param {String|Node} dirty string or DOM node\n * @param {Object} cfg object\n */\n // eslint-disable-next-line complexity\n DOMPurify.sanitize = function (dirty, cfg = {}) {\n let body = null;\n let importedNode = null;\n let currentNode = null;\n let returnNode = null;\n /* Make sure we have a string to sanitize.\n DO NOT return early, as this will return the wrong type if\n the user has requested a DOM object rather than a string */\n IS_EMPTY_INPUT = !dirty;\n if (IS_EMPTY_INPUT) {\n dirty = '';\n }\n\n /* Stringify, in case dirty is an object */\n if (typeof dirty !== 'string' && !_isNode(dirty)) {\n if (typeof dirty.toString === 'function') {\n dirty = dirty.toString();\n if (typeof dirty !== 'string') {\n throw typeErrorCreate('dirty is not a string, aborting');\n }\n } else {\n throw typeErrorCreate('toString is not a function');\n }\n }\n\n /* Return dirty HTML if DOMPurify cannot run */\n if (!DOMPurify.isSupported) {\n return dirty;\n }\n\n /* Assign config vars */\n if (!SET_CONFIG) {\n _parseConfig(cfg);\n }\n\n /* Clean up removed elements */\n DOMPurify.removed = [];\n\n /* Check if dirty is correctly typed for IN_PLACE */\n if (typeof dirty === 'string') {\n IN_PLACE = false;\n }\n\n if (IN_PLACE) {\n /* Do some early pre-sanitization to avoid unsafe root nodes */\n if (dirty.nodeName) {\n const tagName = transformCaseFunc(dirty.nodeName);\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n throw typeErrorCreate(\n 'root node is forbidden and cannot be sanitized in-place'\n );\n }\n }\n } else if (dirty instanceof Node) {\n /* If dirty is a DOM element, append to an empty document to avoid\n elements being stripped by the parser */\n body = _initDocument('');\n importedNode = body.ownerDocument.importNode(dirty, true);\n if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {\n /* Node is already a body, use as is */\n body = importedNode;\n } else if (importedNode.nodeName === 'HTML') {\n body = importedNode;\n } else {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n body.appendChild(importedNode);\n }\n } else {\n /* Exit directly if we have nothing to do */\n if (\n !RETURN_DOM &&\n !SAFE_FOR_TEMPLATES &&\n !WHOLE_DOCUMENT &&\n // eslint-disable-next-line unicorn/prefer-includes\n dirty.indexOf('<') === -1\n ) {\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? trustedTypesPolicy.createHTML(dirty)\n : dirty;\n }\n\n /* Initialize the document to work on */\n body = _initDocument(dirty);\n\n /* Check we have a DOM node from the data */\n if (!body) {\n return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n }\n }\n\n /* Remove first element node (ours) if FORCE_BODY is set */\n if (body && FORCE_BODY) {\n _forceRemove(body.firstChild);\n }\n\n /* Get node iterator */\n const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n\n /* Now start iterating over the created document */\n while ((currentNode = nodeIterator.nextNode())) {\n /* Sanitize tags and elements */\n if (_sanitizeElements(currentNode)) {\n continue;\n }\n\n /* Shadow DOM detected, sanitize it */\n if (currentNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(currentNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(currentNode);\n }\n\n /* If we sanitized `dirty` in-place, return it. */\n if (IN_PLACE) {\n return dirty;\n }\n\n /* Return sanitized string or DOM */\n if (RETURN_DOM) {\n if (RETURN_DOM_FRAGMENT) {\n returnNode = createDocumentFragment.call(body.ownerDocument);\n\n while (body.firstChild) {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n returnNode.appendChild(body.firstChild);\n }\n } else {\n returnNode = body;\n }\n\n if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n /*\n AdoptNode() is not used because internal state is not reset\n (e.g. the past names map of a HTMLFormElement), this is safe\n in theory but we would rather not risk another attack vector.\n The state that is cloned by importNode() is explicitly defined\n by the specs.\n */\n returnNode = importNode.call(originalDocument, returnNode, true);\n }\n\n return returnNode;\n }\n\n let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n /* Serialize doctype if allowed */\n if (\n WHOLE_DOCUMENT &&\n ALLOWED_TAGS['!doctype'] &&\n body.ownerDocument &&\n body.ownerDocument.doctype &&\n body.ownerDocument.doctype.name &&\n regExpTest(EXPRESSIONS.DOCTYPE_NAME, body.ownerDocument.doctype.name)\n ) {\n serializedHTML =\n '\\n' + serializedHTML;\n }\n\n /* Sanitize final string template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n serializedHTML = stringReplace(serializedHTML, expr, ' ');\n });\n }\n\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? trustedTypesPolicy.createHTML(serializedHTML)\n : serializedHTML;\n };\n\n /**\n * Public method to set the configuration once\n * setConfig\n *\n * @param {Object} cfg configuration object\n */\n DOMPurify.setConfig = function (cfg = {}) {\n _parseConfig(cfg);\n SET_CONFIG = true;\n };\n\n /**\n * Public method to remove the configuration\n * clearConfig\n *\n */\n DOMPurify.clearConfig = function () {\n CONFIG = null;\n SET_CONFIG = false;\n };\n\n /**\n * Public method to check if an attribute value is valid.\n * Uses last set config, if any. Otherwise, uses config defaults.\n * isValidAttribute\n *\n * @param {String} tag Tag name of containing element.\n * @param {String} attr Attribute name.\n * @param {String} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.\n */\n DOMPurify.isValidAttribute = function (tag, attr, value) {\n /* Initialize shared config vars if necessary. */\n if (!CONFIG) {\n _parseConfig({});\n }\n\n const lcTag = transformCaseFunc(tag);\n const lcName = transformCaseFunc(attr);\n return _isValidAttribute(lcTag, lcName, value);\n };\n\n /**\n * AddHook\n * Public method to add DOMPurify hooks\n *\n * @param {String} entryPoint entry point for the hook to add\n * @param {Function} hookFunction function to execute\n */\n DOMPurify.addHook = function (entryPoint, hookFunction) {\n if (typeof hookFunction !== 'function') {\n return;\n }\n\n hooks[entryPoint] = hooks[entryPoint] || [];\n arrayPush(hooks[entryPoint], hookFunction);\n };\n\n /**\n * RemoveHook\n * Public method to remove a DOMPurify hook at a given entryPoint\n * (pops it from the stack of hooks if more are present)\n *\n * @param {String} entryPoint entry point for the hook to remove\n * @return {Function} removed(popped) hook\n */\n DOMPurify.removeHook = function (entryPoint) {\n if (hooks[entryPoint]) {\n return arrayPop(hooks[entryPoint]);\n }\n };\n\n /**\n * RemoveHooks\n * Public method to remove all DOMPurify hooks at a given entryPoint\n *\n * @param {String} entryPoint entry point for the hooks to remove\n */\n DOMPurify.removeHooks = function (entryPoint) {\n if (hooks[entryPoint]) {\n hooks[entryPoint] = [];\n }\n };\n\n /**\n * RemoveAllHooks\n * Public method to remove all DOMPurify hooks\n */\n DOMPurify.removeAllHooks = function () {\n hooks = {};\n };\n\n return DOMPurify;\n}\n\nexport default createDOMPurify();\n"],"names":["entries","setPrototypeOf","isFrozen","getPrototypeOf","getOwnPropertyDescriptor","Object","freeze","seal","create","apply","construct","Reflect","x","fun","thisValue","args","Func","arrayForEach","unapply","Array","prototype","forEach","arrayPop","pop","arrayPush","push","stringToLowerCase","String","toLowerCase","stringToString","toString","stringMatch","match","stringReplace","replace","stringIndexOf","indexOf","stringTrim","trim","objectHasOwnProperty","hasOwnProperty","regExpTest","RegExp","test","typeErrorCreate","unconstruct","TypeError","func","thisArg","_len","arguments","length","_key","_len2","_key2","addToSet","set","array","transformCaseFunc","undefined","l","element","lcElement","cleanArray","index","isPropertyExist","clone","object","newObject","property","value","isArray","constructor","lookupGetter","prop","desc","get","fallbackValue","html","svg","svgFilters","svgDisallowed","mathMl","mathMlDisallowed","text","xml","MUSTACHE_EXPR","ERB_EXPR","TMPLIT_EXPR","DATA_ATTR","ARIA_ATTR","IS_ALLOWED_URI","IS_SCRIPT_OR_DATA","ATTR_WHITESPACE","DOCTYPE_NAME","getGlobal","window","_createTrustedTypesPolicy","trustedTypes","purifyHostElement","createPolicy","suffix","ATTR_NAME","hasAttribute","getAttribute","policyName","createHTML","createScriptURL","scriptUrl","_","console","warn","createDOMPurify","DOMPurify","root","version","VERSION","removed","document","nodeType","isSupported","originalDocument","currentScript","DocumentFragment","HTMLTemplateElement","Node","Element","NodeFilter","NamedNodeMap","MozNamedAttrMap","HTMLFormElement","DOMParser","ElementPrototype","cloneNode","getNextSibling","getChildNodes","getParentNode","template","createElement","content","ownerDocument","trustedTypesPolicy","emptyHTML","implementation","createNodeIterator","createDocumentFragment","getElementsByTagName","importNode","hooks","createHTMLDocument","EXPRESSIONS","ALLOWED_TAGS","DEFAULT_ALLOWED_TAGS","TAGS","ALLOWED_ATTR","DEFAULT_ALLOWED_ATTR","ATTRS","CUSTOM_ELEMENT_HANDLING","tagNameCheck","writable","configurable","enumerable","attributeNameCheck","allowCustomizedBuiltInElements","FORBID_TAGS","FORBID_ATTR","ALLOW_ARIA_ATTR","ALLOW_DATA_ATTR","ALLOW_UNKNOWN_PROTOCOLS","ALLOW_SELF_CLOSE_IN_ATTR","SAFE_FOR_TEMPLATES","WHOLE_DOCUMENT","SET_CONFIG","FORCE_BODY","RETURN_DOM","RETURN_DOM_FRAGMENT","RETURN_TRUSTED_TYPE","SANITIZE_DOM","SANITIZE_NAMED_PROPS","SANITIZE_NAMED_PROPS_PREFIX","KEEP_CONTENT","IN_PLACE","USE_PROFILES","FORBID_CONTENTS","DEFAULT_FORBID_CONTENTS","DATA_URI_TAGS","DEFAULT_DATA_URI_TAGS","URI_SAFE_ATTRIBUTES","DEFAULT_URI_SAFE_ATTRIBUTES","MATHML_NAMESPACE","SVG_NAMESPACE","HTML_NAMESPACE","NAMESPACE","IS_EMPTY_INPUT","ALLOWED_NAMESPACES","DEFAULT_ALLOWED_NAMESPACES","PARSER_MEDIA_TYPE","SUPPORTED_PARSER_MEDIA_TYPES","DEFAULT_PARSER_MEDIA_TYPE","CONFIG","formElement","isRegexOrFunction","testValue","Function","_parseConfig","cfg","ADD_URI_SAFE_ATTR","ADD_DATA_URI_TAGS","ALLOWED_URI_REGEXP","ADD_TAGS","ADD_ATTR","table","tbody","TRUSTED_TYPES_POLICY","MATHML_TEXT_INTEGRATION_POINTS","HTML_INTEGRATION_POINTS","COMMON_SVG_AND_HTML_ELEMENTS","ALL_SVG_TAGS","ALL_MATHML_TAGS","_checkValidNamespace","parent","tagName","namespaceURI","parentTagName","Boolean","_forceRemove","node","parentNode","removeChild","remove","_removeAttribute","name","attribute","getAttributeNode","from","removeAttribute","setAttribute","_initDocument","dirty","doc","leadingWhitespace","matches","dirtyPayload","parseFromString","documentElement","createDocument","innerHTML","body","insertBefore","createTextNode","childNodes","call","_createNodeIterator","SHOW_ELEMENT","SHOW_COMMENT","SHOW_TEXT","_isClobbered","elm","nodeName","textContent","attributes","hasChildNodes","_isNode","_executeHook","entryPoint","currentNode","data","hook","_sanitizeElements","allowedTags","firstElementChild","_isBasicCustomElement","childCount","i","expr","_isValidAttribute","lcTag","lcName","_sanitizeAttributes","hookEvent","attrName","attrValue","keepAttr","allowedAttributes","attr","forceKeepAttr","getAttributeType","setAttributeNS","_sanitizeShadowDOM","fragment","shadowNode","shadowIterator","nextNode","sanitize","importedNode","returnNode","appendChild","firstChild","nodeIterator","shadowroot","shadowrootmode","serializedHTML","outerHTML","doctype","setConfig","clearConfig","isValidAttribute","tag","addHook","hookFunction","removeHook","removeHooks","removeAllHooks"],"mappings":"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"} \ No newline at end of file diff --git a/post.js b/post.js index 2d8f669..89cae35 100644 --- a/post.js +++ b/post.js @@ -1,3 +1,5 @@ +import DOMPurify from "./dependencies/dompurify/purify.js"; + const ACCEPT_HEADER = "application/activity+json, application/ld+json, application/json, text/html"; @@ -204,23 +206,35 @@ class DistributedPost extends HTMLElement { summary.textContent = "Sensitive Content (click to view)"; details.appendChild(summary); const content = document.createElement("p"); - // TODO: Sanitize contentSource to remove or escape any harmful HTML content before displaying - content.textContent = contentSource; + + // Sanitize contentSource before displaying + const sanitizedContent = DOMPurify.sanitize(contentSource); + content.innerHTML = sanitizedContent; + details.appendChild(content); this.appendChild(details); } else { - // If not sensitive, display content as usual - this.appendField("Content", contentSource); + // If not sensitive, display content as usual but sanitize first + this.appendField("Content", DOMPurify.sanitize(contentSource), true); } } - appendField(label, value) { + // 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); - p.appendChild(document.createTextNode(` ${value}`)); + 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); } } From 7fae80f3ceab0aac2045a7b552dc21404b3dde3e Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 26 Feb 2024 03:10:38 +0530 Subject: [PATCH 028/149] chore: increase default numPosts value --- outbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outbox.js b/outbox.js index 037d9eb..cbcfd97 100644 --- a/outbox.js +++ b/outbox.js @@ -2,7 +2,7 @@ class DistributedOutbox extends HTMLElement { constructor() { super(); this.renderedItems = new Map(); // Tracks rendered items by ID - this.numPosts = 1; // Default value + this.numPosts = 32; // Default value this.page = 1; // Default value this.totalPages = 0; // Keep track of total pages } From 26577781480a3cb922240c86558ca5effe8157df Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 27 Feb 2024 21:35:54 +0530 Subject: [PATCH 029/149] feat: enhance follow activity presentation --- dependencies/dompurify/.DS_Store | Bin 0 -> 10244 bytes outbox.js | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 dependencies/dompurify/.DS_Store diff --git a/dependencies/dompurify/.DS_Store b/dependencies/dompurify/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..727c6bbb8a17b87d56c236eb21d33aa060bf050b GIT binary patch literal 10244 zcmeHMO;6k~5PhZ`Ky4-3Qx6<+>?uHzdJbrNs6=}~?X}${RM2iVtNjq-mVeZr*uEJ% zKt6UGs6dDvOZM3D%-H)TjdxWjq62SfliScPuAVAE1$UTUMPg(t5`9p!-^xTLCg zh_T<`wg-lQAz%m?0)~Jg@E8za%$Bn8+*4}}0YktLxI#eAhlo{Z5@_M69vy5n1t4AL zV=e464^WvX&?L~pQx&C7pFOxB>OzX)yg270VJA%jEj;z&a9$iPtn5OD;&gS&MamAB zcxtU7U}}Z_UMNP_{9DRequ_>c03y#&8M>oWoI~M&OaqR zMTQ-GMUEl0WgJgA?xD}QK6_s{KIJO%w2xmn<9b8oIwOaSM2AS^xWI^WIr&oMPdO`V z$Q}pOE}SX8ao-cwAUdTd{IW(&+9nZKB!qX&I#Q_-f8jYtqATUvSp}7yJL2>?;PkSh zt%4>sZ!ejRxsyKU%X^iIT~^c*|ASX)kyjlsulq)a@&W`T?+=%_u`(=zt$ zRmPVtI4!b(m-SX`etX@VwfOSy_;MS(N{iIvOPdaf7gh`bL*Tv;c;*#Ha{vEf@&Et# zwY=#y1Pp;kfPh=?9rq5o5w~D7a@QWQerJ^;e1)ef!bY#-N%cCO1G$cW%PPFPLZz^u d1X_5?CoB*CXTVdwrL_EiHEyGqWcFS1|G(&F6OjM_ literal 0 HcmV?d00001 diff --git a/outbox.js b/outbox.js index cbcfd97..0b07750 100644 --- a/outbox.js +++ b/outbox.js @@ -230,7 +230,9 @@ class DistributedActivity extends HTMLElement { } displayFollowActivity() { - const message = `New follow request from ${this.activityData.actor}`; + const from = this.activityData.actor; + const to = this.activityData.object; + const message = `New follow request from ${from} to ${to}`; const messageElement = document.createElement("p"); messageElement.textContent = message; this.appendChild(messageElement); From 1cd4a474c8c24a3c32d0f9ee656e070a5e24a98d Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 27 Feb 2024 21:36:57 +0530 Subject: [PATCH 030/149] chore: remove .DS_Store file --- dependencies/dompurify/.DS_Store | Bin 10244 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dependencies/dompurify/.DS_Store diff --git a/dependencies/dompurify/.DS_Store b/dependencies/dompurify/.DS_Store deleted file mode 100644 index 727c6bbb8a17b87d56c236eb21d33aa060bf050b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMO;6k~5PhZ`Ky4-3Qx6<+>?uHzdJbrNs6=}~?X}${RM2iVtNjq-mVeZr*uEJ% zKt6UGs6dDvOZM3D%-H)TjdxWjq62SfliScPuAVAE1$UTUMPg(t5`9p!-^xTLCg zh_T<`wg-lQAz%m?0)~Jg@E8za%$Bn8+*4}}0YktLxI#eAhlo{Z5@_M69vy5n1t4AL zV=e464^WvX&?L~pQx&C7pFOxB>OzX)yg270VJA%jEj;z&a9$iPtn5OD;&gS&MamAB zcxtU7U}}Z_UMNP_{9DRequ_>c03y#&8M>oWoI~M&OaqR zMTQ-GMUEl0WgJgA?xD}QK6_s{KIJO%w2xmn<9b8oIwOaSM2AS^xWI^WIr&oMPdO`V z$Q}pOE}SX8ao-cwAUdTd{IW(&+9nZKB!qX&I#Q_-f8jYtqATUvSp}7yJL2>?;PkSh zt%4>sZ!ejRxsyKU%X^iIT~^c*|ASX)kyjlsulq)a@&W`T?+=%_u`(=zt$ zRmPVtI4!b(m-SX`etX@VwfOSy_;MS(N{iIvOPdaf7gh`bL*Tv;c;*#Ha{vEf@&Et# zwY=#y1Pp;kfPh=?9rq5o5w~D7a@QWQerJ^;e1)ef!bY#-N%cCO1G$cW%PPFPLZz^u d1X_5?CoB*CXTVdwrL_EiHEyGqWcFS1|G(&F6OjM_ From 151319af320ba3d54685f8473ee548d8f42a360a Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 27 Feb 2024 23:11:51 +0530 Subject: [PATCH 031/149] feat: add support for IPNS and Hyper protocol gateways --- outbox.js | 34 +++++++++++++++++++++-------- post.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/outbox.js b/outbox.js index 0b07750..adc95f7 100644 --- a/outbox.js +++ b/outbox.js @@ -25,7 +25,6 @@ class DistributedOutbox extends HTMLElement { this.processItem(item); } } - processItem(item) { const itemKey = item.id || item.object; @@ -44,18 +43,36 @@ class DistributedOutbox extends HTMLElement { console.error("No outbox URL provided"); return; } - + try { - const response = await fetch(outboxUrl); + let response; + // Check the scheme and adjust the URL for unsupported schemes before fetching + if (outboxUrl.startsWith("hyper://")) { + const gatewayUrl = outboxUrl.replace( + "hyper://", + "https://hyper.hypha.coop/hyper/" + ); + response = await fetch(gatewayUrl); + } else if (outboxUrl.startsWith("ipns://")) { + const gatewayUrl = outboxUrl.replace( + "ipns://", + "https://ipfs.hypha.coop/ipns/" + ); + response = await fetch(gatewayUrl); + } else { + response = await fetch(outboxUrl); + } + if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const outbox = await response.json(); - + // Adjust for both direct items and items loaded via URLs const items = []; for (const itemOrUrl of outbox.orderedItems) { - if (typeof itemOrUrl === 'string') { // URL to an activity + if (typeof itemOrUrl === "string") { + // URL to an activity const itemResponse = await fetch(itemOrUrl); if (itemResponse.ok) { const item = await itemResponse.json(); @@ -65,15 +82,15 @@ class DistributedOutbox extends HTMLElement { items.push(itemOrUrl); // Directly included activity } } - + this.totalPages = Math.ceil(items.length / this.numPosts); this.page = Math.min(this.page, this.totalPages); - + // Calculate the range of items to be loaded based on the current page and numPosts const startIndex = (this.page - 1) * this.numPosts; const endIndex = startIndex + this.numPosts; const itemsToLoad = items.slice(startIndex, endIndex); - + for (const item of itemsToLoad) { yield item; } @@ -81,7 +98,6 @@ class DistributedOutbox extends HTMLElement { console.error("Error fetching outbox:", error); } } - renderItem(item) { const activityElement = document.createElement("distributed-activity"); diff --git a/post.js b/post.js index 89cae35..f118879 100644 --- a/post.js +++ b/post.js @@ -94,6 +94,60 @@ async function fetchJsonLd(jsonLdUrl) { // } // } +// Function to load content from IPNS with fallback to the IPNS HTTP gateway +async function loadPostFromIpns(ipnsUrl) { + try { + const nativeResponse = await fetch(ipnsUrl); + if (nativeResponse.ok) { + return await nativeResponse.text(); + } + } catch (error) { + console.log("Native IPNS loading failed, trying HTTP gateway:", error); + } + + // Fallback to loading content via an HTTP IPNS gateway + const gatewayUrl = ipnsUrl.replace( + "ipns://", + "https://ipfs.hypha.coop/ipns/" + ); + try { + const gatewayResponse = await fetch(gatewayUrl); + if (!gatewayResponse.ok) { + throw new Error(`HTTP error! Status: ${gatewayResponse.status}`); + } + return await gatewayResponse.text(); + } catch (error) { + console.error("Error fetching IPNS content via HTTP gateway:", error); + } +} + +// Function to load content from Hyper with fallback to the Hyper HTTP gateway +async function loadPostFromHyper(hyperUrl) { + try { + const nativeResponse = await fetch(hyperUrl); + if (nativeResponse.ok) { + return await nativeResponse.text(); + } + } catch (error) { + console.log("Native Hyper loading failed, trying HTTP gateway:", error); + } + + // Fallback to loading content via an HTTP Hyper gateway + const gatewayUrl = hyperUrl.replace( + "hyper://", + "https://hyper.hypha.coop/hyper/" + ); + try { + const gatewayResponse = await fetch(gatewayUrl); + if (!gatewayResponse.ok) { + throw new Error(`HTTP error! Status: ${gatewayResponse.status}`); + } + return await gatewayResponse.text(); + } catch (error) { + console.error("Error fetching Hyper content via HTTP gateway:", error); + } +} + async function fetchActorInfo(actorUrl) { try { const response = await fetch(actorUrl); @@ -142,11 +196,12 @@ class DistributedPost extends HTMLElement { // if (postUrl.startsWith("ipfs://")) { // content = await loadPostFromIpfs(postUrl); // } - if ( - postUrl.startsWith("ipns://") || - postUrl.startsWith("hyper://") || - postUrl.startsWith("https://") - ) { + // Attempt to load content using native URLs or HTTP gateways based on the scheme + if (postUrl.startsWith("ipns://")) { + content = await loadPostFromIpns(postUrl); + } else if (postUrl.startsWith("hyper://")) { + content = await loadPostFromHyper(postUrl); + } else if (postUrl.startsWith("https://")) { content = await loadPost(postUrl); } else { this.renderErrorContent("Unsupported URL scheme"); From 46039c0d1bb6ba9473bd82f2a40da3dc3187791a Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Thu, 29 Feb 2024 00:51:33 -0500 Subject: [PATCH 032/149] Implement activity fetching and indexing --- db.js | 143 +++++++++++++++++++++++++++++++++++++++++------------ index.html | 6 +++ 2 files changed, 118 insertions(+), 31 deletions(-) diff --git a/db.js b/db.js index 2f40f8f..a1d6ac2 100644 --- a/db.js +++ b/db.js @@ -1,7 +1,10 @@ import { openDB } from './dependencies/idb/index.js' +export const DEFAULT_DB = 'default' export const ACTORS_STORE = 'actors' export const NOTES_STORE = 'notes' +export const ACTIVITIES_STORE = 'activities' + export const ID_FIELD = 'id' export const URL_FIELD = 'url' export const CREATED_FIELD = 'created' @@ -13,6 +16,13 @@ export const IN_REPLY_TO_FIELD = 'inReplyTo' export const TAG_NAMES_FIELD = 'tag_names' export const ATTRIBUTED_TO_FIELD = 'attributedTo' export const CONVERSATION_FIELD = 'conversation' +export const ACTOR_FIELD = 'actor' + +export const PUBLISHED_SUFFIX = ', published' + +export const TYPE_CREATE = 'Create' +export const TYPE_NOTE = 'Note' +export const TYPE_DELETE = 'Delete' // TODO: When ingesting notes and actors, wrap any dates in `new Date()` // TODO: When ingesting notes add a "tag_names" field which is just the names of the tag @@ -25,7 +35,7 @@ export class ActivityPubDB { this.fetch = fetch } - static async load (name, fetch = globalThis.fetch) { + static async load (name = DEFAULT_DB, fetch = globalThis.fetch) { const db = await openDB(name, 1, { upgrade }) @@ -33,19 +43,38 @@ export class ActivityPubDB { return new ActivityPubDB(db, fetch) } + async #get (url) { + if (url && typeof url === 'object') { + return url + } + // TODO: Signed fetch + const response = await this.fetch.call(globalThis, url, { + headers: { + Accept: 'application/json' + } + }) + if (!response.ok) { + throw new Error(await response.text()) + } + + return await response.json() + } + async getActor (url) { - // Try to load from db - // else try to ingest then load from db - const request = await this.fetch(url, { headers: { Accept: 'application/ld+json' } }) - // Handle 404 - const actor = await request.json() - this.db.add(ACTORS_STORE, actor) + // TODO: Try to load from cache + const actor = await this.#get(url) + this.db.put(ACTORS_STORE, actor) return this.db.get(ACTORS_STORE, actor.id) } async getNote (url) { - // Try to load from db - // Else try to ingest then load from db + try { + return this.db.get(NOTES_STORE, url) + } catch { + const note = await this.#get(url) + await this.ingestNote(note) + return note + } } async * searchNotes (query) { @@ -53,28 +82,74 @@ export class ActivityPubDB { // Everything gets sorted by the `published` time. // We should find a way to allow for arrays of values and open multiple iterators // Kinda like this: https://git.sr.ht/~breatheoutbreathein/ushin-db/tree/master/item/USHINBase.js#L509 + + const tx = this.db.transaction(NOTES_STORE) + + for await (const item of tx.store) { + yield item.value + } } async ingestActor (url) { - // Load actor and save to actors store + const actor = await this.getActor(url) + // TODO: What else can we ingest from an actor? // Ingest outbox as collection + const { outbox } = actor + + // TODO: Use actor.id? + await this.ingestActivityCollection(outbox, url) } - async ingestCollection (url, verifyAttributed = '') { - // Load outbox as async iterator, handle extra pages - // Go through each activity and if it's a note, ingest or delete it + async * iterateCollection (url) { + const collection = await this.#get(url) + // TODO: handle paging and skiping + const items = collection.items || collection.orderedItems + + if (!items) throw new Error(`Unable to find items at ${url}`) + for await (const item of items) { + if (typeof item === 'string') { + const data = await this.#get(item) + yield data + } else yield item + } } - async ingestNote (url) { - // Load by url + async ingestActivityCollection (url, verifyAttributed = '') { + for await (const activity of this.iterateCollection(url)) { + if (verifyAttributed && activity.actor !== verifyAttributed) { + throw new Error(`Collection contained activity not attributed to ${verifyAttributed} at ${url} in activity ${activity.id}`) + } + await this.ingestActivity(activity) + } + } + + async ingestActivity (activity) { + activity.published = new Date(activity.published) + console.log(activity) + await this.db.put(ACTIVITIES_STORE, activity) + if (activity.type === TYPE_CREATE) { + const object = await this.#get(activity.object) + if (object.type === TYPE_NOTE) { + console.log(object) + await this.ingestNote(object) + } + } else if (activity.type === TYPE_DELETE) { + await this.deleteNote(activity.object) + } + } + + async ingestNote (note) { // Convert needed fields to date - // Add tag_names field - // Add to notes store - // Loop through replies (if possible) and ingest them + note.published = new Date(note.published) + // Add tag_names field + note.tag_names = (note.tags || []).map(({ name }) => name) + this.db.put(NOTES_STORE, note) + // TODO: Loop through replies } async deleteNote (url) { // delete note using the url as the `id` from the notes store + this.db.delete(NOTES_STORE, url) } } @@ -93,19 +168,25 @@ function upgrade (db) { autoIncrement: false }) - addRegularIndex(PUBLISHED_FIELD) - addRegularIndex(TO_FIELD) - addRegularIndex(URL_FIELD) - addSortedIndex(TAG_NAMES_FIELD, { multiEntry: true }) - addSortedIndex(IN_REPLY_TO_FIELD) - addSortedIndex(ATTRIBUTED_TO_FIELD) - addSortedIndex(CONVERSATION_FIELD) - addSortedIndex(TO_FIELD) - - function addRegularIndex (field, options = {}) { - notes.createIndex(field, field, options) + addRegularIndex(notes, TO_FIELD) + addRegularIndex(notes, URL_FIELD) + addRegularIndex(notes, TAG_NAMES_FIELD, { multiEntry: true }) + addSortedIndex(notes, IN_REPLY_TO_FIELD) + addSortedIndex(notes, ATTRIBUTED_TO_FIELD) + addSortedIndex(notes, CONVERSATION_FIELD) + addSortedIndex(notes, TO_FIELD) + + const activities = db.createObjectStore(ACTIVITIES_STORE, { + keyPath: 'id', + autoIncrement: false + }) + addSortedIndex(activities, ACTOR_FIELD) + addSortedIndex(activities, TO_FIELD) + + function addRegularIndex (store, field, options = {}) { + store.createIndex(field, field, options) } - function addSortedIndex (field, options = {}) { - notes.createIndex(field, [field, PUBLISHED_FIELD], options) + function addSortedIndex (store, field, options = {}) { + store.createIndex(field + ', published', [field, PUBLISHED_FIELD], options) } } diff --git a/index.html b/index.html index b032c62..5bf3429 100644 --- a/index.html +++ b/index.html @@ -11,3 +11,9 @@ >follow us on GitHub.

+ + From 35995cfe4c9f0e99c53787e4f7d8dc74fbfb67cd Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Thu, 29 Feb 2024 00:51:51 -0500 Subject: [PATCH 033/149] add agregore theming when possible --- style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/style.css b/style.css index 8e1102e..51adda5 100644 --- a/style.css +++ b/style.css @@ -4,6 +4,7 @@ Vars Any colors, sizings, etc should be set as vars and reused */ +@import url("agregore://theme/style.css"); :root { --rdp-font: "system-ui" From cd01213d55acd39f9f8e7a212e3ddb2e0c873d55 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 6 Mar 2024 19:44:09 +0530 Subject: [PATCH 034/149] feat: implement reader post ui --- index.css | 9 ++++ index.html | 4 +- outbox.css | 3 ++ outbox.html | 12 +++-- post.css | 76 ++++++++++++++++++++++++++++++ post.html | 6 ++- post.js | 133 ++++++++++++++++++++++++++++++++++++++++++---------- style.css | 28 ----------- 8 files changed, 210 insertions(+), 61 deletions(-) create mode 100644 index.css create mode 100644 outbox.css create mode 100644 post.css delete mode 100644 style.css diff --git a/index.css b/index.css new file mode 100644 index 0000000..02b5f3a --- /dev/null +++ b/index.css @@ -0,0 +1,9 @@ +/* Vars */ +:root { + --bg-color: #ffffff; +} + +/* Main styles */ +html { + background: var(--bg-color); +} diff --git a/index.html b/index.html index b032c62..0dc7f39 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ -Distributed Press Reader +Social Reader Post Outbox diff --git a/outbox.css b/outbox.css new file mode 100644 index 0000000..10ea263 --- /dev/null +++ b/outbox.css @@ -0,0 +1,3 @@ +.pagination-controls { + text-align: center; +} diff --git a/outbox.html b/outbox.html index 193de64..62dbde5 100644 --- a/outbox.html +++ b/outbox.html @@ -1,8 +1,10 @@ -Distributed Press Reader - +Reader Outbox +
-
- - + +
diff --git a/post.css b/post.css new file mode 100644 index 0000000..88cb1ed --- /dev/null +++ b/post.css @@ -0,0 +1,76 @@ +/* Vars */ +:root { + --rdp-font: "Arial", sans-serif; + --rdp-bg-color: #ebf3f5; + --rdp-text-color: #000000; + --rdp-details-color: #4d626a; + --rdp-border-color: #cccccc; + --rdp-border-radius: 6px; +} + +/* Main styles */ +html { + font-family: var(--rdp-font); +} + +/* Component styles */ +.distributed-post { + background: var(--rdp-bg-color); + border: 1px solid var(--rdp-border-color); + border-radius: var(--rdp-border-radius); + padding: 16px; + max-width: 500px; + margin: 16px auto; +} + +.distributed-post-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.distributed-post-author { + display: flex; + align-items: center; +} + +.actor-icon { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #000000; + margin-right: 8px; +} + +.actor-details { + display: flex; + flex-direction: column; +} + +.actor-name { + color: var(--rdp-text-color); + font-weight: bold; +} + +.actor-username { + color: var(--rdp-details-color); + margin-top: 1px; +} + +.time-ago { + font-size: 0.875rem; + color: var(--rdp-details-color); +} + +.post-content { + font-size: 16px; + color: var(--rdp-text-color); + padding: 6px; + margin-bottom: 16px; +} + +.post-footer { + font-size: 0.875rem; + color: var(--rdp-details-color); +} diff --git a/post.html b/post.html index 32a557a..ce044c7 100644 --- a/post.html +++ b/post.html @@ -1,6 +1,8 @@ -Distributed Press Reader - +Reader Post +
diff --git a/post.js b/post.js index f118879..d3317ba 100644 --- a/post.js +++ b/post.js @@ -160,6 +160,43 @@ async function fetchActorInfo(actorUrl) { } } +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 renderError(message) { const errorElement = document.createElement("p"); errorElement.classList.add("error"); @@ -232,6 +269,14 @@ class DistributedPost extends HTMLElement { // Clear existing content this.innerHTML = ""; + // 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) { @@ -242,17 +287,26 @@ class DistributedPost extends HTMLElement { if (attributedToSource) { const actorInfo = document.createElement("actor-info"); actorInfo.setAttribute("url", attributedToSource); - this.appendChild(actorInfo); + postHeader.appendChild(actorInfo); } - this.appendField("Published", jsonLdData.published); - this.appendField("Author", attributedToSource); + // Published time element + const publishedTime = document.createElement("time"); + 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 - let contentSource = jsonLdData.content; - if ("object" in jsonLdData && "content" in jsonLdData.object) { - contentSource = jsonLdData.object.content; - } + let contentSource = + jsonLdData.content || (jsonLdData.object && jsonLdData.object.content); // Handle sensitive content if (jsonLdData.sensitive) { @@ -261,17 +315,31 @@ class DistributedPost extends HTMLElement { summary.textContent = "Sensitive Content (click to view)"; details.appendChild(summary); const content = document.createElement("p"); - - // Sanitize contentSource before displaying - const sanitizedContent = DOMPurify.sanitize(contentSource); - content.innerHTML = sanitizedContent; - + content.innerHTML = DOMPurify.sanitize(contentSource); details.appendChild(content); - this.appendChild(details); + postContent.appendChild(details); } else { - // If not sensitive, display content as usual but sanitize first - this.appendField("Content", DOMPurify.sanitize(contentSource), true); + const content = document.createElement("p"); + content.innerHTML = DOMPurify.sanitize(contentSource); + postContent.appendChild(content); } + + // Append the content to the post container + postContainer.appendChild(postContent); + + // Footer of the post, which will contain the full published date and platform + const postFooter = document.createElement("footer"); + postFooter.classList.add("post-footer"); + const fullDate = document.createElement("div"); + fullDate.classList.add("full-date"); + fullDate.textContent = formatDate(jsonLdData.published) + " · reader web"; + postFooter.appendChild(fullDate); + + // Append the footer to the post container + postContainer.appendChild(postFooter); + + // Append the whole post container to the custom element + this.appendChild(postContainer); } // appendField to optionally allow HTML content @@ -301,6 +369,7 @@ class DistributedPost extends HTMLElement { const errorElement = document.createElement("p"); errorElement.className = "error"; errorElement.textContent = errorMessage; + errorElement.style.color = "red"; this.appendChild(errorElement); } } @@ -327,31 +396,47 @@ class ActorInfo extends HTMLElement { // Clear existing content this.innerHTML = ""; - if (actorInfo.name) { - const pName = document.createElement("p"); - pName.textContent = actorInfo.name; - this.appendChild(pName); - } + 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 = null; if (actorInfo.icon) { if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) { - // Assume first icon if array iconUrl = actorInfo.icon[0].url; } else if (actorInfo.icon.url) { - // Directly use the URL if object iconUrl = actorInfo.icon.url; } if (iconUrl) { const img = document.createElement("img"); + img.classList.add("actor-icon"); img.src = iconUrl; - img.width = 69; img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; - this.appendChild(img); + author.appendChild(img); } } + + if (actorInfo.name) { + const pName = document.createElement("div"); + pName.classList.add("actor-name"); + pName.textContent = actorInfo.name; + 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); diff --git a/style.css b/style.css deleted file mode 100644 index 8e1102e..0000000 --- a/style.css +++ /dev/null @@ -1,28 +0,0 @@ -/* -Vars - -Any colors, sizings, etc should be set as vars and reused -*/ - - -:root { - --rdp-font: "system-ui" -} - -/* -Main styles - -These should set the default look and feel of the app. -Stuff like font color/size, backgrounds, button/paragraph styles. -*/ - -html { - font-family: var(--rdp-font); -} - - -/* -Component styles -*/ - -// TODO From 506341f6ce2ec9e6321c0e2d69cea6b6b9c4a990 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 6 Mar 2024 20:04:24 +0530 Subject: [PATCH 035/149] feat: add support for update activity type --- outbox.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/outbox.js b/outbox.js index adc95f7..c0e05f0 100644 --- a/outbox.js +++ b/outbox.js @@ -222,13 +222,37 @@ class DistributedActivity extends HTMLElement { this.appendChild(distributedPostElement); } + fetchAndUpdatePost(activityData) { + let postUrl; + // Determine the source of the post (direct activity or URL pointing to the activity) + const isDirectUpdate = + typeof activityData.object === "string" || + activityData.object instanceof String; + + if (isDirectUpdate) { + // If it's a direct update, use the URL from the 'object' property + postUrl = activityData.object; + } else if (activityData.object && activityData.object.id) { + // If the 'object' property contains an 'id', use it as the URL + postUrl = activityData.object.id; + } else { + // Otherwise, use the 'id' property of the activityData itself + postUrl = activityData.id; + } + + this.fetchAndDisplayPost(postUrl); + } + renderActivity() { // Clear existing content this.innerHTML = ""; switch (this.activityType) { case "Create": - this.fetchAndDisplayPost(this.activityData.object); + this.fetchAndDisplayPost(); + break; + case "Update": + this.fetchAndUpdatePost(this.activityData); break; case "Follow": this.displayFollowActivity(); From c57c859a24a8cd288bef49bf1fd51f7525ddcfd1 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 6 Mar 2024 23:00:06 +0530 Subject: [PATCH 036/149] feat: implement reader timeline, ingest actors and activities in db --- db.js | 317 +++++++++++++++++++++++++++++++-------------------- index.html | 42 ++++--- timeline.css | 53 +++++++++ timeline.js | 43 +++++++ 4 files changed, 315 insertions(+), 140 deletions(-) create mode 100644 timeline.css create mode 100644 timeline.js diff --git a/db.js b/db.js index a1d6ac2..0d6c78c 100644 --- a/db.js +++ b/db.js @@ -1,192 +1,261 @@ -import { openDB } from './dependencies/idb/index.js' - -export const DEFAULT_DB = 'default' -export const ACTORS_STORE = 'actors' -export const NOTES_STORE = 'notes' -export const ACTIVITIES_STORE = 'activities' - -export const ID_FIELD = 'id' -export const URL_FIELD = 'url' -export const CREATED_FIELD = 'created' -export const UPDATED_FIELD = 'updated' -export const PUBLISHED_FIELD = 'published' -export const TO_FIELD = 'to' -export const CC_FIELD = 'cc' -export const IN_REPLY_TO_FIELD = 'inReplyTo' -export const TAG_NAMES_FIELD = 'tag_names' -export const ATTRIBUTED_TO_FIELD = 'attributedTo' -export const CONVERSATION_FIELD = 'conversation' -export const ACTOR_FIELD = 'actor' - -export const PUBLISHED_SUFFIX = ', published' - -export const TYPE_CREATE = 'Create' -export const TYPE_NOTE = 'Note' -export const TYPE_DELETE = 'Delete' +import { openDB } from "./dependencies/idb/index.js"; + +export const DEFAULT_DB = "default"; +export const ACTORS_STORE = "actors"; +export const NOTES_STORE = "notes"; +export const ACTIVITIES_STORE = "activities"; + +export const ID_FIELD = "id"; +export const URL_FIELD = "url"; +export const CREATED_FIELD = "created"; +export const UPDATED_FIELD = "updated"; +export const PUBLISHED_FIELD = "published"; +export const TO_FIELD = "to"; +export const CC_FIELD = "cc"; +export const IN_REPLY_TO_FIELD = "inReplyTo"; +export const TAG_NAMES_FIELD = "tag_names"; +export const ATTRIBUTED_TO_FIELD = "attributedTo"; +export const CONVERSATION_FIELD = "conversation"; +export const ACTOR_FIELD = "actor"; + +export const PUBLISHED_SUFFIX = ", published"; + +export const TYPE_CREATE = "Create"; +export const TYPE_NOTE = "Note"; +export const TYPE_DELETE = "Delete"; // TODO: When ingesting notes and actors, wrap any dates in `new Date()` // TODO: When ingesting notes add a "tag_names" field which is just the names of the tag // TODO: When ingesting notes, also load their replies -// TODO: Detect P2P URLs and use gateways (wrap `fetch` with it?) export class ActivityPubDB { - constructor (db, fetch = globalThis.fetch) { - this.db = db - this.fetch = fetch + constructor(db, fetch = globalThis.fetch) { + this.db = db; + this.fetch = fetch; } - static async load (name = DEFAULT_DB, fetch = globalThis.fetch) { + static async load(name = DEFAULT_DB, fetch = globalThis.fetch) { const db = await openDB(name, 1, { - upgrade - }) + upgrade, + }); - return new ActivityPubDB(db, fetch) + return new ActivityPubDB(db, fetch); } - async #get (url) { - if (url && typeof url === 'object') { - return url + async #get(url) { + if (url && typeof url === "object") { + return url; } + + let response; + // Try fetching directly for all URLs (including P2P URLs) // TODO: Signed fetch - const response = await this.fetch.call(globalThis, url, { - headers: { - Accept: 'application/json' + try { + response = await this.fetch.call(globalThis, url, { + headers: { + Accept: "application/json", + }, + }); + } catch (error) { + console.error("P2P loading failed, trying HTTP gateway:", error); + } + + // If direct fetch was not successful, attempt fetching from a gateway for P2P protocols + if (!response || !response.ok) { + let gatewayUrl = url; + + if (url.startsWith("hyper://")) { + gatewayUrl = url.replace("hyper://", "https://hyper.hypha.coop/hyper/"); + } else if (url.startsWith("ipns://")) { + gatewayUrl = url.replace("ipns://", "https://ipfs.hypha.coop/ipns/"); + } + + try { + response = await this.fetch.call(globalThis, gatewayUrl, { + headers: { + Accept: "application/json", + }, + }); + } catch (error) { + console.error("Fetching from gateway failed:", error); + throw new Error(`Failed to fetch ${url} from gateway`); } - }) - if (!response.ok) { - throw new Error(await response.text()) } - return await response.json() + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return await response.json(); } - async getActor (url) { + async getActor(url) { // TODO: Try to load from cache - const actor = await this.#get(url) - this.db.put(ACTORS_STORE, actor) - return this.db.get(ACTORS_STORE, actor.id) + const actor = await this.#get(url); + this.db.put(ACTORS_STORE, actor); + return this.db.get(ACTORS_STORE, actor.id); + } + + async getAllActors() { + const tx = this.db.transaction(ACTORS_STORE); + const actors = []; + let cursor = await tx.store.openCursor(); + + while (cursor) { + actors.push(cursor.value); + cursor = await cursor.continue(); + } + + return actors; } - async getNote (url) { + async getNote(url) { try { - return this.db.get(NOTES_STORE, url) + return this.db.get(NOTES_STORE, url); } catch { - const note = await this.#get(url) - await this.ingestNote(note) - return note + const note = await this.#get(url); + await this.ingestNote(note); + return note; } } - async * searchNotes (query) { - // Query can search by any of the indexed fields. - // Everything gets sorted by the `published` time. - // We should find a way to allow for arrays of values and open multiple iterators - // Kinda like this: https://git.sr.ht/~breatheoutbreathein/ushin-db/tree/master/item/USHINBase.js#L509 - - const tx = this.db.transaction(NOTES_STORE) + async ingestActor(url) { + console.log(`Starting ingestion for actor from URL: ${url}`); + const actor = await this.getActor(url); + console.log(`Actor received:`, actor); - for await (const item of tx.store) { - yield item.value - } - } + try { + // If the actor object has an 'orderedItems' field, use that as the outbox + const outbox = actor.orderedItems + ? { orderedItems: actor.orderedItems } + : actor.outbox; - async ingestActor (url) { - const actor = await this.getActor(url) - // TODO: What else can we ingest from an actor? - // Ingest outbox as collection - const { outbox } = actor + // Check that the outbox URL or orderedItems exist + if (!outbox || (!outbox.orderedItems && !outbox.url)) { + throw new Error(`Actor's outbox is not defined: ${url}`); + } - // TODO: Use actor.id? - await this.ingestActivityCollection(outbox, url) + // If the outbox is an object with orderedItems, process it directly + if (outbox.orderedItems) { + console.log("Outbox orderedItems:", outbox.orderedItems); + for (const itemUrl of outbox.orderedItems) { + const activity = await this.#get(itemUrl); + await this.ingestActivity(activity); + } + } else { + // Otherwise, ingest the outbox by URL as originally intended + console.log("Outbox URL:", outbox.url); + await this.ingestActivityCollection(outbox.url, url); + } + } catch (error) { + console.error(`Error during outbox processing for URL ${url}:`, error); + } } - async * iterateCollection (url) { - const collection = await this.#get(url) + async *iterateCollection(url) { + console.log("Iterating collection URL:", url); // Debug URL + const collection = await this.#get(url); // TODO: handle paging and skiping - const items = collection.items || collection.orderedItems + const items = collection.items || collection.orderedItems; - if (!items) throw new Error(`Unable to find items at ${url}`) + if (!items) throw new Error(`Unable to find items at ${url}`); for await (const item of items) { - if (typeof item === 'string') { - const data = await this.#get(item) - yield data - } else yield item + if (typeof item === "string") { + const data = await this.#get(item); + yield data; + } else yield item; } } - async ingestActivityCollection (url, verifyAttributed = '') { + async ingestActivityCollection(url, verifyAttributed = "") { for await (const activity of this.iterateCollection(url)) { if (verifyAttributed && activity.actor !== verifyAttributed) { - throw new Error(`Collection contained activity not attributed to ${verifyAttributed} at ${url} in activity ${activity.id}`) + throw new Error( + `Collection contained activity not attributed to ${verifyAttributed} at ${url} in activity ${activity.id}` + ); } - await this.ingestActivity(activity) + await this.ingestActivity(activity); } } - async ingestActivity (activity) { - activity.published = new Date(activity.published) - console.log(activity) - await this.db.put(ACTIVITIES_STORE, activity) + async ingestActivity(activity) { + // Check if the activity has an 'id' and create one if it does not + if (!activity.id) { + if (typeof activity.object === "string") { + // Use the URL of the object as the id for the activity + activity.id = activity.object; + } else { + console.error( + "Activity does not have an ID and cannot be processed:", + activity + ); + return; // Skip this activity + } + } + + activity.published = new Date(activity.published); + console.log("Ingesting activity:", activity); + await this.db.put(ACTIVITIES_STORE, activity); + if (activity.type === TYPE_CREATE) { - const object = await this.#get(activity.object) + const object = await this.#get(activity.object); if (object.type === TYPE_NOTE) { - console.log(object) - await this.ingestNote(object) + console.log("Ingesting note:", object); + await this.ingestNote(object); } } else if (activity.type === TYPE_DELETE) { - await this.deleteNote(activity.object) + await this.deleteNote(activity.object); } } - async ingestNote (note) { - // Convert needed fields to date - note.published = new Date(note.published) + async ingestNote(note) { + // Convert needed fields to date + note.published = new Date(note.published); // Add tag_names field - note.tag_names = (note.tags || []).map(({ name }) => name) - this.db.put(NOTES_STORE, note) + note.tag_names = (note.tags || []).map(({ name }) => name); + this.db.put(NOTES_STORE, note); // TODO: Loop through replies } - async deleteNote (url) { + async deleteNote(url) { // delete note using the url as the `id` from the notes store - this.db.delete(NOTES_STORE, url) + this.db.delete(NOTES_STORE, url); } } -function upgrade (db) { +function upgrade(db) { const actors = db.createObjectStore(ACTORS_STORE, { - keyPath: 'id', - autoIncrement: false - }) + keyPath: "id", + autoIncrement: false, + }); - actors.createIndex(CREATED_FIELD, CREATED_FIELD) - actors.createIndex(UPDATED_FIELD, UPDATED_FIELD) - actors.createIndex(URL_FIELD, URL_FIELD) + actors.createIndex(CREATED_FIELD, CREATED_FIELD); + actors.createIndex(UPDATED_FIELD, UPDATED_FIELD); + actors.createIndex(URL_FIELD, URL_FIELD); const notes = db.createObjectStore(NOTES_STORE, { - keyPath: 'id', - autoIncrement: false - }) - - addRegularIndex(notes, TO_FIELD) - addRegularIndex(notes, URL_FIELD) - addRegularIndex(notes, TAG_NAMES_FIELD, { multiEntry: true }) - addSortedIndex(notes, IN_REPLY_TO_FIELD) - addSortedIndex(notes, ATTRIBUTED_TO_FIELD) - addSortedIndex(notes, CONVERSATION_FIELD) - addSortedIndex(notes, TO_FIELD) + keyPath: "id", + autoIncrement: false, + }); + + addRegularIndex(notes, TO_FIELD); + addRegularIndex(notes, URL_FIELD); + addRegularIndex(notes, TAG_NAMES_FIELD, { multiEntry: true }); + addSortedIndex(notes, IN_REPLY_TO_FIELD); + addSortedIndex(notes, ATTRIBUTED_TO_FIELD); + addSortedIndex(notes, CONVERSATION_FIELD); + addSortedIndex(notes, TO_FIELD); const activities = db.createObjectStore(ACTIVITIES_STORE, { - keyPath: 'id', - autoIncrement: false - }) - addSortedIndex(activities, ACTOR_FIELD) - addSortedIndex(activities, TO_FIELD) - - function addRegularIndex (store, field, options = {}) { - store.createIndex(field, field, options) + keyPath: "id", + autoIncrement: false, + }); + addSortedIndex(activities, ACTOR_FIELD); + addSortedIndex(activities, TO_FIELD); + + function addRegularIndex(store, field, options = {}) { + store.createIndex(field, field, options); } - function addSortedIndex (store, field, options = {}) { - store.createIndex(field + ', published', [field, PUBLISHED_FIELD], options) + function addSortedIndex(store, field, options = {}) { + store.createIndex(field + ", published", [field, PUBLISHED_FIELD], options); } } diff --git a/index.html b/index.html index 5bf3429..6501bae 100644 --- a/index.html +++ b/index.html @@ -1,19 +1,29 @@ -Distributed Press Reader +Social Reader -Post -Outbox -

- Work in progress, check back later or - follow us on GitHub. -

- - + +
+ + +
+ + + + + diff --git a/timeline.css b/timeline.css new file mode 100644 index 0000000..3db1128 --- /dev/null +++ b/timeline.css @@ -0,0 +1,53 @@ +body { + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + font-family: var(--rdp-font); +} + +.container { + display: flex; + justify-content: center; + align-items: flex-start; + max-width: 1200px; + width: 100%; + margin-top: 20px; +} + +.sidebar { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.sidebar h1 { + font-family: "Times New Roman", Times, serif; + font-size: 1.8em; + font-weight: normal; + margin-bottom: 0.8em; +} + +.sidebar nav { + display: flex; + flex-direction: column; +} + +.sidebar nav a { + color: var(--rdp-details-color); + text-decoration: none; + font-size: 0.875rem; +} + +.sidebar nav a:hover { + text-decoration: underline; +} + +reader-timeline { + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; +} diff --git a/timeline.js b/timeline.js new file mode 100644 index 0000000..22e8b3c --- /dev/null +++ b/timeline.js @@ -0,0 +1,43 @@ +import { ActivityPubDB } from "./db.js"; + +class ReaderTimeline extends HTMLElement { + constructor() { + super(); + // Default outbox URLs to fetch and display in the timeline + this.outboxUrls = [ + "ipns://staticpub.mauve.moe/outbox.jsonld", + "ipns://hypha.coop/outbox.jsonld", + ]; + } + + connectedCallback() { + this.initTimeline(); + } + + async initTimeline() { + const db = await ActivityPubDB.load(); + + // Clear existing content + this.innerHTML = ""; + + for (const outboxUrl of this.outboxUrls) { + try { + console.log("Ingesting actor from outbox URL:", outboxUrl); + await db.ingestActor(outboxUrl); + + const outboxElement = document.createElement("distributed-outbox"); + outboxElement.setAttribute("url", outboxUrl); + + this.appendChild(outboxElement); + } catch (error) { + console.error( + `Error ingesting actor from outbox URL ${outboxUrl}:`, + error + ); + } + } + } +} + +// Register the reader-timeline element +customElements.define("reader-timeline", ReaderTimeline); From ee7e1cf639986dd46ecac72d567feee4000c9f0b Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 15:27:51 +0530 Subject: [PATCH 037/149] style: adjust image max-width to improve post component layout --- post.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/post.css b/post.css index 88cb1ed..ab25ead 100644 --- a/post.css +++ b/post.css @@ -13,6 +13,10 @@ html { font-family: var(--rdp-font); } +img { + max-width: 100%; +} + /* Component styles */ .distributed-post { background: var(--rdp-bg-color); From 0a5fe30f94b940135369b5e4dc0a7e33f0fd39c4 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 15:33:31 +0530 Subject: [PATCH 038/149] feat: enhance sensitive content handling to support direct and URL-referenced post activities --- post.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/post.js b/post.js index d3317ba..7daa4ce 100644 --- a/post.js +++ b/post.js @@ -308,8 +308,13 @@ class DistributedPost extends HTMLElement { let contentSource = jsonLdData.content || (jsonLdData.object && jsonLdData.object.content); + // Determine if the content is marked as sensitive in either the direct jsonLdData or within jsonLdData.object + let isSensitive = + jsonLdData.sensitive || + (jsonLdData.object && jsonLdData.object.sensitive); + // Handle sensitive content - if (jsonLdData.sensitive) { + if (isSensitive) { const details = document.createElement("details"); const summary = document.createElement("summary"); summary.textContent = "Sensitive Content (click to view)"; From 158f6b09439c6d65756958227df5a3e44e579cf6 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 15:39:46 +0530 Subject: [PATCH 039/149] chore: remove html body tag --- index.html | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 6501bae..4fbfe2f 100644 --- a/index.html +++ b/index.html @@ -6,23 +6,19 @@ @import url("./outbox.css"); @import url("./post.css"); - -
- - +
+ - + +
From e9a901ffc1f86c00a7dc45a348ae3f68a008e2a4 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 15:47:10 +0530 Subject: [PATCH 040/149] refactor: getAllActors method to use async iteration --- db.js | 4 +--- timeline.js | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/db.js b/db.js index 0d6c78c..46375d3 100644 --- a/db.js +++ b/db.js @@ -98,11 +98,9 @@ export class ActivityPubDB { async getAllActors() { const tx = this.db.transaction(ACTORS_STORE); const actors = []; - let cursor = await tx.store.openCursor(); - while (cursor) { + for await (const cursor of tx.store) { actors.push(cursor.value); - cursor = await cursor.continue(); } return actors; diff --git a/timeline.js b/timeline.js index 22e8b3c..ccc7997 100644 --- a/timeline.js +++ b/timeline.js @@ -5,8 +5,8 @@ class ReaderTimeline extends HTMLElement { super(); // Default outbox URLs to fetch and display in the timeline this.outboxUrls = [ - "ipns://staticpub.mauve.moe/outbox.jsonld", - "ipns://hypha.coop/outbox.jsonld", + "https://staticpub.mauve.moe/outbox.jsonld", + "https://hypha.coop/outbox.jsonld", ]; } From abd4e3b9ab1da590567aa4cd23da03d785b03745 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 16:08:29 +0530 Subject: [PATCH 041/149] refactor: initialize DB into a shared module --- dbInstance.js | 3 +++ index.html | 2 +- timeline.js | 8 +++----- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 dbInstance.js diff --git a/dbInstance.js b/dbInstance.js new file mode 100644 index 0000000..88ff977 --- /dev/null +++ b/dbInstance.js @@ -0,0 +1,3 @@ +import { ActivityPubDB } from "./db.js"; + +export const db = await ActivityPubDB.load(); diff --git a/index.html b/index.html index 4fbfe2f..3bc451a 100644 --- a/index.html +++ b/index.html @@ -19,7 +19,7 @@

Social Reader

- + diff --git a/timeline.js b/timeline.js index ccc7997..6ec631d 100644 --- a/timeline.js +++ b/timeline.js @@ -1,12 +1,12 @@ -import { ActivityPubDB } from "./db.js"; +import { db } from "./dbInstance.js"; class ReaderTimeline extends HTMLElement { constructor() { super(); // Default outbox URLs to fetch and display in the timeline this.outboxUrls = [ - "https://staticpub.mauve.moe/outbox.jsonld", - "https://hypha.coop/outbox.jsonld", + "ipns://staticpub.mauve.moe/outbox.jsonld", + "ipns://hypha.coop/outbox.jsonld", ]; } @@ -15,8 +15,6 @@ class ReaderTimeline extends HTMLElement { } async initTimeline() { - const db = await ActivityPubDB.load(); - // Clear existing content this.innerHTML = ""; From 42b75d29abbfc9965ca042dec43ac139de4dba37 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 21:33:15 +0530 Subject: [PATCH 042/149] feat: implement searchNotes for filtering the activities --- .vscode/settings.json | 3 + db.js | 141 ++++++++++++++++++++++++++---------------- timeline.js | 34 +++++----- 3 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/db.js b/db.js index 46375d3..89ce683 100644 --- a/db.js +++ b/db.js @@ -116,62 +116,82 @@ export class ActivityPubDB { } } + async searchNotes(criteria) { + const tx = this.db.transaction(NOTES_STORE, "readonly"); + const notes = []; + const index = criteria.attributedTo + ? tx.store.index("attributedTo") + : tx.store; + + // Use async iteration to iterate over the store or index + for await (const cursor of index.iterate(criteria.attributedTo)) { + notes.push(cursor.value); + } + + // Implement additional filtering logic if needed based on other criteria (like time ranges or tags) + // For example: + // notes.filter(note => note.published >= criteria.startDate && note.published <= criteria.endDate); + return notes.sort((a, b) => b.published - a.published); // Sort by published date in descending order + } + async ingestActor(url) { console.log(`Starting ingestion for actor from URL: ${url}`); const actor = await this.getActor(url); console.log(`Actor received:`, actor); - try { - // If the actor object has an 'orderedItems' field, use that as the outbox - const outbox = actor.orderedItems - ? { orderedItems: actor.orderedItems } - : actor.outbox; - - // Check that the outbox URL or orderedItems exist - if (!outbox || (!outbox.orderedItems && !outbox.url)) { - throw new Error(`Actor's outbox is not defined: ${url}`); - } - - // If the outbox is an object with orderedItems, process it directly - if (outbox.orderedItems) { - console.log("Outbox orderedItems:", outbox.orderedItems); - for (const itemUrl of outbox.orderedItems) { - const activity = await this.#get(itemUrl); - await this.ingestActivity(activity); - } - } else { - // Otherwise, ingest the outbox by URL as originally intended - console.log("Outbox URL:", outbox.url); - await this.ingestActivityCollection(outbox.url, url); - } - } catch (error) { - console.error(`Error during outbox processing for URL ${url}:`, error); + // If actor has an 'outbox', ingest it as a collection + if (actor.outbox) { + await this.ingestActivityCollection(actor.outbox, actor.id); + } else { + console.error(`No outbox found for actor at URL ${url}`); } + + // This is where we might add more features to our actor ingestion process. + // e.g., if (actor.followers) { ... } } - async *iterateCollection(url) { - console.log("Iterating collection URL:", url); // Debug URL - const collection = await this.#get(url); - // TODO: handle paging and skiping - const items = collection.items || collection.orderedItems; - - if (!items) throw new Error(`Unable to find items at ${url}`); - for await (const item of items) { - if (typeof item === "string") { - const data = await this.#get(item); - yield data; - } else yield item; + async ingestActivityCollection(collectionOrUrl, actorId) { + let collection; + console.log( + `Fetching collection for actor ID ${actorId}:`, + collectionOrUrl + ); + + if (typeof collectionOrUrl === "string") { + // If the collection is a URL, fetch it first + collection = await this.#get(collectionOrUrl); + } else { + // If the collection is an object, use it directly + collection = collectionOrUrl; + } + + for await (const activity of this.iterateCollection(collection)) { + await this.ingestActivity(activity, actorId); } } - async ingestActivityCollection(url, verifyAttributed = "") { - for await (const activity of this.iterateCollection(url)) { - if (verifyAttributed && activity.actor !== verifyAttributed) { - throw new Error( - `Collection contained activity not attributed to ${verifyAttributed} at ${url} in activity ${activity.id}` - ); + async *iterateCollection(collection) { + // TODO: handle pagination here, if collection contains a 'next' or 'first' link. + const items = collection.orderedItems || collection.items; + + if (!items) { + console.error(`No items found in collection:`, collection); + return; // Exit if no items to iterate over + } + + for (const itemOrUrl of items) { + let activity; + if (typeof itemOrUrl === "string") { + // Fetch the individual activity if the item is a URL + activity = await this.#get(itemOrUrl); + } else { + // Use the item directly if it's an object + activity = itemOrUrl; + } + + if (activity) { + yield activity; } - await this.ingestActivity(activity); } } @@ -190,18 +210,31 @@ export class ActivityPubDB { } } + // Convert the published date to a Date object activity.published = new Date(activity.published); + + // Store the activity in the ACTIVITIES_STORE console.log("Ingesting activity:", activity); await this.db.put(ACTIVITIES_STORE, activity); - if (activity.type === TYPE_CREATE) { - const object = await this.#get(activity.object); - if (object.type === TYPE_NOTE) { - console.log("Ingesting note:", object); - await this.ingestNote(object); + // Only ingest the note if the activity is not a 'Create' activity. + // The 'Create' activity already includes the note information. + if (activity.type !== TYPE_CREATE) { + let note; + if (typeof activity.object === "string") { + note = await this.#get(activity.object); + } else { + note = activity.object; + note.isPartOfCreateActivity = true; + console.log("Ingesting note:", note); + await this.ingestNote(note); + } + + if (note.type === TYPE_NOTE) { + note.id = activity.id; // Use the Create activity's ID for the note ID + console.log("Ingesting note:", note); + await this.ingestNote(note); } - } else if (activity.type === TYPE_DELETE) { - await this.deleteNote(activity.object); } } @@ -210,7 +243,11 @@ export class ActivityPubDB { note.published = new Date(note.published); // Add tag_names field note.tag_names = (note.tags || []).map(({ name }) => name); - this.db.put(NOTES_STORE, note); + // Check if the note is already in the database to avoid duplicates + const existingNote = await this.db.get(NOTES_STORE, note.id); + if (!existingNote) { + await this.db.put(NOTES_STORE, note); + } // TODO: Loop through replies } diff --git a/timeline.js b/timeline.js index 6ec631d..e442457 100644 --- a/timeline.js +++ b/timeline.js @@ -3,10 +3,10 @@ import { db } from "./dbInstance.js"; class ReaderTimeline extends HTMLElement { constructor() { super(); - // Default outbox URLs to fetch and display in the timeline - this.outboxUrls = [ - "ipns://staticpub.mauve.moe/outbox.jsonld", - "ipns://hypha.coop/outbox.jsonld", + this.actorUrls = [ + "https://staticpub.mauve.moe/about.jsonld", + "https://hypha.coop/about.jsonld", + "https://prueba-cola-de-moderacion-2.sutty.nl/about.jsonld", ]; } @@ -15,27 +15,25 @@ class ReaderTimeline extends HTMLElement { } async initTimeline() { - // Clear existing content - this.innerHTML = ""; + this.innerHTML = ""; // Clear existing content - for (const outboxUrl of this.outboxUrls) { + for (const actorUrl of this.actorUrls) { try { - console.log("Ingesting actor from outbox URL:", outboxUrl); - await db.ingestActor(outboxUrl); + console.log("Loading actor:", actorUrl); + const actorData = await db.ingestActor(actorUrl); + const notes = await db.searchNotes({ attributedTo: actorData }); - const outboxElement = document.createElement("distributed-outbox"); - outboxElement.setAttribute("url", outboxUrl); - - this.appendChild(outboxElement); + notes.forEach((note) => { + console.log(note.id); + const activityElement = document.createElement("distributed-post"); + activityElement.setAttribute("url", note.id); + this.appendChild(activityElement); + }); } catch (error) { - console.error( - `Error ingesting actor from outbox URL ${outboxUrl}:`, - error - ); + console.error(`Error loading actor ${actorUrl}:`, error); } } } } -// Register the reader-timeline element customElements.define("reader-timeline", ReaderTimeline); From 5b9da6cf32aeea0e7de3828ad44ffa55ce95446a Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 21:40:46 +0530 Subject: [PATCH 043/149] feat: add support for UPDATE activity type --- db.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/db.js b/db.js index 89ce683..1d111af 100644 --- a/db.js +++ b/db.js @@ -21,6 +21,7 @@ export const ACTOR_FIELD = "actor"; export const PUBLISHED_SUFFIX = ", published"; export const TYPE_CREATE = "Create"; +export const TYPE_UPDATE = "Update"; export const TYPE_NOTE = "Note"; export const TYPE_DELETE = "Delete"; @@ -217,24 +218,39 @@ export class ActivityPubDB { console.log("Ingesting activity:", activity); await this.db.put(ACTIVITIES_STORE, activity); - // Only ingest the note if the activity is not a 'Create' activity. - // The 'Create' activity already includes the note information. - if (activity.type !== TYPE_CREATE) { + if (activity.type !== TYPE_CREATE || activity.type !== TYPE_UPDATE) { let note; if (typeof activity.object === "string") { note = await this.#get(activity.object); } else { note = activity.object; - note.isPartOfCreateActivity = true; - console.log("Ingesting note:", note); - await this.ingestNote(note); } if (note.type === TYPE_NOTE) { - note.id = activity.id; // Use the Create activity's ID for the note ID + // If it's an update, retain the original note ID, else use the activity's ID + if (activity.type === TYPE_UPDATE) { + // Attempt to retrieve the existing note first to maintain its ID + const existingNote = await this.getNote(note.id); + if (existingNote) { + // Update the existing note with the new content or fields + note = { ...existingNote, ...note }; + } else { + console.warn( + "Update activity received for a non-existent note:", + note.id + ); + } + } else { + // This is a create activity, use the activity's ID for the note ID + note.id = activity.id; + } + console.log("Ingesting note:", note); await this.ingestNote(note); } + } else if (activity.type === TYPE_DELETE) { + // Handle 'Delete' activity type + await this.deleteNote(activity.object); } } From 099439a8563d37ebf0c1bd92e74ed9a15d03cfd6 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Thu, 14 Mar 2024 21:42:17 +0530 Subject: [PATCH 044/149] style: run linter --- .vscode/settings.json | 3 - db.js | 304 +++++++++++++++++++++--------------------- 2 files changed, 152 insertions(+), 155 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6f3a291..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "liveServer.settings.port": 5501 -} \ No newline at end of file diff --git a/db.js b/db.js index 1d111af..7759525 100644 --- a/db.js +++ b/db.js @@ -1,312 +1,312 @@ -import { openDB } from "./dependencies/idb/index.js"; - -export const DEFAULT_DB = "default"; -export const ACTORS_STORE = "actors"; -export const NOTES_STORE = "notes"; -export const ACTIVITIES_STORE = "activities"; - -export const ID_FIELD = "id"; -export const URL_FIELD = "url"; -export const CREATED_FIELD = "created"; -export const UPDATED_FIELD = "updated"; -export const PUBLISHED_FIELD = "published"; -export const TO_FIELD = "to"; -export const CC_FIELD = "cc"; -export const IN_REPLY_TO_FIELD = "inReplyTo"; -export const TAG_NAMES_FIELD = "tag_names"; -export const ATTRIBUTED_TO_FIELD = "attributedTo"; -export const CONVERSATION_FIELD = "conversation"; -export const ACTOR_FIELD = "actor"; - -export const PUBLISHED_SUFFIX = ", published"; - -export const TYPE_CREATE = "Create"; -export const TYPE_UPDATE = "Update"; -export const TYPE_NOTE = "Note"; -export const TYPE_DELETE = "Delete"; +import { openDB } from './dependencies/idb/index.js' + +export const DEFAULT_DB = 'default' +export const ACTORS_STORE = 'actors' +export const NOTES_STORE = 'notes' +export const ACTIVITIES_STORE = 'activities' + +export const ID_FIELD = 'id' +export const URL_FIELD = 'url' +export const CREATED_FIELD = 'created' +export const UPDATED_FIELD = 'updated' +export const PUBLISHED_FIELD = 'published' +export const TO_FIELD = 'to' +export const CC_FIELD = 'cc' +export const IN_REPLY_TO_FIELD = 'inReplyTo' +export const TAG_NAMES_FIELD = 'tag_names' +export const ATTRIBUTED_TO_FIELD = 'attributedTo' +export const CONVERSATION_FIELD = 'conversation' +export const ACTOR_FIELD = 'actor' + +export const PUBLISHED_SUFFIX = ', published' + +export const TYPE_CREATE = 'Create' +export const TYPE_UPDATE = 'Update' +export const TYPE_NOTE = 'Note' +export const TYPE_DELETE = 'Delete' // TODO: When ingesting notes and actors, wrap any dates in `new Date()` // TODO: When ingesting notes add a "tag_names" field which is just the names of the tag // TODO: When ingesting notes, also load their replies export class ActivityPubDB { - constructor(db, fetch = globalThis.fetch) { - this.db = db; - this.fetch = fetch; + constructor (db, fetch = globalThis.fetch) { + this.db = db + this.fetch = fetch } - static async load(name = DEFAULT_DB, fetch = globalThis.fetch) { + static async load (name = DEFAULT_DB, fetch = globalThis.fetch) { const db = await openDB(name, 1, { - upgrade, - }); + upgrade + }) - return new ActivityPubDB(db, fetch); + return new ActivityPubDB(db, fetch) } - async #get(url) { - if (url && typeof url === "object") { - return url; + async #get (url) { + if (url && typeof url === 'object') { + return url } - let response; + let response // Try fetching directly for all URLs (including P2P URLs) // TODO: Signed fetch try { response = await this.fetch.call(globalThis, url, { headers: { - Accept: "application/json", - }, - }); + Accept: 'application/json' + } + }) } catch (error) { - console.error("P2P loading failed, trying HTTP gateway:", error); + console.error('P2P loading failed, trying HTTP gateway:', error) } // If direct fetch was not successful, attempt fetching from a gateway for P2P protocols if (!response || !response.ok) { - let gatewayUrl = url; + let gatewayUrl = url - if (url.startsWith("hyper://")) { - gatewayUrl = url.replace("hyper://", "https://hyper.hypha.coop/hyper/"); - } else if (url.startsWith("ipns://")) { - gatewayUrl = url.replace("ipns://", "https://ipfs.hypha.coop/ipns/"); + if (url.startsWith('hyper://')) { + gatewayUrl = url.replace('hyper://', 'https://hyper.hypha.coop/hyper/') + } else if (url.startsWith('ipns://')) { + gatewayUrl = url.replace('ipns://', 'https://ipfs.hypha.coop/ipns/') } try { response = await this.fetch.call(globalThis, gatewayUrl, { headers: { - Accept: "application/json", - }, - }); + Accept: 'application/json' + } + }) } catch (error) { - console.error("Fetching from gateway failed:", error); - throw new Error(`Failed to fetch ${url} from gateway`); + console.error('Fetching from gateway failed:', error) + throw new Error(`Failed to fetch ${url} from gateway`) } } if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); + throw new Error(`HTTP error! Status: ${response.status}`) } - return await response.json(); + return await response.json() } - async getActor(url) { + async getActor (url) { // TODO: Try to load from cache - const actor = await this.#get(url); - this.db.put(ACTORS_STORE, actor); - return this.db.get(ACTORS_STORE, actor.id); + const actor = await this.#get(url) + this.db.put(ACTORS_STORE, actor) + return this.db.get(ACTORS_STORE, actor.id) } - async getAllActors() { - const tx = this.db.transaction(ACTORS_STORE); - const actors = []; + async getAllActors () { + const tx = this.db.transaction(ACTORS_STORE) + const actors = [] for await (const cursor of tx.store) { - actors.push(cursor.value); + actors.push(cursor.value) } - return actors; + return actors } - async getNote(url) { + async getNote (url) { try { - return this.db.get(NOTES_STORE, url); + return this.db.get(NOTES_STORE, url) } catch { - const note = await this.#get(url); - await this.ingestNote(note); - return note; + const note = await this.#get(url) + await this.ingestNote(note) + return note } } - async searchNotes(criteria) { - const tx = this.db.transaction(NOTES_STORE, "readonly"); - const notes = []; + async searchNotes (criteria) { + const tx = this.db.transaction(NOTES_STORE, 'readonly') + const notes = [] const index = criteria.attributedTo - ? tx.store.index("attributedTo") - : tx.store; + ? tx.store.index('attributedTo') + : tx.store // Use async iteration to iterate over the store or index for await (const cursor of index.iterate(criteria.attributedTo)) { - notes.push(cursor.value); + notes.push(cursor.value) } // Implement additional filtering logic if needed based on other criteria (like time ranges or tags) // For example: // notes.filter(note => note.published >= criteria.startDate && note.published <= criteria.endDate); - return notes.sort((a, b) => b.published - a.published); // Sort by published date in descending order + return notes.sort((a, b) => b.published - a.published) // Sort by published date in descending order } - async ingestActor(url) { - console.log(`Starting ingestion for actor from URL: ${url}`); - const actor = await this.getActor(url); - console.log(`Actor received:`, actor); + async ingestActor (url) { + console.log(`Starting ingestion for actor from URL: ${url}`) + const actor = await this.getActor(url) + console.log('Actor received:', actor) // If actor has an 'outbox', ingest it as a collection if (actor.outbox) { - await this.ingestActivityCollection(actor.outbox, actor.id); + await this.ingestActivityCollection(actor.outbox, actor.id) } else { - console.error(`No outbox found for actor at URL ${url}`); + console.error(`No outbox found for actor at URL ${url}`) } // This is where we might add more features to our actor ingestion process. // e.g., if (actor.followers) { ... } } - async ingestActivityCollection(collectionOrUrl, actorId) { - let collection; + async ingestActivityCollection (collectionOrUrl, actorId) { + let collection console.log( `Fetching collection for actor ID ${actorId}:`, collectionOrUrl - ); + ) - if (typeof collectionOrUrl === "string") { + if (typeof collectionOrUrl === 'string') { // If the collection is a URL, fetch it first - collection = await this.#get(collectionOrUrl); + collection = await this.#get(collectionOrUrl) } else { // If the collection is an object, use it directly - collection = collectionOrUrl; + collection = collectionOrUrl } for await (const activity of this.iterateCollection(collection)) { - await this.ingestActivity(activity, actorId); + await this.ingestActivity(activity, actorId) } } - async *iterateCollection(collection) { + async * iterateCollection (collection) { // TODO: handle pagination here, if collection contains a 'next' or 'first' link. - const items = collection.orderedItems || collection.items; + const items = collection.orderedItems || collection.items if (!items) { - console.error(`No items found in collection:`, collection); - return; // Exit if no items to iterate over + console.error('No items found in collection:', collection) + return // Exit if no items to iterate over } for (const itemOrUrl of items) { - let activity; - if (typeof itemOrUrl === "string") { + let activity + if (typeof itemOrUrl === 'string') { // Fetch the individual activity if the item is a URL - activity = await this.#get(itemOrUrl); + activity = await this.#get(itemOrUrl) } else { // Use the item directly if it's an object - activity = itemOrUrl; + activity = itemOrUrl } if (activity) { - yield activity; + yield activity } } } - async ingestActivity(activity) { + async ingestActivity (activity) { // Check if the activity has an 'id' and create one if it does not if (!activity.id) { - if (typeof activity.object === "string") { + if (typeof activity.object === 'string') { // Use the URL of the object as the id for the activity - activity.id = activity.object; + activity.id = activity.object } else { console.error( - "Activity does not have an ID and cannot be processed:", + 'Activity does not have an ID and cannot be processed:', activity - ); - return; // Skip this activity + ) + return // Skip this activity } } // Convert the published date to a Date object - activity.published = new Date(activity.published); + activity.published = new Date(activity.published) // Store the activity in the ACTIVITIES_STORE - console.log("Ingesting activity:", activity); - await this.db.put(ACTIVITIES_STORE, activity); + console.log('Ingesting activity:', activity) + await this.db.put(ACTIVITIES_STORE, activity) if (activity.type !== TYPE_CREATE || activity.type !== TYPE_UPDATE) { - let note; - if (typeof activity.object === "string") { - note = await this.#get(activity.object); + let note + if (typeof activity.object === 'string') { + note = await this.#get(activity.object) } else { - note = activity.object; + note = activity.object } if (note.type === TYPE_NOTE) { // If it's an update, retain the original note ID, else use the activity's ID if (activity.type === TYPE_UPDATE) { // Attempt to retrieve the existing note first to maintain its ID - const existingNote = await this.getNote(note.id); + const existingNote = await this.getNote(note.id) if (existingNote) { // Update the existing note with the new content or fields - note = { ...existingNote, ...note }; + note = { ...existingNote, ...note } } else { console.warn( - "Update activity received for a non-existent note:", + 'Update activity received for a non-existent note:', note.id - ); + ) } } else { // This is a create activity, use the activity's ID for the note ID - note.id = activity.id; + note.id = activity.id } - console.log("Ingesting note:", note); - await this.ingestNote(note); + console.log('Ingesting note:', note) + await this.ingestNote(note) } } else if (activity.type === TYPE_DELETE) { // Handle 'Delete' activity type - await this.deleteNote(activity.object); + await this.deleteNote(activity.object) } } - async ingestNote(note) { + async ingestNote (note) { // Convert needed fields to date - note.published = new Date(note.published); + note.published = new Date(note.published) // Add tag_names field - note.tag_names = (note.tags || []).map(({ name }) => name); + note.tag_names = (note.tags || []).map(({ name }) => name) // Check if the note is already in the database to avoid duplicates - const existingNote = await this.db.get(NOTES_STORE, note.id); + const existingNote = await this.db.get(NOTES_STORE, note.id) if (!existingNote) { - await this.db.put(NOTES_STORE, note); + await this.db.put(NOTES_STORE, note) } // TODO: Loop through replies } - async deleteNote(url) { + async deleteNote (url) { // delete note using the url as the `id` from the notes store - this.db.delete(NOTES_STORE, url); + this.db.delete(NOTES_STORE, url) } } -function upgrade(db) { +function upgrade (db) { const actors = db.createObjectStore(ACTORS_STORE, { - keyPath: "id", - autoIncrement: false, - }); + keyPath: 'id', + autoIncrement: false + }) - actors.createIndex(CREATED_FIELD, CREATED_FIELD); - actors.createIndex(UPDATED_FIELD, UPDATED_FIELD); - actors.createIndex(URL_FIELD, URL_FIELD); + actors.createIndex(CREATED_FIELD, CREATED_FIELD) + actors.createIndex(UPDATED_FIELD, UPDATED_FIELD) + actors.createIndex(URL_FIELD, URL_FIELD) const notes = db.createObjectStore(NOTES_STORE, { - keyPath: "id", - autoIncrement: false, - }); - - addRegularIndex(notes, TO_FIELD); - addRegularIndex(notes, URL_FIELD); - addRegularIndex(notes, TAG_NAMES_FIELD, { multiEntry: true }); - addSortedIndex(notes, IN_REPLY_TO_FIELD); - addSortedIndex(notes, ATTRIBUTED_TO_FIELD); - addSortedIndex(notes, CONVERSATION_FIELD); - addSortedIndex(notes, TO_FIELD); + keyPath: 'id', + autoIncrement: false + }) + + addRegularIndex(notes, TO_FIELD) + addRegularIndex(notes, URL_FIELD) + addRegularIndex(notes, TAG_NAMES_FIELD, { multiEntry: true }) + addSortedIndex(notes, IN_REPLY_TO_FIELD) + addSortedIndex(notes, ATTRIBUTED_TO_FIELD) + addSortedIndex(notes, CONVERSATION_FIELD) + addSortedIndex(notes, TO_FIELD) const activities = db.createObjectStore(ACTIVITIES_STORE, { - keyPath: "id", - autoIncrement: false, - }); - addSortedIndex(activities, ACTOR_FIELD); - addSortedIndex(activities, TO_FIELD); - - function addRegularIndex(store, field, options = {}) { - store.createIndex(field, field, options); + keyPath: 'id', + autoIncrement: false + }) + addSortedIndex(activities, ACTOR_FIELD) + addSortedIndex(activities, TO_FIELD) + + function addRegularIndex (store, field, options = {}) { + store.createIndex(field, field, options) } - function addSortedIndex(store, field, options = {}) { - store.createIndex(field + ", published", [field, PUBLISHED_FIELD], options); + function addSortedIndex (store, field, options = {}) { + store.createIndex(field + ', published', [field, PUBLISHED_FIELD], options) } } From b39fa9ee2cdafb6c61d2c2c3f7434bbd5bad5d2a Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 02:09:53 +0530 Subject: [PATCH 045/149] refactor: note ID handling for Create activities only --- db.js | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/db.js b/db.js index 7759525..510ea38 100644 --- a/db.js +++ b/db.js @@ -225,28 +225,10 @@ export class ActivityPubDB { } else { note = activity.object } - if (note.type === TYPE_NOTE) { - // If it's an update, retain the original note ID, else use the activity's ID - if (activity.type === TYPE_UPDATE) { - // Attempt to retrieve the existing note first to maintain its ID - const existingNote = await this.getNote(note.id) - if (existingNote) { - // Update the existing note with the new content or fields - note = { ...existingNote, ...note } - } else { - console.warn( - 'Update activity received for a non-existent note:', - note.id - ) - } - } else { - // This is a create activity, use the activity's ID for the note ID - note.id = activity.id - } - - console.log('Ingesting note:', note) - await this.ingestNote(note) + note.id = activity.id; // Use the Create activity's ID for the note ID + console.log("Ingesting note:", note); + await this.ingestNote(note); } } else if (activity.type === TYPE_DELETE) { // Handle 'Delete' activity type From 3ea8344775db5d40f45e199abaff1ade4f67b3dd Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 02:14:22 +0530 Subject: [PATCH 046/149] feat: keep track of already processed notes --- timeline.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/timeline.js b/timeline.js index e442457..dea7a75 100644 --- a/timeline.js +++ b/timeline.js @@ -8,6 +8,7 @@ class ReaderTimeline extends HTMLElement { "https://hypha.coop/about.jsonld", "https://prueba-cola-de-moderacion-2.sutty.nl/about.jsonld", ]; + this.processedNotes = new Set(); // To keep track of notes already processed } connectedCallback() { @@ -24,10 +25,13 @@ class ReaderTimeline extends HTMLElement { const notes = await db.searchNotes({ attributedTo: actorData }); notes.forEach((note) => { - console.log(note.id); - const activityElement = document.createElement("distributed-post"); - activityElement.setAttribute("url", note.id); - this.appendChild(activityElement); + if (!this.processedNotes.has(note.id)) { + console.log(note.id); + const activityElement = document.createElement("distributed-post"); + activityElement.setAttribute("url", note.id); + this.appendChild(activityElement); + this.processedNotes.add(note.id); // Mark this note as processed + } }); } catch (error) { console.error(`Error loading actor ${actorUrl}:`, error); From dc271202b11efcb506621718ece5b2159441bbf7 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Thu, 14 Mar 2024 16:54:15 -0400 Subject: [PATCH 047/149] Add publish workflow --- .github/workflows/publish.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..dc85c12 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + # TODO: Uncomment once released + # branches: [ main ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + - name: Publish to Distributed Press + uses: hyphacoop/actions-distributed-press@v1.1.0 + with: + publish_dir: ./ + dp_url: https://api.distributed.press + refresh_token: ${{ secrets.DISTRIBUTED_PRESS_TOKEN }} + site_url: reader.distributed.press From db553079d2669445c5602624ea74a700f5ead3ad Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 03:31:56 +0530 Subject: [PATCH 048/149] refactor: collection processing to simplify URL handling --- db.js | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/db.js b/db.js index 510ea38..f47c89d 100644 --- a/db.js +++ b/db.js @@ -152,19 +152,12 @@ export class ActivityPubDB { } async ingestActivityCollection (collectionOrUrl, actorId) { - let collection console.log( `Fetching collection for actor ID ${actorId}:`, collectionOrUrl ) - if (typeof collectionOrUrl === 'string') { - // If the collection is a URL, fetch it first - collection = await this.#get(collectionOrUrl) - } else { - // If the collection is an object, use it directly - collection = collectionOrUrl - } + const collection = await this.#get(collectionOrUrl) for await (const activity of this.iterateCollection(collection)) { await this.ingestActivity(activity, actorId) @@ -181,15 +174,8 @@ export class ActivityPubDB { } for (const itemOrUrl of items) { - let activity - if (typeof itemOrUrl === 'string') { - // Fetch the individual activity if the item is a URL - activity = await this.#get(itemOrUrl) - } else { - // Use the item directly if it's an object - activity = itemOrUrl - } - + const activity = await this.#get(itemOrUrl) + if (activity) { yield activity } From c2e75a5224a92eab548a44df33784e03f4e81241 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 03:40:53 +0530 Subject: [PATCH 049/149] feat: enhance note updating logic to consider publish time --- db.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/db.js b/db.js index f47c89d..b1e1db2 100644 --- a/db.js +++ b/db.js @@ -222,18 +222,25 @@ export class ActivityPubDB { } } - async ingestNote (note) { + async ingestNote(note) { // Convert needed fields to date - note.published = new Date(note.published) + note.published = new Date(note.published); // Add tag_names field - note.tag_names = (note.tags || []).map(({ name }) => name) - // Check if the note is already in the database to avoid duplicates - const existingNote = await this.db.get(NOTES_STORE, note.id) - if (!existingNote) { - await this.db.put(NOTES_STORE, note) + note.tag_names = (note.tags || []).map(({ name }) => name); + // Try to retrieve an existing note from the database + const existingNote = await this.db.get(NOTES_STORE, note.id); + // If there's an existing note and the incoming note is newer, update it + if (existingNote && new Date(note.published) > new Date(existingNote.published)) { + console.log(`Updating note with newer version: ${note.id}`); + await this.db.put(NOTES_STORE, note); + } else if (!existingNote) { + // If no existing note, just add the new note + console.log(`Adding new note: ${note.id}`); + await this.db.put(NOTES_STORE, note); } + // If the existing note is newer, do not replace it // TODO: Loop through replies - } + } async deleteNote (url) { // delete note using the url as the `id` from the notes store From 5dff3ea14d41c31bfff30072726b955d620f50fc Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 04:26:46 +0530 Subject: [PATCH 050/149] refactor: initialize timeline to ingest actors first and then search for all notes globally --- timeline.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/timeline.js b/timeline.js index dea7a75..cb36235 100644 --- a/timeline.js +++ b/timeline.js @@ -21,22 +21,30 @@ class ReaderTimeline extends HTMLElement { for (const actorUrl of this.actorUrls) { try { console.log("Loading actor:", actorUrl); - const actorData = await db.ingestActor(actorUrl); - const notes = await db.searchNotes({ attributedTo: actorData }); - - notes.forEach((note) => { - if (!this.processedNotes.has(note.id)) { - console.log(note.id); - const activityElement = document.createElement("distributed-post"); - activityElement.setAttribute("url", note.id); - this.appendChild(activityElement); - this.processedNotes.add(note.id); // Mark this note as processed - } - }); + await db.ingestActor(actorUrl); } catch (error) { console.error(`Error loading actor ${actorUrl}:`, error); } } + + // After ingesting all actors, search for all notes once + try { + const allNotes = await db.searchNotes({}); + // Sort all notes by published date in descending order + allNotes.sort((a, b) => new Date(b.published) - new Date(a.published)); + + // Create and append elements for each note + allNotes.forEach((note) => { + if (!this.processedNotes.has(note.id)) { + const activityElement = document.createElement("distributed-post"); + activityElement.setAttribute("url", note.id); + this.appendChild(activityElement); + this.processedNotes.add(note.id); // Mark this note as processed + } + }); + } catch (error) { + console.error(`Error retrieving notes:`, error); + } } } From d2d8c7a34aa713f8ba613edf7a9ad3bd3476d3c6 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 16:23:18 +0530 Subject: [PATCH 051/149] style: apply text wrapping to prevent overflow in post component --- post.css | 1 + 1 file changed, 1 insertion(+) diff --git a/post.css b/post.css index ab25ead..9a8b1f2 100644 --- a/post.css +++ b/post.css @@ -72,6 +72,7 @@ img { color: var(--rdp-text-color); padding: 6px; margin-bottom: 16px; + overflow-wrap: break-word; } .post-footer { From f0a4ac7d38690c34b8e1a89361eab6329e371114 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 16:38:11 +0530 Subject: [PATCH 052/149] chore: remove initial style.css --- index.css | 15 +++++++++++++++ style.css | 29 ----------------------------- 2 files changed, 15 insertions(+), 29 deletions(-) delete mode 100644 style.css diff --git a/index.css b/index.css index 02b5f3a..983cbda 100644 --- a/index.css +++ b/index.css @@ -7,3 +7,18 @@ html { background: var(--bg-color); } + +@media (max-width: 600px) { + .container { + flex-direction: column-reverse; + } + + .sidebar { + width: 100%; + text-align: center; /* Optional: Center the navigation links if desired */ + } + + reader-timeline { + width: 100%; + } +} diff --git a/style.css b/style.css deleted file mode 100644 index 51adda5..0000000 --- a/style.css +++ /dev/null @@ -1,29 +0,0 @@ -/* -Vars - -Any colors, sizings, etc should be set as vars and reused -*/ - -@import url("agregore://theme/style.css"); - -:root { - --rdp-font: "system-ui" -} - -/* -Main styles - -These should set the default look and feel of the app. -Stuff like font color/size, backgrounds, button/paragraph styles. -*/ - -html { - font-family: var(--rdp-font); -} - - -/* -Component styles -*/ - -// TODO From fb21497ae9acad44840f37e3a8ae458591dcb14f Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 16:38:53 +0530 Subject: [PATCH 053/149] chore: remove initial style.css --- index.css | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/index.css b/index.css index 983cbda..02b5f3a 100644 --- a/index.css +++ b/index.css @@ -7,18 +7,3 @@ html { background: var(--bg-color); } - -@media (max-width: 600px) { - .container { - flex-direction: column-reverse; - } - - .sidebar { - width: 100%; - text-align: center; /* Optional: Center the navigation links if desired */ - } - - reader-timeline { - width: 100%; - } -} From 1c5fd4ebd57479ccbd323281b3529f07da541d65 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 15 Mar 2024 20:24:45 +0530 Subject: [PATCH 054/149] style: improve post responsiveness and reduce font size on smaller screens --- index.html | 2 ++ post.css | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/index.html b/index.html index 3bc451a..bb7b9e8 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,6 @@ + + Social Reader +
+

+ You're following 0 accounts.
Import and export followed list coming soon.. +

+
+
+ + diff --git a/followed-accounts.js b/followed-accounts.js new file mode 100644 index 0000000..bc1386e --- /dev/null +++ b/followed-accounts.js @@ -0,0 +1,56 @@ +import { db } from "./dbInstance.js"; + +function formatDate(dateString) { + const options = { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + }; + const date = new Date(dateString); + return date.toLocaleDateString("en-US", options); +} + +async function displayFollowedActors() { + const followedListElement = document.getElementById("followedList"); + const followedActors = await db.getFollowedActors(); + console.log(followedActors); + + followedActors.forEach((actor) => { + const actorElement = document.createElement("div"); + const formattedDate = formatDate(actor.followedAt); + actorElement.textContent = `- Followed URL: ${actor.url} - Followed At: ${formattedDate}`; + followedListElement.appendChild(actorElement); + }); +} +displayFollowedActors(); + +export async function updateFollowCount() { + const followCountElement = document.getElementById("followCount"); + const followedActors = await db.getFollowedActors(); + followCountElement.textContent = followedActors.length; +} + +// test following/unfollowing +// (async () => { +// const actorUrl1 = "https://example.com/actor/1"; +// const actorUrl2 = "https://example.com/actor/2"; + +// console.log("Following actors..."); +// await db.followActor(actorUrl1); +// await db.followActor(actorUrl2); + +// console.log("Retrieving followed actors..."); +// let followedActors = await db.getFollowedActors(); +// console.log("Followed Actors:", followedActors); + +// console.log("Unfollowing an actor..."); +// await db.unfollowActor(actorUrl2); + +// console.log("Retrieving followed actors after unfollowing..."); +// followedActors = await db.getFollowedActors(); +// console.log("Followed Actors after unfollowing:", followedActors); +// })(); diff --git a/index.html b/index.html index bb7b9e8..047bf94 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,11 @@ + + diff --git a/post.css b/post.css index e0d9114..4d32638 100644 --- a/post.css +++ b/post.css @@ -45,6 +45,7 @@ img { border-radius: 50%; background-color: #000000; margin-right: 8px; + cursor: pointer; } .actor-details { @@ -55,6 +56,7 @@ img { .actor-name { color: var(--rdp-text-color); font-weight: bold; + cursor: pointer; } .actor-username { diff --git a/post.js b/post.js index 7daa4ce..fec52ef 100644 --- a/post.js +++ b/post.js @@ -148,7 +148,7 @@ async function loadPostFromHyper(hyperUrl) { } } -async function fetchActorInfo(actorUrl) { +export async function fetchActorInfo(actorUrl) { try { const response = await fetch(actorUrl); if (!response.ok) { @@ -388,12 +388,22 @@ class ActorInfo extends HTMLElement { 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 fetchActorInfo(url); @@ -421,6 +431,7 @@ class ActorInfo extends HTMLElement { img.classList.add("actor-icon"); img.src = iconUrl; img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + img.addEventListener('click', this.navigateToActorProfile.bind(this)); author.appendChild(img); } } @@ -429,6 +440,7 @@ class ActorInfo extends HTMLElement { const pName = document.createElement("div"); pName.classList.add("actor-name"); pName.textContent = actorInfo.name; + pName.addEventListener('click', this.navigateToActorProfile.bind(this)); authorDetails.appendChild(pName); } diff --git a/profile.html b/profile.html new file mode 100644 index 0000000..fb01839 --- /dev/null +++ b/profile.html @@ -0,0 +1,41 @@ + + + +User Profile + + + +
+ + +
+ + + + + diff --git a/timeline.css b/timeline.css index 69938c4..c7ed68a 100644 --- a/timeline.css +++ b/timeline.css @@ -30,6 +30,21 @@ body { margin-bottom: 0.6em; } +.controls { + margin-bottom: 0.4em; +} + +.controls a { + color: var(--rdp-details-color); + text-decoration: none; + font-size: 0.875rem; + font-weight: bold; + margin-bottom: 0.4em; +} +.controls a:hover { + text-decoration: underline; +} + .sidebar nav { display: flex; flex-direction: column; diff --git a/timeline.js b/timeline.js index cb36235..58f0523 100644 --- a/timeline.js +++ b/timeline.js @@ -3,22 +3,39 @@ import { db } from "./dbInstance.js"; class ReaderTimeline extends HTMLElement { constructor() { super(); - this.actorUrls = [ - "https://staticpub.mauve.moe/about.jsonld", - "https://hypha.coop/about.jsonld", - "https://prueba-cola-de-moderacion-2.sutty.nl/about.jsonld", - ]; - this.processedNotes = new Set(); // To keep track of notes already processed + this.processedNotes = new Set(); // To keep track of already processed notes } connectedCallback() { - this.initTimeline(); + this.initializeDefaultFollowedActors().then(() => this.initTimeline()); + } + + async initializeDefaultFollowedActors() { + const defaultActors = [ + "https://social.dp.chanterelle.xyz/v1/@announcements@social.dp.chanterelle.xyz/", + "https://hypha.coop/about.jsonld", + "https://sutty.nl/about.jsonld", + // "https://akhilesh.sutty.nl/about.jsonld", + // "https://staticpub.mauve.moe/about.jsonld", + ]; + + // Check if followed actors have already been initialized + const hasFollowedActors = await db.hasFollowedActors(); + if (!hasFollowedActors) { + for (const actorUrl of defaultActors) { + await db.followActor(actorUrl); + } + } } async initTimeline() { this.innerHTML = ""; // Clear existing content - for (const actorUrl of this.actorUrls) { + // Dynamically load followed actors + const followedActors = await db.getFollowedActors(); + const actorUrls = followedActors.map(actor => actor.url); + + for (const actorUrl of actorUrls) { try { console.log("Loading actor:", actorUrl); await db.ingestActor(actorUrl); @@ -30,10 +47,8 @@ class ReaderTimeline extends HTMLElement { // After ingesting all actors, search for all notes once try { const allNotes = await db.searchNotes({}); - // Sort all notes by published date in descending order allNotes.sort((a, b) => new Date(b.published) - new Date(a.published)); - // Create and append elements for each note allNotes.forEach((note) => { if (!this.processedNotes.has(note.id)) { const activityElement = document.createElement("distributed-post"); From f7bb914894033838ea07c93f7c6638c5cc58cf17 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 04:54:15 +0530 Subject: [PATCH 057/149] feat: add default profile image for actors without a profile icon --- actor-profile.js | 16 +++++++--------- assets/profile.png | Bin 0 -> 27663 bytes post.js | 18 ++++++++---------- 3 files changed, 15 insertions(+), 19 deletions(-) create mode 100644 assets/profile.png diff --git a/actor-profile.js b/actor-profile.js index 9e3d164..6491cc0 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -40,22 +40,20 @@ class ActorProfile extends HTMLElement { actorContainer.classList.add("actor-container"); // Handle both single icon object and array of icons - let iconUrl = null; + 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; } else if (actorInfo.icon.url) { iconUrl = actorInfo.icon.url; } - - if (iconUrl) { - const img = document.createElement("img"); - img.classList.add("actor-icon"); - img.src = iconUrl; - img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; - actorContainer.appendChild(img); // Append to the actor container - } } + + const img = document.createElement("img"); + img.classList.add("actor-icon"); + img.src = iconUrl; + img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + actorContainer.appendChild(img); // Append to the actor container if (actorInfo.name) { const pName = document.createElement("div"); diff --git a/assets/profile.png b/assets/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..6dfa7b623eb3a4d0499abb23fcb890414ab5a8af GIT binary patch literal 27663 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 0) { iconUrl = actorInfo.icon[0].url; } else if (actorInfo.icon.url) { iconUrl = actorInfo.icon.url; } - - if (iconUrl) { - const img = document.createElement("img"); - img.classList.add("actor-icon"); - img.src = iconUrl; - img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; - img.addEventListener('click', this.navigateToActorProfile.bind(this)); - author.appendChild(img); - } } + + const img = document.createElement("img"); + img.classList.add("actor-icon"); + img.src = iconUrl; + img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; + img.addEventListener('click', this.navigateToActorProfile.bind(this)); + author.appendChild(img); if (actorInfo.name) { const pName = document.createElement("div"); From e06f2503f4a7593c7296cbdc742c89fdde97dce4 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Tue, 19 Mar 2024 11:42:27 -0400 Subject: [PATCH 058/149] Use db to load actors --- actor-profile.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actor-profile.js b/actor-profile.js index 6491cc0..cea8a89 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -1,5 +1,4 @@ import { db } from "./dbInstance.js"; -import { fetchActorInfo } from "./post.js"; class ActorProfile extends HTMLElement { static get observedAttributes() { @@ -17,7 +16,7 @@ class ActorProfile extends HTMLElement { } async fetchAndRenderActorProfile(url) { - const actorInfo = await fetchActorInfo(url); + const actorInfo = await db.getActor(url); console.log(actorInfo); if (actorInfo) { this.renderActorProfile(actorInfo); From 1f8fcd9311e69054686ba8c5206cffc3f08cf2ce Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 22:30:19 +0530 Subject: [PATCH 059/149] refactor: actor-profile to include distributed-outbox as a child component and rename classes --- actor-profile.css | 10 +++++----- actor-profile.js | 25 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/actor-profile.css b/actor-profile.css index cd5dabf..bf81dd5 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -1,9 +1,9 @@ -.actor-profile { +.profile { text-align: center; margin-top: 20px; } -.actor-container { +.profile-container { margin-bottom: 10px; } @@ -14,7 +14,7 @@ margin-bottom: 16px; } -.actor-icon { +.profile-icon { width: 50px; height: 50px; border-radius: 50%; @@ -23,12 +23,12 @@ margin-bottom: 8px; } -.actor-details { +.profile-details { display: flex; flex-direction: column; } -.actor-name { +.profile-name { color: var(--rdp-text-color); font-weight: bold; } diff --git a/actor-profile.js b/actor-profile.js index cea8a89..2a1e981 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -21,9 +21,6 @@ class ActorProfile extends HTMLElement { if (actorInfo) { this.renderActorProfile(actorInfo); this.updateFollowButtonState(); - // Update distributed-outbox URL based on fetched actorInfo - const distributedOutbox = document.querySelector("distributed-outbox"); - distributedOutbox.setAttribute("url", actorInfo.outbox); } } @@ -32,14 +29,14 @@ class ActorProfile extends HTMLElement { this.innerHTML = ""; const profileContainer = document.createElement("div"); - profileContainer.classList.add("actor-profile"); + profileContainer.classList.add("profile"); // Create a container for the actor icon and name, to center them const actorContainer = document.createElement("div"); - actorContainer.classList.add("actor-container"); + actorContainer.classList.add("profile-container"); // Handle both single icon object and array of icons - let iconUrl = './assets/profile.png'; // Default profile image path + 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; @@ -47,16 +44,16 @@ class ActorProfile extends HTMLElement { iconUrl = actorInfo.icon.url; } } - + const img = document.createElement("img"); - img.classList.add("actor-icon"); + img.classList.add("profile-icon"); img.src = iconUrl; img.alt = actorInfo.name ? actorInfo.name : "Actor icon"; actorContainer.appendChild(img); // Append to the actor container if (actorInfo.name) { const pName = document.createElement("div"); - pName.classList.add("actor-name"); + pName.classList.add("profile-name"); pName.textContent = actorInfo.name; actorContainer.appendChild(pName); // Append to the actor container } @@ -70,8 +67,18 @@ class ActorProfile extends HTMLElement { followButton.textContent = "Follow"; profileContainer.appendChild(followButton); + // Create the distributed-outbox component and append it to the profile container + const distributedOutbox = document.createElement("distributed-outbox"); + profileContainer.appendChild(distributedOutbox); + // Append the profile container to the main component this.appendChild(profileContainer); + + // Update distributed-outbox URL based on fetched actorInfo + this.querySelector("distributed-outbox").setAttribute( + "url", + actorInfo.outbox + ); } async updateFollowButtonState() { From 7b860714796c2ac929d083ee8c5c003c7c69fe39 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 23:29:20 +0530 Subject: [PATCH 060/149] refactor: encapsulate follow button logic in custom web component --- actor-profile.css | 10 +++---- actor-profile.js | 70 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/actor-profile.css b/actor-profile.css index bf81dd5..20d4701 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -33,7 +33,7 @@ font-weight: bold; } -#followButton { +follow-button { appearance: none; border: 1px solid rgba(27, 31, 35, 0.15); border-radius: 4px; @@ -57,18 +57,18 @@ white-space: nowrap; } -#followButton.follow { +follow-button[state="follow"] { background-color: #3b82f6; color: #fff; } -#followButton.follow:hover { +follow-button[state="follow"]:hover { background-color: #2563eb; } -#followButton.unfollow { +follow-button[state="unfollow"] { background-color: #ef4444; color: #fff; } -#followButton.unfollow:hover { +follow-button[state="unfollow"]:hover { background-color: #dc2626; } diff --git a/actor-profile.js b/actor-profile.js index 2a1e981..afd57a0 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -20,7 +20,6 @@ class ActorProfile extends HTMLElement { console.log(actorInfo); if (actorInfo) { this.renderActorProfile(actorInfo); - this.updateFollowButtonState(); } } @@ -61,10 +60,9 @@ class ActorProfile extends HTMLElement { // Append the actor container to the profile container profileContainer.appendChild(actorContainer); - // Create and position the follow button - const followButton = document.createElement("button"); - followButton.id = "followButton"; - followButton.textContent = "Follow"; + // Instead of creating a button, create a FollowButton component + const followButton = document.createElement("follow-button"); + followButton.setAttribute("url", this.url); profileContainer.appendChild(followButton); // Create the distributed-outbox component and append it to the profile container @@ -80,23 +78,53 @@ class ActorProfile extends HTMLElement { actorInfo.outbox ); } +} - async updateFollowButtonState() { - const followButton = this.querySelector("#followButton"); - const followedActors = await db.getFollowedActors(); - const isFollowed = followedActors.some((actor) => actor.url === this.url); - - followButton.textContent = isFollowed ? "Unfollow" : "Follow"; - followButton.className = isFollowed ? "unfollow" : "follow"; - followButton.onclick = async () => { - if (isFollowed) { - await db.unfollowActor(this.url); - } else { - await db.followActor(this.url); - } - this.updateFollowButtonState(); - }; +customElements.define("actor-profile", ActorProfile); + +class FollowButton extends HTMLElement { + static get observedAttributes() { + return ["url"]; + } + + constructor() { + super(); + this.url = this.getAttribute("url") || ""; + this.state = "unknown"; + } + + connectedCallback() { + this.updateState(); + this.render(); + this.addEventListener("click", this.toggleFollowState.bind(this)); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === "url" && newValue !== oldValue) { + this.url = newValue; + this.updateState(); + } + } + + async updateState() { + const isFollowed = await db.isActorFollowed(this.url); + this.state = isFollowed ? "unfollow" : "follow"; + this.render(); + } + + async toggleFollowState() { + if (this.state === "follow") { + await db.followActor(this.url); + } else if (this.state === "unfollow") { + await db.unfollowActor(this.url); + } + this.updateState(); + } + + render() { + this.textContent = this.state === "follow" ? "Follow" : "Unfollow"; + this.setAttribute("state", this.state); } } -customElements.define("actor-profile", ActorProfile); +customElements.define("follow-button", FollowButton); From 6dbbced2cb60879244f87588bc4d0965ed738f41 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 23:31:47 +0530 Subject: [PATCH 061/149] chore: remove objectStoreNames check in db --- db.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/db.js b/db.js index 4b025fa..0408c89 100644 --- a/db.js +++ b/db.js @@ -299,11 +299,9 @@ function upgrade (db) { actors.createIndex(UPDATED_FIELD, UPDATED_FIELD) actors.createIndex(URL_FIELD, URL_FIELD) - if (!db.objectStoreNames.contains(FOLLOWED_ACTORS_STORE)) { - db.createObjectStore(FOLLOWED_ACTORS_STORE, { - keyPath: "url", - }); - } + db.createObjectStore(FOLLOWED_ACTORS_STORE, { + keyPath: "url", + }); const notes = db.createObjectStore(NOTES_STORE, { keyPath: 'id', From bd2120d01bcc25159410e4956eec5bb619052861 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Mar 2024 23:44:41 +0530 Subject: [PATCH 062/149] refactor: display followed actors into a reusable web component --- followed-accounts.html | 2 +- followed-accounts.js | 56 +++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/followed-accounts.html b/followed-accounts.html index c07e3d2..dba2714 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -12,7 +12,7 @@ >Import and export followed list coming soon..

-
+
diff --git a/followed-accounts.js b/followed-accounts.js index ee78625..b4b095a 100644 --- a/followed-accounts.js +++ b/followed-accounts.js @@ -11,10 +11,12 @@ class FollowedActorsList extends HTMLElement { async renderFollowedActors() { const followedActors = await db.getFollowedActors(); - this.innerHTML = followedActors.map(actor => { - const formattedDate = this.formatDate(actor.followedAt); - return `
- Followed URL: ${actor.url} - Followed At: ${formattedDate}
`; - }).join(''); + this.innerHTML = followedActors + .map((actor) => { + const formattedDate = this.formatDate(actor.followedAt); + return `
- Followed URL: ${actor.url} - Followed At: ${formattedDate}
`; + }) + .join(""); } formatDate(dateString) { @@ -34,12 +36,23 @@ class FollowedActorsList extends HTMLElement { customElements.define("followed-actors-list", FollowedActorsList); -export async function updateFollowCount() { - const followCountElement = document.getElementById("followCount"); - const followedActors = await db.getFollowedActors(); - followCountElement.textContent = followedActors.length; +class FollowedCount extends HTMLElement { + connectedCallback() { + this.updateCountOnLoad(); + } + + async updateCountOnLoad() { + setTimeout(() => this.updateCount(), 100); + } + + async updateCount() { + const followedActors = await db.getFollowedActors(); + this.textContent = followedActors.length; + } } +customElements.define("followed-count", FollowedCount); + // test following/unfollowing // (async () => { // const actorUrl1 = "https://example.com/actor/1"; diff --git a/index.html b/index.html index 047bf94..7443371 100644 --- a/index.html +++ b/index.html @@ -13,8 +13,8 @@

Social Reader

Following · 0 + >Following ·
- From f1d9aa10b542162a484a5a95620d388724aafec4 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 20 Mar 2024 02:09:18 +0530 Subject: [PATCH 065/149] feat: enhance actor profile display with preferredUsername support --- actor-profile.css | 5 +++++ actor-profile.js | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/actor-profile.css b/actor-profile.css index 340817d..1c54bb5 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -30,6 +30,11 @@ .profile-name { color: var(--rdp-text-color); font-weight: bold; +} + +.profile-username { + color: var(--rdp-text-color); + margin-top: 1px; margin-bottom: 10px; } diff --git a/actor-profile.js b/actor-profile.js index 1a5db35..79017f2 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -57,6 +57,13 @@ class ActorProfile extends HTMLElement { actorContainer.appendChild(pName); // Append to the actor container } + if (actorInfo.preferredUsername) { + const pUserName = document.createElement("div"); + pUserName.classList.add("profile-username"); + pUserName.textContent = `@${actorInfo.preferredUsername}`; + actorContainer.appendChild(pUserName); // Append to the actor container + } + // Instead of creating a button, create a FollowButton component const followButton = document.createElement("follow-button"); followButton.setAttribute("url", this.url); From 6423106beaed8af37b58f13065cd0ccfefef2c94 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 20 Mar 2024 02:23:44 +0530 Subject: [PATCH 066/149] pref: optimize default actor following process with parallel execution --- timeline.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/timeline.js b/timeline.js index 58f0523..b209dc6 100644 --- a/timeline.js +++ b/timeline.js @@ -22,9 +22,11 @@ class ReaderTimeline extends HTMLElement { // Check if followed actors have already been initialized const hasFollowedActors = await db.hasFollowedActors(); if (!hasFollowedActors) { - for (const actorUrl of defaultActors) { - await db.followActor(actorUrl); - } + await Promise.all( + defaultActors.map(async (actorUrl) => { + await db.followActor(actorUrl); + }) + ); } } @@ -33,7 +35,7 @@ class ReaderTimeline extends HTMLElement { // Dynamically load followed actors const followedActors = await db.getFollowedActors(); - const actorUrls = followedActors.map(actor => actor.url); + const actorUrls = followedActors.map((actor) => actor.url); for (const actorUrl of actorUrls) { try { From 128d062e48e1a39b5625887c364bd71a376344cd Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 20 Mar 2024 02:35:35 +0530 Subject: [PATCH 067/149] fix: pagination controls by ensuring activation post outbox URL update in actor-profile --- actor-profile.js | 1 + outbox.js | 2 +- profile.html | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/actor-profile.js b/actor-profile.js index 79017f2..2ef8ad4 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -84,6 +84,7 @@ class ActorProfile extends HTMLElement { "url", actorInfo.outbox ); + this.dispatchEvent(new CustomEvent('outboxUpdated', { bubbles: true })); } } diff --git a/outbox.js b/outbox.js index c0e05f0..80f04cf 100644 --- a/outbox.js +++ b/outbox.js @@ -2,7 +2,7 @@ class DistributedOutbox extends HTMLElement { constructor() { super(); this.renderedItems = new Map(); // Tracks rendered items by ID - this.numPosts = 32; // Default value + this.numPosts = 1; // Default value this.page = 1; // Default value this.totalPages = 0; // Keep track of total pages } diff --git a/profile.html b/profile.html index fb01839..d081439 100644 --- a/profile.html +++ b/profile.html @@ -14,16 +14,18 @@ - diff --git a/followed-accounts.js b/followed-accounts.js index 8250e10..f6af0c6 100644 --- a/followed-accounts.js +++ b/followed-accounts.js @@ -7,12 +7,42 @@ class FollowedActorsList extends HTMLElement { connectedCallback () { this.renderFollowedActors() + + window.addEventListener( + 'exportFollowed', + FollowedActorsList.exportFollowedList + ) + + window.addEventListener('importFollowed', (e) => { + FollowedActorsList.importFollowedList(e.detail.file) + }) + + // Listen for the custom event to refresh the list and count + window.addEventListener('followedActorsUpdated', async () => { + await this.renderFollowedActors() + document.getElementById('followCount').updateCountOnLoad() + }) + } + + disconnectedCallback () { + window.removeEventListener( + 'exportFollowed', + FollowedActorsList.exportFollowedList + ) + window.removeEventListener( + 'importFollowed', + FollowedActorsList.importFollowedList + ) + window.removeEventListener( + 'followedActorsUpdated', + this.renderFollowedActors + ) } async renderFollowedActors () { const followedActors = await db.getFollowedActors() this.innerHTML = '' - followedActors.forEach(actor => { + followedActors.forEach((actor) => { const actorElement = document.createElement('div') const formattedDate = this.formatDate(actor.followedAt) @@ -35,6 +65,36 @@ class FollowedActorsList extends HTMLElement { const date = new Date(dateString) return date.toLocaleDateString('en-US', options) } + + static async exportFollowedList () { + const followedActors = await db.getFollowedActors() + const blob = new Blob([JSON.stringify(followedActors, null, 2)], { + type: 'application/json' + }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'reader-followed-accounts.json' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + static async importFollowedList (file) { + const reader = new FileReader() + reader.onload = async (e) => { + const followedActors = JSON.parse(e.target.result) + for (const actor of followedActors) { + if (!(await db.isActorFollowed(actor.url))) { + await db.followActor(actor.url) + } + } + // After import, dispatch a custom event to notify the component + window.dispatchEvent(new CustomEvent('followedActorsUpdated')) + } + reader.readAsText(file) + } } customElements.define('followed-actors-list', FollowedActorsList) From 255b83a7a72750ddb2f6140ff97b8c19d1e8d467 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Sat, 23 Mar 2024 03:26:36 +0530 Subject: [PATCH 079/149] feat: add sidebar to followed accounts and about pages --- about.css | 25 +++++++++++----- about.html | 68 +++++++++++++++++++++++++++--------------- followed-accounts.css | 14 ++++++++- followed-accounts.html | 59 +++++++++++++++++++++++------------- 4 files changed, 114 insertions(+), 52 deletions(-) diff --git a/about.css b/about.css index 30eeebe..b608a43 100644 --- a/about.css +++ b/about.css @@ -3,21 +3,32 @@ html { font-family: var(--rdp-font); } -.container { - text-align: center; - margin-top: 40px; +.about-container { + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; + margin-top: 10px; } -.about { +.about-info { text-align: left; color: var(--rdp-text-color); - width: 80%; + width: 100%; } -.about a { +.about-info a { color: var(--rdp-details-color); text-decoration: underline; } -.about a:hover { +.about-info a:hover { text-decoration: none; } + +@media screen and (max-width: 768px) { + .about-container { + width: 80%; + margin-top: 130px; + max-width: 100%; + } +} diff --git a/about.html b/about.html index d8204f9..fbc0269 100644 --- a/about.html +++ b/about.html @@ -5,32 +5,52 @@
-
-

- Social Reader is a P2P and offline ActivityPub client for reading and - following microblogs on the Fediverse. -

-

- Unlike traditional platforms, Social Reader does not index data on a - server. It empowers you to load public ActivityPub data directly, turning - your device into a personal indexer. This means - your content, your control. -

-

- Social Reader natively supports content loading over P2P protocols such as - ipfs:// and hyper://. This innovation bypasses - the need for always-online HTTP servers, allowing you to access content - anytime, anywhere—even offline. -

-

- Social Reader is made with minimal dependencies, view and contribute to - our code on - GitHub. -

+ +
+
+

+ Social Reader is a P2P and offline ActivityPub client for reading and + following microblogs on the Fediverse. +

+

+ Unlike traditional platforms, Social Reader does not index data on a + server. It empowers you to load public ActivityPub data directly, + turning your device into a personal indexer. This means + your content, your control. +

+

+ Social Reader natively supports content loading over P2P protocols such + as + ipfs:// and hyper://. This innovation bypasses + the need for always-online HTTP servers, allowing you to access content + anytime, anywhere—even offline. +

+

+ Social Reader is made with minimal dependencies, view and contribute to + our code on + GitHub. +

+
+ diff --git a/followed-accounts.css b/followed-accounts.css index 67c9185..d873863 100644 --- a/followed-accounts.css +++ b/followed-accounts.css @@ -4,11 +4,15 @@ html { } .followed-container { + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; color: var(--rdp-text-color); display: flex; flex-direction: column; align-items: center; - margin-top: 20px; + margin-top: 10px; } .imp-exp-btn { @@ -28,3 +32,11 @@ followed-actors-list { margin: 0 auto; overflow-wrap: break-word; } + +@media screen and (max-width: 768px) { + .followed-container { + width: 80%; + max-width: 100%; + margin-top: 130px; + } +} diff --git a/followed-accounts.html b/followed-accounts.html index 7af354c..3f3cb83 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -1,32 +1,51 @@ + Followed Accounts -
-

- You're following - accounts on the - fediverse.
- To sync your list of followed accounts across multiple devices, be sure - to ⬆️ export your followed list from one device and 📥 import it on the - other. +

+
+

+ You're following + accounts on the + fediverse.
+ To sync your list of followed accounts across multiple devices, be sure + to ⬆️ export your followed list from one device and 📥 import it on the + other. +

+
+ + + +
+ + ⚠️ Before clearing your browser's cache, ensure you export your followed + list. -

-
- - -
- - ⚠️ Before clearing your browser's cache, ensure you export your followed - list.
diff --git a/post.css b/post.css index 5670be9..934d20b 100644 --- a/post.css +++ b/post.css @@ -13,7 +13,8 @@ html { font-family: var(--rdp-font); } -img { +img, +video { max-width: 100%; } diff --git a/profile.html b/profile.html index 6317ca0..70821c7 100644 --- a/profile.html +++ b/profile.html @@ -10,7 +10,7 @@
- +
+ diff --git a/db.js b/db.js index c296521..13a3b71 100644 --- a/db.js +++ b/db.js @@ -462,6 +462,15 @@ export class ActivityPubDB { const followedActors = await this.getFollowedActors() return followedActors.length > 0 } + + async setTheme (themeName) { + await this.db.put('settings', { key: 'theme', value: themeName }) + } + + async getTheme () { + const themeSetting = await this.db.get('settings', 'theme') + return themeSetting ? themeSetting.value : null + } } function upgrade (db) { @@ -506,6 +515,8 @@ function upgrade (db) { function addSortedIndex (store, field, options = {}) { store.createIndex(field + ', published', [field, PUBLISHED_FIELD], options) } + + db.createObjectStore('settings', { keyPath: 'key' }) } async function parsePostHtml (htmlContent) { diff --git a/followed-accounts.css b/followed-accounts.css index d873863..e10a827 100644 --- a/followed-accounts.css +++ b/followed-accounts.css @@ -19,6 +19,11 @@ html { margin-bottom: 20px; } +#exportFollowedList, +#importFollowedList { + cursor: pointer; +} + .cache-warning-msg { font-size: 0.775em; margin-top: 10px; @@ -37,6 +42,6 @@ followed-actors-list { .followed-container { width: 80%; max-width: 100%; - margin-top: 130px; + margin-top: 150px; } } diff --git a/followed-accounts.html b/followed-accounts.html index 3f3cb83..7dbab6e 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -4,19 +4,21 @@ Followed Accounts
+
+ +
diff --git a/followed-accounts.html b/followed-accounts.html index e9d109e..2991b29 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -48,6 +48,9 @@

Social Reader

list.
+
+ +
diff --git a/profile.html b/profile.html index e1d4e7b..b1580df 100644 --- a/profile.html +++ b/profile.html @@ -35,6 +35,9 @@

Social Reader

+
+ +
- + + diff --git a/followed-accounts.js b/followed-accounts.js index f6af0c6..aabf364 100644 --- a/followed-accounts.js +++ b/followed-accounts.js @@ -43,10 +43,9 @@ class FollowedActorsList extends HTMLElement { const followedActors = await db.getFollowedActors() this.innerHTML = '' followedActors.forEach((actor) => { - const actorElement = document.createElement('div') - const formattedDate = this.formatDate(actor.followedAt) - - actorElement.innerText = `- Followed URL: ${actor.url} - Followed At: ${formattedDate}` + const actorElement = document.createElement('actor-mini-profile') + actorElement.setAttribute('url', actor.url) + actorElement.setAttribute('followed-at', this.formatDate(actor.followedAt)) this.appendChild(actorElement) }) @@ -56,11 +55,7 @@ class FollowedActorsList extends HTMLElement { const options = { year: 'numeric', month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZoneName: 'short' + day: 'numeric' } const date = new Date(dateString) return date.toLocaleDateString('en-US', options) From d5d26af0ea2c7a503d38c8fcf7b21671a3208493 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 2 Apr 2024 01:41:27 +0530 Subject: [PATCH 094/149] feat: add FAQ section in about --- about.css | 36 +++++++++++++++++++++++++++++- about.html | 65 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/about.css b/about.css index d41f520..8120ef3 100644 --- a/about.css +++ b/about.css @@ -18,13 +18,47 @@ html { } .about-info a { - color: var(--rdp-details-color); + color: var(--rdp-link-color); text-decoration: underline; } .about-info a:hover { text-decoration: none; } +.faq-section { + margin-top: 2rem; + text-align: left; +} + +.faq-section h2 { + color: var(--rdp-text-color); +} + +.faq-section a { + color: var(--rdp-link-color); + text-decoration: underline; +} +.faq-section a:hover { + text-decoration: none; +} + +.faq-section details { + color: var(--rdp-text-color); + margin-bottom: 1rem; + border-bottom: 1px solid var(--rdp-border-color); + padding-bottom: 1rem; +} + +.faq-section summary { + color: var(--rdp-text-color); + font-weight: bold; + cursor: pointer; +} + +.faq-section p { + margin-top: 0.5rem; +} + @media screen and (max-width: 768px) { .about-container { width: 80%; diff --git a/about.html b/about.html index d5dee9d..52d5eb6 100644 --- a/about.html +++ b/about.html @@ -30,7 +30,8 @@

Social Reader

Social Reader is a P2P and offline ActivityPub client for reading and - following microblogs on the Fediverse. + following microblogs on the + Fediverse.

Unlike traditional platforms, Social Reader does not index data on a @@ -41,18 +42,70 @@

Social Reader

Social Reader natively supports content loading over P2P protocols such as - ipfs:// and hyper://. This innovation bypasses - the need for always-online HTTP servers, allowing you to access content - anytime, anywhere—even offline. + ipfs:// and + hyper://. This innovation bypasses the need for always-online HTTP servers, + allowing you to access content anytime, anywhere—even offline.

- Social Reader is made with minimal dependencies, view and contribute to - our code on + Social Reader is built on principles of low-tech; minimal dependencies, + vanilla JavaScript, unminified scripts, and IndexedDB for local data + storage. View and contribute to our open-source code on GitHub.

+ +
+

FAQs

+
+ How do I create an account on Social Reader? +

+ Social Reader is designed as a reading and following client, which + means you cannot create an account directly within the app. To + actively write and contribute to the Fediverse, you would need to + interact with the + Social Inbox + API. This can be done through platforms like + Sutty CMS or by forking and hosting + your own instance of + Staticpub + repository. +

+
+
+ + Why is Social Reader different from mainstream social platforms? + +

+ Social Reader eliminates the middleman, ensuring direct communication + with your audience without the interference of third-party algorithms. + This ad-free experience prioritizes user autonomy and engagement, + making it ideal for community leaders and organizations seeking + genuine reach and engagement. Unlike traditional social networks where + follower engagement often requires payment, Social Reader and the + broader Fediverse allow for genuine reach and engagement. +

+
+
+ I found a bug. Where do I report it? +

+ If you encounter any issues or have feedback, please file a report on + our + GitHub issues + page. We appreciate your input as it helps us improve Social Reader + for everyone. +

+
+
From 9a78d803d604e091357f3b7cbd8a06f0202b54f8 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 5 Apr 2024 01:54:49 +0530 Subject: [PATCH 095/149] refactor: centralize shared CSS styles into common.css --- about.css | 5 ----- about.html | 1 + common.css | 4 ++++ followed-accounts.css | 5 ----- followed-accounts.html | 1 + index.html | 1 + post.css | 4 ---- profile.html | 1 + 8 files changed, 8 insertions(+), 14 deletions(-) create mode 100644 common.css diff --git a/about.css b/about.css index 8120ef3..3ae79e3 100644 --- a/about.css +++ b/about.css @@ -1,8 +1,3 @@ -html { - background: var(--bg-color); - font-family: var(--rdp-font); -} - .about-container { flex: 1; max-width: 600px; diff --git a/about.html b/about.html index 52d5eb6..fd35fdb 100644 --- a/about.html +++ b/about.html @@ -4,6 +4,7 @@ About Reader
- +

@@ -112,5 +97,6 @@

FAQs

+ diff --git a/common.css b/common.css index be945cb..13be447 100644 --- a/common.css +++ b/common.css @@ -2,3 +2,30 @@ html { background: var(--bg-color); font-family: var(--rdp-font); } + +.container { + display: flex; + justify-content: space-between; + max-width: 1200px; + width: 100%; + margin-top: 20px; + position: relative; +} + +/* Empty right column for balance */ +.right-column { + flex: 0 0 200px; +} + +@media screen and (max-width: 1280px) { + .right-column { + flex: 0 0 100px; + } +} + +@media screen and (max-width: 768px) { + .container { + flex-direction: column; + align-items: center; + } +} diff --git a/followed-accounts.html b/followed-accounts.html index 1b41a34..ee47947 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -12,22 +12,7 @@ @import url("./post.css");
- +

You're following @@ -77,6 +62,7 @@

Social Reader

} }); + diff --git a/index.html b/index.html index a96aac4..6d62669 100644 --- a/index.html +++ b/index.html @@ -11,28 +11,14 @@ @import url("./post.css");
- +
+ diff --git a/profile.html b/profile.html index cdf821e..a52d82c 100644 --- a/profile.html +++ b/profile.html @@ -12,22 +12,7 @@ @import url("./post.css");
- +
@@ -66,6 +51,7 @@

Social Reader

console.error("Actor URL not specified in the query string."); } + diff --git a/sidebar.css b/sidebar.css new file mode 100644 index 0000000..12bf259 --- /dev/null +++ b/sidebar.css @@ -0,0 +1,86 @@ +.header-branding { + color: var(--rdp-text-color); +} +.home-page-link { + text-decoration: none; + cursor: pointer; +} + +.sidebar { + position: sticky; + top: 0; + flex: 0 0 250px; + display: flex; + flex-direction: column; + align-items: flex-start; + height: 100vh; + overflow-y: auto; +} + +.sidebar h1 { + font-family: "Times New Roman", Times, serif; + font-size: 1.8em; + font-weight: normal; + margin-bottom: 0.6em; +} + +.controls a { + color: var(--rdp-text-color); + text-decoration: none; + font-size: 0.875rem; + font-weight: bold; + margin-bottom: 0.4em; +} +.controls a:hover { + text-decoration: underline; +} + +.sidebar nav { + display: flex; + flex-direction: column; +} + +.sidebar nav a { + color: var(--rdp-details-color); + text-decoration: underline; + font-size: 0.775rem; +} + +.sidebar nav a:hover { + text-decoration: none; +} + +@media screen and (max-width: 1280px) { + .sidebar { + flex: 0 0 200px; + margin-left: 14px; + } +} + +@media screen and (max-width: 768px) { + .sidebar { + width: 100%; + position: fixed; + top: 0; + z-index: 100; + max-height: calc(100vh - 82vh); + overflow-y: auto; + align-items: center; + background-color: var(--bg-color); + } + + .sidebar h1 { + margin-bottom: 0.2em; + text-align: center; + } + + .sidebar nav { + text-align: center; + } +} + +@media screen and (max-width: 400px) { + .sidebar { + padding-bottom: 50px; + } +} diff --git a/sidebar.js b/sidebar.js new file mode 100644 index 0000000..7269a7f --- /dev/null +++ b/sidebar.js @@ -0,0 +1,66 @@ +class SidebarNav extends HTMLElement { + constructor () { + super() + this.attachShadow({ mode: 'open' }) + + // Create sidebar container + const sidebar = document.createElement('div') + sidebar.classList.add('sidebar') + + // Branding + const homePage = document.createElement('a') + homePage.href = './' + homePage.classList.add('home-page-link') + const headerBranding = document.createElement('h1') + headerBranding.classList.add('header-branding') + headerBranding.textContent = 'Social Reader' + homePage.appendChild(headerBranding) + sidebar.appendChild(homePage) + + // Controls + const controls = document.createElement('div') + controls.classList.add('controls') + const followingLink = document.createElement('a') + followingLink.href = './followed-accounts.html' + followingLink.textContent = 'Following · ' + + const followedCount = document.createElement('followed-count') + followingLink.appendChild(followedCount) + + controls.appendChild(followingLink) + sidebar.appendChild(controls) + + // Navigation + const nav = document.createElement('nav') + const themeSelector = document.createElement('theme-selector') + nav.appendChild(themeSelector) + const links = [ + { href: './about.html', text: 'About' }, + { + href: 'https://hypha.coop/dripline/announcing-dp-social-inbox/', + text: 'Social Inbox' + }, + { href: 'https://distributed.press', text: 'Distributed Press' } + ] + links.forEach((linkInfo) => { + const a = document.createElement('a') + a.href = linkInfo.href + a.textContent = linkInfo.text + nav.appendChild(a) + }) + sidebar.appendChild(nav) + + // Append the sidebar to the shadow DOM + this.shadowRoot.appendChild(sidebar) + + // Style + const style = document.createElement('style') + style.textContent = ` + @import url("./sidebar.css"); + ` + this.shadowRoot.appendChild(style) + } +} + +// Register the new element with the browser +customElements.define('sidebar-nav', SidebarNav) diff --git a/timeline.css b/timeline.css index e85acfb..e70aabf 100644 --- a/timeline.css +++ b/timeline.css @@ -9,63 +9,6 @@ body { background: var(--bg-color); } -.container { - display: flex; - justify-content: space-between; - max-width: 1200px; - width: 100%; - margin-top: 20px; - position: relative; -} - -.header-branding { - color: var(--rdp-text-color); -} - -.sidebar { - position: sticky; - top: 0; - flex: 0 0 250px; - display: flex; - flex-direction: column; - align-items: flex-start; - height: 100vh; - overflow-y: auto; -} - -.sidebar h1 { - font-family: "Times New Roman", Times, serif; - font-size: 1.8em; - font-weight: normal; - margin-bottom: 0.6em; -} - -.controls a { - color: var(--rdp-text-color); - text-decoration: none; - font-size: 0.875rem; - font-weight: bold; - margin-bottom: 0.4em; -} -.controls a:hover { - text-decoration: underline; -} - -.sidebar nav { - display: flex; - flex-direction: column; -} - -.sidebar nav a { - color: var(--rdp-details-color); - text-decoration: underline; - font-size: 0.775rem; -} - -.sidebar nav a:hover { - text-decoration: none; -} - reader-timeline { flex: 1; max-width: 600px; @@ -73,57 +16,10 @@ reader-timeline { margin: 0 20px; } -/* Empty right column for balance */ -.right-column { - flex: 0 0 200px; -} - -@media screen and (max-width: 1280px) { - .sidebar { - flex: 0 0 200px; - margin-left: 14px; - } - - .right-column { - flex: 0 0 100px; - } -} - @media screen and (max-width: 768px) { - .container { - flex-direction: column; - align-items: center; - } - - .sidebar { - width: 100%; - position: fixed; - top: 0; - z-index: 100; - max-height: calc(100vh - 82vh); - overflow-y: auto; - align-items: center; - background-color: var(--bg-color); - } - - .sidebar h1 { - margin-bottom: 0.2em; - text-align: center; - } - - .sidebar nav { - text-align: center; - } - reader-timeline { width: 100%; max-width: 100%; margin-top: 150px; } } - -@media screen and (max-width: 400px) { - .sidebar { - padding-bottom: 50px; - } -} From 441cc0043d916ec287b8324a6d8feb0be0311416 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 5 Apr 2024 22:51:38 +0530 Subject: [PATCH 097/149] refactor: about page sections for semantic consistency --- about.css | 30 ++++++------------------------ about.html | 12 ++++++------ 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/about.css b/about.css index 3ae79e3..fbc3d60 100644 --- a/about.css +++ b/about.css @@ -6,54 +6,36 @@ margin-top: 10px; } -.about-info { +/* Apply general styles to all section elements within about-container */ +.about-container > section { text-align: left; color: var(--rdp-text-color); width: 100%; + margin-bottom: 2rem; } -.about-info a { - color: var(--rdp-link-color); - text-decoration: underline; -} -.about-info a:hover { - text-decoration: none; -} - -.faq-section { - margin-top: 2rem; - text-align: left; -} - -.faq-section h2 { - color: var(--rdp-text-color); -} - +.about-info a, .faq-section a { color: var(--rdp-link-color); text-decoration: underline; } + +.about-info a:hover, .faq-section a:hover { text-decoration: none; } .faq-section details { - color: var(--rdp-text-color); margin-bottom: 1rem; border-bottom: 1px solid var(--rdp-border-color); padding-bottom: 1rem; } .faq-section summary { - color: var(--rdp-text-color); font-weight: bold; cursor: pointer; } -.faq-section p { - margin-top: 0.5rem; -} - @media screen and (max-width: 768px) { .about-container { width: 80%; diff --git a/about.html b/about.html index e3a5208..ac96d24 100644 --- a/about.html +++ b/about.html @@ -12,8 +12,8 @@
-
-
+
+

Social Reader is a P2P and offline ActivityPub client for reading and following microblogs on the @@ -41,9 +41,9 @@ >GitHub.

-
+ -
+

FAQs

How do I create an account on Social Reader? @@ -91,8 +91,8 @@

FAQs

for everyone.

-
-
+ +
From 81d6c5471794563aad6d180820026d85936bff2e Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 5 Apr 2024 23:01:16 +0530 Subject: [PATCH 098/149] refactor: actor-mini-profile to use button for clickable elements --- actor-mini-profile.css | 6 ++++++ actor-mini-profile.js | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/actor-mini-profile.css b/actor-mini-profile.css index 544f006..5579e0c 100644 --- a/actor-mini-profile.css +++ b/actor-mini-profile.css @@ -3,6 +3,12 @@ align-items: center; text-align: left; cursor: pointer; + background: none; + border: none; + padding: 0; + margin-bottom: 4px; + color: inherit; + font: inherit; } .profile-mini-icon { diff --git a/actor-mini-profile.js b/actor-mini-profile.js index 052d337..923cf66 100644 --- a/actor-mini-profile.js +++ b/actor-mini-profile.js @@ -37,9 +37,10 @@ class ActorMiniProfile extends HTMLElement { // Clear existing content this.innerHTML = '' - // Container for the icon and name, which should be clickable - const clickableContainer = document.createElement('div') + // Container for the icon and name, which should be a button for clickable actions + const clickableContainer = document.createElement('button') clickableContainer.className = 'mini-profile' + clickableContainer.setAttribute('type', 'button') let iconUrl = './assets/profile.png' if (actorInfo.icon) { From 804a4ad74540d125cc77fa0b9d05c092ff73280e Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 5 Apr 2024 23:51:20 +0530 Subject: [PATCH 099/149] style: standardize font styles for actor profile and follow button --- actor-profile.css | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/actor-profile.css b/actor-profile.css index 79f657c..f2bdbc7 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -49,15 +49,14 @@ follow-button { appearance: none; - border: 1px solid rgba(27, 31, 35, 0.15); + border: 1px solid var(--rdp-border-color); border-radius: 4px; box-shadow: rgba(27, 31, 35, 0.1) 0 1px 0; box-sizing: border-box; cursor: pointer; display: inline-block; - font-family: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, - sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - font-size: 14px; + font-family: inherit; + font-size: inherit; font-weight: 600; line-height: 20px; padding: 4px 16px; @@ -71,9 +70,13 @@ follow-button { white-space: nowrap; } +follow-button[state="follow"], +follow-button[state="unfollow"] { + color: #fff; +} + follow-button[state="follow"] { background-color: #3b82f6; - color: #fff; } follow-button[state="follow"]:hover { background-color: #2563eb; @@ -81,7 +84,6 @@ follow-button[state="follow"]:hover { follow-button[state="unfollow"] { background-color: #ef4444; - color: #fff; } follow-button[state="unfollow"]:hover { background-color: #dc2626; From d7908e5ba25f463cfb698d6b724f016bfc9e47cd Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 5 Apr 2024 23:54:07 +0530 Subject: [PATCH 100/149] style: remove user-select from follow-button styles for improved text selection ux --- actor-profile.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/actor-profile.css b/actor-profile.css index f2bdbc7..4dfcf42 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -63,8 +63,6 @@ follow-button { position: relative; text-align: center; text-decoration: none; - user-select: none; - -webkit-user-select: none; touch-action: manipulation; vertical-align: middle; white-space: nowrap; From e2392335420a25979a41c5bf564fe736f00a8450 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Sat, 6 Apr 2024 02:24:22 +0530 Subject: [PATCH 101/149] feat: implement error-message component for consistent error handling --- actor-profile.js | 21 +++++++++++++++++---- error-message.js | 36 ++++++++++++++++++++++++++++++++++++ index.html | 1 + post.html | 1 + post.js | 15 +++------------ profile.html | 1 + 6 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 error-message.js diff --git a/actor-profile.js b/actor-profile.js index 6f352f4..3a01638 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -16,10 +16,16 @@ class ActorProfile extends HTMLElement { } async fetchAndRenderActorProfile (url) { - const actorInfo = await db.getActor(url) - console.log(actorInfo) - if (actorInfo) { - this.renderActorProfile(actorInfo) + try { + const actorInfo = await db.getActor(url) + if (actorInfo) { + this.renderActorProfile(actorInfo) + } else { + this.renderError('Actor information not found') + } + } catch (error) { + console.error('Error fetching actor info:', error) + this.renderError('An error occurred while fetching actor information.') } } @@ -93,6 +99,13 @@ class ActorProfile extends HTMLElement { ) this.dispatchEvent(new CustomEvent('outboxUpdated', { bubbles: true })) } + + renderError (message) { + this.innerHTML = '' // Clear existing content + const errorComponent = document.createElement('error-message') + errorComponent.setAttribute('message', message) + this.appendChild(errorComponent) + } } customElements.define('actor-profile', ActorProfile) diff --git a/error-message.js b/error-message.js new file mode 100644 index 0000000..b8d5ad5 --- /dev/null +++ b/error-message.js @@ -0,0 +1,36 @@ +class ErrorMessage extends HTMLElement { + constructor () { + super() + this.attachShadow({ mode: 'open' }) + + // Create the main element for the error message + const errorElement = document.createElement('p') + errorElement.classList.add('error') + errorElement.textContent = + this.getAttribute('message') || 'An error occurred' + + const style = document.createElement('style') + style.textContent = ` + .error { + color: var(--rdp-details-color); + text-align: center; + margin: 20px; + font-size: 1rem; + } + ` + + this.shadowRoot.append(style, errorElement) + } + + static get observedAttributes () { + return ['message'] + } + + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'message' && oldValue !== newValue) { + this.shadowRoot.querySelector('.error').textContent = newValue + } + } +} + +customElements.define('error-message', ErrorMessage) diff --git a/index.html b/index.html index 6d62669..54d695c 100644 --- a/index.html +++ b/index.html @@ -24,3 +24,4 @@ + diff --git a/post.html b/post.html index ce044c7..4954f3c 100644 --- a/post.html +++ b/post.html @@ -23,3 +23,4 @@ console.error("URL parameter is missing"); } + diff --git a/post.js b/post.js index 634632d..f056f3c 100644 --- a/post.js +++ b/post.js @@ -39,13 +39,6 @@ function timeSince (dateString) { return Math.floor(seconds) + 's' } -function renderError (message) { - const errorElement = document.createElement('p') - errorElement.classList.add('error') - errorElement.textContent = message - return errorElement -} - // Define a class for the web component class DistributedPost extends HTMLElement { static get observedAttributes () { @@ -206,11 +199,9 @@ class DistributedPost extends HTMLElement { // Clear existing content this.innerHTML = '' - const errorElement = document.createElement('p') - errorElement.className = 'error' - errorElement.textContent = errorMessage - errorElement.style.color = 'red' - this.appendChild(errorElement) + const errorComponent = document.createElement('error-message') + errorComponent.setAttribute('message', errorMessage) + this.appendChild(errorComponent) } } diff --git a/profile.html b/profile.html index a52d82c..1d5624b 100644 --- a/profile.html +++ b/profile.html @@ -57,3 +57,4 @@ + From 048a839f2c5deadd545d0f79101a5d455ea48411 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 8 Apr 2024 16:29:12 +0530 Subject: [PATCH 102/149] refactor: generalize main content styling and responsiveness --- about.css | 8 -------- about.html | 2 +- actor-profile.css | 6 ------ common.css | 9 +++++++++ followed-accounts.css | 8 -------- followed-accounts.html | 2 +- profile.html | 2 +- 7 files changed, 12 insertions(+), 25 deletions(-) diff --git a/about.css b/about.css index fbc3d60..07cad08 100644 --- a/about.css +++ b/about.css @@ -35,11 +35,3 @@ font-weight: bold; cursor: pointer; } - -@media screen and (max-width: 768px) { - .about-container { - width: 80%; - max-width: 100%; - margin-top: 150px; - } -} diff --git a/about.html b/about.html index ac96d24..4b3f196 100644 --- a/about.html +++ b/about.html @@ -12,7 +12,7 @@
-
+

Social Reader is a P2P and offline ActivityPub client for reading and diff --git a/actor-profile.css b/actor-profile.css index 4dfcf42..e50fbc9 100644 --- a/actor-profile.css +++ b/actor-profile.css @@ -98,10 +98,4 @@ follow-button[state="unfollow"]:hover { .profile-summary { width: 100%; } - - .actor-profile { - width: 100%; - max-width: 100%; - margin-top: 150px; - } } diff --git a/common.css b/common.css index 13be447..cd1084b 100644 --- a/common.css +++ b/common.css @@ -28,4 +28,13 @@ html { flex-direction: column; align-items: center; } + + .main-content { + width: 80%; + max-width: 100%; + margin-top: 150px; + } + .actor-profile{ + width: 100%; + } } diff --git a/followed-accounts.css b/followed-accounts.css index 6652ad5..6cd25e7 100644 --- a/followed-accounts.css +++ b/followed-accounts.css @@ -32,11 +32,3 @@ followed-actors-list { margin: 0 auto; overflow-wrap: break-word; } - -@media screen and (max-width: 768px) { - .followed-container { - width: 80%; - max-width: 100%; - margin-top: 150px; - } -} diff --git a/followed-accounts.html b/followed-accounts.html index ee47947..a864486 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -13,7 +13,7 @@

-
+

You're following accounts on the diff --git a/profile.html b/profile.html index 1d5624b..23076e0 100644 --- a/profile.html +++ b/profile.html @@ -13,7 +13,7 @@

-
+
From 309a24d95cd26fafb4f7642829a6410c9b834fdb Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 8 Apr 2024 16:35:28 +0530 Subject: [PATCH 103/149] chore: remove query selector from actor-profile --- actor-profile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actor-profile.js b/actor-profile.js index 3a01638..4e7ec3b 100644 --- a/actor-profile.js +++ b/actor-profile.js @@ -93,7 +93,7 @@ class ActorProfile extends HTMLElement { this.appendChild(profileContainer) // Update distributed-outbox URL based on fetched actorInfo - this.querySelector('distributed-outbox').setAttribute( + distributedOutbox.setAttribute( 'url', actorInfo.outbox ) From 17b5184ab42b38144f694261645b3757e236bc42 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 8 Apr 2024 16:41:31 +0530 Subject: [PATCH 104/149] refactor: move img and video themes in common css --- common.css | 6 ++++++ post.css | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/common.css b/common.css index cd1084b..cab9abd 100644 --- a/common.css +++ b/common.css @@ -3,6 +3,12 @@ html { font-family: var(--rdp-font); } +/* Main styles */ +img, +video { + max-width: 100%; +} + .container { display: flex; justify-content: space-between; diff --git a/post.css b/post.css index 8ba6508..8138b5a 100644 --- a/post.css +++ b/post.css @@ -10,12 +10,6 @@ --rdp-border-radius: 6px; } -/* Main styles */ -img, -video { - max-width: 100%; -} - /* Component styles */ .distributed-post { background: var(--rdp-bg-color); From 3ff100ba4d0053f4b9206f7c587fde284760e481 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 10 Apr 2024 19:35:35 +0530 Subject: [PATCH 105/149] refactor: extend EventTarget in ActivityPubDB --- db.js | 5 +++- followed-accounts.html | 31 ++++++++++------------ followed-accounts.js | 59 +++++++++++++++++++----------------------- 3 files changed, 45 insertions(+), 50 deletions(-) diff --git a/db.js b/db.js index a469abb..fe89e3b 100644 --- a/db.js +++ b/db.js @@ -41,8 +41,9 @@ export function isP2P (url) { return url.startsWith(HYPER_PREFIX) || url.startsWith(IPNS_PREFIX) } -export class ActivityPubDB { +export class ActivityPubDB extends EventTarget { constructor (db, fetch = globalThis.fetch) { + super() this.db = db this.fetch = fetch } @@ -394,6 +395,7 @@ export class ActivityPubDB { const followedAt = new Date() await this.db.put(FOLLOWED_ACTORS_STORE, { url, followedAt }) console.log(`Followed actor: ${url} at ${followedAt}`) + this.dispatchEvent(new CustomEvent('actorFollowed', { detail: { url, followedAt } })) } // Method to unfollow an actor @@ -401,6 +403,7 @@ export class ActivityPubDB { await this.db.delete(FOLLOWED_ACTORS_STORE, url) await this.purgeActor(url) console.log(`Unfollowed and purged actor: ${url}`) + this.dispatchEvent(new CustomEvent('actorUnfollowed', { detail: { url } })) } async purgeActor (url) { diff --git a/followed-accounts.html b/followed-accounts.html index a864486..1a328c9 100644 --- a/followed-accounts.html +++ b/followed-accounts.html @@ -39,26 +39,23 @@
- diff --git a/followed-accounts.js b/followed-accounts.js index aabf364..1b5de4c 100644 --- a/followed-accounts.js +++ b/followed-accounts.js @@ -1,42 +1,37 @@ import { db } from './dbInstance.js' -class FollowedActorsList extends HTMLElement { +export class FollowedActorsList extends HTMLElement { constructor () { super() + this.updateFollowedActors = this.updateFollowedActors.bind(this) } connectedCallback () { this.renderFollowedActors() - window.addEventListener( - 'exportFollowed', - FollowedActorsList.exportFollowedList - ) + db.addEventListener('actorFollowed', this.updateFollowedActors) + db.addEventListener('actorUnfollowed', this.updateFollowedActors) - window.addEventListener('importFollowed', (e) => { + this.addEventListener('exportFollowed', FollowedActorsList.exportFollowedList) + this.addEventListener('importFollowed', (e) => { FollowedActorsList.importFollowedList(e.detail.file) }) - - // Listen for the custom event to refresh the list and count - window.addEventListener('followedActorsUpdated', async () => { - await this.renderFollowedActors() - document.getElementById('followCount').updateCountOnLoad() - }) } disconnectedCallback () { - window.removeEventListener( - 'exportFollowed', - FollowedActorsList.exportFollowedList - ) - window.removeEventListener( - 'importFollowed', - FollowedActorsList.importFollowedList - ) - window.removeEventListener( - 'followedActorsUpdated', - this.renderFollowedActors - ) + db.removeEventListener('actorFollowed', this.updateFollowedActors) + db.removeEventListener('actorUnfollowed', this.updateFollowedActors) + + this.removeEventListener('exportFollowed', FollowedActorsList.exportFollowedList) + this.removeEventListener('importFollowed', FollowedActorsList.importFollowedList) + } + + async updateFollowedActors () { + await this.renderFollowedActors() + const followCount = document.querySelector('followed-count') + if (followCount) { + followCount.updateCount() + } } async renderFollowedActors () { @@ -46,17 +41,12 @@ class FollowedActorsList extends HTMLElement { const actorElement = document.createElement('actor-mini-profile') actorElement.setAttribute('url', actor.url) actorElement.setAttribute('followed-at', this.formatDate(actor.followedAt)) - this.appendChild(actorElement) }) } formatDate (dateString) { - const options = { - year: 'numeric', - month: 'long', - day: 'numeric' - } + const options = { year: 'numeric', month: 'long', day: 'numeric' } const date = new Date(dateString) return date.toLocaleDateString('en-US', options) } @@ -85,8 +75,6 @@ class FollowedActorsList extends HTMLElement { await db.followActor(actor.url) } } - // After import, dispatch a custom event to notify the component - window.dispatchEvent(new CustomEvent('followedActorsUpdated')) } reader.readAsText(file) } @@ -97,6 +85,13 @@ customElements.define('followed-actors-list', FollowedActorsList) class FollowedCount extends HTMLElement { connectedCallback () { this.updateCountOnLoad() + db.addEventListener('actorFollowed', () => this.updateCount()) + db.addEventListener('actorUnfollowed', () => this.updateCount()) + } + + disconnectedCallback () { + db.removeEventListener('actorFollowed', () => this.updateCount()) + db.removeEventListener('actorUnfollowed', () => this.updateCount()) } async updateCountOnLoad () { From ae81943747711d8c1142dc2435d8e05860c60164 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 10 Apr 2024 19:37:34 +0530 Subject: [PATCH 106/149] style: remove cursor pointer for summary --- about.css | 1 - post.css | 4 ---- 2 files changed, 5 deletions(-) diff --git a/about.css b/about.css index 07cad08..f9ec7f6 100644 --- a/about.css +++ b/about.css @@ -33,5 +33,4 @@ .faq-section summary { font-weight: bold; - cursor: pointer; } diff --git a/post.css b/post.css index 8138b5a..8c0fdd0 100644 --- a/post.css +++ b/post.css @@ -62,10 +62,6 @@ color: var(--rdp-details-color); } -summary { - cursor: pointer; -} - .cw-summary { color: var(--rdp-cw-color); } From f2cb519dd3d9dee53d0ef0b849c09cf0866a0cd1 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 10 Apr 2024 19:39:07 +0530 Subject: [PATCH 107/149] chore: remove objectStoreNames for theme settings and increment db version --- db.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/db.js b/db.js index fe89e3b..056fe30 100644 --- a/db.js +++ b/db.js @@ -49,7 +49,7 @@ export class ActivityPubDB extends EventTarget { } static async load (name = DEFAULT_DB, fetch = globalThis.fetch) { - const db = await openDB(name, 1, { + const db = await openDB(name, 2, { upgrade }) @@ -519,9 +519,7 @@ function upgrade (db) { store.createIndex(field + ', published', [field, PUBLISHED_FIELD], options) } - if (!db.objectStoreNames.contains('settings')) { - db.createObjectStore('settings', { keyPath: 'key' }) - } + db.createObjectStore('settings', { keyPath: 'key' }) } async function parsePostHtml (htmlContent) { From 568e1eafeeb01f39621c07f8c90afe51323306cc Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 10 Apr 2024 20:10:39 +0530 Subject: [PATCH 108/149] chore: move post and outbox html in a new example directory --- outbox.html => example/outbox.html | 11 ++++++----- post.html => example/post.html | 11 +++++------ 2 files changed, 11 insertions(+), 11 deletions(-) rename outbox.html => example/outbox.html (76%) rename post.html => example/post.html (65%) diff --git a/outbox.html b/example/outbox.html similarity index 76% rename from outbox.html rename to example/outbox.html index 6e6fd1e..cd94e0d 100644 --- a/outbox.html +++ b/example/outbox.html @@ -1,22 +1,23 @@ Reader Outbox
- - + + - + - + From cdca770bbf2a79ef244675591c9a59ef25a54d87 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 01:55:40 +0530 Subject: [PATCH 109/149] feat: implement load more skip/limit in timeline --- db.js | 9 +++++---- timeline.css | 6 ++++++ timeline.js | 56 +++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/db.js b/db.js index 056fe30..07444cb 100644 --- a/db.js +++ b/db.js @@ -219,7 +219,7 @@ export class ActivityPubDB extends EventTarget { await tx.done() } - async searchNotes (criteria) { + async searchNotes (criteria, { skip = 0, limit = 10 } = {}) { const tx = this.db.transaction(NOTES_STORE, 'readonly') const notes = [] const index = criteria.attributedTo @@ -232,9 +232,10 @@ export class ActivityPubDB extends EventTarget { } // Implement additional filtering logic if needed based on other criteria (like time ranges or tags) - // For example: - // notes.filter(note => note.published >= criteria.startDate && note.published <= criteria.endDate); - return notes.sort((a, b) => b.published - a.published) // Sort by published date in descending order + const sortedNotes = notes.sort((a, b) => b.published - a.published) + const paginatedNotes = sortedNotes.slice(skip, skip + limit) + + return paginatedNotes } async ingestActor (url) { diff --git a/timeline.css b/timeline.css index e70aabf..c44f722 100644 --- a/timeline.css +++ b/timeline.css @@ -9,6 +9,12 @@ body { background: var(--bg-color); } +.load-more-btn-container { + text-align: center; + margin-top: 15px; + margin-bottom: 15px; +} + reader-timeline { flex: 1; max-width: 600px; diff --git a/timeline.js b/timeline.js index 38045d8..8c6485c 100644 --- a/timeline.js +++ b/timeline.js @@ -3,6 +3,24 @@ import { db } from './dbInstance.js' let hasLoaded = false class ReaderTimeline extends HTMLElement { + skip = 0 + limit = 10 + hasMoreItems = true + loadMoreBtn = null + + constructor () { + super() + this.loadMoreBtn = document.createElement('button') + this.loadMoreBtn.textContent = 'Load More..' + this.loadMoreBtn.className = 'load-more-btn' + + this.loadMoreBtnWrapper = document.createElement('div') + this.loadMoreBtnWrapper.className = 'load-more-btn-container' + this.loadMoreBtnWrapper.appendChild(this.loadMoreBtn) + + this.loadMoreBtn.addEventListener('click', () => this.loadMore()) + } + connectedCallback () { this.initializeDefaultFollowedActors().then(() => this.initTimeline()) } @@ -28,25 +46,35 @@ class ReaderTimeline extends HTMLElement { } async initTimeline () { - // Todo: Use filters from attributes - // TODO: Use async iterator to render - const allNotes = await db.searchNotes({}) - this.innerHTML = '' // Clear existing content - - for (const note of allNotes) { - const activityElement = document.createElement('distributed-post') - activityElement.setAttribute('url', note.id) - this.appendChild(activityElement) - } - if (!hasLoaded) { hasLoaded = true - // Dynamically load followed actors const followedActors = await db.getFollowedActors() - await Promise.all(followedActors.map(({ url }) => db.ingestActor(url))) - await this.initTimeline() } + this.loadMore() + } + + async loadMore () { + // Remove the button before loading more items + this.loadMoreBtnWrapper.remove() + + const notes = await db.searchNotes({}, { skip: this.skip, limit: this.limit }) + notes.forEach(note => this.appendNoteElement(note)) + + // Update skip value and determine if there are more items + this.skip += this.limit + this.hasMoreItems = notes.length === this.limit + + // Append the button at the end if there are more items + if (this.hasMoreItems) { + this.appendChild(this.loadMoreBtnWrapper) + } + } + + appendNoteElement (note) { + const activityElement = document.createElement('distributed-post') + activityElement.setAttribute('url', note.id) + this.appendChild(activityElement) } } From 45feccf9233ff59cc911fe855e7300ff058aa252 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 18:44:43 +0530 Subject: [PATCH 110/149] feat: implement loadmore skip/limit in outbox --- outbox.js | 114 +++++++++++++++++++++++++++------------------------ profile.html | 4 -- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/outbox.js b/outbox.js index 185ea0b..d3fda4f 100644 --- a/outbox.js +++ b/outbox.js @@ -1,33 +1,57 @@ import { db } from './dbInstance.js' class DistributedOutbox extends HTMLElement { + skip = 0 + limit = 10 + constructor () { super() this.renderedItems = new Map() // Tracks rendered items by ID - this.numPosts = 32 // Default value - this.page = 1 // Default value - this.totalPages = 0 // Keep track of total pages } static get observedAttributes () { - return ['url', 'num-posts', 'page'] + return ['url'] } connectedCallback () { - // Use attributes or default values - this.numPosts = - parseInt(this.getAttribute('num-posts'), 10) || this.numPosts - this.page = parseInt(this.getAttribute('page'), 10) || this.page - this.loadOutbox(this.getAttribute('url')) + this.outboxUrl = this.getAttribute('url') + this.loadOutbox(this.outboxUrl) } async loadOutbox (outboxUrl) { this.clearContent() - for await (const item of this.fetchOutboxItems(outboxUrl)) { - this.processItem(item) + const items = await this.collectItems(outboxUrl, { skip: this.skip, limit: this.limit + 1 }) + items.slice(0, this.limit).forEach(item => this.processItem(item)) + + // Update skip for next potential load + this.skip += this.limit + + // Check if there are more items to load + if (items.length > this.limit) { + this.createLoadMoreButton() } } + async loadMore () { + this.removeLoadMoreButton() + const items = await this.collectItems(this.outboxUrl, { skip: this.skip, limit: this.limit + 1 }) + items.slice(0, this.limit).forEach(item => this.processItem(item)) + + this.skip += this.limit + + if (items.length > this.limit) { + this.createLoadMoreButton() + } + } + + async collectItems (outboxUrl, { skip, limit }) { + const items = [] + for await (const item of db.iterateCollection(outboxUrl, { skip, limit })) { + items.push(item) + } + return items + } + processItem (item) { const itemKey = item.id || item.object if (!itemKey) { @@ -40,27 +64,6 @@ class DistributedOutbox extends HTMLElement { } } - async * fetchOutboxItems (outboxUrl) { - if (!outboxUrl) { - console.error('No outbox URL provided') - return - } - - /* - this.totalPages = Math.ceil(items.length / this.numPosts); - this.page = Math.min(this.page, this.totalPages); - - // Calculate the range of items to be loaded based on the current page and numPosts - const startIndex = (this.page - 1) * this.numPosts; - const endIndex = startIndex + this.numPosts; - - const itemsToLoad = items.slice(startIndex, endIndex); - */ - - // TODO: Ingest actor and searchActivities instead - yield * db.iterateCollection(outboxUrl) - } - renderItem (item) { const activityElement = document.createElement('distributed-activity') activityElement.type = item.type @@ -68,36 +71,39 @@ class DistributedOutbox extends HTMLElement { this.appendChild(activityElement) } - nextPage () { - const currentPage = this.page - if (currentPage < this.totalPages) { - this.setAttribute('page', currentPage + 1) - } - } + createLoadMoreButton () { + this.removeLoadMoreButton() - prevPage () { - const currentPage = this.page - this.setAttribute('page', Math.max(1, currentPage - 1)) - } + const loadMoreBtn = document.createElement('button') + loadMoreBtn.textContent = 'Load More' + loadMoreBtn.className = 'load-more-btn' - attributeChangedCallback (name, oldValue, newValue) { - if (name === 'url') { - this.clearContent() - this.loadOutbox(newValue) - } else if (name === 'num-posts' || name === 'page') { - // Convert attribute name from kebab-case to camelCase - const propName = name.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) - this[propName] = parseInt(newValue, 10) - this.clearContent() - this.loadOutbox(this.getAttribute('url')) - } + const loadMoreBtnWrapper = document.createElement('div') + loadMoreBtnWrapper.className = 'load-more-btn-container' + loadMoreBtnWrapper.appendChild(loadMoreBtn) + + loadMoreBtn.addEventListener('click', () => this.loadMore()) + this.appendChild(loadMoreBtnWrapper) } clearContent () { - // Clear existing content this.innerHTML = '' this.renderedItems.clear() } + + removeLoadMoreButton () { + const loadMoreBtnWrapper = this.querySelector('.load-more-btn-container') + if (loadMoreBtnWrapper) { + loadMoreBtnWrapper.remove() + } + } + + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'url' && newValue !== oldValue) { + this.outboxUrl = newValue + this.loadOutbox(this.outboxUrl) + } + } } // Register the new element with the browser diff --git a/profile.html b/profile.html index 23076e0..83c9e8b 100644 --- a/profile.html +++ b/profile.html @@ -16,10 +16,6 @@
-
- - -
From bfa700f3b6e53dd9c0b07fd4ab2461e5e1bda5f3 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 18:48:36 +0530 Subject: [PATCH 111/149] refactor: move loadmore styles in common.css --- common.css | 6 ++++++ timeline.css | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/common.css b/common.css index cab9abd..1be7639 100644 --- a/common.css +++ b/common.css @@ -18,6 +18,12 @@ video { position: relative; } +.load-more-btn-container { + text-align: center; + margin-top: 15px; + margin-bottom: 15px; +} + /* Empty right column for balance */ .right-column { flex: 0 0 200px; diff --git a/timeline.css b/timeline.css index c44f722..e70aabf 100644 --- a/timeline.css +++ b/timeline.css @@ -9,12 +9,6 @@ body { background: var(--bg-color); } -.load-more-btn-container { - text-align: center; - margin-top: 15px; - margin-bottom: 15px; -} - reader-timeline { flex: 1; max-width: 600px; From 293e6509cf540531c350347b5a22bc4d13b816fc Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 18:52:37 +0530 Subject: [PATCH 112/149] chore: remove old pagination from outbox example --- example/outbox.html | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/example/outbox.html b/example/outbox.html index cd94e0d..474b62f 100644 --- a/example/outbox.html +++ b/example/outbox.html @@ -12,21 +12,6 @@ url="ipns://hypha.coop/outbox.jsonld" >
-
- - -
- + From 280b384372bd9c28fd6d61e5de80776df27ede70 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 19:25:31 +0530 Subject: [PATCH 113/149] feat: add sorting functionality to iterateCollection method --- db.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/db.js b/db.js index 07444cb..0550e53 100644 --- a/db.js +++ b/db.js @@ -270,11 +270,15 @@ export class ActivityPubDB extends EventTarget { } } - async * iterateCollection (collectionOrUrl, { skip = 0, limit = 32 } = {}) { + async * iterateCollection (collectionOrUrl, { skip = 0, limit = 32, sort = 1 } = {}) { const collection = await this.#get(collectionOrUrl) - // TODO: handle pagination here, if collection contains a 'next' or 'first' link. - const items = collection.orderedItems || collection.items + let items = collection.orderedItems || collection.items + + // Sort items based on the sort parameter (1 for ascending, -1 for descending) + if (sort === -1) { + items = items.reverse() // Reverse the order for descending + } let toSkip = skip let count = 0 @@ -293,12 +297,16 @@ export class ActivityPubDB extends EventTarget { return } - // TODO: Error if no first page let next = collection.first while (next) { const page = await this.#get(next) next = page.next - const items = page.orderedItems || page.items + items = page.orderedItems || page.items + + if (sort === -1) { + items = items.reverse() // Reverse the order for descending + } + for await (const item of this.#getAll(items)) { if (toSkip) { toSkip-- From 92eb66b3b5909cf256ac321bbb99cef1fc54a419 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 23:40:34 +0530 Subject: [PATCH 114/149] refactor: sidebar to use HTML template for improved maintainability --- sidebar.html | 17 ++++++++++++++ sidebar.js | 64 ++++++++++------------------------------------------ 2 files changed, 29 insertions(+), 52 deletions(-) create mode 100644 sidebar.html diff --git a/sidebar.html b/sidebar.html new file mode 100644 index 0000000..7771739 --- /dev/null +++ b/sidebar.html @@ -0,0 +1,17 @@ + + \ No newline at end of file diff --git a/sidebar.js b/sidebar.js index 7269a7f..f0387bb 100644 --- a/sidebar.js +++ b/sidebar.js @@ -2,65 +2,25 @@ class SidebarNav extends HTMLElement { constructor () { super() this.attachShadow({ mode: 'open' }) + this.initSidebar() + } - // Create sidebar container - const sidebar = document.createElement('div') - sidebar.classList.add('sidebar') - - // Branding - const homePage = document.createElement('a') - homePage.href = './' - homePage.classList.add('home-page-link') - const headerBranding = document.createElement('h1') - headerBranding.classList.add('header-branding') - headerBranding.textContent = 'Social Reader' - homePage.appendChild(headerBranding) - sidebar.appendChild(homePage) - - // Controls - const controls = document.createElement('div') - controls.classList.add('controls') - const followingLink = document.createElement('a') - followingLink.href = './followed-accounts.html' - followingLink.textContent = 'Following · ' - - const followedCount = document.createElement('followed-count') - followingLink.appendChild(followedCount) - - controls.appendChild(followingLink) - sidebar.appendChild(controls) - - // Navigation - const nav = document.createElement('nav') - const themeSelector = document.createElement('theme-selector') - nav.appendChild(themeSelector) - const links = [ - { href: './about.html', text: 'About' }, - { - href: 'https://hypha.coop/dripline/announcing-dp-social-inbox/', - text: 'Social Inbox' - }, - { href: 'https://distributed.press', text: 'Distributed Press' } - ] - links.forEach((linkInfo) => { - const a = document.createElement('a') - a.href = linkInfo.href - a.textContent = linkInfo.text - nav.appendChild(a) - }) - sidebar.appendChild(nav) + async initSidebar () { + const response = await fetch('./sidebar.html') + const text = await response.text() + const parser = new DOMParser() + const doc = parser.parseFromString(text, 'text/html') + const template = doc.getElementById('sidebar-template') - // Append the sidebar to the shadow DOM - this.shadowRoot.appendChild(sidebar) + const instance = template.content.cloneNode(true) + this.shadowRoot.appendChild(instance) - // Style const style = document.createElement('style') - style.textContent = ` - @import url("./sidebar.css"); + style.textContent = ` + @import url("./sidebar.css"); ` this.shadowRoot.appendChild(style) } } -// Register the new element with the browser customElements.define('sidebar-nav', SidebarNav) From e8f56c7d28943150c9b6213ba2f25312368a7c60 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 23:48:55 +0530 Subject: [PATCH 115/149] style: fix mobile responsiveness for sidebar --- common.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common.css b/common.css index 1be7639..06f8ffe 100644 --- a/common.css +++ b/common.css @@ -49,4 +49,8 @@ video { .actor-profile{ width: 100%; } + + sidebar-nav { + width: 100%; + } } From 49c0559ef83da37febf6a147cb4814d9b0be9a5c Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Apr 2024 23:54:35 +0530 Subject: [PATCH 116/149] feat: support query parameters with default fallback for post rendering --- example/post.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/example/post.html b/example/post.html index 7c30fe0..b1a6987 100644 --- a/example/post.html +++ b/example/post.html @@ -13,7 +13,10 @@ + + + + + + + diff --git a/post.js b/post.js index b4b5a9e..ec93e25 100644 --- a/post.js +++ b/post.js @@ -66,7 +66,7 @@ class DistributedPost extends HTMLElement { } } - renderPostContent (jsonLdData) { + async renderPostContent (jsonLdData) { // Clear existing content this.innerHTML = '' @@ -113,20 +113,41 @@ class DistributedPost extends HTMLElement { const parser = new DOMParser() const contentDOM = parser.parseFromString(sanitizedContent, 'text/html') - // Process all anchor elements + // Process all anchor elements to handle actor and posts mentions const anchors = contentDOM.querySelectorAll('a') - anchors.forEach(anchor => { + anchors.forEach(async (anchor) => { const href = anchor.getAttribute('href') if (href) { - const fediverseMatch = href.match(/^https?:\/\/([^\/]+)\/@(\w+)$/) - if (fediverseMatch) { - // If it matches the pattern, assume it's an actor profile from a Fediverse instance - anchor.setAttribute('href', `/profile.html?actor=${encodeURIComponent(href)}`) - } else if (href.endsWith('about.jsonld')) { - // If it ends with 'about.jsonld', treat as a reader profile + 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 { - // If not recognized, keep the original href anchor.setAttribute('href', href) } } From 438a9fa0ad110c25431c99908717217460e2a399 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 3 May 2024 20:00:12 +0530 Subject: [PATCH 139/149] feat: update footer published-date and time-ago stamps to link to post view --- post.css | 10 ++++++++++ post.js | 27 +++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/post.css b/post.css index bcb0afc..f788527 100644 --- a/post.css +++ b/post.css @@ -61,6 +61,16 @@ font-size: 0.875rem; color: var(--rdp-details-color); } +.time-ago:hover{ + text-decoration: none; +} + +.full-date{ + color: var(--rdp-details-color); +} +.full-date:hover{ + text-decoration: none; +} .cw-summary { color: var(--rdp-cw-color); diff --git a/post.js b/post.js index ec93e25..35ab271 100644 --- a/post.js +++ b/post.js @@ -92,7 +92,8 @@ class DistributedPost extends HTMLElement { } // Published time element - const publishedTime = document.createElement('time') + const publishedTime = document.createElement('a') + publishedTime.href = `/post.html?url=${encodeURIComponent(jsonLdData.id)}` publishedTime.classList.add('time-ago') const elapsed = timeSince(jsonLdData.published) publishedTime.textContent = elapsed @@ -204,13 +205,27 @@ class DistributedPost extends HTMLElement { // Append the content to the post container postContainer.appendChild(postContent) - // Footer of the post, which will contain the full published date and platform + // 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') - const fullDate = document.createElement('div') - fullDate.classList.add('full-date') - fullDate.textContent = formatDate(jsonLdData.published) + ' · reader web' - postFooter.appendChild(fullDate) + + // 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) // Handle attachments of other Fedi instances if (!isSensitive && !jsonLdData.summary && jsonLdData.attachment && jsonLdData.attachment.length > 0) { From f138ff516b0b474e8374a4ef07c3a361e941e170 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 3 May 2024 20:07:29 +0530 Subject: [PATCH 140/149] feat: update actor and post links to use primary URLs or IDs --- post.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/post.js b/post.js index 35ab271..46fe5c5 100644 --- a/post.js +++ b/post.js @@ -93,7 +93,8 @@ class DistributedPost extends HTMLElement { // Published time element const publishedTime = document.createElement('a') - publishedTime.href = `/post.html?url=${encodeURIComponent(jsonLdData.id)}` + const postUrl = jsonLdData.id || jsonLdData.object.id + publishedTime.href = `/post.html?url=${encodeURIComponent(postUrl)}` publishedTime.classList.add('time-ago') const elapsed = timeSince(jsonLdData.published) publishedTime.textContent = elapsed @@ -332,9 +333,9 @@ class ActorInfo extends HTMLElement { 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 + iconUrl = actorInfo.icon[0].url || actorInfo.id } else if (actorInfo.icon.url) { - iconUrl = actorInfo.icon.url + iconUrl = actorInfo.icon.url || actorInfo.id } } From c20c9dbd10262aafbfc8d63d9722be0785b1811a Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 3 May 2024 20:18:13 +0530 Subject: [PATCH 141/149] feat: load default followed actors from p2p protocols --- timeline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/timeline.js b/timeline.js index eec2a5c..72546d4 100644 --- a/timeline.js +++ b/timeline.js @@ -28,8 +28,8 @@ class ReaderTimeline extends HTMLElement { async initializeDefaultFollowedActors () { const defaultActors = [ 'https://social.distributed.press/v1/@announcements@social.distributed.press/', - 'https://distributed.press/about.jsonld', - 'https://hypha.coop/about.jsonld', + 'ipns://distributed.press/about.jsonld', + 'hyper://hypha.coop/about.jsonld', 'https://sutty.nl/about.jsonld' // "https://akhilesh.sutty.nl/about.jsonld", // "https://staticpub.mauve.moe/about.jsonld", From 28b1e660dfb3bd133dcb6aa89b5934bd9f6b8c93 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Sat, 4 May 2024 01:20:30 +0530 Subject: [PATCH 142/149] refactor: use sidebar component name instead of :host in styles --- sidebar.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sidebar.css b/sidebar.css index fec8796..999bbbc 100644 --- a/sidebar.css +++ b/sidebar.css @@ -6,7 +6,7 @@ cursor: pointer; } -:host { +sidebar-nav { position: sticky; top: 0; flex: 0 0 250px; @@ -51,14 +51,14 @@ nav a:hover { } @media screen and (max-width: 1280px) { - :host { + sidebar-nav { flex: 0 0 200px; margin-left: 14px; } } @media screen and (max-width: 768px) { - :host { + sidebar-nav { width: 100%; position: fixed; top: 0; @@ -80,7 +80,7 @@ nav a:hover { } @media screen and (max-width: 400px) { - :host { + sidebar-nav { padding-bottom: 50px; } } From f1107960938fb965b1d6a199f6531dc3c1e5c79e Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Sat, 4 May 2024 01:33:09 +0530 Subject: [PATCH 143/149] fix: ensure item array initialization for TypeError in iterateCollection --- db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db.js b/db.js index 2c80b4c..9dcd007 100644 --- a/db.js +++ b/db.js @@ -291,7 +291,7 @@ export class ActivityPubDB extends EventTarget { async * iterateCollection (collectionOrUrl, { skip = 0, limit = DEFAULT_LIMIT, sort = 1 } = {}) { const collection = await this.#get(collectionOrUrl) - let items = collection.orderedItems || collection.items + let items = collection.orderedItems || collection.items || [] let next, prev if (sort === -1) { From 25035fd43949e79e5188ccb13500d4b7097a0510 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 7 May 2024 17:23:11 +0530 Subject: [PATCH 144/149] chore: update p2p actor and post URLs --- example/post.html | 2 +- post.html | 2 +- timeline.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/post.html b/example/post.html index b1a6987..5b99b51 100644 --- a/example/post.html +++ b/example/post.html @@ -15,7 +15,7 @@