From e6403f19e46651be30498e2ab8c1bf49760c853f Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 14 Aug 2021 10:16:22 +0200 Subject: [PATCH 1/6] fix: local gateway and ipfs.io when cannot preview License: MIT Signed-off-by: Henrique Dias --- public/locales/en/files.json | 2 +- src/files/file-preview/FilePreview.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/public/locales/en/files.json b/public/locales/en/files.json index 9e379bca4..e595c81df 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -10,7 +10,7 @@ "individualFilesOnly": "Only available for individual files", "noPDFSupport": "Your browser does not support PDFs. Please download the PDF to view it:", "downloadPDF": "Download PDF", - "downloadInstead": "Try <1>downloading it instead.", + "openInstead": "Try opening it instead <1>with your local gateway or <1>with ipfs.io.", "cantBePreviewed": "Sorry, this file can’t be previewed", "addByPath": "From IPFS", "newFolder": "New folder", diff --git a/src/files/file-preview/FilePreview.js b/src/files/file-preview/FilePreview.js index b4292d79d..38968b806 100644 --- a/src/files/file-preview/FilePreview.js +++ b/src/files/file-preview/FilePreview.js @@ -28,7 +28,7 @@ const Preview = (props) => { } -const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl: gatewayUrl, read, onDownload }) => { +const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl, gatewayUrl: localGatewayUrl, read, onDownload }) => { const [content, setContent] = useState(null) const [hasMoreContent, setHasMoreContent] = useState(false) const [buffer, setBuffer] = useState(null) @@ -56,7 +56,7 @@ const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl: gatewayUrl }, // eslint-disable-next-line react-hooks/exhaustive-deps []) - const src = `${gatewayUrl}/ipfs/${cid}?filename=${encodeURIComponent(name)}` + const src = `${availableGatewayUrl}/ipfs/${cid}?filename=${encodeURIComponent(name)}` const className = 'mw-100 mt3 bg-snow-muted pa2 br2 border-box' switch (type) { @@ -84,12 +84,15 @@ const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl: gatewayUrl case 'image': return {name} default: { + const srcLocal = `${localGatewayUrl}/ipfs/${cid}?filename=${encodeURIComponent(name)}` + const srcIpfsIo = `https://ipfs.io/ipfs/${cid}?filename=${encodeURIComponent(name)}` + const cantPreview = (

{t('cantBePreviewed')} 😢

- - Try downloading it instead. + + Try opening it instead with your local gateway or with ipfs.io.

@@ -138,5 +141,6 @@ Preview.propTypes = { export default connect( 'selectAvailableGatewayUrl', + 'selectGatewayUrl', withTranslation('files')(Preview) ) From 372aaf2e70f0f2dabadc5a34f4194b3c023f6a63 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 17 Aug 2021 17:31:33 +0200 Subject: [PATCH 2/6] custom public gateway --- public/locales/en/app.json | 4 ++ public/locales/en/files.json | 3 +- public/locales/en/settings.json | 1 + src/bundles/config.js | 13 ++-- src/bundles/gateway.js | 41 ++++++++++++- src/bundles/ipfs-provider.js | 40 +----------- src/bundles/local-storage.js | 38 ++++++++++++ .../public-gateway-form/PublicGatewayForm.js | 61 +++++++++++++++++++ src/files/file-preview/FilePreview.js | 20 +++--- src/settings/SettingsPage.js | 11 ++++ tsconfig.json | 2 + 11 files changed, 180 insertions(+), 54 deletions(-) create mode 100644 src/bundles/local-storage.js create mode 100644 src/components/public-gateway-form/PublicGatewayForm.js diff --git a/public/locales/en/app.json b/public/locales/en/app.json index 3cf14b332..82ba213de 100644 --- a/public/locales/en/app.json +++ b/public/locales/en/app.json @@ -45,6 +45,9 @@ "apiAddressForm": { "placeholder": "Enter a URL (http://127.0.0.1:5001) or a Multiaddr (/ip4/127.0.0.1/tcp/5001)" }, + "publicGatewayForm": { + "placeholder": "Enter a URL (https://dweb.link)" + }, "terms": { "address": "Address", "addresses": "Addresses", @@ -75,6 +78,7 @@ "pins": "Pins", "pinStatus": "Pin Status", "publicKey": "Public key", + "publicGateway": "Public Gateway", "rateIn": "Rate in", "rateOut": "Rate out", "repo": "Repo", diff --git a/public/locales/en/files.json b/public/locales/en/files.json index e595c81df..ea76414b1 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -10,7 +10,8 @@ "individualFilesOnly": "Only available for individual files", "noPDFSupport": "Your browser does not support PDFs. Please download the PDF to view it:", "downloadPDF": "Download PDF", - "openInstead": "Try opening it instead <1>with your local gateway or <1>with ipfs.io.", + "openWithPublicGateway": "Try opening it instead with your <1>public gateway.", + "openWithLocalAndPublicGateway": "Try opening it instead with your <1>local gateway or <3>public gateway.", "cantBePreviewed": "Sorry, this file can’t be previewed", "addByPath": "From IPFS", "newFolder": "New folder", diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 43b83d6aa..04d13ca6b 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -22,6 +22,7 @@ "translationProjectLink": "Join the IPFS Translation Project" }, "apiDescription": "<0>If your node is configured with a <1>custom API address, including a port other than the default 5001, enter it here.", + "publicGatewayDescription": "<0>Choose which <1>public gateway you want to use to open your files.", "cliDescription": "<0>Enable this option to display a \"view code\" <1> icon next to common IPFS commands. Clicking it opens a modal with that command's CLI code, so you can paste it into the IPFS command-line interface in your terminal.", "cliModal": { "extraNotesJsonConfig": "If you've made changes to the config in this page's code editor that you'd like to save, click the download icon next to the copy button to download it as a JSON file." diff --git a/src/bundles/config.js b/src/bundles/config.js index dbc65a0b9..91c79c621 100644 --- a/src/bundles/config.js +++ b/src/bundles/config.js @@ -2,7 +2,6 @@ import memoize from 'p-memoize' import toUri from 'multiaddr-to-uri' import { createAsyncResourceBundle, createSelector } from 'redux-bundler' -const DEFAULT_URI = 'https://ipfs.io' const LOCAL_HOSTNAMES = ['127.0.0.1', '[::1]', '0.0.0.0', '[::]'] const bundle = createAsyncResourceBundle({ @@ -22,7 +21,9 @@ const bundle = createAsyncResourceBundle({ } const config = JSON.parse(conf) - const url = getURLFromAddress('Gateway', config) || DEFAULT_URI + + const publicGateway = store.selectPublicGateway() + const url = getURLFromAddress('Gateway', config) || publicGateway // Normalize local hostnames to localhost // to leverage subdomain gateway, if present @@ -38,7 +39,7 @@ const bundle = createAsyncResourceBundle({ } if (!await checkIfGatewayUrlIsAccessible(url)) { - store.doSetAvailableGateway(DEFAULT_URI) + store.doSetAvailableGateway(publicGateway) } // stringy json for quick compares @@ -54,12 +55,14 @@ bundle.selectConfigObject = createSelector( bundle.selectApiUrl = createSelector( 'selectConfigObject', - (config) => getURLFromAddress('API', config) || DEFAULT_URI + 'selectPublicGateway', + (config, publicGateway) => getURLFromAddress('API', config) || publicGateway ) bundle.selectGatewayUrl = createSelector( 'selectConfigObject', - (config) => getURLFromAddress('Gateway', config) || DEFAULT_URI + 'selectPublicGateway', + (config, publicGateway) => getURLFromAddress('Gateway', config) || publicGateway ) bundle.selectAvailableGatewayUrl = createSelector( diff --git a/src/bundles/gateway.js b/src/bundles/gateway.js index 47152e0b2..53cc77ee5 100644 --- a/src/bundles/gateway.js +++ b/src/bundles/gateway.js @@ -1,17 +1,54 @@ +import { readSetting, writeSetting } from './local-storage' + +const DEFAULT_GATEWAY = 'https://dweb.link' + +const readPublicGatewaySetting = () => { + const setting = readSetting('ipfsPublicGateway') + return setting || DEFAULT_GATEWAY +} + +const init = () => ({ + availableGateway: null, + publicGateway: readPublicGatewaySetting() +}) + +export const checkValidHttpUrl = (value) => { + let url + + try { + url = new URL(value) + } catch (_) { + return false + } + + return url.protocol === 'http:' || url.protocol === 'https:' +} + const bundle = { name: 'gateway', - reducer: (state = { availableGateway: null }, action) => { + reducer: (state = init(), action) => { if (action.type === 'SET_AVAILABLE_GATEWAY') { return { ...state, availableGateway: action.payload } } + if (action.type === 'SET_PUBLIC_GATEWAY') { + return { ...state, publicGateway: action.payload } + } + return state }, doSetAvailableGateway: url => ({ dispatch }) => dispatch({ type: 'SET_AVAILABLE_GATEWAY', payload: url }), - selectAvailableGateway: (state) => state?.gateway?.availableGateway + doUpdatePublicGateway: (address) => async ({ dispatch }) => { + await writeSetting('ipfsPublicGateway', address) + dispatch({ type: 'SET_PUBLIC_GATEWAY', payload: address }) + }, + + selectAvailableGateway: (state) => state?.gateway?.availableGateway, + + selectPublicGateway: (state) => state?.gateway?.publicGateway } export default bundle diff --git a/src/bundles/ipfs-provider.js b/src/bundles/ipfs-provider.js index fc86c63c1..ca1bbc08f 100644 --- a/src/bundles/ipfs-provider.js +++ b/src/bundles/ipfs-provider.js @@ -6,6 +6,7 @@ import first from 'it-first' import last from 'it-last' import * as Enum from './enum' import { perform } from './task' +import { readSetting, writeSetting } from './local-storage' /* TODO: restore when no longer bundle standalone ipld with ipld-explorer * context: https://github.com/ipfs/ipld-explorer-components/pull/289 @@ -273,45 +274,6 @@ const readHTTPClientOptions = (value) => { } } -/** - * Reads setting from the `localStorage` with a given `id` as JSON. If JSON - * parse is failed setting is interpreted as a string value. - * @param {string} id - * @returns {string|object|null} - */ -const readSetting = (id) => { - /** @type {string|null} */ - let setting = null - if (window.localStorage) { - try { - setting = window.localStorage.getItem(id) - } catch (error) { - console.error(`Error reading '${id}' value from localStorage`, error) - } - - try { - return JSON.parse(setting || '') - } catch (_) { - // res was probably a string, so pass it on. - return setting - } - } - - return setting -} - -/** - * @param {string} id - * @param {string|number|boolean|object} value - */ -const writeSetting = (id, value) => { - try { - window.localStorage.setItem(id, JSON.stringify(value)) - } catch (error) { - console.error(`Error writing '${id}' value to localStorage`, error) - } -} - /** @type {IPFSService|null} */ let ipfs = null diff --git a/src/bundles/local-storage.js b/src/bundles/local-storage.js new file mode 100644 index 000000000..51487e530 --- /dev/null +++ b/src/bundles/local-storage.js @@ -0,0 +1,38 @@ +/** + * Reads setting from the `localStorage` with a given `id` as JSON. If JSON + * parse is failed setting is interpreted as a string value. + * @param {string} id + * @returns {string|object|null} + */ +export const readSetting = (id) => { + /** @type {string|null} */ + let setting = null + if (window.localStorage) { + try { + setting = window.localStorage.getItem(id) + } catch (error) { + console.error(`Error reading '${id}' value from localStorage`, error) + } + + try { + return JSON.parse(setting || '') + } catch (_) { + // res was probably a string, so pass it on. + return setting + } + } + + return setting +} + +/** + * @param {string} id + * @param {string|number|boolean|object} value + */ +export const writeSetting = (id, value) => { + try { + window.localStorage.setItem(id, JSON.stringify(value)) + } catch (error) { + console.error(`Error writing '${id}' value to localStorage`, error) + } +} diff --git a/src/components/public-gateway-form/PublicGatewayForm.js b/src/components/public-gateway-form/PublicGatewayForm.js new file mode 100644 index 000000000..75f3f5f6c --- /dev/null +++ b/src/components/public-gateway-form/PublicGatewayForm.js @@ -0,0 +1,61 @@ +import React, { useState, useEffect } from 'react' +import { connect } from 'redux-bundler-react' +import { withTranslation } from 'react-i18next' +import Button from '../button/Button' +import { checkValidHttpUrl } from '../../bundles/gateway' + +const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => { + const [value, setValue] = useState(publicGateway) + const initialIsValidGatewayUrl = !checkValidHttpUrl(value) + const [showFailState, setShowFailState] = useState(initialIsValidGatewayUrl) + const [isValidGatewayUrl, setIsValidGatewayUrl] = useState(initialIsValidGatewayUrl) + + // Updates the border of the input to indicate validity + useEffect(() => { + setShowFailState(!isValidGatewayUrl) + }, [isValidGatewayUrl]) + + // Updates the border of the input to indicate validity + useEffect(() => { + const isValid = checkValidHttpUrl(value) + setIsValidGatewayUrl(isValid) + setShowFailState(!isValid) + }, [value]) + + const onChange = (event) => setValue(event.target.value) + + const onSubmit = async (event) => { + event.preventDefault() + doUpdatePublicGateway(value) + } + + const onKeyPress = (event) => { + if (event.key === 'Enter') { + onSubmit(event) + } + } + + return ( +
+ +
+ +
+
+ ) +} + +export default connect( + 'doUpdatePublicGateway', + 'selectPublicGateway', + withTranslation('app')(PublicGatewayForm) +) diff --git a/src/files/file-preview/FilePreview.js b/src/files/file-preview/FilePreview.js index 38968b806..94328c950 100644 --- a/src/files/file-preview/FilePreview.js +++ b/src/files/file-preview/FilePreview.js @@ -28,7 +28,7 @@ const Preview = (props) => { } -const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl, gatewayUrl: localGatewayUrl, read, onDownload }) => { +const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl, publicGateway, read, onDownload }) => { const [content, setContent] = useState(null) const [hasMoreContent, setHasMoreContent] = useState(false) const [buffer, setBuffer] = useState(null) @@ -84,16 +84,22 @@ const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl, gatewayUrl case 'image': return {name} default: { - const srcLocal = `${localGatewayUrl}/ipfs/${cid}?filename=${encodeURIComponent(name)}` - const srcIpfsIo = `https://ipfs.io/ipfs/${cid}?filename=${encodeURIComponent(name)}` + const srcPublic = `${publicGateway}/ipfs/${cid}?filename=${encodeURIComponent(name)}` const cantPreview = (

{t('cantBePreviewed')} 😢

- - Try opening it instead with your local gateway or with ipfs.io. - + { availableGatewayUrl === publicGateway + ? + Try opening it instead with your public gateway. + + : + Try opening it instead with your local gateway or public gateway. + + + } +

) @@ -141,6 +147,6 @@ Preview.propTypes = { export default connect( 'selectAvailableGatewayUrl', - 'selectGatewayUrl', + 'selectPublicGateway', withTranslation('files')(Preview) ) diff --git a/src/settings/SettingsPage.js b/src/settings/SettingsPage.js index dc4ef6adc..aab39c9cb 100644 --- a/src/settings/SettingsPage.js +++ b/src/settings/SettingsPage.js @@ -15,6 +15,7 @@ import LanguageSelector from '../components/language-selector/LanguageSelector' import PinningManager from '../components/pinning-manager/PinningManager' import AnalyticsToggle from '../components/analytics-toggle/AnalyticsToggle' import ApiAddressForm from '../components/api-address-form/ApiAddressForm' +import PublicGatewayForm from '../components/public-gateway-form/PublicGatewayForm' import JsonEditor from './editor/JsonEditor' import Experiments from '../components/experiments/ExperimentsPanel' import Title from './Title' @@ -58,6 +59,16 @@ export const SettingsPage = ({ + +
+ {t('app:terms.publicGateway')} + +

Choose which public gateway you want to use to open your files.

+
+ +
+
+ {t('pinningServices.title')}

diff --git a/tsconfig.json b/tsconfig.json index d495c7c7f..163bd9735 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -66,6 +66,8 @@ "src/bundles/ipfs-desktop.js", "src/bundles/ipfs-provider.js", "src/bundles/retry-init.js", + "src/bundles/retry-init.js", + "src/bundles/local-storage.js", "src/bundles/task.js", "src/lib/count-dirs.js", "src/lib/sort.js", From e12efeccc80c1f4834175008a931ed8dd2e317af Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 19 Aug 2021 09:37:41 +0200 Subject: [PATCH 3/6] fix: opening non-unixfs DAG License: MIT Signed-off-by: Henrique Dias --- public/locales/en/files.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en/files.json b/public/locales/en/files.json index ea76414b1..9d441b5b7 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -110,6 +110,6 @@ "filesDescription": "Total size of data in the current directory (if a subdirectory, the size of all data in Files is also displayed)", "more": "More", "files": "Files", - "cidNotFileNorDir": "The current link isn't a file, nor a directory. Try to <0>inspect it instead.", + "cidNotFileNorDir": "The current link isn't a file, nor a directory. Try to <1>inspect it instead.", "sortBy": "Sort items by {name}" } From 0b1938dff0b793230cd80662a0d8a1aed597775d Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 19 Aug 2021 09:41:09 +0200 Subject: [PATCH 4/6] fix: use custom public gateway on shareable links License: MIT Signed-off-by: Henrique Dias --- src/bundles/files/actions.js | 6 ++++-- src/lib/files.js | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js index b948bba48..e37eb7ca9 100644 --- a/src/bundles/files/actions.js +++ b/src/bundles/files/actions.js @@ -156,6 +156,7 @@ const getPins = async function * (ipfs) { * @typedef {Object} ConfigSelectors * @property {function():string} selectApiUrl * @property {function():string} selectGatewayUrl + * @property {function():string} selectPublicGateway * * @typedef {Object} UnkonwActions * @property {function(string):Promise} doUpdateHash @@ -423,9 +424,10 @@ const actions = () => ({ * Generates sharable link for the provided files. * @param {FileStat[]} files */ - doFilesShareLink: (files) => perform(ACTIONS.SHARE_LINK, async (ipfs) => { + doFilesShareLink: (files) => perform(ACTIONS.SHARE_LINK, async (ipfs, { store }) => { // ensureMFS deliberately omitted here, see https://github.com/ipfs/ipfs-webui/issues/1744 for context. - return await getShareableLink(files, ipfs) + const publicGateway = store.selectPublicGateway() + return await getShareableLink(files, publicGateway, ipfs) }), /** diff --git a/src/lib/files.js b/src/lib/files.js index 9044e1b88..8c9367b3f 100644 --- a/src/lib/files.js +++ b/src/lib/files.js @@ -121,10 +121,11 @@ export async function getDownloadLink (files, gatewayUrl, apiUrl, ipfs) { /** * @param {FileStat[]} files + * @param {string} gatewayUrl * @param {IPFSService} ipfs * @returns {Promise} */ -export async function getShareableLink (files, ipfs) { +export async function getShareableLink (files, gatewayUrl, ipfs) { let cid let filename @@ -137,7 +138,7 @@ export async function getShareableLink (files, ipfs) { cid = await makeCIDFromFiles(files, ipfs) } - return `https://ipfs.io/ipfs/${cid}${filename || ''}` + return `${gatewayUrl}/ipfs/${cid}${filename || ''}` } /** From a697e0888673a7710b8dbd1bf0529f97f3e40f1f Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 1 Sep 2021 20:40:40 +0200 Subject: [PATCH 5/6] fix: ux of submit/reset cosmetic fix that adds reset button to public gateway section in Settings and disables buttons when they are no-op. --- src/bundles/gateway.js | 2 +- .../api-address-form/ApiAddressForm.js | 8 +++++- .../public-gateway-form/PublicGatewayForm.js | 25 +++++++++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/bundles/gateway.js b/src/bundles/gateway.js index 53cc77ee5..cbd488f57 100644 --- a/src/bundles/gateway.js +++ b/src/bundles/gateway.js @@ -1,6 +1,6 @@ import { readSetting, writeSetting } from './local-storage' -const DEFAULT_GATEWAY = 'https://dweb.link' +export const DEFAULT_GATEWAY = 'https://dweb.link' const readPublicGatewaySetting = () => { const setting = readSetting('ipfsPublicGateway') diff --git a/src/components/api-address-form/ApiAddressForm.js b/src/components/api-address-form/ApiAddressForm.js index f6f5901fd..ad75735b9 100644 --- a/src/components/api-address-form/ApiAddressForm.js +++ b/src/components/api-address-form/ApiAddressForm.js @@ -48,7 +48,13 @@ const ApiAddressForm = ({ t, doUpdateIpfsApiAddress, ipfsApiAddress, ipfsInitFai value={value} />

- +
) diff --git a/src/components/public-gateway-form/PublicGatewayForm.js b/src/components/public-gateway-form/PublicGatewayForm.js index 75f3f5f6c..20752f142 100644 --- a/src/components/public-gateway-form/PublicGatewayForm.js +++ b/src/components/public-gateway-form/PublicGatewayForm.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react' import { connect } from 'redux-bundler-react' import { withTranslation } from 'react-i18next' import Button from '../button/Button' -import { checkValidHttpUrl } from '../../bundles/gateway' +import { checkValidHttpUrl, DEFAULT_GATEWAY } from '../../bundles/gateway' const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => { const [value, setValue] = useState(publicGateway) @@ -29,6 +29,12 @@ const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => { doUpdatePublicGateway(value) } + const onReset = async (event) => { + event.preventDefault() + setValue(DEFAULT_GATEWAY) + doUpdatePublicGateway(DEFAULT_GATEWAY) + } + const onKeyPress = (event) => { if (event.key === 'Enter') { onSubmit(event) @@ -48,7 +54,22 @@ const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => { value={value} />
- + +
) From 1bd72c1ff9a3034d01cbf7907ae538ce7a97c026 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 1 Sep 2021 20:59:14 +0200 Subject: [PATCH 6/6] style: public gateway description shareable links are better way to sonvey the practical meaning of this setting, other places are incidental --- public/locales/en/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 04d13ca6b..291f1524d 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -22,7 +22,7 @@ "translationProjectLink": "Join the IPFS Translation Project" }, "apiDescription": "<0>If your node is configured with a <1>custom API address, including a port other than the default 5001, enter it here.", - "publicGatewayDescription": "<0>Choose which <1>public gateway you want to use to open your files.", + "publicGatewayDescription": "<0>Choose which <1>public gateway you want to use when generating shareable links.", "cliDescription": "<0>Enable this option to display a \"view code\" <1> icon next to common IPFS commands. Clicking it opens a modal with that command's CLI code, so you can paste it into the IPFS command-line interface in your terminal.", "cliModal": { "extraNotesJsonConfig": "If you've made changes to the config in this page's code editor that you'd like to save, click the download icon next to the copy button to download it as a JSON file."