-
Notifications
You must be signed in to change notification settings - Fork 490
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: add files page #669
feat: add files page #669
Changes from 21 commits
6b5c046
81a7171
b71169e
338ef51
9b81b00
5acc5d6
c7b8854
12508ac
af62ed4
4b6fd6b
72bcab7
88dbb95
9d74d90
2e1ce94
87331b3
136deee
3837533
7648cd2
6dd84ce
b8eb6a1
bfca078
3371e3c
5d9514a
566605d
a2fe297
6344b9d
28878bf
1251464
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { createAsyncResourceBundle, createSelector } from 'redux-bundler' | ||
import { join } from '../lib/path' | ||
|
||
const bundle = createAsyncResourceBundle({ | ||
name: 'files', | ||
actionBaseType: 'FILES', | ||
getPromise: (args) => { | ||
const {store, getIpfs} = args | ||
let path = store.selectRouteParams().path | ||
|
||
if (!path) { | ||
store.doUpdateHash('/files/') | ||
return Promise.resolve() | ||
} | ||
|
||
// FIX: dirs with actually encoded names | ||
// will get decoded... it shouldn't happen | ||
path = decodeURIComponent(path) | ||
|
||
return getIpfs().files.stat(path) | ||
.then(stats => { | ||
if (stats.type === 'directory') { | ||
return getIpfs().files.ls(path, {l: true}).then((res) => { | ||
// FIX: open PR on js-ipfs-api | ||
if (res) { | ||
res = res.map(file => { | ||
file.type = file.type === 0 ? 'file' : 'directory' | ||
return file | ||
}) | ||
} | ||
|
||
return { | ||
path: path, | ||
type: 'directory', | ||
files: res | ||
} | ||
}) | ||
} else { | ||
stats.name = path | ||
|
||
return { | ||
path: path, | ||
type: 'file', | ||
stats: stats, | ||
read: () => getIpfs().files.read(path) | ||
} | ||
} | ||
}) | ||
}, | ||
staleAfter: 100, | ||
checkIfOnline: false | ||
}) | ||
|
||
bundle.reactFilesFetch = createSelector( | ||
'selectFilesShouldUpdate', | ||
'selectIpfsReady', | ||
'selectRouteInfo', | ||
'selectFiles', | ||
(shouldUpdate, ipfsReady, {url, params}, files) => { | ||
if (shouldUpdate && ipfsReady && url.startsWith('/files')) { | ||
if (!files || files.path !== params.path) { | ||
return { actionCreator: 'doFetchFiles' } | ||
} | ||
} | ||
} | ||
) | ||
|
||
bundle.doFilesDelete = (files) => ({dispatch, getIpfs, store}) => { | ||
dispatch({ type: 'FILES_DELETE_STARTED' }) | ||
|
||
const promises = files.map(file => getIpfs().files.rm(file, { recursive: true })) | ||
Promise.all(promises) | ||
.then(() => { | ||
store.doFetchFiles() | ||
dispatch({ type: 'FILES_DELETE_FINISHED' }) | ||
}) | ||
.catch((error) => { | ||
dispatch({ type: 'FILES_DELETE_ERRORED', payload: error }) | ||
}) | ||
} | ||
|
||
function runAndFetch ({ dispatch, getIpfs, store }, type, action, args) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice ✨ |
||
dispatch({ type: `${type}_STARTED` }) | ||
|
||
return getIpfs().files[action](...args) | ||
.then(() => { | ||
store.doFetchFiles() | ||
dispatch({ type: `${type}_FINISHED` }) | ||
}) | ||
.catch((error) => { | ||
dispatch({ type: `${type}_ERRORED`, payload: error }) | ||
}) | ||
} | ||
|
||
bundle.doFilesRename = (from, to) => (args) => { | ||
runAndFetch(args, 'FILES_RENAME', 'mv', [[from, to]]) | ||
} | ||
|
||
bundle.doFilesCopy = (from, to) => (args) => { | ||
runAndFetch(args, 'FILES_RENAME', 'cp', [[from, to]]) | ||
} | ||
|
||
bundle.doFilesMakeDir = (path) => (args) => { | ||
runAndFetch(args, 'FILES_MKDIR', 'mkdir', [path, { parents: true }]) | ||
} | ||
|
||
bundle.doFilesWrite = (root, files) => ({dispatch, getIpfs, store}) => { | ||
dispatch({ type: 'FILES_WRITE_STARTED' }) | ||
|
||
return Promise.all(files.map((file) => { | ||
const target = join(root, file.name) | ||
return getIpfs().files.write(target, file.content, { create: true }) | ||
})).then(() => { | ||
store.doFetchFiles() | ||
dispatch({ type: 'FILES_WRITE_FINISHED' }) | ||
}).catch((error) => { | ||
dispatch({ type: 'FILES_WRITE_ERRORED', payload: error }) | ||
}) | ||
} | ||
|
||
export default bundle |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,15 @@ export default { | |
if (action.type === 'IPFS_INIT_FINISHED') { | ||
return { ...state, ipfsReady: true } | ||
} | ||
|
||
if (action.type === 'IPFS_GATEWAY_URL_FINISHED') { | ||
return { ...state, gatewayUrl: action.payload } | ||
} | ||
|
||
if (!state.gatewayUrl) { | ||
return { ...state, gatewayUrl: 'https://ipfs.io' } | ||
} | ||
|
||
return state | ||
}, | ||
|
||
|
@@ -17,6 +26,8 @@ export default { | |
|
||
selectIpfsReady: state => state.ipfs.ipfsReady, | ||
|
||
selectGatewayUrl: state => state.ipfs.gatewayUrl, | ||
|
||
doInitIpfs: () => async ({ dispatch }) => { | ||
dispatch({ type: 'IPFS_INIT_STARTED' }) | ||
|
||
|
@@ -27,5 +38,21 @@ export default { | |
} | ||
|
||
dispatch({ type: 'IPFS_INIT_FINISHED' }) | ||
}, | ||
|
||
doGetGatewayUrl: () => async ({ dispatch }) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What you have is good for now. We need to think about how we make this work against a js-ipfs instance, as we connect to window.ipfs, so we can't be sure that the user has http gateway. We could just use the public one in that case, but it would be neat if we could load the users files directly via window.ipfs rather than assuming an http gateway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uhh, forgot to call it. Now it's called after doInitIPFS. 😄 It doesn't work when using companion though because it doesn't allow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should start thinking on adding Initial idea was to add a mechanism for pre-emptively requesting permissions, as suggested in ipfs/ipfs-companion#454 (comment) and ipfs/ipfs-companion#454 (comment), it could be extended with sandbox flag:
The problem is that other dapps could easily disable sandboxing this way. Is this a blocker or just an inconvenience? |
||
dispatch({ type: 'IPFS_GATWAY_URL_STARTED' }) | ||
|
||
getIpfs().config.get('Addresses.Gateway', (err, res) => { | ||
if (err) { | ||
dispatch({ type: 'IPFS_GATWAY_URL_ERRORED', payload: err }) | ||
return | ||
} | ||
|
||
const split = res.split('/') | ||
const gateway = '//' + split[2] + ':' + split[4] | ||
|
||
dispatch({ type: 'IPFS_GATWAY_URL_FINISHED', payload: gateway }) | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,114 @@ | ||
import React from 'react' | ||
export default () => <h1 data-id='title'>Files</h1> | ||
import PropTypes from 'prop-types' | ||
import { connect } from 'redux-bundler-react' | ||
import Breadcrumbs from './breadcrumbs/Breadcrumbs' | ||
import FilesList from './files-list/FilesList' | ||
import FilePreview from './file-preview/FilePreview' | ||
import FileInput from './file-input/FileInput' | ||
|
||
const action = (name) => { | ||
return (...args) => { | ||
console.log(name, args) | ||
} | ||
} | ||
|
||
const empty = ( | ||
<div> | ||
<h2>It seems a bit lonely here :(</h2> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @akrych when you get time, could you create some ideas for a "you have no files yet, why not add some" page for the files view? It could use some of the space to introduce some IPFS or at least some of the issues around "they are only on your machine until a peer fetches them from you", and "this is p2p. this is not a cloud" |
||
</div> | ||
) | ||
|
||
class FilesPage extends React.Component { | ||
static propTypes = { | ||
files: PropTypes.object | ||
} | ||
|
||
state = { | ||
clipboard: [], | ||
copy: false | ||
} | ||
|
||
onLinkClick = (link) => { | ||
const {doUpdateHash} = this.props | ||
doUpdateHash(`/files${link}`) | ||
} | ||
|
||
onInspect = (hash) => { | ||
const {doUpdateHash} = this.props | ||
doUpdateHash(`/explore/ipfs/${hash}`) | ||
} | ||
|
||
onRename = ([path]) => { | ||
const oldName = path.split('/').pop() | ||
const newName = window.prompt('Insert the new name:') | ||
|
||
if (newName) { | ||
this.props.doFilesRename(path, path.replace(oldName, newName)) | ||
} | ||
} | ||
|
||
onFilesUpload = (files) => { | ||
this.props.doFilesWrite(this.props.files.path, files) | ||
} | ||
|
||
componentWillMount () { | ||
console.log('WUOLL MOUNT') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WULLY There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't even remember why I added this listeners... 😆 |
||
} | ||
|
||
componentWillUnmount () { | ||
console.log('WILL UNMOUNT') | ||
} | ||
|
||
render () { | ||
const {files} = this.props | ||
|
||
if (!files) { | ||
return <div> | ||
<h1 data-id='title'>Files</h1> | ||
</div> | ||
} | ||
|
||
let body | ||
if (files.type === 'directory') { | ||
if (files.files.length === 0) { | ||
body = empty | ||
} else { | ||
body = <FilesList | ||
maxWidth='calc(100% - 240px)' | ||
root={files.path} | ||
files={files.files} | ||
onShare={action('Share')} | ||
onInspect={this.onInspect} | ||
onRename={this.onRename} | ||
onDownload={action('Download')} | ||
onDelete={this.props.doFilesDelete} | ||
onNavigate={this.onLinkClick} | ||
onCancelUpload={action('Cancel Upload')} | ||
/> | ||
} | ||
} else { | ||
body = <FilePreview {...files} gatewayUrl={this.props.gatewayUrl} /> | ||
} | ||
|
||
return ( | ||
<div> | ||
<div className='flex items-center justify-between mb4'> | ||
<Breadcrumbs path={files.path} onClick={this.onLinkClick} /> | ||
<FileInput upload={this.onFilesUpload} /> | ||
</div> | ||
{body} | ||
<h1 data-id='title'>Files</h1> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default connect( | ||
'doUpdateHash', | ||
'doFilesDelete', | ||
'doFilesRename', | ||
'doFilesWrite', | ||
'selectFiles', | ||
'selectGatewayUrl', | ||
FilesPage | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doGetGatewayUrl
can be removed here, it's not used.