Rewritten filesharing to move responsability of filesharing to RoomClient. Removed and rewrote some code to clean up.
parent
60fb9c735e
commit
db9d423feb
|
|
@ -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' });
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import magnet from 'magnet-uri';
|
||||
|
||||
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
class File extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
torrentSupport,
|
||||
file
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='file-entry'>
|
||||
<img className='file-avatar' src={file.picture || DEFAULT_PICTURE} />
|
||||
|
||||
<div className='file-content'>
|
||||
<Choose>
|
||||
<When condition={file.me}>
|
||||
<p>You shared a file.</p>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>{file.displayName} shared a file.</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<If condition={!file.active && !file.files}>
|
||||
<div className='file-info'>
|
||||
<Choose>
|
||||
<When condition={torrentSupport}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.handleDownload(file.magnetUri);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/download-icon.svg' />
|
||||
</span>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>
|
||||
Your browser does not support downloading files using WebTorrent.
|
||||
</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
<p>{magnet.decode(file.magnetUri).dn}</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={file.timeout}>
|
||||
<Fragment>
|
||||
<p>
|
||||
If this process takes a long time, there might not be anyone seeding
|
||||
this torrent. Try asking someone to reupload the file that you want.
|
||||
</p>
|
||||
</Fragment>
|
||||
</If>
|
||||
|
||||
<If condition={file.active}>
|
||||
<progress value={file.progress} />
|
||||
</If>
|
||||
|
||||
<If condition={file.files}>
|
||||
<Fragment>
|
||||
<p>File finished downloading.</p>
|
||||
|
||||
{file.files.map((sharedFile, i) => (
|
||||
<div className='file-info' key={i}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.saveFile(sharedFile);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/save-icon.svg' />
|
||||
</span>
|
||||
|
||||
<p>{sharedFile.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File.propTypes = {
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { magnetUri }) =>
|
||||
{
|
||||
return {
|
||||
file : state.files[magnetUri],
|
||||
torrentSupport : state.room.torrentSupport
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(File));
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import magnet from 'magnet-uri';
|
||||
import WebTorrent from 'webtorrent';
|
||||
import * as requestActions from '../../redux/requestActions';
|
||||
import { saveAs } from 'file-saver/FileSaver';
|
||||
import { client } from './index';
|
||||
|
||||
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
class FileEntry extends Component
|
||||
{
|
||||
state = {
|
||||
active : false,
|
||||
numPeers : 0,
|
||||
progress : 0,
|
||||
files : null
|
||||
};
|
||||
|
||||
saveFile = (file) =>
|
||||
{
|
||||
file.getBlob((err, blob) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
return this.props.notify({
|
||||
text : 'An error occurred while saving a file'
|
||||
});
|
||||
}
|
||||
|
||||
saveAs(blob, file.name);
|
||||
});
|
||||
};
|
||||
|
||||
handleTorrent = (torrent) =>
|
||||
{
|
||||
// Torrent already done, this can happen if the
|
||||
// same file was sent multiple times.
|
||||
if (torrent.progress === 1)
|
||||
{
|
||||
this.setState({
|
||||
files : torrent.files,
|
||||
numPeers : torrent.numPeers,
|
||||
progress : 1,
|
||||
active : false,
|
||||
timeout : false
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const onProgress = () =>
|
||||
{
|
||||
this.setState({
|
||||
numPeers : torrent.numPeers,
|
||||
progress : torrent.progress
|
||||
});
|
||||
};
|
||||
|
||||
onProgress();
|
||||
|
||||
setInterval(onProgress, 500);
|
||||
|
||||
torrent.on('done', () =>
|
||||
{
|
||||
onProgress();
|
||||
clearInterval(onProgress);
|
||||
|
||||
this.setState({
|
||||
files : torrent.files,
|
||||
active : false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleDownload = () =>
|
||||
{
|
||||
this.setState({
|
||||
active : true
|
||||
});
|
||||
|
||||
const magnetURI = this.props.data.file.magnet;
|
||||
|
||||
const existingTorrent = client.get(magnetURI);
|
||||
|
||||
if (existingTorrent)
|
||||
{
|
||||
// Never add duplicate torrents, use the existing one instead.
|
||||
return this.handleTorrent(existingTorrent);
|
||||
}
|
||||
|
||||
client.add(magnetURI, this.handleTorrent);
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (this.state.active && this.state.numPeers === 0)
|
||||
{
|
||||
this.setState({
|
||||
timeout : true
|
||||
});
|
||||
}
|
||||
}, 10 * 1000);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<div className='file-entry'>
|
||||
<img className='file-avatar' src={this.props.data.picture || DEFAULT_PICTURE} />
|
||||
|
||||
<div className='file-content'>
|
||||
<Choose>
|
||||
<When condition={this.props.data.me}>
|
||||
<p>You shared a file.</p>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>{this.props.data.name} shared a file.</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<If condition={!this.state.active && !this.state.files}>
|
||||
<div className='file-info'>
|
||||
<Choose>
|
||||
<When condition={WebTorrent.WEBRTC_SUPPORT}>
|
||||
<span className='button' onClick={this.handleDownload}>
|
||||
<img src='resources/images/download-icon.svg' />
|
||||
</span>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>
|
||||
Your browser does not support downloading files using WebTorrent.
|
||||
</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
<p>{magnet.decode(this.props.data.file.magnet).dn}</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={this.state.active && this.state.numPeers === 0}>
|
||||
<Fragment>
|
||||
<p>
|
||||
Locating peers
|
||||
</p>
|
||||
|
||||
{this.state.timeout && (
|
||||
<p>
|
||||
If this process takes a long time, there might not be anyone seeding
|
||||
this torrent. Try asking someone to reupload the file that you want.
|
||||
</p>
|
||||
)}
|
||||
</Fragment>
|
||||
</If>
|
||||
|
||||
<If condition={this.state.active && this.state.numPeers > 0}>
|
||||
<progress value={this.state.progress} />
|
||||
</If>
|
||||
|
||||
<If condition={this.state.files}>
|
||||
<Fragment>
|
||||
<p>Torrent finished downloading.</p>
|
||||
|
||||
{this.state.files.map((file, i) => (
|
||||
<div className='file-info' key={i}>
|
||||
<span className='button' onClick={() => this.saveFile(file)}>
|
||||
<img src='resources/images/save-icon.svg' />
|
||||
</span>
|
||||
|
||||
<p>{file.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const FileEntryProps = {
|
||||
data : PropTypes.shape({
|
||||
name : PropTypes.string.isRequired,
|
||||
picture : PropTypes.string,
|
||||
file : PropTypes.shape({
|
||||
magnet : PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
me : PropTypes.bool
|
||||
}).isRequired,
|
||||
notify : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
FileEntry.propTypes = FileEntryProps;
|
||||
|
||||
const mapDispatchToProps = {
|
||||
notify : requestActions.notify
|
||||
};
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(FileEntry);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import scrollToBottom from '../Chat/scrollToBottom';
|
||||
import File from './File';
|
||||
|
||||
class FileList extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
files
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='shared-files'>
|
||||
{ Object.keys(files).map((magnetUri) =>
|
||||
<File key={magnetUri} magnetUri={magnetUri} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileList.propTypes = {
|
||||
files : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
files : state.files
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(FileList);
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import FileList from './FileList';
|
||||
|
||||
class FileSharing extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._fileInput = React.createRef();
|
||||
}
|
||||
|
||||
handleFileChange = async (event) =>
|
||||
{
|
||||
if (event.target.files.length > 0)
|
||||
{
|
||||
this.props.roomClient.shareFiles(event.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = () =>
|
||||
{
|
||||
if (this.props.torrentSupport)
|
||||
{
|
||||
// We want to open the file dialog when we click a button
|
||||
// instead of actually rendering the input element itself.
|
||||
this._fileInput.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
torrentSupport
|
||||
} = this.props;
|
||||
|
||||
const buttonDescription = torrentSupport ?
|
||||
'Share file' : 'File sharing not supported';
|
||||
|
||||
return (
|
||||
<div data-component='FileSharing'>
|
||||
<div className='sharing-toolbar'>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
ref={this._fileInput}
|
||||
type='file'
|
||||
onChange={this.handleFileChange}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<div
|
||||
type='button'
|
||||
onClick={this.handleClick}
|
||||
className={classNames('share-file', {
|
||||
disabled : !torrentSupport
|
||||
})}
|
||||
>
|
||||
<span>{buttonDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileSharing.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
torrentSupport : state.room.torrentSupport,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(FileSharing));
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import FileEntry, { FileEntryProps } from './FileEntry';
|
||||
import scrollToBottom from '../Chat/scrollToBottom';
|
||||
|
||||
/**
|
||||
* This component cannot be pure, as we need to use
|
||||
* refs to scroll to the bottom when new files arrive.
|
||||
*/
|
||||
class SharedFilesList extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const { sharing } = this.props;
|
||||
|
||||
return (
|
||||
<div className='shared-files'>
|
||||
<Choose>
|
||||
<When condition={sharing.length > 0}>
|
||||
{
|
||||
sharing.map((entry, i) => (
|
||||
<FileEntry
|
||||
data={entry}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</When>
|
||||
<Otherwise>
|
||||
<div className='empty'>
|
||||
<p>No one has shared files yet...</p>
|
||||
</div>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SharedFilesList.propTypes = {
|
||||
sharing : PropTypes.arrayOf(FileEntryProps.data).isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
sharing : state.sharing,
|
||||
|
||||
// Included to scroll to the bottom when the user
|
||||
// actually opens the tab. When the component first
|
||||
// mounts, the component is not visible and so the
|
||||
// component has no height which can be used for scrolling.
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(SharedFilesList);
|
||||
|
|
@ -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;
|
||||
|
|
@ -145,14 +145,11 @@ class Filmstrip extends Component
|
|||
|
||||
<div className='filmstrip'>
|
||||
<div className='filmstrip-content'>
|
||||
{ Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
Object.keys(peers).map((peerName) =>
|
||||
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
|
||||
{
|
||||
<If
|
||||
condition={
|
||||
spotlights.find((spotlightsElement) => spotlightsElement === peerName)
|
||||
}
|
||||
>
|
||||
return (
|
||||
<div
|
||||
key={peerName}
|
||||
onClick={() => roomClient.setSelectedPeer(peerName)}
|
||||
|
|
@ -168,9 +165,9 @@ class Filmstrip extends Component
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</If>;
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden-peer-container'>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div data-component='Peers' ref={this.peersRef}>
|
||||
{ Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
peers.map((peer) =>
|
||||
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
|
||||
{
|
||||
<If
|
||||
condition={
|
||||
spotlights.find((spotlightsElement) => spotlightsElement === peer.name)
|
||||
}
|
||||
>
|
||||
<Appear key={peer.name} duration={1000}>
|
||||
return (
|
||||
<Appear key={peerName} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'selected' : this.props.selectedPeerName === peer.name,
|
||||
'active-speaker' : peer.name === activeSpeakerName
|
||||
'selected' : this.props.selectedPeerName === peerName,
|
||||
'active-speaker' : peerName === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<div className='peer-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peer.name}
|
||||
name={peerName}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
</If>;
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
})}
|
||||
<div className='hidden-peer-container'>
|
||||
<If condition={spotlightsLength < peers.length}>
|
||||
<HiddenPeers
|
||||
|
|
@ -147,7 +143,7 @@ class Peers extends React.Component
|
|||
Peers.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
peers : PropTypes.object.isRequired,
|
||||
boxes : PropTypes.number,
|
||||
activeSpeakerName : PropTypes.string,
|
||||
selectedPeerName : PropTypes.string,
|
||||
|
|
@ -157,14 +153,13 @@ Peers.propTypes =
|
|||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Fragment>
|
||||
<HoldingOverlay />
|
||||
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<CookieConsent>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
const initialState = [];
|
||||
|
||||
const notifications = (state = initialState, action) =>
|
||||
const notifications = (state = [], action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
Loading…
Reference in New Issue