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

fix: local and public gateway links when cannot preview #1834

Merged
merged 7 commits into from
Sep 1, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -75,6 +78,7 @@
"pins": "Pins",
"pinStatus": "Pin Status",
"publicKey": "Public key",
"publicGateway": "Public Gateway",
"rateIn": "Rate in",
"rateOut": "Rate out",
"repo": "Repo",
Expand Down
5 changes: 3 additions & 2 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"downloadInstead": "Try <1>downloading</1> it instead.",
"openWithPublicGateway": "Try opening it instead with your <1>public gateway</1>.",
"openWithLocalAndPublicGateway": "Try opening it instead with your <1>local gateway</1> or <3>public gateway</3>.",
"cantBePreviewed": "Sorry, this file can’t be previewed",
"addByPath": "From IPFS",
"newFolder": "New folder",
Expand Down Expand Up @@ -109,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</0> it instead.",
"cidNotFileNorDir": "The current link isn't a file, nor a directory. Try to <1>inspect</1> it instead.",
"sortBy": "Sort items by {name}"
}
1 change: 1 addition & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"translationProjectLink": "Join the IPFS Translation Project"
},
"apiDescription": "<0>If your node is configured with a <1>custom API address</1>, including a port other than the default 5001, enter it here.</0>",
"publicGatewayDescription": "<0>Choose which <1>public gateway</1> you want to use when generating shareable links.</0>",
"cliDescription": "<0>Enable this option to display a \"view code\" <1></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.</0>",
"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."
Expand Down
13 changes: 8 additions & 5 deletions src/bundles/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand All @@ -38,7 +39,7 @@ const bundle = createAsyncResourceBundle({
}

if (!await checkIfGatewayUrlIsAccessible(url)) {
store.doSetAvailableGateway(DEFAULT_URI)
store.doSetAvailableGateway(publicGateway)
}

// stringy json for quick compares
Expand All @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>} doUpdateHash
Expand Down Expand Up @@ -421,9 +422,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)
}),

/**
Expand Down
41 changes: 39 additions & 2 deletions src/bundles/gateway.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
import { readSetting, writeSetting } from './local-storage'

export 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
40 changes: 1 addition & 39 deletions src/bundles/ipfs-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions src/bundles/local-storage.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
8 changes: 7 additions & 1 deletion src/components/api-address-form/ApiAddressForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ const ApiAddressForm = ({ t, doUpdateIpfsApiAddress, ipfsApiAddress, ipfsInitFai
value={value}
/>
<div className='tr'>
<Button className='tc' disabled={!isValidApiAddress}>{t('actions.submit')}</Button>
<Button
minWidth={100}
height={40}
className='mt2 mt0-l ml2-l tc'
disabled={!isValidApiAddress || value === ipfsApiAddress}>
{t('actions.submit')}
</Button>
</div>
</form>
)
Expand Down
82 changes: 82 additions & 0 deletions src/components/public-gateway-form/PublicGatewayForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react'
import { connect } from 'redux-bundler-react'
import { withTranslation } from 'react-i18next'
import Button from '../button/Button'
import { checkValidHttpUrl, DEFAULT_GATEWAY } 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 onReset = async (event) => {
event.preventDefault()
setValue(DEFAULT_GATEWAY)
doUpdatePublicGateway(DEFAULT_GATEWAY)
}

const onKeyPress = (event) => {
if (event.key === 'Enter') {
onSubmit(event)
}
}

return (
<form onSubmit={onSubmit}>
<input
id='public-gateway'
aria-label={t('terms.publicGateway')}
placeholder={t('publicGatewayForm.placeholder')}
type='text'
className={`w-100 lh-copy monospace f5 pl1 pv1 mb2 charcoal input-reset ba b--black-20 br1 ${showFailState ? 'focus-outline-red b--red-muted' : 'focus-outline-green b--green-muted'}`}
onChange={onChange}
onKeyPress={onKeyPress}
value={value}
/>
<div className='tr'>
<Button
minWidth={100}
height={40}
bg='bg-charcoal'
className='tc'
disabled={value === DEFAULT_GATEWAY}
onClick={onReset}>
{t('app:actions.reset')}
</Button>
<Button
minWidth={100}
height={40}
className='mt2 mt0-l ml2-l tc'
disabled={!isValidGatewayUrl || value === publicGateway}>
{t('actions.submit')}
</Button>
</div>
</form>
)
}

export default connect(
'doUpdatePublicGateway',
'selectPublicGateway',
withTranslation('app')(PublicGatewayForm)
)
Loading