Rewritten filesharing to move responsability of filesharing to RoomClient. Removed and rewrote some code to clean up.

This commit is contained in:
Håvar Aambø Fosstveit
2018-12-17 15:13:22 +01:00
parent 60fb9c735e
commit db9d423feb
21 changed files with 608 additions and 918 deletions
+113
View File
@@ -0,0 +1,113 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRoomContext } from '../../RoomContext';
import magnet from 'magnet-uri';
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
class File extends Component
{
render()
{
const {
roomClient,
torrentSupport,
file
} = this.props;
return (
<div className='file-entry'>
<img className='file-avatar' src={file.picture || DEFAULT_PICTURE} />
<div className='file-content'>
<Choose>
<When condition={file.me}>
<p>You shared a file.</p>
</When>
<Otherwise>
<p>{file.displayName} shared a file.</p>
</Otherwise>
</Choose>
<If condition={!file.active && !file.files}>
<div className='file-info'>
<Choose>
<When condition={torrentSupport}>
<span
className='button'
onClick={() =>
{
roomClient.handleDownload(file.magnetUri);
}}
>
<img src='resources/images/download-icon.svg' />
</span>
</When>
<Otherwise>
<p>
Your browser does not support downloading files using WebTorrent.
</p>
</Otherwise>
</Choose>
<p>{magnet.decode(file.magnetUri).dn}</p>
</div>
</If>
<If condition={file.timeout}>
<Fragment>
<p>
If this process takes a long time, there might not be anyone seeding
this torrent. Try asking someone to reupload the file that you want.
</p>
</Fragment>
</If>
<If condition={file.active}>
<progress value={file.progress} />
</If>
<If condition={file.files}>
<Fragment>
<p>File finished downloading.</p>
{file.files.map((sharedFile, i) => (
<div className='file-info' key={i}>
<span
className='button'
onClick={() =>
{
roomClient.saveFile(sharedFile);
}}
>
<img src='resources/images/save-icon.svg' />
</span>
<p>{sharedFile.name}</p>
</div>
))}
</Fragment>
</If>
</div>
</div>
);
}
}
File.propTypes = {
roomClient : PropTypes.object.isRequired,
torrentSupport : PropTypes.bool.isRequired,
file : PropTypes.object.isRequired
};
const mapStateToProps = (state, { magnetUri }) =>
{
return {
file : state.files[magnetUri],
torrentSupport : state.room.torrentSupport
};
};
export default withRoomContext(connect(
mapStateToProps
)(File));
@@ -1,201 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import magnet from 'magnet-uri';
import WebTorrent from 'webtorrent';
import * as requestActions from '../../redux/requestActions';
import { saveAs } from 'file-saver/FileSaver';
import { client } from './index';
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
class FileEntry extends Component
{
state = {
active : false,
numPeers : 0,
progress : 0,
files : null
};
saveFile = (file) =>
{
file.getBlob((err, blob) =>
{
if (err)
{
return this.props.notify({
text : 'An error occurred while saving a file'
});
}
saveAs(blob, file.name);
});
};
handleTorrent = (torrent) =>
{
// Torrent already done, this can happen if the
// same file was sent multiple times.
if (torrent.progress === 1)
{
this.setState({
files : torrent.files,
numPeers : torrent.numPeers,
progress : 1,
active : false,
timeout : false
});
return;
}
const onProgress = () =>
{
this.setState({
numPeers : torrent.numPeers,
progress : torrent.progress
});
};
onProgress();
setInterval(onProgress, 500);
torrent.on('done', () =>
{
onProgress();
clearInterval(onProgress);
this.setState({
files : torrent.files,
active : false
});
});
};
handleDownload = () =>
{
this.setState({
active : true
});
const magnetURI = this.props.data.file.magnet;
const existingTorrent = client.get(magnetURI);
if (existingTorrent)
{
// Never add duplicate torrents, use the existing one instead.
return this.handleTorrent(existingTorrent);
}
client.add(magnetURI, this.handleTorrent);
setTimeout(() =>
{
if (this.state.active && this.state.numPeers === 0)
{
this.setState({
timeout : true
});
}
}, 10 * 1000);
}
render()
{
return (
<div className='file-entry'>
<img className='file-avatar' src={this.props.data.picture || DEFAULT_PICTURE} />
<div className='file-content'>
<Choose>
<When condition={this.props.data.me}>
<p>You shared a file.</p>
</When>
<Otherwise>
<p>{this.props.data.name} shared a file.</p>
</Otherwise>
</Choose>
<If condition={!this.state.active && !this.state.files}>
<div className='file-info'>
<Choose>
<When condition={WebTorrent.WEBRTC_SUPPORT}>
<span className='button' onClick={this.handleDownload}>
<img src='resources/images/download-icon.svg' />
</span>
</When>
<Otherwise>
<p>
Your browser does not support downloading files using WebTorrent.
</p>
</Otherwise>
</Choose>
<p>{magnet.decode(this.props.data.file.magnet).dn}</p>
</div>
</If>
<If condition={this.state.active && this.state.numPeers === 0}>
<Fragment>
<p>
Locating peers
</p>
{this.state.timeout && (
<p>
If this process takes a long time, there might not be anyone seeding
this torrent. Try asking someone to reupload the file that you want.
</p>
)}
</Fragment>
</If>
<If condition={this.state.active && this.state.numPeers > 0}>
<progress value={this.state.progress} />
</If>
<If condition={this.state.files}>
<Fragment>
<p>Torrent finished downloading.</p>
{this.state.files.map((file, i) => (
<div className='file-info' key={i}>
<span className='button' onClick={() => this.saveFile(file)}>
<img src='resources/images/save-icon.svg' />
</span>
<p>{file.name}</p>
</div>
))}
</Fragment>
</If>
</div>
</div>
);
}
}
export const FileEntryProps = {
data : PropTypes.shape({
name : PropTypes.string.isRequired,
picture : PropTypes.string,
file : PropTypes.shape({
magnet : PropTypes.string.isRequired
}).isRequired,
me : PropTypes.bool
}).isRequired,
notify : PropTypes.func.isRequired
};
FileEntry.propTypes = FileEntryProps;
const mapDispatchToProps = {
notify : requestActions.notify
};
export default connect(
undefined,
mapDispatchToProps
)(FileEntry);
@@ -0,0 +1,40 @@
import React, { Component } from 'react';
import { compose } from 'redux';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import scrollToBottom from '../Chat/scrollToBottom';
import File from './File';
class FileList extends Component
{
render()
{
const {
files
} = this.props;
return (
<div className='shared-files'>
{ Object.keys(files).map((magnetUri) =>
<File key={magnetUri} magnetUri={magnetUri} />
)}
</div>
);
}
}
FileList.propTypes = {
files : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
files : state.files
};
};
export default compose(
connect(mapStateToProps),
scrollToBottom()
)(FileList);
@@ -0,0 +1,88 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { withRoomContext } from '../../RoomContext';
import FileList from './FileList';
class FileSharing extends Component
{
constructor(props)
{
super(props);
this._fileInput = React.createRef();
}
handleFileChange = async (event) =>
{
if (event.target.files.length > 0)
{
this.props.roomClient.shareFiles(event.target.files);
}
};
handleClick = () =>
{
if (this.props.torrentSupport)
{
// We want to open the file dialog when we click a button
// instead of actually rendering the input element itself.
this._fileInput.current.click();
}
};
render()
{
const {
torrentSupport
} = this.props;
const buttonDescription = torrentSupport ?
'Share file' : 'File sharing not supported';
return (
<div data-component='FileSharing'>
<div className='sharing-toolbar'>
<input
style={{ display: 'none' }}
ref={this._fileInput}
type='file'
onChange={this.handleFileChange}
multiple
/>
<div
type='button'
onClick={this.handleClick}
className={classNames('share-file', {
disabled : !torrentSupport
})}
>
<span>{buttonDescription}</span>
</div>
</div>
<FileList />
</div>
);
}
}
FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired,
torrentSupport : PropTypes.bool.isRequired,
tabOpen : PropTypes.bool.isRequired
};
const mapStateToProps = (state) =>
{
return {
torrentSupport : state.room.torrentSupport,
tabOpen : state.toolarea.currentToolTab === 'files'
};
};
export default withRoomContext(connect(
mapStateToProps
)(FileSharing));
@@ -1,60 +0,0 @@
import React, { Component } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import FileEntry, { FileEntryProps } from './FileEntry';
import scrollToBottom from '../Chat/scrollToBottom';
/**
* This component cannot be pure, as we need to use
* refs to scroll to the bottom when new files arrive.
*/
class SharedFilesList extends Component
{
render()
{
const { sharing } = this.props;
return (
<div className='shared-files'>
<Choose>
<When condition={sharing.length > 0}>
{
sharing.map((entry, i) => (
<FileEntry
data={entry}
key={i}
/>
))
}
</When>
<Otherwise>
<div className='empty'>
<p>No one has shared files yet...</p>
</div>
</Otherwise>
</Choose>
</div>
);
}
}
SharedFilesList.propTypes = {
sharing : PropTypes.arrayOf(FileEntryProps.data).isRequired
};
const mapStateToProps = (state) =>
({
sharing : state.sharing,
// Included to scroll to the bottom when the user
// actually opens the tab. When the component first
// mounts, the component is not visible and so the
// component has no height which can be used for scrolling.
tabOpen : state.toolarea.currentToolTab === 'files'
});
export default compose(
connect(mapStateToProps),
scrollToBottom()
)(SharedFilesList);
-131
View File
@@ -1,131 +0,0 @@
import React, { Component } from 'react';
import WebTorrent from 'webtorrent';
import createTorrent from 'create-torrent';
import randomString from 'random-string';
import classNames from 'classnames';
import * as stateActions from '../../redux/stateActions';
import * as requestActions from '../../redux/requestActions';
import { store } from '../../store';
import config from '../../../config';
import SharedFilesList from './SharedFilesList';
export const client = WebTorrent.WEBRTC_SUPPORT && new WebTorrent({
tracker : {
rtcConfig : {
iceServers : config.turnServers
}
}
});
const notifyPeers = (file) =>
{
const { displayName, picture } = store.getState().me;
store.dispatch(requestActions.sendFile(file, displayName, picture));
};
export const shareFiles = async (files) =>
{
const notification =
{
id : randomString({ length: 6 }).toLowerCase(),
text : 'Creating torrent',
type : 'info'
};
store.dispatch(stateActions.addNotification(notification));
createTorrent(files, (err, torrent) =>
{
if (err)
{
return store.dispatch(requestActions.notify({
text : 'An error occured while uploading a file'
}));
}
const existingTorrent = client.get(torrent);
if (existingTorrent)
{
return notifyPeers({
magnet : existingTorrent.magnetURI
});
}
client.seed(files, (newTorrent) =>
{
store.dispatch(stateActions.removeNotification(notification.id));
store.dispatch(requestActions.notify({
text : 'Torrent successfully created'
}));
notifyPeers({
magnet : newTorrent.magnetURI
});
});
});
};
class FileSharing extends Component
{
constructor(props)
{
super(props);
this.fileInput = React.createRef();
}
handleFileChange = async (event) =>
{
if (event.target.files.length > 0)
{
await shareFiles(event.target.files);
}
};
handleClick = () =>
{
if (WebTorrent.WEBRTC_SUPPORT)
{
// We want to open the file dialog when we click a button
// instead of actually rendering the input element itself.
this.fileInput.current.click();
}
};
render()
{
const buttonDescription = WebTorrent.WEBRTC_SUPPORT ?
'Share file' : 'File sharing not supported';
return (
<div data-component='FileSharing'>
<div className='sharing-toolbar'>
<input
style={{ display: 'none' }}
ref={this.fileInput}
type='file'
onChange={this.handleFileChange}
multiple
/>
<div
type='button'
onClick={this.handleClick}
className={classNames('share-file', {
disabled : !WebTorrent.WEBRTC_SUPPORT
})}
>
<span>{buttonDescription}</span>
</div>
</div>
<SharedFilesList />
</div>
);
}
}
export default FileSharing;