From 82060156e3d469a74e930368287ddf97a1d17f39 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Tue, 10 Mar 2026 18:01:50 +0100 Subject: [PATCH] feat: add local gateway URL options to share modal allow copying local gateway links for use in external apps, with optional localhost subdomain mode for web apps - actions.js: doFilesShareLink returns local and subdomain links - ShareModal: checkbox to toggle local link, nested checkbox for localhost subdomains, QR hidden in local mode - Modals: passes new link variants to ShareModal - en/files.json: added translation keys for new UI elements --- public/locales/en/files.json | 5 +- src/bundles/files/actions.js | 16 +++- src/files/modals/Modals.js | 14 ++- src/files/modals/share-modal/ShareModal.js | 93 +++++++++++++------ .../modals/share-modal/ShareModal.stories.js | 2 + 5 files changed, 98 insertions(+), 32 deletions(-) diff --git a/public/locales/en/files.json b/public/locales/en/files.json index dd96da470..d1a3094e5 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -28,7 +28,10 @@ }, "shareModal": { "title": "Share files", - "description": "Copy the link below and share it with your friends." + "description": "Copy the link below and share it with your friends.", + "descriptionLocal": "Use this link to open in apps running on this machine.", + "useLocalLink": "Local link for other apps on this machine", + "useSubdomains": "Use localhost subdomains for web apps" }, "renameModal": { "titleFile": "Rename file", diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js index 44bca1289..401c18612 100644 --- a/src/bundles/files/actions.js +++ b/src/bundles/files/actions.js @@ -600,12 +600,26 @@ const actions = () => ({ // ensureMFS deliberately omitted here, see https://github.com/ipfs/ipfs-webui/issues/1744 for context. const publicGateway = store.selectPublicGateway() const publicSubdomainGateway = store.selectPublicSubdomainGateway() + const gatewayUrl = store.selectGatewayUrl() const { link: shareableLink, cid } = await getShareableLink(files, publicGateway, publicSubdomainGateway, ipfs) + // Build local gateway link for use in external apps + let filename = '' + if (files.length === 1 && files[0].type === 'file') { + filename = `?filename=${encodeURIComponent(files[0].name)}` + } + const localLink = `${gatewayUrl}/ipfs/${cid}${filename}` + + // Build localhost subdomain link for web apps (origin isolation) + const gwUrl = new URL(gatewayUrl) + const base32Cid = cid.toV1().toString() + const port = gwUrl.port ? `:${gwUrl.port}` : '' + const subdomainLocalLink = `http://${base32Cid}.ipfs.localhost${port}/${filename}` + // Trigger background provide operation with the CID from getShareableLink dispatchAsyncProvide(cid, ipfs) - return shareableLink + return { link: shareableLink, localLink, subdomainLocalLink } }), /** diff --git a/src/files/modals/Modals.js b/src/files/modals/Modals.js index 25e013acd..e4284ac5e 100644 --- a/src/files/modals/Modals.js +++ b/src/files/modals/Modals.js @@ -64,6 +64,8 @@ class Modals extends React.Component { files: [] }, link: '', + localLink: '', + subdomainLocalLink: '', command: 'ipfs --help' } @@ -132,10 +134,16 @@ class Modals extends React.Component { case SHARE: { this.setState({ link: t('generating'), + localLink: '', + subdomainLocalLink: '', readyToShow: true }) - onShareLink(files).then(link => this.setState({ link })) + onShareLink(files).then(result => this.setState({ + link: result.link, + localLink: result.localLink, + subdomainLocalLink: result.subdomainLocalLink + })) break } case RENAME: { @@ -245,7 +253,7 @@ class Modals extends React.Component { render () { const { show, t } = this.props - const { readyToShow, link, rename, command } = this.state + const { readyToShow, link, localLink, subdomainLocalLink, rename, command } = this.state return (
@@ -259,6 +267,8 @@ class Modals extends React.Component { diff --git a/src/files/modals/share-modal/ShareModal.js b/src/files/modals/share-modal/ShareModal.js index 8ea14daf8..794c4f6fb 100644 --- a/src/files/modals/share-modal/ShareModal.js +++ b/src/files/modals/share-modal/ShareModal.js @@ -1,48 +1,85 @@ -import React from 'react' +import React, { useState } from 'react' import PropTypes from 'prop-types' import QRCode from 'react-qr-code' import Button from '../../../components/button/button.tsx' +import Checkbox from '../../../components/checkbox/Checkbox.js' import { withTranslation } from 'react-i18next' import { CopyToClipboard } from 'react-copy-to-clipboard' import { Modal, ModalActions, ModalBody } from '../../../components/modal/modal' -const ShareModal = ({ t, tReady, onLeave, link, className, ...props }) => ( - - -

{t('shareModal.description')}

-
- -
-
- -
-
+const ShareModal = ({ t, tReady, onLeave, link, localLink, subdomainLocalLink, className, ...props }) => { + const [useLocalLink, setUseLocalLink] = useState(false) + const [useSubdomains, setUseSubdomains] = useState(false) - - - - - - -
-) + let activeLink = link + if (useLocalLink && localLink) { + activeLink = useSubdomains && subdomainLocalLink ? subdomainLocalLink : localLink + } + + return ( + + +

+ {useLocalLink ? t('shareModal.descriptionLocal') : t('shareModal.description')} +

+ {!useLocalLink && ( +
+ +
+ )} +
+ +
+ {localLink && ( +
+ +
+ )} + {useLocalLink && subdomainLocalLink && ( +
+ +
+ )} +
+ + + + + + + +
+ ) +} ShareModal.propTypes = { onLeave: PropTypes.func.isRequired, link: PropTypes.string, + localLink: PropTypes.string, + subdomainLocalLink: PropTypes.string, t: PropTypes.func.isRequired, tReady: PropTypes.bool.isRequired } ShareModal.defaultProps = { - className: '' + className: '', + localLink: '', + subdomainLocalLink: '' } export default withTranslation('files')(ShareModal) diff --git a/src/files/modals/share-modal/ShareModal.stories.js b/src/files/modals/share-modal/ShareModal.stories.js index 5cde1cc5f..299310f8c 100644 --- a/src/files/modals/share-modal/ShareModal.stories.js +++ b/src/files/modals/share-modal/ShareModal.stories.js @@ -19,6 +19,8 @@ export const Share = () => (
)