commit
48ac6c8391
13
README.md
13
README.md
|
|
@ -21,6 +21,19 @@ $ cd server
|
||||||
$ npm install
|
$ npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In addition, the server requires a screen to be installed for the server
|
||||||
|
to be able to seed shared torrent files. This is because the headless
|
||||||
|
Electron instance used by WebTorrent expects one. This means that in order
|
||||||
|
to run the project on a server, you need to install a virtual screen
|
||||||
|
such as `xvfb` by running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install xvfb
|
||||||
|
```
|
||||||
|
|
||||||
|
See [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid) for
|
||||||
|
more information about this.
|
||||||
|
|
||||||
* Copy `config.example.js` as `config.js` and customize it for your scenario:
|
* Copy `config.example.js` as `config.js` and customize it for your scenario:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ module.exports =
|
||||||
'semi': [ 2, 'always' ],
|
'semi': [ 2, 'always' ],
|
||||||
'semi-spacing': 2,
|
'semi-spacing': 2,
|
||||||
'space-before-blocks': 2,
|
'space-before-blocks': 2,
|
||||||
'space-before-function-paren': [ 2, 'never' ],
|
'space-before-function-paren': [ 2, { anonymous: 'never', named: 'never', 'asyncArrow': 'always'}],
|
||||||
'space-in-parens': [ 2, 'never' ],
|
'space-in-parens': [ 2, 'never' ],
|
||||||
'spaced-comment': [ 2, 'always' ],
|
'spaced-comment': [ 2, 'always' ],
|
||||||
'strict': 2,
|
'strict': 2,
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,22 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendFile(file)
|
||||||
|
{
|
||||||
|
logger.debug('sendFile() [file: %o]', file);
|
||||||
|
|
||||||
|
return this._protoo.send('send-file', { file })
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('sendFile() | failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify({
|
||||||
|
typ : 'error',
|
||||||
|
text : 'An error occurred while sharing a file'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getChatHistory()
|
getChatHistory()
|
||||||
{
|
{
|
||||||
logger.debug('getChatHistory()');
|
logger.debug('getChatHistory()');
|
||||||
|
|
@ -222,6 +238,22 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFileHistory()
|
||||||
|
{
|
||||||
|
logger.debug('getFileHistory()');
|
||||||
|
|
||||||
|
return this._protoo.send('file-history', {})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('getFileHistory() | failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify({
|
||||||
|
type : 'error',
|
||||||
|
text : 'Could not get file history'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
muteMic()
|
muteMic()
|
||||||
{
|
{
|
||||||
logger.debug('muteMic()');
|
logger.debug('muteMic()');
|
||||||
|
|
@ -1136,6 +1168,37 @@ export default class RoomClient
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'file-receive':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const payload = request.data.file;
|
||||||
|
|
||||||
|
this._dispatch(stateActions.addFile(payload));
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify({
|
||||||
|
text : `${payload.name} shared a file`
|
||||||
|
}));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'file-history-receive':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const files = request.data.fileHistory;
|
||||||
|
|
||||||
|
if (files.length > 0)
|
||||||
|
{
|
||||||
|
logger.debug('Got files history');
|
||||||
|
|
||||||
|
this._dispatch(stateActions.addFileHistory(files));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
logger.error('unknown protoo method "%s"', request.method);
|
logger.error('unknown protoo method "%s"', request.method);
|
||||||
|
|
@ -1273,6 +1336,7 @@ export default class RoomClient
|
||||||
this._dispatch(stateActions.removeAllNotifications());
|
this._dispatch(stateActions.removeAllNotifications());
|
||||||
|
|
||||||
this.getChatHistory();
|
this.getChatHistory();
|
||||||
|
this.getFileHistory();
|
||||||
|
|
||||||
this._dispatch(requestActions.notify(
|
this._dispatch(requestActions.notify(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import WebTorrent from 'webtorrent';
|
||||||
|
import dragDrop from 'drag-drop';
|
||||||
|
import { shareFiles } from './index';
|
||||||
|
|
||||||
|
export const configureDragDrop = () =>
|
||||||
|
{
|
||||||
|
if (WebTorrent.WEBRTC_SUPPORT)
|
||||||
|
{
|
||||||
|
dragDrop('body', async (files) => await shareFiles(files));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HoldingOverlay = () => (
|
||||||
|
<div id='holding-overlay'>
|
||||||
|
Drop files here to share them
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
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'>
|
||||||
|
{this.props.data.me ? (
|
||||||
|
<p>You shared a file.</p>
|
||||||
|
) : (
|
||||||
|
<p>{this.props.data.name} shared a file.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!this.state.active && !this.state.files && (
|
||||||
|
<div className='file-info'>
|
||||||
|
{WebTorrent.WEBRTC_SUPPORT ? (
|
||||||
|
<span className='button' onClick={this.handleDownload}>
|
||||||
|
<img src='resources/images/download-icon.svg' />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
Your browser does not support downloading files using WebTorrent.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p>{magnet.decode(this.props.data.file.magnet).dn}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.active && this.state.numPeers > 0 && (
|
||||||
|
<progress value={this.state.progress} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</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,47 @@
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div className='shared-files'>
|
||||||
|
{this.props.sharing.map((entry, i) => (
|
||||||
|
<FileEntry
|
||||||
|
data={entry}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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);
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
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;
|
||||||
|
|
@ -41,7 +41,7 @@ const FullScreenView = (props) =>
|
||||||
<div className='controls'>
|
<div className='controls'>
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'fullscreen', 'room-controls', {
|
className={classnames('button', 'fullscreen', 'room-controls', {
|
||||||
visible: toolbarsVisible
|
visible : toolbarsVisible
|
||||||
})}
|
})}
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from 'react-tooltip';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
@ -18,6 +18,9 @@ import Draggable from 'react-draggable';
|
||||||
import { idle } from '../utils';
|
import { idle } from '../utils';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import Filmstrip from './Filmstrip';
|
import Filmstrip from './Filmstrip';
|
||||||
|
import { configureDragDrop, HoldingOverlay } from './FileSharing/DragDropSharing';
|
||||||
|
|
||||||
|
configureDragDrop();
|
||||||
|
|
||||||
// Hide toolbars after 10 seconds of inactivity.
|
// Hide toolbars after 10 seconds of inactivity.
|
||||||
const TIMEOUT = 10 * 1000;
|
const TIMEOUT = 10 * 1000;
|
||||||
|
|
@ -73,13 +76,16 @@ class Room extends React.Component
|
||||||
}[room.mode];
|
}[room.mode];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<HoldingOverlay />
|
||||||
|
|
||||||
<Appear duration={300}>
|
<Appear duration={300}>
|
||||||
<div data-component='Room'>
|
<div data-component='Room'>
|
||||||
<FullScreenView advancedMode={room.advancedMode} />
|
<FullScreenView advancedMode={room.advancedMode} />
|
||||||
<div
|
<div
|
||||||
className='room-wrapper'
|
className='room-wrapper'
|
||||||
style={{
|
style={{
|
||||||
width : toolAreaOpen ? '80%' : '100%'
|
width : toolAreaOpen ? '75%' : '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
|
@ -157,7 +163,7 @@ class Room extends React.Component
|
||||||
<div
|
<div
|
||||||
className='toolarea-wrapper'
|
className='toolarea-wrapper'
|
||||||
style={{
|
style={{
|
||||||
width : toolAreaOpen ? '20%' : '0%'
|
width : toolAreaOpen ? '25%' : '0%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{toolAreaOpen ?
|
{toolAreaOpen ?
|
||||||
|
|
@ -169,6 +175,7 @@ class Room extends React.Component
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Appear>
|
</Appear>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import * as toolTabActions from '../../redux/stateActions';
|
||||||
import ParticipantList from '../ParticipantList/ParticipantList';
|
import ParticipantList from '../ParticipantList/ParticipantList';
|
||||||
import Chat from '../Chat/Chat';
|
import Chat from '../Chat/Chat';
|
||||||
import Settings from '../Settings';
|
import Settings from '../Settings';
|
||||||
|
import FileSharing from '../FileSharing';
|
||||||
|
|
||||||
class ToolArea extends React.Component
|
class ToolArea extends React.Component
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +18,8 @@ class ToolArea extends React.Component
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
currentToolTab,
|
currentToolTab,
|
||||||
unread,
|
unreadMessages,
|
||||||
|
unreadFiles,
|
||||||
setToolTab
|
setToolTab
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|
@ -37,8 +39,8 @@ class ToolArea extends React.Component
|
||||||
<label htmlFor='tab-chat'>
|
<label htmlFor='tab-chat'>
|
||||||
Chat
|
Chat
|
||||||
|
|
||||||
{unread > 0 && (
|
{unreadMessages > 0 && (
|
||||||
<span className='badge'>{unread}</span>
|
<span className='badge'>{unreadMessages}</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
@ -46,6 +48,25 @@ class ToolArea extends React.Component
|
||||||
<Chat />
|
<Chat />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='tabs'
|
||||||
|
id='tab-files'
|
||||||
|
onChange={() => setToolTab('files')}
|
||||||
|
checked={currentToolTab === 'files'}
|
||||||
|
/>
|
||||||
|
<label htmlFor='tab-files'>
|
||||||
|
Files
|
||||||
|
|
||||||
|
{unreadFiles > 0 && (
|
||||||
|
<span className='badge'>{unreadFiles}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='tab'>
|
||||||
|
<FileSharing />
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type='radio'
|
type='radio'
|
||||||
name='tabs'
|
name='tabs'
|
||||||
|
|
@ -88,12 +109,14 @@ ToolArea.propTypes =
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
currentToolTab : PropTypes.string.isRequired,
|
currentToolTab : PropTypes.string.isRequired,
|
||||||
setToolTab : PropTypes.func.isRequired,
|
setToolTab : PropTypes.func.isRequired,
|
||||||
unread : PropTypes.number.isRequired
|
unreadMessages : PropTypes.number.isRequired,
|
||||||
|
unreadFiles : PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
currentToolTab : state.toolarea.currentToolTab,
|
currentToolTab : state.toolarea.currentToolTab,
|
||||||
unread : state.toolarea.unread
|
unreadMessages : state.toolarea.unreadMessages,
|
||||||
|
unreadFiles : state.toolarea.unreadFiles
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class ToolAreaButton extends React.Component
|
||||||
onClick={() => toggleToolArea()}
|
onClick={() => toggleToolArea()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{unread > 0 && (
|
{!toolAreaOpen && unread > 0 && (
|
||||||
<span className={classnames('badge', { long: unread >= 10 })}>
|
<span className={classnames('badge', { long: unread >= 10 })}>
|
||||||
{unread}
|
{unread}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -50,7 +50,7 @@ const mapStateToProps = (state) =>
|
||||||
return {
|
return {
|
||||||
toolAreaOpen : state.toolarea.toolAreaOpen,
|
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||||
visible : state.room.toolbarsVisible,
|
visible : state.room.toolbarsVisible,
|
||||||
unread : state.toolarea.unread
|
unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,6 @@ import UrlParse from 'url-parse';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import {
|
|
||||||
applyMiddleware as applyReduxMiddleware,
|
|
||||||
createStore as createReduxStore,
|
|
||||||
compose as composeRedux
|
|
||||||
} from 'redux';
|
|
||||||
import thunk from 'redux-thunk';
|
|
||||||
import { createLogger as createReduxLogger } from 'redux-logger';
|
|
||||||
import { getDeviceInfo } from 'mediasoup-client';
|
import { getDeviceInfo } from 'mediasoup-client';
|
||||||
import randomString from 'random-string';
|
import randomString from 'random-string';
|
||||||
import Logger from './Logger';
|
import Logger from './Logger';
|
||||||
|
|
@ -17,48 +10,11 @@ import * as utils from './utils';
|
||||||
import * as cookiesManager from './cookiesManager';
|
import * as cookiesManager from './cookiesManager';
|
||||||
import * as requestActions from './redux/requestActions';
|
import * as requestActions from './redux/requestActions';
|
||||||
import * as stateActions from './redux/stateActions';
|
import * as stateActions from './redux/stateActions';
|
||||||
import reducers from './redux/reducers';
|
|
||||||
import roomClientMiddleware from './redux/roomClientMiddleware';
|
|
||||||
import Room from './components/Room';
|
import Room from './components/Room';
|
||||||
import { loginEnabled } from '../config';
|
import { loginEnabled } from '../config';
|
||||||
|
import { store } from './store';
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
const reduxMiddlewares =
|
|
||||||
[
|
|
||||||
thunk,
|
|
||||||
roomClientMiddleware
|
|
||||||
];
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development')
|
|
||||||
{
|
|
||||||
const reduxLogger = createReduxLogger(
|
|
||||||
{
|
|
||||||
duration : true,
|
|
||||||
timestamp : false,
|
|
||||||
level : 'log',
|
|
||||||
logErrors : true
|
|
||||||
});
|
|
||||||
|
|
||||||
reduxMiddlewares.push(reduxLogger);
|
|
||||||
}
|
|
||||||
|
|
||||||
const composeEnhancers =
|
|
||||||
typeof window === 'object' &&
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
|
||||||
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
|
||||||
}) : composeRedux;
|
|
||||||
|
|
||||||
const enhancer = composeEnhancers(
|
|
||||||
applyReduxMiddleware(...reduxMiddlewares)
|
|
||||||
// other store enhancers if any
|
|
||||||
);
|
|
||||||
|
|
||||||
const store = createReduxStore(
|
|
||||||
reducers,
|
|
||||||
undefined,
|
|
||||||
enhancer
|
|
||||||
);
|
|
||||||
|
|
||||||
domready(() =>
|
domready(() =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const chatmessages = (state = [], action) =>
|
||||||
{
|
{
|
||||||
const { text } = action.payload;
|
const { text } = action.payload;
|
||||||
|
|
||||||
const message = createNewMessage(text, 'client', 'Me');
|
const message = createNewMessage(text, 'client', 'Me', undefined);
|
||||||
|
|
||||||
return [ ...state, message ];
|
return [ ...state, message ];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import notifications from './notifications';
|
||||||
import chatmessages from './chatmessages';
|
import chatmessages from './chatmessages';
|
||||||
import chatbehavior from './chatbehavior';
|
import chatbehavior from './chatbehavior';
|
||||||
import toolarea from './toolarea';
|
import toolarea from './toolarea';
|
||||||
|
import sharing from './sharing';
|
||||||
|
|
||||||
const reducers = combineReducers(
|
const reducers = combineReducers(
|
||||||
{
|
{
|
||||||
|
|
@ -19,7 +20,8 @@ const reducers = combineReducers(
|
||||||
notifications,
|
notifications,
|
||||||
chatmessages,
|
chatmessages,
|
||||||
chatbehavior,
|
chatbehavior,
|
||||||
toolarea
|
toolarea,
|
||||||
|
sharing
|
||||||
});
|
});
|
||||||
|
|
||||||
export default reducers;
|
export default reducers;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
const sharing = (state = [], action) =>
|
||||||
|
{
|
||||||
|
switch (action.type)
|
||||||
|
{
|
||||||
|
case 'SEND_FILE':
|
||||||
|
return [ ...state, { ...action.payload, me: true } ];
|
||||||
|
|
||||||
|
case 'ADD_FILE':
|
||||||
|
return [ ...state, action.payload ];
|
||||||
|
|
||||||
|
case 'ADD_FILE_HISTORY':
|
||||||
|
return [ ...action.payload.fileHistory, ...state ];
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sharing;
|
||||||
|
|
@ -2,7 +2,8 @@ const initialState =
|
||||||
{
|
{
|
||||||
toolAreaOpen : false,
|
toolAreaOpen : false,
|
||||||
currentToolTab : 'chat', // chat, settings, users
|
currentToolTab : 'chat', // chat, settings, users
|
||||||
unread : 0
|
unreadMessages : 0,
|
||||||
|
unreadFiles : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolarea = (state = initialState, action) =>
|
const toolarea = (state = initialState, action) =>
|
||||||
|
|
@ -12,17 +13,19 @@ const toolarea = (state = initialState, action) =>
|
||||||
case 'TOGGLE_TOOL_AREA':
|
case 'TOGGLE_TOOL_AREA':
|
||||||
{
|
{
|
||||||
const toolAreaOpen = !state.toolAreaOpen;
|
const toolAreaOpen = !state.toolAreaOpen;
|
||||||
const unread = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unread;
|
const unreadMessages = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||||
|
const unreadFiles = toolAreaOpen && state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||||
|
|
||||||
return { ...state, toolAreaOpen, unread };
|
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_TOOL_TAB':
|
case 'SET_TOOL_TAB':
|
||||||
{
|
{
|
||||||
const { toolTab } = action.payload;
|
const { toolTab } = action.payload;
|
||||||
const unread = toolTab === 'chat' ? 0 : state.unread;
|
const unreadMessages = toolTab === 'chat' ? 0 : state.unreadMessages;
|
||||||
|
const unreadFiles = toolTab === 'files' ? 0 : state.unreadFiles;
|
||||||
|
|
||||||
return { ...state, currentToolTab: toolTab, unread };
|
return { ...state, currentToolTab: toolTab, unreadMessages, unreadFiles };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'ADD_NEW_RESPONSE_MESSAGE':
|
case 'ADD_NEW_RESPONSE_MESSAGE':
|
||||||
|
|
@ -32,7 +35,17 @@ const toolarea = (state = initialState, action) =>
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...state, unread: state.unread + 1 };
|
return { ...state, unreadMessages: state.unreadMessages + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_FILE':
|
||||||
|
{
|
||||||
|
if (state.toolAreaOpen && state.currentToolTab === 'files')
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, unreadFiles: state.unreadFiles + 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,14 @@ export const sendChatMessage = (text, name, picture) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendFile = (file, name, picture) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SEND_FILE',
|
||||||
|
payload : { file, name, picture }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// This returns a redux-thunk action (a function).
|
// This returns a redux-thunk action (a function).
|
||||||
export const notify = ({ type = 'info', text, timeout }) =>
|
export const notify = ({ type = 'info', text, timeout }) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,12 @@ export default ({ dispatch, getState }) => (next) =>
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SEND_FILE':
|
||||||
|
{
|
||||||
|
client.sendFile(action.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,14 @@ export const addUserMessage = (text) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addUserFile = (file) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'ADD_NEW_USER_FILE',
|
||||||
|
payload : { file }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const addResponseMessage = (message) =>
|
export const addResponseMessage = (message) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -433,6 +441,22 @@ export const dropMessages = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addFile = (payload) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'ADD_FILE',
|
||||||
|
payload
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addFileHistory = (fileHistory) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'ADD_FILE_HISTORY',
|
||||||
|
payload : { fileHistory }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setPicture = (picture) =>
|
export const setPicture = (picture) =>
|
||||||
({
|
({
|
||||||
type : 'SET_PICTURE',
|
type : 'SET_PICTURE',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {
|
||||||
|
applyMiddleware,
|
||||||
|
createStore,
|
||||||
|
compose
|
||||||
|
} from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { createLogger } from 'redux-logger';
|
||||||
|
import reducers from './redux/reducers';
|
||||||
|
import roomClientMiddleware from './redux/roomClientMiddleware';
|
||||||
|
|
||||||
|
const reduxMiddlewares =
|
||||||
|
[
|
||||||
|
thunk,
|
||||||
|
roomClientMiddleware
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development')
|
||||||
|
{
|
||||||
|
const reduxLogger = createLogger(
|
||||||
|
{
|
||||||
|
duration : true,
|
||||||
|
timestamp : false,
|
||||||
|
level : 'log',
|
||||||
|
logErrors : true
|
||||||
|
});
|
||||||
|
|
||||||
|
reduxMiddlewares.push(reduxLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeEnhancers =
|
||||||
|
typeof window === 'object' &&
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
||||||
|
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
||||||
|
}) : compose;
|
||||||
|
|
||||||
|
const enhancer = composeEnhancers(
|
||||||
|
applyMiddleware(...reduxMiddlewares)
|
||||||
|
// other store enhancers if any
|
||||||
|
);
|
||||||
|
|
||||||
|
export const store = createStore(
|
||||||
|
reducers,
|
||||||
|
undefined,
|
||||||
|
enhancer
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,11 +9,15 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"create-torrent": "^3.32.1",
|
||||||
"debug": "^3.1.0",
|
"debug": "^3.1.0",
|
||||||
"domready": "^1.0.8",
|
"domready": "^1.0.8",
|
||||||
|
"drag-drop": "^4.2.0",
|
||||||
|
"file-saver": "^1.3.8",
|
||||||
"fscreen": "^1.0.2",
|
"fscreen": "^1.0.2",
|
||||||
"hark": "^1.2.2",
|
"hark": "^1.2.2",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
|
"magnet-uri": "^5.2.3",
|
||||||
"marked": "^0.4.0",
|
"marked": "^0.4.0",
|
||||||
"mediasoup-client": "^2.1.1",
|
"mediasoup-client": "^2.1.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
|
|
@ -33,7 +37,8 @@
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"resize-observer-polyfill": "^1.5.0",
|
"resize-observer-polyfill": "^1.5.0",
|
||||||
"riek": "^1.1.0",
|
"riek": "^1.1.0",
|
||||||
"url-parse": "^1.4.1"
|
"url-parse": "^1.4.1",
|
||||||
|
"webtorrent": "^0.101.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg fill="#FFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg fill="#FFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 304 B |
|
|
@ -0,0 +1,95 @@
|
||||||
|
[data-component='FileSharing'] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> .sharing-toolbar {
|
||||||
|
> .share-file {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
background: #252525;
|
||||||
|
border: 1px solid #151515;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 5px solid #151515;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .shared-files {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
> .file-entry {
|
||||||
|
background-color: rgba(0,0,0,0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file-avatar {
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
> p:not(:first-child) {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file-info {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #252525;
|
||||||
|
border: 1px solid #151515;
|
||||||
|
padding: 0.3rem;
|
||||||
|
border-bottom: 5px solid #151515;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#holding-overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag #holding-overlay {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #FFF;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: background ease 0.2s;
|
transition: background ease 0.2s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 33.33%;
|
width: 25%;
|
||||||
font-size: 1.3vmin;
|
font-size: 1.3vmin;
|
||||||
height: 3vmin;
|
height: 3vmin;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,25 +35,26 @@ body {
|
||||||
#multiparty-meeting {
|
#multiparty-meeting {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
// Components
|
|
||||||
@import './components/Room';
|
|
||||||
@import './components/Sidebar';
|
|
||||||
@import './components/Me';
|
|
||||||
@import './components/Peers';
|
|
||||||
@import './components/Peer';
|
|
||||||
@import './components/PeerView';
|
|
||||||
@import './components/ScreenView';
|
|
||||||
@import './components/Notifications';
|
|
||||||
@import './components/Chat';
|
|
||||||
@import './components/Settings';
|
|
||||||
@import './components/ToolArea';
|
|
||||||
@import './components/ParticipantList';
|
|
||||||
@import './components/FullScreenView';
|
|
||||||
@import './components/FullView';
|
|
||||||
@import './components/Filmstrip';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Components
|
||||||
|
@import './components/Room';
|
||||||
|
@import './components/Sidebar';
|
||||||
|
@import './components/Me';
|
||||||
|
@import './components/Peers';
|
||||||
|
@import './components/Peer';
|
||||||
|
@import './components/PeerView';
|
||||||
|
@import './components/ScreenView';
|
||||||
|
@import './components/Notifications';
|
||||||
|
@import './components/Chat';
|
||||||
|
@import './components/Settings';
|
||||||
|
@import './components/ToolArea';
|
||||||
|
@import './components/ParticipantList';
|
||||||
|
@import './components/FullScreenView';
|
||||||
|
@import './components/FullView';
|
||||||
|
@import './components/Filmstrip';
|
||||||
|
@import './components/FileSharing';
|
||||||
|
|
||||||
// Hack to detect in JS the current media query
|
// Hack to detect in JS the current media query
|
||||||
#multiparty-meeting-media-query-detector {
|
#multiparty-meeting-media-query-detector {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,15 @@ module.exports =
|
||||||
},
|
},
|
||||||
// Listening port for https server.
|
// Listening port for https server.
|
||||||
listeningPort : 3443,
|
listeningPort : 3443,
|
||||||
|
turnServers : [
|
||||||
|
{
|
||||||
|
urls : [
|
||||||
|
'turn:example.com:443?transport=tcp'
|
||||||
|
],
|
||||||
|
username : 'example',
|
||||||
|
credential : 'example'
|
||||||
|
}
|
||||||
|
],
|
||||||
mediasoup :
|
mediasoup :
|
||||||
{
|
{
|
||||||
// mediasoup Server settings.
|
// mediasoup Server settings.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const EventEmitter = require('events').EventEmitter;
|
const EventEmitter = require('events').EventEmitter;
|
||||||
const protooServer = require('protoo-server');
|
const protooServer = require('protoo-server');
|
||||||
|
const WebTorrent = require('webtorrent-hybrid');
|
||||||
const Logger = require('./Logger');
|
const Logger = require('./Logger');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
@ -11,6 +12,14 @@ const BITRATE_FACTOR = 0.75;
|
||||||
|
|
||||||
const logger = new Logger('Room');
|
const logger = new Logger('Room');
|
||||||
|
|
||||||
|
const torrentClient = new WebTorrent({
|
||||||
|
tracker : {
|
||||||
|
rtcConfig : {
|
||||||
|
iceServers : config.turnServers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
class Room extends EventEmitter
|
class Room extends EventEmitter
|
||||||
{
|
{
|
||||||
constructor(roomId, mediaServer)
|
constructor(roomId, mediaServer)
|
||||||
|
|
@ -28,6 +37,8 @@ class Room extends EventEmitter
|
||||||
|
|
||||||
this._chatHistory = [];
|
this._chatHistory = [];
|
||||||
|
|
||||||
|
this._fileHistory = [];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Protoo Room instance.
|
// Protoo Room instance.
|
||||||
|
|
@ -272,6 +283,37 @@ class Room extends EventEmitter
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'send-file':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const fileData = request.data.file;
|
||||||
|
|
||||||
|
this._fileHistory.push(fileData);
|
||||||
|
|
||||||
|
if (!torrentClient.get(fileData.file.magnet))
|
||||||
|
{
|
||||||
|
torrentClient.add(fileData.file.magnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._protooRoom.spread('file-receive', {
|
||||||
|
file : fileData
|
||||||
|
}, [ protooPeer ]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'file-history':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
protooPeer.send('file-history-receive', {
|
||||||
|
fileHistory : this._fileHistory
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'raisehand-message':
|
case 'raisehand-message':
|
||||||
{
|
{
|
||||||
accept();
|
accept();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,7 +13,8 @@
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.3",
|
||||||
"mediasoup": "^2.1.0",
|
"mediasoup": "^2.1.0",
|
||||||
"passport-dataporten": "^1.3.0",
|
"passport-dataporten": "^1.3.0",
|
||||||
"protoo-server": "^2.0.7"
|
"protoo-server": "^2.0.7",
|
||||||
|
"webtorrent-hybrid": "^1.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue