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

master
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

View File

@ -1,5 +1,8 @@
import io from 'socket.io-client'; import io from 'socket.io-client';
import * as mediasoupClient from 'mediasoup-client'; import * as mediasoupClient from 'mediasoup-client';
import WebTorrent from 'webtorrent';
import createTorrent from 'create-torrent';
import { saveAs } from 'file-saver/FileSaver';
import Logger from './Logger'; import Logger from './Logger';
import hark from 'hark'; import hark from 'hark';
import ScreenShare from './ScreenShare'; import ScreenShare from './ScreenShare';
@ -61,6 +64,9 @@ export default class RoomClient
// Whether we should produce. // Whether we should produce.
this._produce = produce; this._produce = produce;
// Torrent support
this._torrentSupport = WebTorrent.WEBRTC_SUPPORT;
// Whether simulcast should be used. // Whether simulcast should be used.
this._useSimulcast = useSimulcast; this._useSimulcast = useSimulcast;
@ -83,6 +89,15 @@ export default class RoomClient
this._room = new mediasoupClient.Room(ROOM_OPTIONS); this._room = new mediasoupClient.Room(ROOM_OPTIONS);
this._room.roomId = roomId; this._room.roomId = roomId;
// Our WebTorrent client
this._webTorrent = this._torrentSupport && new WebTorrent({
tracker : {
rtcConfig : {
iceServers : ROOM_OPTIONS.turnServers
}
}
});
// Max spotlights // Max spotlights
this._maxSpotlights = ROOM_OPTIONS.maxSpotlights; this._maxSpotlights = ROOM_OPTIONS.maxSpotlights;
@ -344,7 +359,128 @@ export default class RoomClient
} }
} }
async sendFile(file) saveFile(file)
{
file.getBlob((err, blob) =>
{
if (err)
{
return this.props.notify({
text : 'An error occurred while saving a file'
});
}
saveAs(blob, file.name);
});
}
handleDownload(magnetUri)
{
store.dispatch(
stateActions.setFileActive(magnetUri));
const existingTorrent = this._webTorrent.get(magnetUri);
if (existingTorrent)
{
// Never add duplicate torrents, use the existing one instead.
return this._handleTorrent(existingTorrent);
}
this._webTorrent.add(magnetUri, this._handleTorrent);
}
_handleTorrent(torrent)
{
// Torrent already done, this can happen if the
// same file was sent multiple times.
if (torrent.progress === 1)
{
store.dispatch(
stateActions.setFileDone(
torrent.magnetURI,
torrent.files
));
return;
}
torrent.on('download', () =>
{
store.dispatch(
stateActions.setFileProgress(
torrent.magnetURI,
torrent.progress
));
});
torrent.on('done', () =>
{
store.dispatch(
stateActions.setFileDone(
torrent.magnetURI,
torrent.files
));
});
}
async shareFiles(files)
{
this.notify('Creating torrent');
createTorrent(files, (err, torrent) =>
{
if (err)
{
return this.notify(
'An error occured while uploading a file'
);
}
const existingTorrent = this._webTorrent.get(torrent);
if (existingTorrent)
{
const { displayName, picture } = store.getState().me;
const file = {
magnetUri : existingTorrent.magnetURI,
displayName,
picture
};
return this._sendFile(file);
}
this._webTorrent.seed(files, (newTorrent) =>
{
this.notify(
'Torrent successfully created'
);
const { displayName, picture } = store.getState().me;
const file = {
magnetUri : newTorrent.magnetURI,
displayName,
picture
};
store.dispatch(stateActions.addFile(
{
magnetUri : file.magnetUri,
displayName : displayName,
picture : picture,
me : true
}));
this._sendFile(file);
});
});
}
// { file, name, picture }
async _sendFile(file)
{ {
logger.debug('sendFile() [file: %o]', file); logger.debug('sendFile() [file: %o]', file);
@ -728,68 +864,6 @@ export default class RoomClient
stateActions.setWebcamInProgress(false)); stateActions.setWebcamInProgress(false));
} }
async changeWebcamResolution()
{
logger.debug('changeWebcamResolution()');
let oldResolution;
let newResolution;
store.dispatch(
stateActions.setWebcamInProgress(true));
try
{
oldResolution = this._webcam.resolution;
switch (oldResolution)
{
case 'qvga':
newResolution = 'vga';
break;
case 'vga':
newResolution = 'hd';
break;
case 'hd':
newResolution = 'qvga';
break;
}
this._webcam.resolution = newResolution;
const { device } = this._webcam;
logger.debug('changeWebcamResolution() | calling getUserMedia()');
const stream = await navigator.mediaDevices.getUserMedia(
{
video :
{
deviceId : { exact: device.deviceId },
...VIDEO_CONSTRAINS
}
});
const track = stream.getVideoTracks()[0];
const newTrack = await this._webcamProducer.replaceTrack(track);
track.stop();
store.dispatch(
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
}
catch (error)
{
logger.error('changeWebcamResolution() failed: %o', error);
this._webcam.resolution = oldResolution;
}
store.dispatch(
stateActions.setWebcamInProgress(false));
}
setSelectedPeer(peerName) setSelectedPeer(peerName)
{ {
logger.debug('setSelectedPeer() [peerName:"%s"]', peerName); logger.debug('setSelectedPeer() [peerName:"%s"]', peerName);
@ -800,266 +874,59 @@ export default class RoomClient
stateActions.setSelectedPeer(peerName)); stateActions.setSelectedPeer(peerName));
} }
async mutePeerAudio(peerName) // type: mic/webcam/screen
// mute: true/false
modifyPeerConsumer(peerName, type, mute)
{ {
logger.debug('mutePeerAudio() [peerName:"%s"]', peerName); logger.debug(
'modifyPeerConsumer() [peerName:"%s", type:"%s"]',
store.dispatch( peerName,
stateActions.setPeerAudioInProgress(peerName, true)); type
);
try
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'mic')
continue;
await consumer.pause('mute-audio');
}
}
}
}
catch (error)
{
logger.error('mutePeerAudio() failed: %o', error);
}
store.dispatch(
stateActions.setPeerAudioInProgress(peerName, false));
}
async unmutePeerAudio(peerName)
{
logger.debug('unmutePeerAudio() [peerName:"%s"]', peerName);
store.dispatch(
stateActions.setPeerAudioInProgress(peerName, true));
try
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'mic' || !consumer.supported)
continue;
await consumer.resume();
}
}
}
}
catch (error)
{
logger.error('unmutePeerAudio() failed: %o', error);
}
store.dispatch(
stateActions.setPeerAudioInProgress(peerName, false));
}
async pausePeerVideo(peerName)
{
logger.debug('pausePeerVideo() [peerName:"%s"]', peerName);
store.dispatch(
stateActions.setPeerVideoInProgress(peerName, true));
try
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'webcam')
continue;
await consumer.pause('pause-video');
}
}
}
}
catch (error)
{
logger.error('pausePeerVideo() failed: %o', error);
}
store.dispatch(
stateActions.setPeerVideoInProgress(peerName, false));
}
async resumePeerVideo(peerName)
{
logger.debug('resumePeerVideo() [peerName:"%s"]', peerName);
store.dispatch(
stateActions.setPeerVideoInProgress(peerName, true));
try
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'webcam' || !consumer.supported)
continue;
await consumer.resume();
}
}
}
}
catch (error)
{
logger.error('resumePeerVideo() failed: %o', error);
}
store.dispatch(
stateActions.setPeerVideoInProgress(peerName, false));
}
async pausePeerScreen(peerName)
{
logger.debug('pausePeerScreen() [peerName:"%s"]', peerName);
store.dispatch(
stateActions.setPeerScreenInProgress(peerName, true));
try
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'screen')
continue;
await consumer.pause('pause-screen');
}
}
}
}
catch (error)
{
logger.error('pausePeerScreen() failed: %o', error);
}
store.dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
}
async resumePeerScreen(peerName)
{
logger.debug('resumePeerScreen() [peerName:"%s"]', peerName);
store.dispatch(
stateActions.setPeerScreenInProgress(peerName, true));
try
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'screen' || !consumer.supported)
continue;
await consumer.resume();
}
}
}
}
catch (error)
{
logger.error('resumePeerScreen() failed: %o', error);
}
store.dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
}
async enableAudioOnly()
{
logger.debug('enableAudioOnly()');
store.dispatch(
stateActions.setAudioOnlyInProgress(true));
try
{
if (this._webcamProducer)
await this._webcamProducer.close();
for (const peer of this._room.peers)
{
for (const consumer of peer.consumers)
{
if (consumer.kind !== 'video')
continue;
await consumer.pause('audio-only-mode');
}
}
if (type === 'mic')
store.dispatch( store.dispatch(
stateActions.setAudioOnlyState(true)); stateActions.setPeerAudioInProgress(peerName, true));
} else if (type === 'webcam')
catch (error) store.dispatch(
{ stateActions.setPeerVideoInProgress(peerName, true));
logger.error('enableAudioOnly() failed: %o', error); else if (type === 'screen')
} store.dispatch(
stateActions.setPeerScreenInProgress(peerName, true));
store.dispatch(
stateActions.setAudioOnlyInProgress(false));
}
async disableAudioOnly()
{
logger.debug('disableAudioOnly()');
store.dispatch(
stateActions.setAudioOnlyInProgress(true));
try try
{ {
if (!this._webcamProducer && this._room.canSend('video'))
await this.enableWebcam();
for (const peer of this._room.peers) for (const peer of this._room.peers)
{ {
for (const consumer of peer.consumers) if (peer.name === peerName)
{ {
if (consumer.kind !== 'video' || !consumer.supported) for (const consumer of peer.consumers)
continue; {
if (consumer.appData.source !== type || !consumer.supported)
continue;
await consumer.resume(); if (mute)
consumer.pause(`mute-${type}`);
else
consumer.resume();
}
} }
} }
store.dispatch(
stateActions.setAudioOnlyState(false));
} }
catch (error) catch (error)
{ {
logger.error('disableAudioOnly() failed: %o', error); logger.error('modifyPeerConsumer() failed: %o', error);
} }
store.dispatch( if (type === 'mic')
stateActions.setAudioOnlyInProgress(false)); store.dispatch(
stateActions.setPeerAudioInProgress(peerName, false));
else if (type === 'webcam')
store.dispatch(
stateActions.setPeerVideoInProgress(peerName, false));
else if (type === 'screen')
store.dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
} }
async sendRaiseHandState(state) async sendRaiseHandState(state)
@ -1338,6 +1205,9 @@ export default class RoomClient
{ {
await this._room.join(this._peerName, { displayName, device }); await this._room.join(this._peerName, { displayName, device });
store.dispatch(
stateActions.setFileSharingSupported(this._torrentSupport));
this._sendTransport = this._sendTransport =
this._room.createTransport('send', { media: 'SEND_MIC_WEBCAM' }); this._room.createTransport('send', { media: 'SEND_MIC_WEBCAM' });

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));

View 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);

View File

@ -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);

View File

@ -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));

View File

@ -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);

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;

View File

@ -145,14 +145,11 @@ class Filmstrip extends Component
<div className='filmstrip'> <div className='filmstrip'>
<div className='filmstrip-content'> <div className='filmstrip-content'>
{ Object.keys(peers).map((peerName) =>
{ {
Object.keys(peers).map((peerName) => if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
{ {
<If return (
condition={
spotlights.find((spotlightsElement) => spotlightsElement === peerName)
}
>
<div <div
key={peerName} key={peerName}
onClick={() => roomClient.setSelectedPeer(peerName)} onClick={() => roomClient.setSelectedPeer(peerName)}
@ -168,9 +165,9 @@ class Filmstrip extends Component
/> />
</div> </div>
</div> </div>
</If>; );
}) }
} })}
</div> </div>
</div> </div>
<div className='hidden-peer-container'> <div className='hidden-peer-container'>

View File

@ -63,8 +63,8 @@ const ListPeer = (props) =>
{ {
e.stopPropagation(); e.stopPropagation();
screenVisible ? screenVisible ?
roomClient.pausePeerScreen(peer.name) : roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
roomClient.resumePeerScreen(peer.name); roomClient.modifyPeerConsumer(peer.name, 'screen', false);
}} }}
/> />
</If> </If>
@ -78,8 +78,8 @@ const ListPeer = (props) =>
{ {
e.stopPropagation(); e.stopPropagation();
micEnabled ? micEnabled ?
roomClient.mutePeerAudio(peer.name) : roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.unmutePeerAudio(peer.name); roomClient.modifyPeerConsumer(peer.name, 'mic', false);
}} }}
/> />
</div> </div>

View File

@ -121,8 +121,8 @@ class Peer extends Component
{ {
e.stopPropagation(); e.stopPropagation();
micEnabled ? micEnabled ?
roomClient.mutePeerAudio(peer.name) : roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.unmutePeerAudio(peer.name); roomClient.modifyPeerConsumer(peer.name, 'mic', false);
}} }}
/> />

View File

@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import * as appPropTypes from './appPropTypes';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Peer from './Peer'; import Peer from './Peer';
import HiddenPeers from './HiddenPeers'; import HiddenPeers from './HiddenPeers';
@ -105,33 +104,30 @@ class Peers extends React.Component
return ( return (
<div data-component='Peers' ref={this.peersRef}> <div data-component='Peers' ref={this.peersRef}>
{ Object.keys(peers).map((peerName) =>
{ {
peers.map((peer) => if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
{ {
<If return (
condition={ <Appear key={peerName} duration={1000}>
spotlights.find((spotlightsElement) => spotlightsElement === peer.name)
}
>
<Appear key={peer.name} duration={1000}>
<div <div
className={classnames('peer-container', { className={classnames('peer-container', {
'selected' : this.props.selectedPeerName === peer.name, 'selected' : this.props.selectedPeerName === peerName,
'active-speaker' : peer.name === activeSpeakerName 'active-speaker' : peerName === activeSpeakerName
})} })}
> >
<div className='peer-content'> <div className='peer-content'>
<Peer <Peer
advancedMode={advancedMode} advancedMode={advancedMode}
name={peer.name} name={peerName}
style={style} style={style}
/> />
</div> </div>
</div> </div>
</Appear> </Appear>
</If>; );
}) }
} })}
<div className='hidden-peer-container'> <div className='hidden-peer-container'>
<If condition={spotlightsLength < peers.length}> <If condition={spotlightsLength < peers.length}>
<HiddenPeers <HiddenPeers
@ -147,7 +143,7 @@ class Peers extends React.Component
Peers.propTypes = Peers.propTypes =
{ {
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, peers : PropTypes.object.isRequired,
boxes : PropTypes.number, boxes : PropTypes.number,
activeSpeakerName : PropTypes.string, activeSpeakerName : PropTypes.string,
selectedPeerName : PropTypes.string, selectedPeerName : PropTypes.string,
@ -157,14 +153,13 @@ Peers.propTypes =
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
const peers = Object.values(state.peers);
const spotlights = state.room.spotlights; const spotlights = state.room.spotlights;
const spotlightsLength = spotlights ? state.room.spotlights.length : 0; const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
const boxes = spotlightsLength + Object.values(state.consumers) const boxes = spotlightsLength + Object.values(state.consumers)
.filter((consumer) => consumer.source === 'screen').length; .filter((consumer) => consumer.source === 'screen').length;
return { return {
peers, peers : state.peers,
boxes, boxes,
activeSpeakerName : state.room.activeSpeakerName, activeSpeakerName : state.room.activeSpeakerName,
selectedPeerName : state.room.selectedPeerName, selectedPeerName : state.room.selectedPeerName,

View File

@ -20,9 +20,6 @@ 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;
@ -78,8 +75,6 @@ class Room extends React.Component
return ( return (
<Fragment> <Fragment>
<HoldingOverlay />
<Appear duration={300}> <Appear duration={300}>
<div data-component='Room'> <div data-component='Room'>
<CookieConsent> <CookieConsent>

View File

@ -6,7 +6,7 @@ import * as stateActions 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'; import FileSharing from '../FileSharing/FileSharing';
import TabHeader from './TabHeader'; import TabHeader from './TabHeader';
class ToolArea extends React.Component class ToolArea extends React.Component

View File

@ -1,77 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as stateActions from '../../redux/stateActions';
class ToolAreaButton extends React.Component
{
render()
{
const {
toolAreaOpen,
toggleToolArea,
unread,
visible
} = this.props;
return (
<div
data-component='ToolAreaButton'
className={classnames('room-controls', {
on : toolAreaOpen,
visible
})}
>
<div
className={classnames('button toolarea-button', {
on : toolAreaOpen
})}
data-tip='Open tools'
data-type='dark'
onClick={() => toggleToolArea()}
/>
{!toolAreaOpen && unread > 0 && (
<span className={classnames('badge', { long: unread >= 10 })}>
{unread}
</span>
)}
</div>
);
}
}
ToolAreaButton.propTypes =
{
toolAreaOpen : PropTypes.bool.isRequired,
toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
visible : PropTypes.bool.isRequired
};
const mapStateToProps = (state) =>
{
return {
toolAreaOpen : state.toolarea.toolAreaOpen,
visible : state.room.toolbarsVisible,
unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleToolArea : () =>
{
dispatch(stateActions.toggleToolArea());
}
};
};
const ToolAreaButtonContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ToolAreaButton);
export default ToolAreaButtonContainer;

View File

@ -76,3 +76,16 @@ export const Message = PropTypes.shape(
text : PropTypes.string, text : PropTypes.string,
sender : PropTypes.string sender : PropTypes.string
}); });
export const FileEntryProps = PropTypes.shape(
{
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
});

View File

@ -0,0 +1,99 @@
const files = (state = {}, action) =>
{
switch (action.type)
{
case 'ADD_FILE':
{
const { file } = action.payload;
const newFile = {
active : false,
progress : 0,
files : null,
me : false,
...file
};
return { ...state, [file.magnetUri]: newFile };
}
case 'ADD_FILE_HISTORY':
{
const { fileHistory } = action.payload;
const newFileHistory = {};
fileHistory.map((file) =>
{
const newFile = {
active : false,
progress : 0,
files : null,
me : false,
...file
};
newFileHistory[file.magnetUri] = newFile;
});
return { ...state, ...newFileHistory };
}
case 'SET_FILE_ACTIVE':
{
const { magnetUri } = action.payload;
const file = state[magnetUri];
const newFile = { ...file, active: true };
return { ...state, [magnetUri]: newFile };
}
case 'SET_FILE_INACTIVE':
{
const { magnetUri } = action.payload;
const file = state[magnetUri];
const newFile = { ...file, active: false };
return { ...state, [magnetUri]: newFile };
}
case 'SET_FILE_PROGRESS':
{
const { magnetUri, progress } = action.payload;
const file = state[magnetUri];
const newFile = { ...file, progress: progress };
return { ...state, [magnetUri]: newFile };
}
case 'SET_FILE_DONE':
{
const { magnetUri, sharedFiles } = action.payload;
const file = state[magnetUri];
const newFile = {
...file,
files : sharedFiles,
progress : 1,
active : false,
timeout : false
};
return { ...state, [magnetUri]: newFile };
}
case 'REMOVE_FILE':
{
const { magnetUri } = action.payload;
return state.filter((file) => file.magnetUri !== magnetUri);
}
default:
return state;
}
};
export default files;

View File

@ -8,7 +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'; import files from './files';
const reducers = combineReducers( const reducers = combineReducers(
{ {
@ -21,7 +21,7 @@ const reducers = combineReducers(
chatmessages, chatmessages,
chatbehavior, chatbehavior,
toolarea, toolarea,
sharing files
}); });
export default reducers; export default reducers;

View File

@ -1,6 +1,4 @@
const initialState = []; const notifications = (state = [], action) =>
const notifications = (state = initialState, action) =>
{ {
switch (action.type) switch (action.type)
{ {

View File

@ -3,6 +3,7 @@ const initialState =
url : null, url : null,
state : 'new', // new/connecting/connected/disconnected/closed, state : 'new', // new/connecting/connected/disconnected/closed,
activeSpeakerName : null, activeSpeakerName : null,
torrentSupport : false,
showSettings : false, showSettings : false,
advancedMode : false, advancedMode : false,
fullScreenConsumer : null, // ConsumerID fullScreenConsumer : null, // ConsumerID
@ -41,6 +42,13 @@ const room = (state = initialState, action) =>
return { ...state, activeSpeakerName: peerName }; return { ...state, activeSpeakerName: peerName };
} }
case 'FILE_SHARING_SUPPORTED':
{
const { supported } = action.payload;
return { ...state, torrentSupport: supported };
}
case 'TOGGLE_SETTINGS': case 'TOGGLE_SETTINGS':
{ {
const showSettings = !state.showSettings; const showSettings = !state.showSettings;

View File

@ -70,6 +70,14 @@ export const setWebcamDevices = (devices) =>
}; };
}; };
export const setFileSharingSupported = (supported) =>
{
return {
type : 'FILE_SHARING_SUPPORTED',
payload : { supported }
};
};
export const setDisplayName = (displayName) => export const setDisplayName = (displayName) =>
{ {
return { return {
@ -455,11 +463,11 @@ export const dropMessages = () =>
}; };
}; };
export const addFile = (payload) => export const addFile = (file) =>
{ {
return { return {
type : 'ADD_FILE', type : 'ADD_FILE',
payload payload : { file }
}; };
}; };
@ -471,6 +479,38 @@ export const addFileHistory = (fileHistory) =>
}; };
}; };
export const setFileActive = (magnetUri) =>
{
return {
type : 'SET_FILE_ACTIVE',
payload : { magnetUri }
};
};
export const setFileInActive = (magnetUri) =>
{
return {
type : 'SET_FILE_INACTIVE',
payload : { magnetUri }
};
};
export const setFileProgress = (magnetUri, progress) =>
{
return {
type : 'SET_FILE_PROGRESS',
payload : { magnetUri, progress }
};
};
export const setFileDone = (magnetUri, sharedFiles) =>
{
return {
type : 'SET_FILE_DONE',
payload : { magnetUri, sharedFiles }
};
};
export const setPicture = (picture) => export const setPicture = (picture) =>
({ ({
type : 'SET_PICTURE', type : 'SET_PICTURE',

View File

@ -19,12 +19,6 @@
} }
} }
[data-component='ToolAreaButton'] {
&.on {
right: 80%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 80%; width: 80%;
@ -153,12 +147,6 @@
} }
@media (min-width: 600px) { @media (min-width: 600px) {
[data-component='ToolAreaButton'] {
&.on {
right: 60%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 60%; width: 60%;
@ -167,12 +155,6 @@
} }
@media (min-width: 900px) { @media (min-width: 900px) {
[data-component='ToolAreaButton'] {
&.on {
right: 40%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 40%; width: 40%;
@ -181,12 +163,6 @@
} }
@media (min-width: 1500px) { @media (min-width: 1500px) {
[data-component='ToolAreaButton'] {
&.on {
right: 25%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 25%; width: 25%;
@ -194,79 +170,6 @@
} }
} }
[data-component='ToolAreaButton'] {
position: absolute;
z-index: 1020;
right: 0;
height: 36px;
width: 36px;
margin: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: right 0.3s;
> .button {
flex: 0 0 auto;
margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
border-radius: 100%;
+desktop() {
height: 36px;
width: 36px;
}
+mobile() {
height: 32px;
width: 32px;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.toolarea-button {
background-image: url('/resources/images/icon_tool_area_white.svg');
&.on {
background-image: url('/resources/images/icon_tool_area_black.svg');
}
}
}
> .badge {
border-radius: 50%;
font-size: 1rem;
background: #b12525;
color: #fff;
text-align: center;
margin-top: -8px;
line-height: 1rem;
margin-right: -8px;
position: absolute;
padding: 0.2rem 0.4rem;
top: 0;
right: 0;
&.long {
border-radius: 25% / 50%;
}
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
width: 100%; width: 100%;
height: 100%; height: 100%;