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 2 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
3 changes: 2 additions & 1 deletion 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
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 to open your files.</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
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'

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)
}
}
61 changes: 61 additions & 0 deletions src/components/public-gateway-form/PublicGatewayForm.js
Original file line number Diff line number Diff line change
@@ -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 (
<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 className='tc' disabled={!isValidGatewayUrl}>{t('actions.submit')}</Button>
</div>
</form>
)
}

export default connect(
'doUpdatePublicGateway',
'selectPublicGateway',
withTranslation('app')(PublicGatewayForm)
)
20 changes: 15 additions & 5 deletions src/files/file-preview/FilePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const Preview = (props) => {
</div>
}

const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl: gatewayUrl, 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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -84,13 +84,22 @@ const PreviewItem = ({ t, name, cid, size, type, availableGatewayUrl: gatewayUrl
case 'image':
return <img className={className} alt={name} src={src} />
default: {
const srcPublic = `${publicGateway}/ipfs/${cid}?filename=${encodeURIComponent(name)}`

const cantPreview = (
<div className='mt4'>
<p className='b'>{t('cantBePreviewed')} <span role='img' aria-label='sad'>😢</span></p>
<p>
<Trans i18nKey='downloadInstead' t={t}>
Try <a href={src} download target='_blank' rel='noopener noreferrer' className='link blue' >downloading</a> it instead.
</Trans>
{ availableGatewayUrl === publicGateway
? <Trans i18nKey='openWithPublicGateway' t={t}>
Try opening it instead with your <a href={src} download target='_blank' rel='noopener noreferrer' className='link blue'>public gateway</a>.
</Trans>
: <Trans i18nKey='openWithLocalAndPublicGateway' t={t}>
Try opening it instead with your <a href={src} download target='_blank' rel='noopener noreferrer' className='link blue'>local gateway</a> or <a href={srcPublic} download target='_blank' rel='noopener noreferrer' className='link blue'>public gateway</a>.
</Trans>

}

</p>
</div>
)
Expand Down Expand Up @@ -138,5 +147,6 @@ Preview.propTypes = {

export default connect(
'selectAvailableGatewayUrl',
'selectPublicGateway',
withTranslation('files')(Preview)
)
11 changes: 11 additions & 0 deletions src/settings/SettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -58,6 +59,16 @@ export const SettingsPage = ({
</div>
</Box>

<Box className='mb3 pa4-l pa2'>
<div className='lh-copy charcoal'>
<Title>{t('app:terms.publicGateway')}</Title>
<Trans i18nKey='publicGatewayDescription' t={t}>
<p>Choose which <a className='link blue' href="http://docs.ipfs.io/concepts/ipfs-gateway/#public-gateways" target='_blank' rel='noopener noreferrer'>public gateway</a> you want to use to open your files.</p>
</Trans>
<PublicGatewayForm/>
</div>
</Box>

<Box className='mb3 pa4-l pa2 joyride-settings-pinning'>
<Title>{t('pinningServices.title')}</Title>
<p className='ma0 mr2 lh-copy charcoal f6'>
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down