diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index bbdc1ba..fe54c3b 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -1,5 +1,8 @@ import io from 'socket.io-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 hark from 'hark'; import ScreenShare from './ScreenShare'; @@ -61,6 +64,9 @@ export default class RoomClient // Whether we should produce. this._produce = produce; + // Torrent support + this._torrentSupport = WebTorrent.WEBRTC_SUPPORT; + // Whether simulcast should be used. this._useSimulcast = useSimulcast; @@ -83,6 +89,15 @@ export default class RoomClient this._room = new mediasoupClient.Room(ROOM_OPTIONS); this._room.roomId = roomId; + // Our WebTorrent client + this._webTorrent = this._torrentSupport && new WebTorrent({ + tracker : { + rtcConfig : { + iceServers : ROOM_OPTIONS.turnServers + } + } + }); + // Max spotlights 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); @@ -728,68 +864,6 @@ export default class RoomClient 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) { logger.debug('setSelectedPeer() [peerName:"%s"]', peerName); @@ -800,266 +874,59 @@ export default class RoomClient stateActions.setSelectedPeer(peerName)); } - async mutePeerAudio(peerName) + // type: mic/webcam/screen + // mute: true/false + modifyPeerConsumer(peerName, type, mute) { - logger.debug('mutePeerAudio() [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') - 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'); - } - } + logger.debug( + 'modifyPeerConsumer() [peerName:"%s", type:"%s"]', + peerName, + type + ); + if (type === 'mic') store.dispatch( - stateActions.setAudioOnlyState(true)); - } - catch (error) - { - logger.error('enableAudioOnly() failed: %o', error); - } - - store.dispatch( - stateActions.setAudioOnlyInProgress(false)); - } - - async disableAudioOnly() - { - logger.debug('disableAudioOnly()'); - - store.dispatch( - stateActions.setAudioOnlyInProgress(true)); + stateActions.setPeerAudioInProgress(peerName, true)); + else if (type === 'webcam') + store.dispatch( + stateActions.setPeerVideoInProgress(peerName, true)); + else if (type === 'screen') + store.dispatch( + stateActions.setPeerScreenInProgress(peerName, true)); try { - if (!this._webcamProducer && this._room.canSend('video')) - await this.enableWebcam(); - for (const peer of this._room.peers) { - for (const consumer of peer.consumers) + if (peer.name === peerName) { - if (consumer.kind !== 'video' || !consumer.supported) - continue; + for (const consumer of peer.consumers) + { + 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) { - logger.error('disableAudioOnly() failed: %o', error); + logger.error('modifyPeerConsumer() failed: %o', error); } - store.dispatch( - stateActions.setAudioOnlyInProgress(false)); + if (type === 'mic') + 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) @@ -1338,6 +1205,9 @@ export default class RoomClient { await this._room.join(this._peerName, { displayName, device }); + store.dispatch( + stateActions.setFileSharingSupported(this._torrentSupport)); + this._sendTransport = this._room.createTransport('send', { media: 'SEND_MIC_WEBCAM' }); diff --git a/app/lib/components/FileSharing/File.jsx b/app/lib/components/FileSharing/File.jsx new file mode 100644 index 0000000..96f15d6 --- /dev/null +++ b/app/lib/components/FileSharing/File.jsx @@ -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 ( +
+ + +
+ + +

You shared a file.

+
+ +

{file.displayName} shared a file.

+
+
+ + +
+ + + + { + roomClient.handleDownload(file.magnetUri); + }} + > + + + + +

+ Your browser does not support downloading files using WebTorrent. +

+
+
+

{magnet.decode(file.magnetUri).dn}

+
+
+ + + +

+ 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. +

+
+
+ + + + + + + +

File finished downloading.

+ + {file.files.map((sharedFile, i) => ( +
+ + { + roomClient.saveFile(sharedFile); + }} + > + + + +

{sharedFile.name}

+
+ ))} +
+
+
+
+ ); + } +} + +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)); \ No newline at end of file diff --git a/app/lib/components/FileSharing/FileEntry.jsx b/app/lib/components/FileSharing/FileEntry.jsx deleted file mode 100644 index 71ed6be..0000000 --- a/app/lib/components/FileSharing/FileEntry.jsx +++ /dev/null @@ -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 ( -
- - -
- - -

You shared a file.

-
- -

{this.props.data.name} shared a file.

-
-
- - -
- - - - - - - -

- Your browser does not support downloading files using WebTorrent. -

-
-
-

{magnet.decode(this.props.data.file.magnet).dn}

-
-
- - - -

- Locating peers -

- - {this.state.timeout && ( -

- 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. -

- )} -
-
- - 0}> - - - - - -

Torrent finished downloading.

- - {this.state.files.map((file, i) => ( -
- this.saveFile(file)}> - - - -

{file.name}

-
- ))} -
-
-
-
- ); - } -} - -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); \ No newline at end of file diff --git a/app/lib/components/FileSharing/FileList.jsx b/app/lib/components/FileSharing/FileList.jsx new file mode 100644 index 0000000..9215e3a --- /dev/null +++ b/app/lib/components/FileSharing/FileList.jsx @@ -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 ( +
+ { Object.keys(files).map((magnetUri) => + + )} +
+ ); + } +} + +FileList.propTypes = { + files : PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => +{ + return { + files : state.files + }; +}; + +export default compose( + connect(mapStateToProps), + scrollToBottom() +)(FileList); diff --git a/app/lib/components/FileSharing/FileSharing.jsx b/app/lib/components/FileSharing/FileSharing.jsx new file mode 100644 index 0000000..84fccac --- /dev/null +++ b/app/lib/components/FileSharing/FileSharing.jsx @@ -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 ( +
+
+ + +
+ {buttonDescription} +
+
+ + +
+ ); + } +} + +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)); diff --git a/app/lib/components/FileSharing/SharedFilesList.jsx b/app/lib/components/FileSharing/SharedFilesList.jsx deleted file mode 100644 index f4f18c8..0000000 --- a/app/lib/components/FileSharing/SharedFilesList.jsx +++ /dev/null @@ -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 ( -
- - 0}> - { - sharing.map((entry, i) => ( - - )) - } - - -
-

No one has shared files yet...

-
-
-
-
- ); - } -} - -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); diff --git a/app/lib/components/FileSharing/index.jsx b/app/lib/components/FileSharing/index.jsx deleted file mode 100644 index fd41051..0000000 --- a/app/lib/components/FileSharing/index.jsx +++ /dev/null @@ -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 ( -
-
- - -
- {buttonDescription} -
-
- - -
- ); - } -} - -export default FileSharing; \ No newline at end of file diff --git a/app/lib/components/Filmstrip.jsx b/app/lib/components/Filmstrip.jsx index 63a80fb..9ebe692 100644 --- a/app/lib/components/Filmstrip.jsx +++ b/app/lib/components/Filmstrip.jsx @@ -145,14 +145,11 @@ class Filmstrip extends Component
+ { Object.keys(peers).map((peerName) => { - Object.keys(peers).map((peerName) => + if (spotlights.find((spotlightsElement) => spotlightsElement === peerName)) { - spotlightsElement === peerName) - } - > + return (
roomClient.setSelectedPeer(peerName)} @@ -168,9 +165,9 @@ class Filmstrip extends Component />
- ; - }) - } + ); + } + })}
diff --git a/app/lib/components/ParticipantList/ListPeer.jsx b/app/lib/components/ParticipantList/ListPeer.jsx index d9f8861..abb56b7 100644 --- a/app/lib/components/ParticipantList/ListPeer.jsx +++ b/app/lib/components/ParticipantList/ListPeer.jsx @@ -63,8 +63,8 @@ const ListPeer = (props) => { e.stopPropagation(); screenVisible ? - roomClient.pausePeerScreen(peer.name) : - roomClient.resumePeerScreen(peer.name); + roomClient.modifyPeerConsumer(peer.name, 'screen', true) : + roomClient.modifyPeerConsumer(peer.name, 'screen', false); }} /> @@ -78,8 +78,8 @@ const ListPeer = (props) => { e.stopPropagation(); micEnabled ? - roomClient.mutePeerAudio(peer.name) : - roomClient.unmutePeerAudio(peer.name); + roomClient.modifyPeerConsumer(peer.name, 'mic', true) : + roomClient.modifyPeerConsumer(peer.name, 'mic', false); }} />
diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index f90e52d..214d374 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -121,8 +121,8 @@ class Peer extends Component { e.stopPropagation(); micEnabled ? - roomClient.mutePeerAudio(peer.name) : - roomClient.unmutePeerAudio(peer.name); + roomClient.modifyPeerConsumer(peer.name, 'mic', true) : + roomClient.modifyPeerConsumer(peer.name, 'mic', false); }} /> diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index dcdc747..840c371 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import debounce from 'lodash/debounce'; -import * as appPropTypes from './appPropTypes'; import { Appear } from './transitions'; import Peer from './Peer'; import HiddenPeers from './HiddenPeers'; @@ -105,33 +104,30 @@ class Peers extends React.Component return (
+ { Object.keys(peers).map((peerName) => { - peers.map((peer) => + if (spotlights.find((spotlightsElement) => spotlightsElement === peerName)) { - spotlightsElement === peer.name) - } - > - + return ( +
-
; - }) - } + ); + } + })}
{ - const peers = Object.values(state.peers); const spotlights = state.room.spotlights; const spotlightsLength = spotlights ? state.room.spotlights.length : 0; const boxes = spotlightsLength + Object.values(state.consumers) .filter((consumer) => consumer.source === 'screen').length; return { - peers, + peers : state.peers, boxes, activeSpeakerName : state.room.activeSpeakerName, selectedPeerName : state.room.selectedPeerName, diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index ca8d2b4..63ab009 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -20,9 +20,6 @@ import Draggable from 'react-draggable'; import { idle } from '../utils'; import Sidebar from './Sidebar'; import Filmstrip from './Filmstrip'; -import { configureDragDrop, HoldingOverlay } from './FileSharing/DragDropSharing'; - -configureDragDrop(); // Hide toolbars after 10 seconds of inactivity. const TIMEOUT = 10 * 1000; @@ -78,8 +75,6 @@ class Room extends React.Component return ( - -
diff --git a/app/lib/components/ToolArea/ToolArea.jsx b/app/lib/components/ToolArea/ToolArea.jsx index 5165796..dfbf2a3 100644 --- a/app/lib/components/ToolArea/ToolArea.jsx +++ b/app/lib/components/ToolArea/ToolArea.jsx @@ -6,7 +6,7 @@ import * as stateActions from '../../redux/stateActions'; import ParticipantList from '../ParticipantList/ParticipantList'; import Chat from '../Chat/Chat'; import Settings from '../Settings'; -import FileSharing from '../FileSharing'; +import FileSharing from '../FileSharing/FileSharing'; import TabHeader from './TabHeader'; class ToolArea extends React.Component diff --git a/app/lib/components/ToolArea/ToolAreaButton.jsx b/app/lib/components/ToolArea/ToolAreaButton.jsx deleted file mode 100644 index 20b0024..0000000 --- a/app/lib/components/ToolArea/ToolAreaButton.jsx +++ /dev/null @@ -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 ( -
-
toggleToolArea()} - /> - - {!toolAreaOpen && unread > 0 && ( - = 10 })}> - {unread} - - )} -
- ); - } -} - -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; diff --git a/app/lib/components/appPropTypes.js b/app/lib/components/appPropTypes.js index 392e854..74b52e3 100644 --- a/app/lib/components/appPropTypes.js +++ b/app/lib/components/appPropTypes.js @@ -76,3 +76,16 @@ export const Message = PropTypes.shape( text : 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 + }); \ No newline at end of file diff --git a/app/lib/redux/reducers/files.js b/app/lib/redux/reducers/files.js new file mode 100644 index 0000000..19d5369 --- /dev/null +++ b/app/lib/redux/reducers/files.js @@ -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; diff --git a/app/lib/redux/reducers/index.js b/app/lib/redux/reducers/index.js index 1f59ace..fcafba6 100644 --- a/app/lib/redux/reducers/index.js +++ b/app/lib/redux/reducers/index.js @@ -8,7 +8,7 @@ import notifications from './notifications'; import chatmessages from './chatmessages'; import chatbehavior from './chatbehavior'; import toolarea from './toolarea'; -import sharing from './sharing'; +import files from './files'; const reducers = combineReducers( { @@ -21,7 +21,7 @@ const reducers = combineReducers( chatmessages, chatbehavior, toolarea, - sharing + files }); export default reducers; diff --git a/app/lib/redux/reducers/notifications.js b/app/lib/redux/reducers/notifications.js index bf3e6c0..142308e 100644 --- a/app/lib/redux/reducers/notifications.js +++ b/app/lib/redux/reducers/notifications.js @@ -1,6 +1,4 @@ -const initialState = []; - -const notifications = (state = initialState, action) => +const notifications = (state = [], action) => { switch (action.type) { diff --git a/app/lib/redux/reducers/room.js b/app/lib/redux/reducers/room.js index 89d2b21..eed2e8a 100644 --- a/app/lib/redux/reducers/room.js +++ b/app/lib/redux/reducers/room.js @@ -3,6 +3,7 @@ const initialState = url : null, state : 'new', // new/connecting/connected/disconnected/closed, activeSpeakerName : null, + torrentSupport : false, showSettings : false, advancedMode : false, fullScreenConsumer : null, // ConsumerID @@ -41,6 +42,13 @@ const room = (state = initialState, action) => return { ...state, activeSpeakerName: peerName }; } + case 'FILE_SHARING_SUPPORTED': + { + const { supported } = action.payload; + + return { ...state, torrentSupport: supported }; + } + case 'TOGGLE_SETTINGS': { const showSettings = !state.showSettings; diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 3ced6b6..a837631 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -70,6 +70,14 @@ export const setWebcamDevices = (devices) => }; }; +export const setFileSharingSupported = (supported) => +{ + return { + type : 'FILE_SHARING_SUPPORTED', + payload : { supported } + }; +}; + export const setDisplayName = (displayName) => { return { @@ -455,11 +463,11 @@ export const dropMessages = () => }; }; -export const addFile = (payload) => +export const addFile = (file) => { return { - type : 'ADD_FILE', - payload + type : 'ADD_FILE', + 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) => ({ type : 'SET_PICTURE', diff --git a/app/stylus/components/ToolArea.styl b/app/stylus/components/ToolArea.styl index 9283d42..038ca8d 100644 --- a/app/stylus/components/ToolArea.styl +++ b/app/stylus/components/ToolArea.styl @@ -19,12 +19,6 @@ } } -[data-component='ToolAreaButton'] { - &.on { - right: 80%; - } -} - [data-component='ToolArea'] { &.open { width: 80%; @@ -153,12 +147,6 @@ } @media (min-width: 600px) { - [data-component='ToolAreaButton'] { - &.on { - right: 60%; - } - } - [data-component='ToolArea'] { &.open { width: 60%; @@ -167,12 +155,6 @@ } @media (min-width: 900px) { - [data-component='ToolAreaButton'] { - &.on { - right: 40%; - } - } - [data-component='ToolArea'] { &.open { width: 40%; @@ -181,12 +163,6 @@ } @media (min-width: 1500px) { - [data-component='ToolAreaButton'] { - &.on { - right: 25%; - } - } - [data-component='ToolArea'] { &.open { 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'] { width: 100%; height: 100%;