Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add p2p URL support across all content in the reader #20

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions actor-mini-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ class ActorMiniProfile extends HTMLElement {
}

// Actor icon
const img = document.createElement('img')
img.className = 'profile-mini-icon'
img.src = iconUrl
img.alt = actorInfo.name ? actorInfo.name : 'Actor icon'
clickableContainer.appendChild(img)
const p2pImage = document.createElement('p2p-image')
p2pImage.className = 'profile-mini-icon'
p2pImage.alt = actorInfo.name ? actorInfo.name : 'Actor icon'
clickableContainer.appendChild(p2pImage)

// Actor name
if (actorInfo.name) {
Expand Down
10 changes: 5 additions & 5 deletions actor-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ class ActorProfile extends HTMLElement {
}
}

const img = document.createElement('img')
img.classList.add('profile-icon')
img.src = iconUrl
img.alt = actorInfo.name ? actorInfo.name : 'Actor icon'
actorContainer.appendChild(img) // Append to the actor container
const p2pImage = document.createElement('p2p-image')
p2pImage.setAttribute('src', iconUrl)
p2pImage.classList.add('profile-icon')
p2pImage.alt = actorInfo.name ? actorInfo.name : 'Actor icon'
actorContainer.appendChild(p2pImage) // Append to the actor container

if (actorInfo.name) {
const pName = document.createElement('div')
Expand Down
23 changes: 23 additions & 0 deletions db.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ export function isP2P (url) {
return url.startsWith(HYPER_PREFIX) || url.startsWith(IPNS_PREFIX)
}

export async function supportsP2P (url) {
try {
const response = await fetch(url)
return response.ok
} catch (error) {
console.log('P2P URL loading failed:', error)
return false
}
}

export function resolveP2PUrl (url) {
if (!url) return url

if (url.startsWith(HYPER_PREFIX)) {
return url.replace(HYPER_PREFIX, 'https://hyper.hypha.coop/hyper/')
} else if (url.startsWith(IPNS_PREFIX)) {
return url.replace(IPNS_PREFIX, 'https://ipfs.hypha.coop/ipns/')
}

return url
}

export class ActivityPubDB extends EventTarget {
constructor (db, fetch = globalThis.fetch) {
super()
Expand Down Expand Up @@ -103,6 +125,7 @@ export class ActivityPubDB extends EventTarget {
if (url && typeof url === 'object') {
return url
}

let response
// Try fetching directly for all URLs (including P2P URLs)
// TODO: Signed fetch
Expand Down
1 change: 1 addition & 0 deletions followed-accounts.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@
<script type="module" src="./sidebar.js"></script>
<script type="module" src="./followed-accounts.js"></script>
<script type="module" src="./actor-mini-profile.js"></script>
<script type="module" src="./p2p-media.js"></script>
<script type="module" src="./theme-selector.js"></script>
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<script type="module" src="./timeline.js"></script>
<script type="module" src="./outbox.js"></script>
<script type="module" src="./post.js"></script>
<script type="module" src="./p2p-media.js"></script>
<script type="module" src="./followed-accounts.js"></script>
<script type="module" src="./theme-selector.js"></script>
<script type="module" src="./error-message.js"></script>
102 changes: 102 additions & 0 deletions p2p-media.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { supportsP2P, resolveP2PUrl } from './db.js'

class P2PImage extends HTMLElement {
constructor () {
super()

this.img = document.createElement('img')
this.attachShadow({ mode: 'open' })
this.shadowRoot.appendChild(this.img)
this.img.addEventListener('error', () => this.handleError())
}

static get observedAttributes () {
return ['src']
}

connectedCallback () {
if (this.hasAttribute('src')) {
this.loadImage(this.getAttribute('src'))
}
}

attributeChangedCallback (name, oldValue, newValue) {
if (name === 'src' && newValue !== oldValue) {
this.loadImage(newValue)
}
}

async loadImage (src) {
try {
const p2pSupported = await supportsP2P(src)
if (p2pSupported) {
this.img.src = src // Attempt to load the original P2P URL
} else {
this.handleError()
}
} catch (error) {
this.handleError()
}
}

async handleError () {
const fallbackSrc = resolveP2PUrl(this.getAttribute('src'))
console.log(`Failed to load image. Resolving to gateway URL: ${fallbackSrc}`)
this.img.src = fallbackSrc
}
}

customElements.define('p2p-image', P2PImage)

class P2PVideo extends HTMLElement {
constructor () {
super()
this.video = document.createElement('video')
this.video.controls = true
this.attachShadow({ mode: 'open' })
this.shadowRoot.appendChild(this.video)
}

static get observedAttributes () {
return ['src']
}

connectedCallback () {
if (this.hasAttribute('src')) {
this.loadVideo(this.getAttribute('src'))
}
}

attributeChangedCallback (name, oldValue, newValue) {
if (name === 'src' && newValue !== oldValue) {
this.loadVideo(newValue)
}
}

async loadVideo (src) {
this.video.innerHTML = '' // Clear any existing sources
const source = document.createElement('source')
source.src = src

try {
const p2pSupported = await supportsP2P(src)
if (!p2pSupported) {
throw new Error('P2P not supported for this URL')
}
} catch (error) {
this.handleError(source)
return // Skip setting the source if not supported
}

source.onerror = () => this.handleError(source)
this.video.appendChild(source)
}

handleError (source) {
const fallbackSrc = resolveP2PUrl(source.src)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, check if the current img.src is p2p

console.log(`Failed to load video source. Resolving to gateway URL: ${fallbackSrc}`)
source.src = fallbackSrc
}
}

customElements.define('p2p-video', P2PVideo)
1 change: 1 addition & 0 deletions post.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<script type="module" src="./sidebar.js"></script>
<script type="module" src="./timeline.js"></script>
<script type="module" src="./post.js"></script>
<script type="module" src="./p2p-media.js"></script>
<script type="module" src="./followed-accounts.js"></script>
<script type="module" src="./theme-selector.js"></script>
<script type="module" src="./error-message.js"></script>
52 changes: 47 additions & 5 deletions post.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global customElements, HTMLElement */
import DOMPurify from './dependencies/dompurify/purify.js'
import { db } from './dbInstance.js'
import { resolveP2PUrl, isP2P } from './db.js'

function formatDate (dateString) {
const options = { year: 'numeric', month: 'short', day: 'numeric' }
Expand Down Expand Up @@ -39,6 +40,45 @@ function timeSince (dateString) {
return Math.floor(seconds) + 's'
}

function insertImagesAndVideos (content) {
const parser = new DOMParser()
const contentDOM = parser.parseFromString(content, 'text/html')

// Replace all <img> tags with <p2p-image> tags
contentDOM.querySelectorAll('img').forEach(img => {
const originalSrc = img.getAttribute('src')
console.log(`Original img src: ${originalSrc}`)
const p2pImg = document.createElement('p2p-image')
p2pImg.setAttribute('src', originalSrc)
img.parentNode.replaceChild(p2pImg, img)
console.log(`Replaced img with p2p-image having src: ${p2pImg.getAttribute('src')}`)
})

// Replace all <video> tags with <p2p-video> tags
contentDOM.querySelectorAll('video').forEach(video => {
const p2pVideo = document.createElement('p2p-video')
if (video.hasAttribute('src')) {
const originalSrc = video.getAttribute('src')
p2pVideo.setAttribute('src', originalSrc)
}
Array.from(video.children).forEach(source => {
if (source.tagName === 'SOURCE') {
const originalSrc = source.getAttribute('src')
console.log(`Original video src: ${originalSrc}`)
const srcType = source.getAttribute('type')
const p2pSource = document.createElement('source')
p2pSource.setAttribute('src', originalSrc)
p2pSource.setAttribute('type', srcType)
p2pVideo.appendChild(p2pSource)
console.log(`Replaced video with p2p-video having src: ${p2pSource.getAttribute('src')}`)
}
})
video.parentNode.replaceChild(p2pVideo, video)
})

return contentDOM.body.innerHTML
}

// Define a class for the <distributed-post> web component
class DistributedPost extends HTMLElement {
static get observedAttributes () {
Expand All @@ -57,9 +97,11 @@ class DistributedPost extends HTMLElement {

try {
const content = await db.getNote(postUrl)

// Assuming JSON-LD content has a "summary" field
this.renderPostContent(content)
if (content && content.content) {
content.content = insertImagesAndVideos(content.content) // Resolve URLs before rendering
// Assuming JSON-LD content has a "summary" field
this.renderPostContent(content)
}
} catch (error) {
console.error(error)
this.renderErrorContent(error.message)
Expand Down Expand Up @@ -115,7 +157,6 @@ class DistributedPost extends HTMLElement {
// Determine content source based on structure of jsonLdData
const contentSource = jsonLdData.content || (jsonLdData.object && jsonLdData.object.content)

// Sanitize content and create a DOM from it
const sanitizedContent = DOMPurify.sanitize(contentSource)
const parser = new DOMParser()
const contentDOM = parser.parseFromString(sanitizedContent, 'text/html')
Expand Down Expand Up @@ -322,6 +363,7 @@ class ActorInfo extends HTMLElement {
}

async fetchAndRenderActorInfo (url) {
url = await resolveP2PUrl(url)
try {
const actorInfo = await db.getActor(url)
if (actorInfo) {
Expand All @@ -346,7 +388,7 @@ class ActorInfo extends HTMLElement {

const img = document.createElement('img')
img.classList.add('actor-icon')
img.src = iconUrl
img.src = resolveP2PUrl(iconUrl)
img.alt = actorInfo.name ? actorInfo.name : 'Actor icon'
img.addEventListener('click', this.navigateToActorProfile.bind(this))
author.appendChild(img)
Expand Down
1 change: 1 addition & 0 deletions profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<script type="module" src="./sidebar.js"></script>
<script type="module" src="./actor-profile.js"></script>
<script type="module" src="./post.js"></script>
<script type="module" src="./p2p-media.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have post.js import any of it's dependencies directly instead of doing it top level.

Similarly outbox should import post.

<script type="module" src="./outbox.js"></script>
<script type="module" src="./followed-accounts.js"></script>
<script type="module" src="./theme-selector.js"></script>
Expand Down