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: embed provider webrtc maddr into share link #206

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
13,639 changes: 5,201 additions & 8,438 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@helia/mfs": "^4.0.1",
"@helia/unixfs": "^4.0.0",
"@libp2p/devtools-metrics": "^1.2.0",
"@multiformats/multiaddr": "^12.3.4",
"@multiformats/multiaddr-matcher": "^1.6.0",
"@sgtpooki/file-type": "^1.0.1",
"blob-to-it": "^2.0.7",
Expand Down
16 changes: 2 additions & 14 deletions src/components/box/box.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import classNames from 'classnames'
import React, { forwardRef, useEffect } from 'react'
import React, { forwardRef } from 'react'
import { useDrop } from 'react-dnd'
import { NativeTypes } from 'react-dnd-html5-backend'
import { Trans, useTranslation } from 'react-i18next'
import { useFiles, useFilesDispatch, useAddFiles } from '../../hooks/use-files.js'
import { useHelia } from '../../hooks/use-helia.js'
import { useDownloadInfo } from '../../providers/download-provider.jsx'
import { AddFiles } from '../add-files/add-files.jsx'
import { DownloadFiles } from '../download-files/download-files.jsx'
import { FileTree } from '../file-tree/file-tree.jsx'
Expand All @@ -23,19 +22,8 @@ export const Box = forwardRef<HTMLDivElement, { children: any, className?: strin
Box.displayName = 'Box'

export const BoxDownload = (): React.JSX.Element => {
const dispatch = useFilesDispatch()
const { files, filesToFetch } = useFiles()
const { cid, filename } = useDownloadInfo()
const { helia, nodeInfo } = useHelia()
const { multiaddrs } = nodeInfo ?? { multiaddrs: [] }
const isLoading = filesToFetch.length !== 0 || Object.keys(files).length === 0

useEffect(() => {
if (helia == null) return
// if (multiaddrs.length === 0) return
if (cid == null) return
dispatch({ type: 'fetch_start', cid, filename })
}, [cid, filename, helia, multiaddrs.length])
const isLoading = filesToFetch.length > 0 || Object.keys(files).length === 0

return (
<Box>
Expand Down
5 changes: 3 additions & 2 deletions src/components/file/file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CircularProgressbar } from 'react-circular-progressbar'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { useTranslation } from 'react-i18next'
import { useHelia } from '../../hooks/use-helia.js'
import { getWebRTCAddrs } from '../../lib/share-addresses.js'
import { formatBytes } from '../../lib/size.js'
import IconDownload from '../../media/icons/download.svg'
import IconView from '../../media/icons/view.svg'
Expand All @@ -24,7 +25,7 @@ export const File = ({ file, isDownload }: { file: FileState, isDownload?: boole
// TODO: implement progress
const [progress] = useState(100)
const [copied, setCopied] = useState(false)
const { unixfs } = useHelia()
const { unixfs, nodeInfo } = useHelia()

const [showModalView, setShowModalView] = useState(false)

Expand Down Expand Up @@ -137,7 +138,7 @@ export const File = ({ file, isDownload }: { file: FileState, isDownload?: boole
const fileNameClass = classnames({ charcoal: error == null, gray: error }, ['FileLinkName ph2 f6 b truncate'])
const fileSizeClass = classnames({ 'charcoal-muted': error == null, gray: error }, ['f6'])

const url = getShareLink(cid, name)
const url = getShareLink({ cid, name, webrtcMaddrs: getWebRTCAddrs(nodeInfo?.multiaddrs) })

return (
<div className='mv2 flex items-center justify-between'>
Expand Down
17 changes: 13 additions & 4 deletions src/components/file/utils/get-share-link.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { type CID } from 'multiformats/cid'
export function getShareLink (cid: string | CID, name?: string): string {
if (name === undefined) {
return new URL(`/#/${cid}`, window.location.href).toString()
import type { Multiaddr } from '@multiformats/multiaddr'

export function getShareLink ({ cid, name, webrtcMaddrs }: { cid: string | CID, name?: string, webrtcMaddrs?: Multiaddr[] }): string {
const url = new URL(`/#/${cid}?`, window.location.href)

if (name !== undefined) {
url.hash += `filename=${encodeURIComponent(name)}`
}

if (webrtcMaddrs !== undefined) {
const encodedMaddrs = webrtcMaddrs.map(addr => addr.toString()).join(',')
url.hash += `&maddrs=${encodeURIComponent(encodedMaddrs)}`
}

return new URL(`/#/${cid}?filename=${encodeURI(name)}`, window.location.href).toString()
return url.toString()
}
2 changes: 1 addition & 1 deletion src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const Header = (): React.JSX.Element => {
<div className='h3 ba mh2 aqua' />
<div className='ml2 pb2 f2 fw1 aqua montserrat'>{t('header')}</div>
</div>
<div className='ml-auto mt2-l mb0-l pa3 pb0 w-100 order-2-l pl3 pl4-ns mw7-l' style={{ marginLeft: 'auto' }}>
<div className='ml-auto mt2-l mb0-l pa3 pb0 w-100 order-2-l pl3 pl4-ns mw7-l'>
<NodeInfo />
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/node-info/node-info-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export interface NodeInfoDetailProps {
export const NodeInfoDetail: React.FC<NodeInfoDetailProps> = ({ label, value }) => {
return (
<p>
<span className='aqua'>{label}: </span><span>{value}</span>
<span className='aqua'>{label}: </span>
<span style={{ overflowWrap: 'break-word' }}>{value}</span>
</p>
)
}
55 changes: 38 additions & 17 deletions src/components/node-info/node-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type Connection } from '@libp2p/interface'
import { type Multiaddr } from '@multiformats/multiaddr'
import { Circuit, WebRTC, WebRTCDirect, WebSockets, WebSocketsSecure, WebTransport } from '@multiformats/multiaddr-matcher'
import React, { useMemo } from 'react'
import { useFilesDispatch, useFiles } from '../../hooks/use-files.js'
import { useHelia } from '../../hooks/use-helia.js'
import { NodeInfoDetail } from './node-info-detail.jsx'

Expand All @@ -12,41 +13,45 @@ export interface NodeInfoProps {

export const NodeInfo: React.FC<NodeInfoProps> = () => {
const { nodeInfo } = useHelia()
const dispatch = useFilesDispatch()
const { peerId, multiaddrs, connections } = nodeInfo ?? {}
const { provideToDHT } = useFiles()

const { listeningAddrs, circuitRelayAddrs, webRtc, webRtcDirect, webTransport, webSockets, webSocketsSecure } = useMemo(() => {
const base = {
listeningAddrs: 0,
circuitRelayAddrs: 0,
webRtc: 0,
webRtcDirect: 0,
webTransport: 0,
webSockets: 0,
webSocketsSecure: 0
listeningAddrs: 0, // total listening addrs
circuitRelayAddrs: 0, // circuit relay addrs
webRtc: 0, // Any WebRTC address including a TCP relay one which isn't very useful for other browsers
webRtcDirect: 0, // WebRTC Direct circuit relay addrs which can be used for browser WebRTC signalling
webTransport: 0, // WebTransport circuit relay addrs which can be used for browser WebRTC signalling
webSockets: 0, // WebSockets circuit relay addrs which can be used for browser WebRTC signalling
webSocketsSecure: 0 // Secure WebSockets circuit relay addrs which can be used for browser WebRTC signalling
}

if (multiaddrs == null) {
return base
}
return multiaddrs.reduce(
(acc: typeof base, addr: Multiaddr) => {
acc.listeningAddrs++
if (Circuit.exactMatch(addr)) {
acc.circuitRelayAddrs++
} else if (WebRTC.exactMatch(addr)) {
}
if (WebRTC.exactMatch(addr)) {
acc.webRtc++
} else if (WebRTCDirect.exactMatch(addr)) {
}
if (Circuit.exactMatch(addr) && WebRTCDirect.matches(addr)) {
acc.webRtcDirect++
} else if (WebTransport.exactMatch(addr)) {
}
if (Circuit.exactMatch(addr) && WebTransport.matches(addr)) {
acc.webTransport++
} else if (WebSockets.exactMatch(addr)) {
}
if (Circuit.exactMatch(addr) && WebSockets.matches(addr)) {
acc.webSockets++
} else if (WebSocketsSecure.exactMatch(addr)) {
}
if (Circuit.exactMatch(addr) && WebSocketsSecure.matches(addr)) {
acc.webSocketsSecure++
} else {
// eslint-disable-next-line no-console
console.log('unrecognized listen addr', addr.toString())
}
acc.listeningAddrs++
return acc
},
base
Expand Down Expand Up @@ -83,8 +88,24 @@ export const NodeInfo: React.FC<NodeInfoProps> = () => {
return (
<div className='ml2 pb2 f5 gray-muted montserrat mw7'>
<NodeInfoDetail label='Peer ID' value={peerId} />
<NodeInfoDetail label='ListeningAddrs' value={`${listeningAddrs} (relayed: ${circuitRelayAddrs}, webRtc: ${webRtc}, webRtcDirect: ${webRtcDirect}, webTransport: ${webTransport}, ws: ${webSockets}, wss: ${webSocketsSecure})`} />
<NodeInfoDetail label='ListeningAddrs' value={`${listeningAddrs} (relayed: ${circuitRelayAddrs}, WebRTC: ${webRtc}, Secure WebSockets: ${webSocketsSecure}, WebRTC Direct: ${webRtcDirect}, WebTransport: ${webTransport}, WebSockets: ${webSockets})`} />
{/*
Dialable from other Browsers exclude:
- WebTransport which is not included in the default Helia transports due to flaky browser support.
- WebSocket when in secure contexts
*/}
<NodeInfoDetail label='Dialable from other Browsers' value={` ${(webRtcDirect + webSocketsSecure + (window.isSecureContext ? webSockets : 0)) > 0 ? '✅' : '❌'}`} />
<NodeInfoDetail label='Connections' value={`${totalConns} (${inboundConns} in, ${outboundConns} out, ${unlimitedConns} unlimited)`} />
<div className='flex items-center mt2'>
<input
type="checkbox"
id="provideToDHT"
checked={provideToDHT}
onChange={(e) => { dispatch({ type: 'set_provide_to_dht', provideToDHT: e.target.checked }) }}
className="mr2"
/>
<label htmlFor="provideToDHT" className="ma0">Provide CIDs to DHT</label>
</div>
</div>
)
}
Expand Down
19 changes: 15 additions & 4 deletions src/hooks/use-current-page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { multiaddr } from '@multiformats/multiaddr'
import { cid } from 'is-ipfs'
import { useEffect } from 'react'
import { useHashLocation } from 'wouter/use-hash-location'
import { useDownloadInfo } from '../providers/download-provider'
import { useFilesDispatch } from './use-files'

/**
* * `(?<=\/)` — Positive lookbehind to ensure that the match is preceded by / without including it in the result.
Expand All @@ -12,17 +13,27 @@ const cidRegex = /(?<=\/)[^/?]+(?=\?|$)/

const filenameRegex = /(?<=filename=)[^&]+/

const maddrsRegex = /(?<=maddrs=)[^&]+/

export type CurrentPage = 'add' | 'download'
export const useCurrentPage = (): CurrentPage => {
const [location] = useHashLocation()
const { setDownloadInfo } = useDownloadInfo()
const dispatch = useFilesDispatch()
const maybeCid = location.match(cidRegex)?.[0] ?? null
const filename = location.match(filenameRegex)?.[0] ?? null
const maddrs = location.match(maddrsRegex)?.[0] ?? null

// Dispatch the fetch_start action if the URL contains a cid
useEffect(() => {
if (maybeCid == null) return
setDownloadInfo(maybeCid, filename)
}, [maybeCid, filename])
dispatch({ type: 'reset_files' })
const decodedFilename = decodeURIComponent(filename ?? '')
// Decode the provider's maddrs from the URL
const decodedMaddrs = maddrs != null ? decodeURIComponent(maddrs).split(',') : null
const multiaddrs = decodedMaddrs != null ? decodedMaddrs.map(maddr => multiaddr(maddr)) : null
Comment on lines +32 to +33
Copy link
Member

Choose a reason for hiding this comment

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

nit: I feel like a separate useMemo on a maddrs dep with the fully resolved multiaddrs would be a little better.. but there is likely little difference.. sometimes react can get wonky if you have different effects too separated.


dispatch({ type: 'fetch_start', cid: maybeCid, filename: decodedFilename, providerMaddrs: multiaddrs })
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to cancel the fetch on unmount. maybe create an abort controller and pass signal to fetch_start, and abort on unmount?

Suggested change
dispatch({ type: 'fetch_start', cid: maybeCid, filename: decodedFilename, providerMaddrs: multiaddrs })
const abrtCtl = new AbortController()
// ...
dispatch({ type: 'fetch_start', cid: maybeCid, filename: decodedFilename, providerMaddrs: multiaddrs, signal: abrtCtl.signal })
return () => {
// cancel fetching
abrtCtl.abort('unmounted effect..')
}

}, [maybeCid, filename, maddrs])

if (location.startsWith('/add') || !cid(maybeCid ?? '')) {
return 'add'
Expand Down
1 change: 0 additions & 1 deletion src/hooks/use-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export function useAddFiles (dispatch: Dispatch<FilesAction>, heliaState: HeliaC
size: _file.size,
progress: 0,
cid,
pending: true,
published: false
}
dispatch({ type: 'add_start', ...file })
Expand Down
3 changes: 0 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { createRoot } from 'react-dom/client'
import { I18nextProvider } from 'react-i18next'
import { App } from './app.jsx'
import i18n from './i18n'
import { DownloadProvider } from './providers/download-provider.jsx'
import { FilesProvider } from './providers/files-provider.jsx'
import { HeliaProvider } from './providers/helia-provider.jsx'
import registerServiceWorker from './register-service-worker.js'
Expand All @@ -21,9 +20,7 @@ root.render(
<I18nextProvider i18n={i18n} >
<HeliaProvider>
<FilesProvider>
<DownloadProvider>
<App />
</DownloadProvider>
</FilesProvider>
</HeliaProvider>
</I18nextProvider>
Expand Down
7 changes: 7 additions & 0 deletions src/lib/share-addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { WebRTC } from '@multiformats/multiaddr-matcher'
import type { Multiaddr } from '@multiformats/multiaddr'

export const getWebRTCAddrs = (addrs?: Multiaddr[]): Multiaddr[] => {
// TODO: Get only WebRTC addrs dialable from other browsers, e.g. WebRTC Direct, WebRTC Secure WebSockets, and WebRTC WebTransport (currently not included in Helia transports)
return addrs?.filter((addr: Multiaddr) => WebRTC.exactMatch(addr)) ?? []
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

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

are you talking about:

Suggested change
// TODO: Get only WebRTC addrs dialable from other browsers, e.g. WebRTC Direct, WebRTC Secure WebSockets, and WebRTC WebTransport (currently not included in Helia transports)
return addrs?.filter((addr: Multiaddr) => WebRTC.exactMatch(addr)) ?? []
return addrs?.filter((addr: Multiaddr) => WebRTC.exactMatch(addr) || (Circuit.exactMatch(addr) && (WebRTCDirect.matches(addr) || WebTransport.matches(addr) || WebSockets.matches(addr) || WebSocketsSecure.matches(addr)))) ?? []

}
44 changes: 0 additions & 44 deletions src/providers/download-provider.tsx

This file was deleted.

Loading
Loading