Fixed and merge from develop to master
commit
d6c854a8c3
|
|
@ -22,6 +22,15 @@ $ cp server/config.example.js server/config.js
|
||||||
|
|
||||||
* Copy `app/config.example.js` to `app/config.js` :
|
* Copy `app/config.example.js` to `app/config.js` :
|
||||||
|
|
||||||
|
In addition, the server requires a screen to be installed for the server
|
||||||
|
to be able to seed shared torrent files. This is because the headless
|
||||||
|
Electron instance used by WebTorrent expects one.
|
||||||
|
|
||||||
|
See [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid) for
|
||||||
|
more information about this.
|
||||||
|
|
||||||
|
* Copy `config.example.js` as `config.js` and customize it for your scenario:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cp app/config.example.js app/config.js
|
$ cp app/config.example.js app/config.js
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@ module.exports =
|
||||||
version: '15'
|
version: '15'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
parser: 'babel-eslint',
|
||||||
parserOptions:
|
parserOptions:
|
||||||
{
|
{
|
||||||
ecmaVersion: 6,
|
ecmaVersion: 9,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaFeatures:
|
ecmaFeatures:
|
||||||
{
|
{
|
||||||
impliedStrict: true,
|
impliedStrict: true,
|
||||||
experimentalObjectRestSpread: true,
|
|
||||||
jsx: true
|
jsx: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -121,7 +121,6 @@ module.exports =
|
||||||
'no-implicit-globals': 2,
|
'no-implicit-globals': 2,
|
||||||
'no-inner-declarations': 2,
|
'no-inner-declarations': 2,
|
||||||
'no-invalid-regexp': 2,
|
'no-invalid-regexp': 2,
|
||||||
'no-invalid-this': 2,
|
|
||||||
'no-irregular-whitespace': 2,
|
'no-irregular-whitespace': 2,
|
||||||
'no-lonely-if': 2,
|
'no-lonely-if': 2,
|
||||||
'no-mixed-operators': 2,
|
'no-mixed-operators': 2,
|
||||||
|
|
@ -173,7 +172,7 @@ module.exports =
|
||||||
'semi': [ 2, 'always' ],
|
'semi': [ 2, 'always' ],
|
||||||
'semi-spacing': 2,
|
'semi-spacing': 2,
|
||||||
'space-before-blocks': 2,
|
'space-before-blocks': 2,
|
||||||
'space-before-function-paren': [ 2, 'never' ],
|
'space-before-function-paren': [ 2, { anonymous: 'never', named: 'never', 'asyncArrow': 'always'}],
|
||||||
'space-in-parens': [ 2, 'never' ],
|
'space-in-parens': [ 2, 'never' ],
|
||||||
'spaced-comment': [ 2, 'always' ],
|
'spaced-comment': [ 2, 'always' ],
|
||||||
'strict': 2,
|
'strict': 2,
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,7 @@ function bundle(options)
|
||||||
})
|
})
|
||||||
.transform('babelify',
|
.transform('babelify',
|
||||||
{
|
{
|
||||||
presets : [ 'es2015', 'react' ],
|
presets : [ 'env', 'react-app' ]
|
||||||
plugins :
|
|
||||||
[
|
|
||||||
'transform-runtime',
|
|
||||||
'transform-object-assign',
|
|
||||||
'transform-object-rest-spread'
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
.transform(envify(
|
.transform(envify(
|
||||||
{
|
{
|
||||||
|
|
@ -132,21 +126,29 @@ function changeHTML(content)
|
||||||
|
|
||||||
gulp.task('clean', () => del(OUTPUT_DIR, { force: true }));
|
gulp.task('clean', () => del(OUTPUT_DIR, { force: true }));
|
||||||
|
|
||||||
gulp.task('lint', () =>
|
const LINTING_FILES = [
|
||||||
{
|
|
||||||
const src =
|
|
||||||
[
|
|
||||||
'gulpfile.js',
|
'gulpfile.js',
|
||||||
'lib/**/*.js',
|
'lib/**/*.js',
|
||||||
'lib/**/*.jsx'
|
'lib/**/*.jsx'
|
||||||
];
|
];
|
||||||
|
|
||||||
return gulp.src(src)
|
gulp.task('lint', () =>
|
||||||
|
{
|
||||||
|
return gulp.src(LINTING_FILES)
|
||||||
.pipe(plumber())
|
.pipe(plumber())
|
||||||
.pipe(eslint())
|
.pipe(eslint())
|
||||||
.pipe(eslint.format());
|
.pipe(eslint.format());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('lint-fix', function()
|
||||||
|
{
|
||||||
|
return gulp.src(LINTING_FILES)
|
||||||
|
.pipe(plumber())
|
||||||
|
.pipe(eslint({ fix: true }))
|
||||||
|
.pipe(eslint.format())
|
||||||
|
.pipe(gulp.dest((file) => file.base));
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task('css', () =>
|
gulp.task('css', () =>
|
||||||
{
|
{
|
||||||
return gulp.src('stylus/index.styl')
|
return gulp.src('stylus/index.styl')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import protooClient from 'protoo-client';
|
import protooClient from 'protoo-client';
|
||||||
import * as mediasoupClient from 'mediasoup-client';
|
import * as mediasoupClient from 'mediasoup-client';
|
||||||
import Logger from './Logger';
|
import Logger from './Logger';
|
||||||
|
import hark from 'hark';
|
||||||
import ScreenShare from './ScreenShare';
|
import ScreenShare from './ScreenShare';
|
||||||
import { getProtooUrl } from './urlFactory';
|
import { getProtooUrl } from './urlFactory';
|
||||||
import * as cookiesManager from './cookiesManager';
|
import * as cookiesManager from './cookiesManager';
|
||||||
|
|
@ -128,13 +129,16 @@ export default class RoomClient
|
||||||
|
|
||||||
login()
|
login()
|
||||||
{
|
{
|
||||||
this._dispatch(stateActions.setLoginInProgress(true));
|
|
||||||
|
|
||||||
const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`;
|
const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`;
|
||||||
|
|
||||||
this._loginWindow = window.open(url, 'loginWindow');
|
this._loginWindow = window.open(url, 'loginWindow');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout()
|
||||||
|
{
|
||||||
|
window.location = '/logout';
|
||||||
|
}
|
||||||
|
|
||||||
closeLoginWindow()
|
closeLoginWindow()
|
||||||
{
|
{
|
||||||
this._loginWindow.close();
|
this._loginWindow.close();
|
||||||
|
|
@ -174,6 +178,16 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeProfilePicture(picture)
|
||||||
|
{
|
||||||
|
logger.debug('changeProfilePicture() [picture: "%s"]', picture);
|
||||||
|
|
||||||
|
this._protoo.send('change-profile-picture', { picture }).catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('shareProfilePicure() | failed: %o', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sendChatMessage(chatMessage)
|
sendChatMessage(chatMessage)
|
||||||
{
|
{
|
||||||
logger.debug('sendChatMessage() [chatMessage:"%s"]', chatMessage);
|
logger.debug('sendChatMessage() [chatMessage:"%s"]', chatMessage);
|
||||||
|
|
@ -191,6 +205,22 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendFile(file)
|
||||||
|
{
|
||||||
|
logger.debug('sendFile() [file: %o]', file);
|
||||||
|
|
||||||
|
return this._protoo.send('send-file', { file })
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('sendFile() | failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify({
|
||||||
|
typ : 'error',
|
||||||
|
text : 'An error occurred while sharing a file'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getChatHistory()
|
getChatHistory()
|
||||||
{
|
{
|
||||||
logger.debug('getChatHistory()');
|
logger.debug('getChatHistory()');
|
||||||
|
|
@ -208,6 +238,22 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFileHistory()
|
||||||
|
{
|
||||||
|
logger.debug('getFileHistory()');
|
||||||
|
|
||||||
|
return this._protoo.send('file-history', {})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('getFileHistory() | failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify({
|
||||||
|
type : 'error',
|
||||||
|
text : 'Could not get file history'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
muteMic()
|
muteMic()
|
||||||
{
|
{
|
||||||
logger.debug('muteMic()');
|
logger.debug('muteMic()');
|
||||||
|
|
@ -899,11 +945,6 @@ export default class RoomClient
|
||||||
{
|
{
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setMyRaiseHandState(state));
|
stateActions.setMyRaiseHandState(state));
|
||||||
|
|
||||||
this._dispatch(requestActions.notify(
|
|
||||||
{
|
|
||||||
text : 'raiseHand state changed'
|
|
||||||
}));
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setMyRaiseHandStateInProgress(false));
|
stateActions.setMyRaiseHandStateInProgress(false));
|
||||||
})
|
})
|
||||||
|
|
@ -1051,34 +1092,38 @@ export default class RoomClient
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This means: server wants to change MY displayName
|
case 'profile-picture-changed':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const { peerName, picture } = request.data;
|
||||||
|
|
||||||
|
this._dispatch(stateActions.setPeerPicture(peerName, picture));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This means: server wants to change MY user information
|
||||||
case 'auth':
|
case 'auth':
|
||||||
{
|
{
|
||||||
logger.debug('got auth event from server', request.data);
|
logger.debug('got auth event from server', request.data);
|
||||||
accept();
|
accept();
|
||||||
|
|
||||||
if (request.data.verified == true)
|
|
||||||
{
|
|
||||||
this.changeDisplayName(request.data.name);
|
this.changeDisplayName(request.data.name);
|
||||||
|
|
||||||
|
this.changeProfilePicture(request.data.picture);
|
||||||
|
this._dispatch(stateActions.setPicture(request.data.picture));
|
||||||
|
this._dispatch(stateActions.loggedIn());
|
||||||
|
|
||||||
this._dispatch(requestActions.notify(
|
this._dispatch(requestActions.notify(
|
||||||
{
|
{
|
||||||
text : `Authenticated successfully: ${request.data}`
|
text : `Authenticated successfully: ${request.data}`
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this._dispatch(requestActions.notify(
|
|
||||||
{
|
|
||||||
text : `Authentication failed: ${request.data}`
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
this.closeLoginWindow();
|
this.closeLoginWindow();
|
||||||
|
|
||||||
this._dispatch(stateActions.setLoginInProgress(false));
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'raisehand-message':
|
case 'raisehand-message':
|
||||||
|
|
@ -1102,7 +1147,7 @@ export default class RoomClient
|
||||||
logger.debug('Got chat from "%s"', peerName);
|
logger.debug('Got chat from "%s"', peerName);
|
||||||
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.addResponseMessage(chatMessage));
|
stateActions.addResponseMessage({ ...chatMessage, peerName }));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -1123,6 +1168,37 @@ export default class RoomClient
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'file-receive':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const payload = request.data.file;
|
||||||
|
|
||||||
|
this._dispatch(stateActions.addFile(payload));
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify({
|
||||||
|
text : `${payload.name} shared a file`
|
||||||
|
}));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'file-history-receive':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const files = request.data.fileHistory;
|
||||||
|
|
||||||
|
if (files.length > 0)
|
||||||
|
{
|
||||||
|
logger.debug('Got files history');
|
||||||
|
|
||||||
|
this._dispatch(stateActions.addFileHistory(files));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
logger.error('unknown protoo method "%s"', request.method);
|
logger.error('unknown protoo method "%s"', request.method);
|
||||||
|
|
@ -1260,6 +1336,7 @@ export default class RoomClient
|
||||||
this._dispatch(stateActions.removeAllNotifications());
|
this._dispatch(stateActions.removeAllNotifications());
|
||||||
|
|
||||||
this.getChatHistory();
|
this.getChatHistory();
|
||||||
|
this.getFileHistory();
|
||||||
|
|
||||||
this._dispatch(requestActions.notify(
|
this._dispatch(requestActions.notify(
|
||||||
{
|
{
|
||||||
|
|
@ -1380,7 +1457,33 @@ export default class RoomClient
|
||||||
})
|
})
|
||||||
.then(() =>
|
.then(() =>
|
||||||
{
|
{
|
||||||
|
const stream = new MediaStream;
|
||||||
|
|
||||||
logger.debug('_setMicProducer() succeeded');
|
logger.debug('_setMicProducer() succeeded');
|
||||||
|
stream.addTrack(producer.track);
|
||||||
|
if (!stream.getAudioTracks()[0])
|
||||||
|
throw new Error('_setMicProducer(): given stream has no audio track');
|
||||||
|
producer.hark = hark(stream, { play: false });
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
producer.hark.on('volume_change', (dBs, threshold) =>
|
||||||
|
{
|
||||||
|
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
|
||||||
|
// Math.pow(10, dBs / 20)
|
||||||
|
// However it does not produce a visually useful output, so let exagerate
|
||||||
|
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
|
||||||
|
// minimize component renderings.
|
||||||
|
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
|
||||||
|
|
||||||
|
if (volume === 1)
|
||||||
|
volume = 0;
|
||||||
|
|
||||||
|
if (volume !== producer.volume)
|
||||||
|
{
|
||||||
|
producer.volume = volume;
|
||||||
|
this._dispatch(stateActions.setProducerVolume(producer.id, volume));
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
{
|
{
|
||||||
|
|
@ -1765,6 +1868,7 @@ export default class RoomClient
|
||||||
name : peer.name,
|
name : peer.name,
|
||||||
displayName : displayName,
|
displayName : displayName,
|
||||||
device : peer.appData.device,
|
device : peer.appData.device,
|
||||||
|
raiseHandState : peer.appData.raiseHandState,
|
||||||
consumers : []
|
consumers : []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -1823,7 +1927,8 @@ export default class RoomClient
|
||||||
track : null,
|
track : null,
|
||||||
codec : codec ? codec.name : null
|
codec : codec ? codec.name : null
|
||||||
},
|
},
|
||||||
consumer.peer.name));
|
consumer.peer.name)
|
||||||
|
);
|
||||||
|
|
||||||
consumer.on('close', (originator) =>
|
consumer.on('close', (originator) =>
|
||||||
{
|
{
|
||||||
|
|
@ -1835,6 +1940,43 @@ export default class RoomClient
|
||||||
consumer.id, consumer.peer.name));
|
consumer.id, consumer.peer.name));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
consumer.on('handled', (originator) =>
|
||||||
|
{
|
||||||
|
logger.debug(
|
||||||
|
'consumer "handled" event [id:%s, originator:%s, consumer:%o]',
|
||||||
|
consumer.id, originator, consumer);
|
||||||
|
if (consumer.kind === 'audio')
|
||||||
|
{
|
||||||
|
const stream = new MediaStream;
|
||||||
|
|
||||||
|
stream.addTrack(consumer.track);
|
||||||
|
if (!stream.getAudioTracks()[0])
|
||||||
|
throw new Error('consumer.on("handled" | given stream has no audio track');
|
||||||
|
|
||||||
|
consumer.hark = hark(stream, { play: false });
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
consumer.hark.on('volume_change', (dBs, threshold) =>
|
||||||
|
{
|
||||||
|
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
|
||||||
|
// Math.pow(10, dBs / 20)
|
||||||
|
// However it does not produce a visually useful output, so let exagerate
|
||||||
|
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
|
||||||
|
// minimize component renderings.
|
||||||
|
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
|
||||||
|
|
||||||
|
if (volume === 1)
|
||||||
|
volume = 0;
|
||||||
|
|
||||||
|
if (volume !== consumer.volume)
|
||||||
|
{
|
||||||
|
consumer.volume = volume;
|
||||||
|
this._dispatch(stateActions.setConsumerVolume(consumer.id, volume));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
consumer.on('pause', (originator) =>
|
consumer.on('pause', (originator) =>
|
||||||
{
|
{
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ class Chat extends Component
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
disabledInput,
|
disabledInput,
|
||||||
autofocus,
|
autofocus,
|
||||||
displayName
|
displayName,
|
||||||
|
picture
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -22,7 +23,7 @@ class Chat extends Component
|
||||||
<MessageList />
|
<MessageList />
|
||||||
<form
|
<form
|
||||||
data-component='Sender'
|
data-component='Sender'
|
||||||
onSubmit={(e) => { onSendMessage(e, displayName); }}
|
onSubmit={(e) => { onSendMessage(e, displayName, picture); }}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
|
|
@ -45,7 +46,8 @@ Chat.propTypes =
|
||||||
onSendMessage : PropTypes.func,
|
onSendMessage : PropTypes.func,
|
||||||
disabledInput : PropTypes.bool,
|
disabledInput : PropTypes.bool,
|
||||||
autofocus : PropTypes.bool,
|
autofocus : PropTypes.bool,
|
||||||
displayName : PropTypes.string
|
displayName : PropTypes.string,
|
||||||
|
picture : PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
Chat.defaultProps =
|
Chat.defaultProps =
|
||||||
|
|
@ -59,14 +61,15 @@ const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
disabledInput : state.chatbehavior.disabledInput,
|
disabledInput : state.chatbehavior.disabledInput,
|
||||||
displayName : state.me.displayName
|
displayName : state.me.displayName,
|
||||||
|
picture : state.me.picture
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
const mapDispatchToProps = (dispatch) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
onSendMessage : (event, displayName) =>
|
onSendMessage : (event, displayName, picture) =>
|
||||||
{
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const userInput = event.target.message.value;
|
const userInput = event.target.message.value;
|
||||||
|
|
@ -74,7 +77,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
if (userInput)
|
if (userInput)
|
||||||
{
|
{
|
||||||
dispatch(stateActions.addUserMessage(userInput));
|
dispatch(stateActions.addUserMessage(userInput));
|
||||||
dispatch(requestActions.sendChatMessage(userInput, displayName));
|
dispatch(requestActions.sendChatMessage(userInput, displayName, picture));
|
||||||
}
|
}
|
||||||
event.target.message.value = '';
|
event.target.message.value = '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { compose } from 'redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import scrollToBottom from './scrollToBottom';
|
||||||
const scrollToBottom = () =>
|
|
||||||
{
|
|
||||||
const messagesDiv = document.getElementById('messages');
|
|
||||||
|
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
const linkRenderer = new marked.Renderer();
|
const linkRenderer = new marked.Renderer();
|
||||||
|
|
||||||
|
|
@ -22,16 +17,6 @@ linkRenderer.link = (href, title, text) =>
|
||||||
|
|
||||||
class MessageList extends Component
|
class MessageList extends Component
|
||||||
{
|
{
|
||||||
componentDidMount()
|
|
||||||
{
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate()
|
|
||||||
{
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimeString(time)
|
getTimeString(time)
|
||||||
{
|
{
|
||||||
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
|
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
|
||||||
|
|
@ -50,9 +35,15 @@ class MessageList extends Component
|
||||||
{
|
{
|
||||||
const messageTime = new Date(message.time);
|
const messageTime = new Date(message.time);
|
||||||
|
|
||||||
|
const picture = (message.sender === 'response' ?
|
||||||
|
message.picture : this.props.myPicture) || 'resources/images/avatar-empty.jpeg';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='message' key={i}>
|
<div className='message' key={i}>
|
||||||
<div className={message.sender}>
|
<div className={message.sender}>
|
||||||
|
<img className='message-avatar' src={picture} />
|
||||||
|
|
||||||
|
<div className='message-content'>
|
||||||
<div
|
<div
|
||||||
className='message-text'
|
className='message-text'
|
||||||
// eslint-disable-next-line react/no-danger
|
// eslint-disable-next-line react/no-danger
|
||||||
|
|
@ -61,11 +52,13 @@ class MessageList extends Component
|
||||||
{ sanitize: true, renderer: linkRenderer }
|
{ sanitize: true, renderer: linkRenderer }
|
||||||
) }}
|
) }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className='message-time'>
|
<span className='message-time'>
|
||||||
{message.name} - {this.getTimeString(messageTime)}
|
{message.name} - {this.getTimeString(messageTime)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -76,18 +69,21 @@ class MessageList extends Component
|
||||||
|
|
||||||
MessageList.propTypes =
|
MessageList.propTypes =
|
||||||
{
|
{
|
||||||
chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired
|
chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
myPicture : PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
chatmessages : state.chatmessages
|
chatmessages : state.chatmessages,
|
||||||
|
myPicture : state.me.picture
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageListContainer = connect(
|
const MessageListContainer = compose(
|
||||||
mapStateToProps
|
connect(mapStateToProps),
|
||||||
|
scrollToBottom()
|
||||||
)(MessageList);
|
)(MessageList);
|
||||||
|
|
||||||
export default MessageListContainer;
|
export default MessageListContainer;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { findDOMNode } from 'react-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A higher order component which scrolls the user to the bottom of the
|
||||||
|
* wrapped component, provided that the user already was at the bottom
|
||||||
|
* of the wrapped component. Useful for chats and similar use cases.
|
||||||
|
* @param {number} treshold The required distance from the bottom required.
|
||||||
|
*/
|
||||||
|
const scrollToBottom = (treshold = 0) => (WrappedComponent) =>
|
||||||
|
{
|
||||||
|
return class AutoScroller extends Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.ref = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshotBeforeUpdate()
|
||||||
|
{
|
||||||
|
// Check if the user has scrolled close enough to the bottom for
|
||||||
|
// us to scroll to the bottom or not.
|
||||||
|
return this.elem.scrollHeight - this.elem.scrollTop <=
|
||||||
|
this.elem.clientHeight - treshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom = () =>
|
||||||
|
{
|
||||||
|
// Scroll the user to the bottom of the wrapped element.
|
||||||
|
this.elem.scrollTop = this.elem.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line react/no-find-dom-node
|
||||||
|
this.elem = findDOMNode(this.ref.current);
|
||||||
|
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState, atBottom)
|
||||||
|
{
|
||||||
|
if (atBottom)
|
||||||
|
{
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<WrappedComponent
|
||||||
|
ref={this.ref}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default scrollToBottom;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import WebTorrent from 'webtorrent';
|
||||||
|
import dragDrop from 'drag-drop';
|
||||||
|
import { shareFiles } from './index';
|
||||||
|
|
||||||
|
export const configureDragDrop = () =>
|
||||||
|
{
|
||||||
|
if (WebTorrent.WEBRTC_SUPPORT)
|
||||||
|
{
|
||||||
|
dragDrop('body', async (files) => await shareFiles(files));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HoldingOverlay = () => (
|
||||||
|
<div id='holding-overlay'>
|
||||||
|
Drop files here to share them
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import magnet from 'magnet-uri';
|
||||||
|
import WebTorrent from 'webtorrent';
|
||||||
|
import * as requestActions from '../../redux/requestActions';
|
||||||
|
import { saveAs } from 'file-saver/FileSaver';
|
||||||
|
import { client } from './index';
|
||||||
|
|
||||||
|
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
|
||||||
|
|
||||||
|
class FileEntry extends Component
|
||||||
|
{
|
||||||
|
state = {
|
||||||
|
active : false,
|
||||||
|
numPeers : 0,
|
||||||
|
progress : 0,
|
||||||
|
files : null
|
||||||
|
};
|
||||||
|
|
||||||
|
saveFile = (file) =>
|
||||||
|
{
|
||||||
|
file.getBlob((err, blob) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return this.props.notify({
|
||||||
|
text : 'An error occurred while saving a file'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAs(blob, file.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTorrent = (torrent) =>
|
||||||
|
{
|
||||||
|
// Torrent already done, this can happen if the
|
||||||
|
// same file was sent multiple times.
|
||||||
|
if (torrent.progress === 1)
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
files : torrent.files,
|
||||||
|
numPeers : torrent.numPeers,
|
||||||
|
progress : 1,
|
||||||
|
active : false,
|
||||||
|
timeout : false
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProgress = () =>
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
numPeers : torrent.numPeers,
|
||||||
|
progress : torrent.progress
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onProgress();
|
||||||
|
|
||||||
|
setInterval(onProgress, 500);
|
||||||
|
|
||||||
|
torrent.on('done', () =>
|
||||||
|
{
|
||||||
|
onProgress();
|
||||||
|
clearInterval(onProgress);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
files : torrent.files,
|
||||||
|
active : false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDownload = () =>
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
active : true
|
||||||
|
});
|
||||||
|
|
||||||
|
const magnetURI = this.props.data.file.magnet;
|
||||||
|
|
||||||
|
const existingTorrent = client.get(magnetURI);
|
||||||
|
|
||||||
|
if (existingTorrent)
|
||||||
|
{
|
||||||
|
// Never add duplicate torrents, use the existing one instead.
|
||||||
|
return this.handleTorrent(existingTorrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.add(magnetURI, this.handleTorrent);
|
||||||
|
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
if (this.state.active && this.state.numPeers === 0)
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
timeout : true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 10 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div className='file-entry'>
|
||||||
|
<img className='file-avatar' src={this.props.data.picture || DEFAULT_PICTURE} />
|
||||||
|
|
||||||
|
<div className='file-content'>
|
||||||
|
{this.props.data.me ? (
|
||||||
|
<p>You shared a file.</p>
|
||||||
|
) : (
|
||||||
|
<p>{this.props.data.name} shared a file.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!this.state.active && !this.state.files && (
|
||||||
|
<div className='file-info'>
|
||||||
|
{WebTorrent.WEBRTC_SUPPORT ? (
|
||||||
|
<span className='button' onClick={this.handleDownload}>
|
||||||
|
<img src='resources/images/download-icon.svg' />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
Your browser does not support downloading files using WebTorrent.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p>{magnet.decode(this.props.data.file.magnet).dn}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.active && this.state.numPeers === 0 && (
|
||||||
|
<Fragment>
|
||||||
|
<p>
|
||||||
|
Locating peers
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{this.state.timeout && (
|
||||||
|
<p>
|
||||||
|
If this process takes a long time, there might not be anyone seeding
|
||||||
|
this torrent. Try asking someone to reupload the file that you want.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.active && this.state.numPeers > 0 && (
|
||||||
|
<progress value={this.state.progress} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.files && (
|
||||||
|
<Fragment>
|
||||||
|
<p>Torrent finished downloading.</p>
|
||||||
|
|
||||||
|
{this.state.files.map((file, i) => (
|
||||||
|
<div className='file-info' key={i}>
|
||||||
|
<span className='button' onClick={() => this.saveFile(file)}>
|
||||||
|
<img src='resources/images/save-icon.svg' />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p>{file.name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileEntryProps = {
|
||||||
|
data : PropTypes.shape({
|
||||||
|
name : PropTypes.string.isRequired,
|
||||||
|
picture : PropTypes.string,
|
||||||
|
file : PropTypes.shape({
|
||||||
|
magnet : PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
me : PropTypes.bool
|
||||||
|
}).isRequired,
|
||||||
|
notify : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
FileEntry.propTypes = FileEntryProps;
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
notify : requestActions.notify
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
undefined,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(FileEntry);
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import FileEntry, { FileEntryProps } from './FileEntry';
|
||||||
|
import scrollToBottom from '../Chat/scrollToBottom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component cannot be pure, as we need to use
|
||||||
|
* refs to scroll to the bottom when new files arrive.
|
||||||
|
*/
|
||||||
|
class SharedFilesList extends Component
|
||||||
|
{
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div className='shared-files'>
|
||||||
|
{this.props.sharing.map((entry, i) => (
|
||||||
|
<FileEntry
|
||||||
|
data={entry}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedFilesList.propTypes = {
|
||||||
|
sharing : PropTypes.arrayOf(FileEntryProps.data).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
({
|
||||||
|
sharing : state.sharing,
|
||||||
|
|
||||||
|
// Included to scroll to the bottom when the user
|
||||||
|
// actually opens the tab. When the component first
|
||||||
|
// mounts, the component is not visible and so the
|
||||||
|
// component has no height which can be used for scrolling.
|
||||||
|
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
connect(mapStateToProps),
|
||||||
|
scrollToBottom()
|
||||||
|
)(SharedFilesList);
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import WebTorrent from 'webtorrent';
|
||||||
|
import createTorrent from 'create-torrent';
|
||||||
|
import randomString from 'random-string';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as stateActions from '../../redux/stateActions';
|
||||||
|
import * as requestActions from '../../redux/requestActions';
|
||||||
|
import { store } from '../../store';
|
||||||
|
import config from '../../../config';
|
||||||
|
import SharedFilesList from './SharedFilesList';
|
||||||
|
|
||||||
|
export const client = WebTorrent.WEBRTC_SUPPORT && new WebTorrent({
|
||||||
|
tracker : {
|
||||||
|
rtcConfig : {
|
||||||
|
iceServers : config.turnServers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifyPeers = (file) =>
|
||||||
|
{
|
||||||
|
const { displayName, picture } = store.getState().me;
|
||||||
|
|
||||||
|
store.dispatch(requestActions.sendFile(file, displayName, picture));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shareFiles = async (files) =>
|
||||||
|
{
|
||||||
|
const notification =
|
||||||
|
{
|
||||||
|
id : randomString({ length: 6 }).toLowerCase(),
|
||||||
|
text : 'Creating torrent',
|
||||||
|
type : 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch(stateActions.addNotification(notification));
|
||||||
|
|
||||||
|
createTorrent(files, (err, torrent) =>
|
||||||
|
{
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
return store.dispatch(requestActions.notify({
|
||||||
|
text : 'An error occured while uploading a file'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingTorrent = client.get(torrent);
|
||||||
|
|
||||||
|
if (existingTorrent)
|
||||||
|
{
|
||||||
|
return notifyPeers({
|
||||||
|
magnet : existingTorrent.magnetURI
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
client.seed(files, (newTorrent) =>
|
||||||
|
{
|
||||||
|
store.dispatch(stateActions.removeNotification(notification.id));
|
||||||
|
|
||||||
|
store.dispatch(requestActions.notify({
|
||||||
|
text : 'Torrent successfully created'
|
||||||
|
}));
|
||||||
|
|
||||||
|
notifyPeers({
|
||||||
|
magnet : newTorrent.magnetURI
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileSharing extends Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.fileInput = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileChange = async (event) =>
|
||||||
|
{
|
||||||
|
if (event.target.files.length > 0)
|
||||||
|
{
|
||||||
|
await shareFiles(event.target.files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () =>
|
||||||
|
{
|
||||||
|
if (WebTorrent.WEBRTC_SUPPORT)
|
||||||
|
{
|
||||||
|
// We want to open the file dialog when we click a button
|
||||||
|
// instead of actually rendering the input element itself.
|
||||||
|
this.fileInput.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const buttonDescription = WebTorrent.WEBRTC_SUPPORT ?
|
||||||
|
'Share file' : 'File sharing not supported';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='FileSharing'>
|
||||||
|
<div className='sharing-toolbar'>
|
||||||
|
<input
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
ref={this.fileInput}
|
||||||
|
type='file'
|
||||||
|
onChange={this.handleFileChange}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
type='button'
|
||||||
|
onClick={this.handleClick}
|
||||||
|
className={classNames('share-file', {
|
||||||
|
disabled : !WebTorrent.WEBRTC_SUPPORT
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>{buttonDescription}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SharedFilesList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileSharing;
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import * as stateActions from '../redux/stateActions';
|
||||||
|
import Peer from './Peer';
|
||||||
|
|
||||||
|
class Filmstrip extends Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.activePeerContainer = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
lastSpeaker : null,
|
||||||
|
width : 400
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the name of the peer which is currently speaking. This is either
|
||||||
|
// the latest active speaker, or the manually selected peer, or, if no
|
||||||
|
// person has spoken yet, the first peer in the list of peers.
|
||||||
|
getActivePeerName = () =>
|
||||||
|
{
|
||||||
|
if (this.props.selectedPeerName)
|
||||||
|
{
|
||||||
|
return this.props.selectedPeerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.lastSpeaker)
|
||||||
|
{
|
||||||
|
return this.state.lastSpeaker;
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerNames = Object.keys(this.props.peers);
|
||||||
|
|
||||||
|
if (peerNames.length > 0)
|
||||||
|
{
|
||||||
|
return peerNames[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
isSharingCamera = (peerName) => this.props.peers[peerName] &&
|
||||||
|
this.props.peers[peerName].consumers.some((consumer) =>
|
||||||
|
this.props.consumers[consumer].source === 'screen');
|
||||||
|
|
||||||
|
getRatio = () =>
|
||||||
|
{
|
||||||
|
let ratio = 4 / 3;
|
||||||
|
|
||||||
|
if (this.isSharingCamera(this.getActivePeerName()))
|
||||||
|
{
|
||||||
|
ratio *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ratio;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDimensions = () =>
|
||||||
|
{
|
||||||
|
const container = this.activePeerContainer.current;
|
||||||
|
|
||||||
|
if (container)
|
||||||
|
{
|
||||||
|
const ratio = this.getRatio();
|
||||||
|
|
||||||
|
let width = container.clientWidth;
|
||||||
|
|
||||||
|
if (width / ratio > container.clientHeight)
|
||||||
|
{
|
||||||
|
width = container.clientHeight * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
window.addEventListener('resize', this.updateDimensions);
|
||||||
|
const observer = new ResizeObserver(this.updateDimensions);
|
||||||
|
|
||||||
|
observer.observe(this.activePeerContainer.current);
|
||||||
|
this.updateDimensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount()
|
||||||
|
{
|
||||||
|
window.removeEventListener('resize', this.updateDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps)
|
||||||
|
{
|
||||||
|
if (prevProps !== this.props)
|
||||||
|
{
|
||||||
|
this.updateDimensions();
|
||||||
|
|
||||||
|
if (this.props.activeSpeakerName !== this.props.myName)
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
|
this.setState({
|
||||||
|
lastSpeaker : this.props.activeSpeakerName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const { peers, advancedMode } = this.props;
|
||||||
|
|
||||||
|
const activePeerName = this.getActivePeerName();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='Filmstrip'>
|
||||||
|
<div className='active-peer-container' ref={this.activePeerContainer}>
|
||||||
|
{peers[activePeerName] && (
|
||||||
|
<div
|
||||||
|
className='active-peer'
|
||||||
|
style={{
|
||||||
|
width : this.state.width,
|
||||||
|
height : this.state.width / this.getRatio()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Peer
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
name={activePeerName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='filmstrip'>
|
||||||
|
<div className='filmstrip-content'>
|
||||||
|
{Object.keys(peers).map((peerName) => (
|
||||||
|
<div
|
||||||
|
key={peerName}
|
||||||
|
onClick={() => this.props.setSelectedPeer(peerName)}
|
||||||
|
className={classnames('film', {
|
||||||
|
selected : this.props.selectedPeerName === peerName,
|
||||||
|
active : this.state.lastSpeaker === peerName
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className='film-content'>
|
||||||
|
<Peer
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
name={peerName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Filmstrip.propTypes = {
|
||||||
|
activeSpeakerName : PropTypes.string,
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
|
peers : PropTypes.object.isRequired,
|
||||||
|
consumers : PropTypes.object.isRequired,
|
||||||
|
myName : PropTypes.string.isRequired,
|
||||||
|
selectedPeerName : PropTypes.string,
|
||||||
|
setSelectedPeer : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
activeSpeakerName : state.room.activeSpeakerName,
|
||||||
|
selectedPeerName : state.room.selectedPeerName,
|
||||||
|
peers : state.peers,
|
||||||
|
consumers : state.consumers,
|
||||||
|
myName : state.me.name
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setSelectedPeer : stateActions.setSelectedPeer
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Filmstrip);
|
||||||
|
|
@ -11,7 +11,8 @@ const FullScreenView = (props) =>
|
||||||
const {
|
const {
|
||||||
advancedMode,
|
advancedMode,
|
||||||
consumer,
|
consumer,
|
||||||
toggleConsumerFullscreen
|
toggleConsumerFullscreen,
|
||||||
|
toolbarsVisible
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (!consumer)
|
if (!consumer)
|
||||||
|
|
@ -39,7 +40,9 @@ const FullScreenView = (props) =>
|
||||||
|
|
||||||
<div className='controls'>
|
<div className='controls'>
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'fullscreen')}
|
className={classnames('button', 'fullscreen', 'room-controls', {
|
||||||
|
visible : toolbarsVisible
|
||||||
|
})}
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
{
|
{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -53,6 +56,7 @@ const FullScreenView = (props) =>
|
||||||
videoTrack={consumer ? consumer.track : null}
|
videoTrack={consumer ? consumer.track : null}
|
||||||
videoVisible={consumerVisible}
|
videoVisible={consumerVisible}
|
||||||
videoProfile={consumerProfile}
|
videoProfile={consumerProfile}
|
||||||
|
toggleFullscreen={() => toggleConsumerFullscreen(consumer)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -62,13 +66,15 @@ FullScreenView.propTypes =
|
||||||
{
|
{
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
consumer : appPropTypes.Consumer,
|
consumer : appPropTypes.Consumer,
|
||||||
toggleConsumerFullscreen : PropTypes.func.isRequired
|
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||||
|
toolbarsVisible : PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
consumer : state.consumers[state.room.fullScreenConsumer]
|
consumer : state.consumers[state.room.fullScreenConsumer],
|
||||||
|
toolbarsVisible : state.room.toolbarsVisible
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export default class FullView extends React.Component
|
||||||
// Latest received video track.
|
// Latest received video track.
|
||||||
// @type {MediaStreamTrack}
|
// @type {MediaStreamTrack}
|
||||||
this._videoTrack = null;
|
this._videoTrack = null;
|
||||||
|
|
||||||
|
this.video = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
|
|
@ -24,7 +26,7 @@ export default class FullView extends React.Component
|
||||||
return (
|
return (
|
||||||
<div data-component='FullView'>
|
<div data-component='FullView'>
|
||||||
<video
|
<video
|
||||||
ref='video'
|
ref={this.video}
|
||||||
className={classnames({
|
className={classnames({
|
||||||
hidden : !videoVisible,
|
hidden : !videoVisible,
|
||||||
loading : videoProfile === 'none'
|
loading : videoProfile === 'none'
|
||||||
|
|
@ -50,9 +52,9 @@ export default class FullView extends React.Component
|
||||||
this._setTracks(videoTrack);
|
this._setTracks(videoTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps)
|
componentDidUpdate()
|
||||||
{
|
{
|
||||||
const { videoTrack } = nextProps;
|
const { videoTrack } = this.props;
|
||||||
|
|
||||||
this._setTracks(videoTrack);
|
this._setTracks(videoTrack);
|
||||||
}
|
}
|
||||||
|
|
@ -64,15 +66,13 @@ export default class FullView extends React.Component
|
||||||
|
|
||||||
this._videoTrack = videoTrack;
|
this._videoTrack = videoTrack;
|
||||||
|
|
||||||
const { video } = this.refs;
|
const video = this.video.current;
|
||||||
|
|
||||||
if (videoTrack)
|
if (videoTrack)
|
||||||
{
|
{
|
||||||
const stream = new MediaStream;
|
const stream = new MediaStream;
|
||||||
|
|
||||||
if (videoTrack)
|
|
||||||
stream.addTrack(videoTrack);
|
stream.addTrack(videoTrack);
|
||||||
|
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -86,5 +86,6 @@ FullView.propTypes =
|
||||||
{
|
{
|
||||||
videoTrack : PropTypes.any,
|
videoTrack : PropTypes.any,
|
||||||
videoVisible : PropTypes.bool,
|
videoVisible : PropTypes.bool,
|
||||||
videoProfile : PropTypes.string
|
videoProfile : PropTypes.string,
|
||||||
|
toggleFullscreen : PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,24 @@ import ScreenView from './ScreenView';
|
||||||
|
|
||||||
class Me extends React.Component
|
class Me extends React.Component
|
||||||
{
|
{
|
||||||
|
state = {
|
||||||
|
controlsVisible : false
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseOver = () =>
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
controlsVisible : true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseOut = () =>
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
controlsVisible : false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props)
|
constructor(props)
|
||||||
{
|
{
|
||||||
super(props);
|
super(props);
|
||||||
|
|
@ -85,10 +103,15 @@ class Me extends React.Component
|
||||||
data-tip={tip}
|
data-tip={tip}
|
||||||
data-tip-disable={!tip}
|
data-tip-disable={!tip}
|
||||||
data-type='dark'
|
data-type='dark'
|
||||||
|
onMouseOver={this.handleMouseOver}
|
||||||
|
onMouseOut={this.handleMouseOut}
|
||||||
>
|
>
|
||||||
<div className={classnames('view-container', 'webcam')}>
|
<div className={classnames('view-container', 'webcam')}>
|
||||||
{connected ?
|
{connected ?
|
||||||
<div className='controls'>
|
<div className={classnames('controls', {
|
||||||
|
visible : this.state.controlsVisible
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'mic', micState, {
|
className={classnames('button', 'mic', micState, {
|
||||||
disabled : me.audioInProgress
|
disabled : me.audioInProgress
|
||||||
|
|
@ -117,6 +140,7 @@ class Me extends React.Component
|
||||||
advancedMode={advancedMode}
|
advancedMode={advancedMode}
|
||||||
peer={me}
|
peer={me}
|
||||||
audioTrack={micProducer ? micProducer.track : null}
|
audioTrack={micProducer ? micProducer.track : null}
|
||||||
|
volume={micProducer ? micProducer.volume : null}
|
||||||
videoTrack={webcamProducer ? webcamProducer.track : null}
|
videoTrack={webcamProducer ? webcamProducer.track : null}
|
||||||
videoVisible={videoVisible}
|
videoVisible={videoVisible}
|
||||||
audioCodec={micProducer ? micProducer.codec : null}
|
audioCodec={micProducer ? micProducer.codec : null}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,15 @@ import * as appPropTypes from './appPropTypes';
|
||||||
import * as stateActions from '../redux/stateActions';
|
import * as stateActions from '../redux/stateActions';
|
||||||
import { Appear } from './transitions';
|
import { Appear } from './transitions';
|
||||||
|
|
||||||
const Notifications = ({ notifications, onClick }) =>
|
const Notifications = ({ notifications, onClick, toolAreaOpen }) =>
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<div data-component='Notifications'>
|
<div
|
||||||
|
data-component='Notifications'
|
||||||
|
className={classnames({
|
||||||
|
'toolarea-open' : toolAreaOpen
|
||||||
|
})}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
notifications.map((notification) =>
|
notifications.map((notification) =>
|
||||||
{
|
{
|
||||||
|
|
@ -33,14 +38,18 @@ const Notifications = ({ notifications, onClick }) =>
|
||||||
Notifications.propTypes =
|
Notifications.propTypes =
|
||||||
{
|
{
|
||||||
notifications : PropTypes.arrayOf(appPropTypes.Notification).isRequired,
|
notifications : PropTypes.arrayOf(appPropTypes.Notification).isRequired,
|
||||||
onClick : PropTypes.func.isRequired
|
onClick : PropTypes.func.isRequired,
|
||||||
|
toolAreaOpen : PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
const { notifications } = state;
|
const { notifications } = state;
|
||||||
|
|
||||||
return { notifications };
|
return {
|
||||||
|
notifications,
|
||||||
|
toolAreaOpen : state.toolarea.toolAreaOpen
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Me } from '../appPropTypes';
|
||||||
|
|
||||||
|
const ListMe = ({ me }) =>
|
||||||
|
{
|
||||||
|
const picture = me.picture || 'resources/images/avatar-empty.jpeg';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className='list-item me'>
|
||||||
|
<div data-component='ListPeer'>
|
||||||
|
<img className='avatar' src={picture} />
|
||||||
|
|
||||||
|
<div className='peer-info'>
|
||||||
|
{me.displayName}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='indicators'>
|
||||||
|
{me.raisedHand && (
|
||||||
|
<div className='icon raise-hand on' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ListMe.propTypes = {
|
||||||
|
me : Me.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
me : state.me
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps
|
||||||
|
)(ListMe);
|
||||||
|
|
@ -38,12 +38,29 @@ const ListPeer = (props) =>
|
||||||
!screenConsumer.remotelyPaused
|
!screenConsumer.remotelyPaused
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const picture = peer.picture || 'resources/images/avatar-empty.jpeg';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component='ListPeer'>
|
<div data-component='ListPeer'>
|
||||||
<img className='avatar' />
|
<img className='avatar' src={picture} />
|
||||||
|
|
||||||
<div className='peer-info'>
|
<div className='peer-info'>
|
||||||
{peer.displayName}
|
{peer.displayName}
|
||||||
</div>
|
</div>
|
||||||
|
<div className='indicators'>
|
||||||
|
{peer.raiseHandState ?
|
||||||
|
<div className={
|
||||||
|
classnames(
|
||||||
|
'icon', 'raise-hand', {
|
||||||
|
on : peer.raiseHandState,
|
||||||
|
off : !peer.raiseHandState
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div className='controls'>
|
<div className='controls'>
|
||||||
{ screenConsumer ?
|
{ screenConsumer ?
|
||||||
<div
|
<div
|
||||||
|
|
@ -67,6 +84,8 @@ const ListPeer = (props) =>
|
||||||
off : !micEnabled,
|
off : !micEnabled,
|
||||||
disabled : peer.peerAudioInProgress
|
disabled : peer.peerAudioInProgress
|
||||||
})}
|
})}
|
||||||
|
style={{ opacity : micEnabled && micConsumer ? (micConsumer.volume/10)
|
||||||
|
+ 0.2 :1 }}
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
{
|
{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,38 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
import * as appPropTypes from '../appPropTypes';
|
import * as appPropTypes from '../appPropTypes';
|
||||||
import * as requestActions from '../../redux/requestActions';
|
|
||||||
import * as stateActions from '../../redux/stateActions';
|
import * as stateActions from '../../redux/stateActions';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ListPeer from './ListPeer';
|
import ListPeer from './ListPeer';
|
||||||
|
import ListMe from './ListMe';
|
||||||
|
|
||||||
class ParticipantList extends React.Component
|
const ParticipantList = ({ advancedMode, peers, setSelectedPeer, selectedPeerName }) => (
|
||||||
{
|
|
||||||
constructor(props)
|
|
||||||
{
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
render()
|
|
||||||
{
|
|
||||||
const {
|
|
||||||
advancedMode,
|
|
||||||
peers
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-component='ParticipantList'>
|
<div data-component='ParticipantList'>
|
||||||
<ul className='list'>
|
<ul className='list'>
|
||||||
{
|
<ListMe />
|
||||||
peers.map((peer) =>
|
|
||||||
{
|
{peers.map((peer) => (
|
||||||
return (
|
<li
|
||||||
<li key={peer.name} className='list-item'>
|
key={peer.name}
|
||||||
|
className={classNames('list-item', {
|
||||||
|
selected : peer.name === selectedPeerName
|
||||||
|
})}
|
||||||
|
onClick={() => setSelectedPeer(peer.name)}
|
||||||
|
>
|
||||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||||
</li>
|
</li>
|
||||||
);
|
))}
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ParticipantList.propTypes =
|
ParticipantList.propTypes =
|
||||||
{
|
{
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
|
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||||
|
setSelectedPeer : PropTypes.func.isRequired,
|
||||||
|
selectedPeerName : PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
|
|
@ -50,26 +40,13 @@ const mapStateToProps = (state) =>
|
||||||
const peersArray = Object.values(state.peers);
|
const peersArray = Object.values(state.peers);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peers : peersArray
|
peers : peersArray,
|
||||||
|
selectedPeerName : state.room.selectedPeerName
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
const mapDispatchToProps = {
|
||||||
{
|
setSelectedPeer : stateActions.setSelectedPeer
|
||||||
return {
|
|
||||||
handleChangeWebcam : (device) =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.changeWebcam(device.value));
|
|
||||||
},
|
|
||||||
handleChangeAudioDevice : (device) =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.changeAudioDevice(device.value));
|
|
||||||
},
|
|
||||||
onToggleAdvancedMode : () =>
|
|
||||||
{
|
|
||||||
dispatch(stateActions.toggleAdvancedMode());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ParticipantListContainer = connect(
|
const ParticipantListContainer = connect(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
@ -8,7 +8,27 @@ import * as stateActions from '../redux/stateActions';
|
||||||
import PeerView from './PeerView';
|
import PeerView from './PeerView';
|
||||||
import ScreenView from './ScreenView';
|
import ScreenView from './ScreenView';
|
||||||
|
|
||||||
const Peer = (props) =>
|
class Peer extends Component
|
||||||
|
{
|
||||||
|
state = {
|
||||||
|
controlsVisible : false
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseOver = () =>
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
controlsVisible : true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseOut = () =>
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
controlsVisible : false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render()
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
advancedMode,
|
advancedMode,
|
||||||
|
|
@ -24,7 +44,7 @@ const Peer = (props) =>
|
||||||
onEnableScreen,
|
onEnableScreen,
|
||||||
toggleConsumerFullscreen,
|
toggleConsumerFullscreen,
|
||||||
style
|
style
|
||||||
} = props;
|
} = this.props;
|
||||||
|
|
||||||
const micEnabled = (
|
const micEnabled = (
|
||||||
Boolean(micConsumer) &&
|
Boolean(micConsumer) &&
|
||||||
|
|
@ -60,6 +80,8 @@ const Peer = (props) =>
|
||||||
className={classnames({
|
className={classnames({
|
||||||
screen : screenConsumer
|
screen : screenConsumer
|
||||||
})}
|
})}
|
||||||
|
onMouseOver={this.handleMouseOver}
|
||||||
|
onMouseOut={this.handleMouseOut}
|
||||||
>
|
>
|
||||||
{videoVisible && !webcamConsumer.supported ?
|
{videoVisible && !webcamConsumer.supported ?
|
||||||
<div className='incompatible-video'>
|
<div className='incompatible-video'>
|
||||||
|
|
@ -69,7 +91,25 @@ const Peer = (props) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={classnames('view-container', 'webcam')} style={style}>
|
<div className={classnames('view-container', 'webcam')} style={style}>
|
||||||
<div className='controls'>
|
<div className='indicators'>
|
||||||
|
{peer.raiseHandState ?
|
||||||
|
<div className={
|
||||||
|
classnames(
|
||||||
|
'icon', 'raise-hand', {
|
||||||
|
on : peer.raiseHandState,
|
||||||
|
off : !peer.raiseHandState
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classnames('controls', {
|
||||||
|
visible : this.state.controlsVisible
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'mic', {
|
className={classnames('button', 'mic', {
|
||||||
on : micEnabled,
|
on : micEnabled,
|
||||||
|
|
@ -110,6 +150,7 @@ const Peer = (props) =>
|
||||||
advancedMode={advancedMode}
|
advancedMode={advancedMode}
|
||||||
peer={peer}
|
peer={peer}
|
||||||
audioTrack={micConsumer ? micConsumer.track : null}
|
audioTrack={micConsumer ? micConsumer.track : null}
|
||||||
|
volume={micConsumer ? micConsumer.volume : null}
|
||||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||||
videoVisible={videoVisible}
|
videoVisible={videoVisible}
|
||||||
videoProfile={videoProfile}
|
videoProfile={videoProfile}
|
||||||
|
|
@ -120,7 +161,11 @@ const Peer = (props) =>
|
||||||
|
|
||||||
{screenConsumer ?
|
{screenConsumer ?
|
||||||
<div className={classnames('view-container', 'screen')} style={style}>
|
<div className={classnames('view-container', 'screen')} style={style}>
|
||||||
<div className='controls'>
|
<div
|
||||||
|
className={classnames('controls', {
|
||||||
|
visible : this.state.controlsVisible
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'screen', {
|
className={classnames('button', 'screen', {
|
||||||
on : screenVisible,
|
on : screenVisible,
|
||||||
|
|
@ -156,7 +201,8 @@ const Peer = (props) =>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Peer.propTypes =
|
Peer.propTypes =
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Spinner from 'react-spinner';
|
import Spinner from 'react-spinner';
|
||||||
import hark from 'hark';
|
|
||||||
import * as appPropTypes from './appPropTypes';
|
import * as appPropTypes from './appPropTypes';
|
||||||
import EditableInput from './EditableInput';
|
import EditableInput from './EditableInput';
|
||||||
|
|
||||||
|
|
@ -27,10 +26,6 @@ export default class PeerView extends React.Component
|
||||||
// @type {MediaStreamTrack}
|
// @type {MediaStreamTrack}
|
||||||
this._videoTrack = null;
|
this._videoTrack = null;
|
||||||
|
|
||||||
// Hark instance.
|
|
||||||
// @type {Object}
|
|
||||||
this._hark = null;
|
|
||||||
|
|
||||||
// Periodic timer for showing video resolution.
|
// Periodic timer for showing video resolution.
|
||||||
this._videoResolutionTimer = null;
|
this._videoResolutionTimer = null;
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +35,7 @@ export default class PeerView extends React.Component
|
||||||
const {
|
const {
|
||||||
isMe,
|
isMe,
|
||||||
peer,
|
peer,
|
||||||
|
volume,
|
||||||
advancedMode,
|
advancedMode,
|
||||||
videoVisible,
|
videoVisible,
|
||||||
videoProfile,
|
videoProfile,
|
||||||
|
|
@ -49,7 +45,6 @@ export default class PeerView extends React.Component
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
volume,
|
|
||||||
videoWidth,
|
videoWidth,
|
||||||
videoHeight
|
videoHeight
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
@ -149,9 +144,6 @@ export default class PeerView extends React.Component
|
||||||
|
|
||||||
componentWillUnmount()
|
componentWillUnmount()
|
||||||
{
|
{
|
||||||
if (this._hark)
|
|
||||||
this._hark.stop();
|
|
||||||
|
|
||||||
clearInterval(this._videoResolutionTimer);
|
clearInterval(this._videoResolutionTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,6 +152,7 @@ export default class PeerView extends React.Component
|
||||||
const { audioTrack, videoTrack } = nextProps;
|
const { audioTrack, videoTrack } = nextProps;
|
||||||
|
|
||||||
this._setTracks(audioTrack, videoTrack);
|
this._setTracks(audioTrack, videoTrack);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setTracks(audioTrack, videoTrack)
|
_setTracks(audioTrack, videoTrack)
|
||||||
|
|
@ -170,9 +163,6 @@ export default class PeerView extends React.Component
|
||||||
this._audioTrack = audioTrack;
|
this._audioTrack = audioTrack;
|
||||||
this._videoTrack = videoTrack;
|
this._videoTrack = videoTrack;
|
||||||
|
|
||||||
if (this._hark)
|
|
||||||
this._hark.stop();
|
|
||||||
|
|
||||||
clearInterval(this._videoResolutionTimer);
|
clearInterval(this._videoResolutionTimer);
|
||||||
this._hideVideoResolution();
|
this._hideVideoResolution();
|
||||||
|
|
||||||
|
|
@ -190,9 +180,6 @@ export default class PeerView extends React.Component
|
||||||
|
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
|
|
||||||
if (audioTrack)
|
|
||||||
this._runHark(stream);
|
|
||||||
|
|
||||||
if (videoTrack)
|
if (videoTrack)
|
||||||
this._showVideoResolution();
|
this._showVideoResolution();
|
||||||
}
|
}
|
||||||
|
|
@ -202,31 +189,6 @@ export default class PeerView extends React.Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_runHark(stream)
|
|
||||||
{
|
|
||||||
if (!stream.getAudioTracks()[0])
|
|
||||||
throw new Error('_runHark() | given stream has no audio track');
|
|
||||||
|
|
||||||
this._hark = hark(stream, { play: false });
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
this._hark.on('volume_change', (dBs, threshold) =>
|
|
||||||
{
|
|
||||||
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
|
|
||||||
// Math.pow(10, dBs / 20)
|
|
||||||
// However it does not produce a visually useful output, so let exagerate
|
|
||||||
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
|
|
||||||
// minimize component renderings.
|
|
||||||
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
|
|
||||||
|
|
||||||
if (volume === 1)
|
|
||||||
volume = 0;
|
|
||||||
|
|
||||||
if (volume !== this.state.volume)
|
|
||||||
this.setState({ volume: volume });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_showVideoResolution()
|
_showVideoResolution()
|
||||||
{
|
{
|
||||||
this._videoResolutionTimer = setInterval(() =>
|
this._videoResolutionTimer = setInterval(() =>
|
||||||
|
|
@ -259,6 +221,7 @@ PeerView.propTypes =
|
||||||
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
audioTrack : PropTypes.any,
|
audioTrack : PropTypes.any,
|
||||||
|
volume : PropTypes.number,
|
||||||
videoTrack : PropTypes.any,
|
videoTrack : PropTypes.any,
|
||||||
videoVisible : PropTypes.bool.isRequired,
|
videoVisible : PropTypes.bool.isRequired,
|
||||||
videoProfile : PropTypes.string,
|
videoProfile : PropTypes.string,
|
||||||
|
|
|
||||||
|
|
@ -5,46 +5,51 @@ import classnames from 'classnames';
|
||||||
import * as appPropTypes from './appPropTypes';
|
import * as appPropTypes from './appPropTypes';
|
||||||
import { Appear } from './transitions';
|
import { Appear } from './transitions';
|
||||||
import Peer from './Peer';
|
import Peer from './Peer';
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
|
||||||
|
const RATIO = 1.334;
|
||||||
|
|
||||||
class Peers extends React.Component
|
class Peers extends React.Component
|
||||||
{
|
{
|
||||||
constructor(props)
|
constructor(props)
|
||||||
{
|
{
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
peerWidth : 400,
|
peerWidth : 400,
|
||||||
peerHeight : 300,
|
peerHeight : 300
|
||||||
ratio : 1.334
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.peersRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeUpdate()
|
updateDimensions = () =>
|
||||||
{
|
{
|
||||||
this.updateDimensions();
|
if (!this.peersRef.current)
|
||||||
}
|
|
||||||
|
|
||||||
updateDimensions(props = this.props)
|
|
||||||
{
|
|
||||||
const n = props.videoStreams ? props.videoStreams : 0;
|
|
||||||
|
|
||||||
if (n == 0)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = this.refs.peers.clientWidth;
|
const n = this.props.boxes;
|
||||||
const height = this.refs.peers.clientHeight;
|
|
||||||
|
if (n === 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = this.peersRef.current.clientWidth;
|
||||||
|
const height = this.peersRef.current.clientHeight;
|
||||||
|
|
||||||
let x, y, space;
|
let x, y, space;
|
||||||
|
|
||||||
for (let rows = 1; rows < 100; rows = rows + 1)
|
for (let rows = 1; rows < 100; rows = rows + 1)
|
||||||
{
|
{
|
||||||
x = width / Math.ceil(n / rows);
|
x = width / Math.ceil(n / rows);
|
||||||
y = x / this.state.ratio;
|
y = x / RATIO;
|
||||||
if (height < (y * rows))
|
if (height < (y * rows))
|
||||||
{
|
{
|
||||||
y = height / rows;
|
y = height / rows;
|
||||||
x = this.state.ratio * y;
|
x = RATIO * y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
space = height - (y * (rows));
|
space = height - (y * (rows));
|
||||||
|
|
@ -60,21 +65,24 @@ class Peers extends React.Component
|
||||||
peerHeight : 0.9 * y
|
peerHeight : 0.9 * y
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
componentDidMount()
|
componentDidMount()
|
||||||
{
|
{
|
||||||
window.addEventListener('resize', this.resizeUpdate.bind(this));
|
window.addEventListener('resize', this.updateDimensions);
|
||||||
|
const observer = new ResizeObserver(this.updateDimensions);
|
||||||
|
|
||||||
|
observer.observe(this.peersRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount()
|
componentWillUnmount()
|
||||||
{
|
{
|
||||||
window.removeEventListener('resize', this.resizeUpdate.bind(this));
|
window.removeEventListener('resize', this.updateDimensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps)
|
componentDidUpdate()
|
||||||
{
|
{
|
||||||
this.updateDimensions(nextProps);
|
this.updateDimensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
|
|
@ -82,8 +90,7 @@ class Peers extends React.Component
|
||||||
const {
|
const {
|
||||||
advancedMode,
|
advancedMode,
|
||||||
activeSpeakerName,
|
activeSpeakerName,
|
||||||
peers,
|
peers
|
||||||
toolAreaOpen
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const style =
|
const style =
|
||||||
|
|
@ -93,7 +100,7 @@ class Peers extends React.Component
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component='Peers' ref='peers'>
|
<div data-component='Peers' ref={this.peersRef}>
|
||||||
{
|
{
|
||||||
peers.map((peer) =>
|
peers.map((peer) =>
|
||||||
{
|
{
|
||||||
|
|
@ -108,7 +115,6 @@ class Peers extends React.Component
|
||||||
advancedMode={advancedMode}
|
advancedMode={advancedMode}
|
||||||
name={peer.name}
|
name={peer.name}
|
||||||
style={style}
|
style={style}
|
||||||
toolAreaOpen={toolAreaOpen}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Appear>
|
</Appear>
|
||||||
|
|
@ -124,26 +130,21 @@ Peers.propTypes =
|
||||||
{
|
{
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||||
videoStreams : PropTypes.any,
|
boxes : PropTypes.number,
|
||||||
activeSpeakerName : PropTypes.string,
|
activeSpeakerName : PropTypes.string
|
||||||
toolAreaOpen : PropTypes.bool
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
const peersArray = Object.values(state.peers);
|
const peers = Object.values(state.peers);
|
||||||
const videoStreamsArray = Object.values(state.consumers);
|
|
||||||
const videoStreams =
|
const boxes = peers.length + Object.values(state.consumers)
|
||||||
videoStreamsArray.filter((consumer) =>
|
.filter((consumer) => consumer.source === 'screen').length;
|
||||||
{
|
|
||||||
return (consumer.source === 'webcam' || consumer.source === 'screen');
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peers : peersArray,
|
peers,
|
||||||
videoStreams : videoStreams,
|
boxes,
|
||||||
activeSpeakerName : state.room.activeSpeakerName,
|
activeSpeakerName : state.room.activeSpeakerName
|
||||||
toolAreaOpen : state.toolarea.toolAreaOpen
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from 'react-tooltip';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import ClipboardButton from 'react-clipboard.js';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import * as appPropTypes from './appPropTypes';
|
import * as appPropTypes from './appPropTypes';
|
||||||
import * as requestActions from '../redux/requestActions';
|
import * as requestActions from '../redux/requestActions';
|
||||||
|
import * as stateActions from '../redux/stateActions';
|
||||||
import { Appear } from './transitions';
|
import { Appear } from './transitions';
|
||||||
import Me from './Me';
|
import Me from './Me';
|
||||||
import Peers from './Peers';
|
import Peers from './Peers';
|
||||||
|
|
@ -14,60 +15,76 @@ import ToolAreaButton from './ToolArea/ToolAreaButton';
|
||||||
import ToolArea from './ToolArea/ToolArea';
|
import ToolArea from './ToolArea/ToolArea';
|
||||||
import FullScreenView from './FullScreenView';
|
import FullScreenView from './FullScreenView';
|
||||||
import Draggable from 'react-draggable';
|
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;
|
||||||
|
|
||||||
class Room extends React.Component
|
class Room extends React.Component
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Hides the different toolbars on the page after a
|
||||||
|
* given amount of time has passed since the
|
||||||
|
* last time the cursor was moved.
|
||||||
|
*/
|
||||||
|
waitForHide = idle(() =>
|
||||||
|
{
|
||||||
|
this.props.setToolbarsVisible(false);
|
||||||
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
handleMovement = () =>
|
||||||
|
{
|
||||||
|
// If the toolbars were hidden, show them again when
|
||||||
|
// the user moves their cursor.
|
||||||
|
if (!this.props.room.toolbarsVisible)
|
||||||
|
{
|
||||||
|
this.props.setToolbarsVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.waitForHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
window.addEventListener('mousemove', this.handleMovement);
|
||||||
|
window.addEventListener('touchstart', this.handleMovement);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount()
|
||||||
|
{
|
||||||
|
window.removeEventListener('mousemove', this.handleMovement);
|
||||||
|
window.removeEventListener('touchstart', this.handleMovement);
|
||||||
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
room,
|
room,
|
||||||
me,
|
|
||||||
toolAreaOpen,
|
toolAreaOpen,
|
||||||
amActiveSpeaker,
|
amActiveSpeaker,
|
||||||
screenProducer,
|
onRoomLinkCopy
|
||||||
onRoomLinkCopy,
|
|
||||||
onLogin,
|
|
||||||
onShareScreen,
|
|
||||||
onUnShareScreen,
|
|
||||||
onNeedExtension,
|
|
||||||
onLeaveMeeting
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let screenState;
|
const View = {
|
||||||
let screenTip;
|
filmstrip : Filmstrip,
|
||||||
|
democratic : Peers
|
||||||
if (me.needExtension)
|
}[room.mode];
|
||||||
{
|
|
||||||
screenState = 'need-extension';
|
|
||||||
screenTip = 'Install screen sharing extension';
|
|
||||||
}
|
|
||||||
else if (!me.canShareScreen)
|
|
||||||
{
|
|
||||||
screenState = 'unsupported';
|
|
||||||
screenTip = 'Screen sharing not supported';
|
|
||||||
}
|
|
||||||
else if (screenProducer)
|
|
||||||
{
|
|
||||||
screenState = 'on';
|
|
||||||
screenTip = 'Stop screen sharing';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
screenState = 'off';
|
|
||||||
screenTip = 'Start screen sharing';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<HoldingOverlay />
|
||||||
|
|
||||||
<Appear duration={300}>
|
<Appear duration={300}>
|
||||||
<div data-component='Room'>
|
<div data-component='Room'>
|
||||||
<FullScreenView advancedMode={room.advancedMode} />
|
<FullScreenView advancedMode={room.advancedMode} />
|
||||||
<div
|
<div className='room-wrapper'>
|
||||||
className='room-wrapper'
|
|
||||||
style={{
|
|
||||||
width : toolAreaOpen ? '80%' : '100%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
|
||||||
<ToolAreaButton />
|
<ToolAreaButton />
|
||||||
|
|
||||||
{room.advancedMode ?
|
{room.advancedMode ?
|
||||||
|
|
@ -78,16 +95,22 @@ class Room extends React.Component
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className='room-link-wrapper'>
|
<div
|
||||||
|
className={classnames('room-link-wrapper room-controls', {
|
||||||
|
'visible' : this.props.room.toolbarsVisible
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className='room-link'>
|
<div className='room-link'>
|
||||||
<ClipboardButton
|
<CopyToClipboard
|
||||||
component='a'
|
text={room.url}
|
||||||
|
onCopy={onRoomLinkCopy}
|
||||||
|
>
|
||||||
|
<a
|
||||||
className='link'
|
className='link'
|
||||||
button-href={room.url}
|
href={room.url}
|
||||||
button-target='_blank'
|
target='_blank'
|
||||||
data-tip='Click to copy room link'
|
data-tip='Click to copy room link'
|
||||||
data-clipboard-text={room.url}
|
rel='noopener noreferrer'
|
||||||
onSuccess={onRoomLinkCopy}
|
|
||||||
onClick={(event) =>
|
onClick={(event) =>
|
||||||
{
|
{
|
||||||
// If this is a 'Open in new window/tab' don't prevent
|
// If this is a 'Open in new window/tab' don't prevent
|
||||||
|
|
@ -105,13 +128,12 @@ class Room extends React.Component
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
invitation link
|
invitation link
|
||||||
</ClipboardButton>
|
</a>
|
||||||
|
</CopyToClipboard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Peers
|
<View advancedMode={room.advancedMode} />
|
||||||
advancedMode={room.advancedMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Draggable handle='.me-container' bounds='body' cancel='.display-name'>
|
<Draggable handle='.me-container' bounds='body' cancel='.display-name'>
|
||||||
<div
|
<div
|
||||||
|
|
@ -125,57 +147,7 @@ class Room extends React.Component
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|
||||||
<div className='sidebar'>
|
<Sidebar />
|
||||||
<div
|
|
||||||
className={classnames('button', 'screen', screenState)}
|
|
||||||
data-tip={screenTip}
|
|
||||||
data-type='dark'
|
|
||||||
onClick={() =>
|
|
||||||
{
|
|
||||||
switch (screenState)
|
|
||||||
{
|
|
||||||
case 'on':
|
|
||||||
{
|
|
||||||
onUnShareScreen();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'off':
|
|
||||||
{
|
|
||||||
onShareScreen();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'need-extension':
|
|
||||||
{
|
|
||||||
onNeedExtension();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{me.loginEnabled ?
|
|
||||||
<div
|
|
||||||
className={classnames('button', 'login', 'off', {
|
|
||||||
disabled : me.loginInProgress
|
|
||||||
})}
|
|
||||||
data-tip='Login'
|
|
||||||
data-type='dark'
|
|
||||||
onClick={() => onLogin()}
|
|
||||||
/>
|
|
||||||
:null
|
|
||||||
}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classnames('button', 'leave-meeting')}
|
|
||||||
data-tip='Leave meeting'
|
|
||||||
data-type='dark'
|
|
||||||
onClick={() => onLeaveMeeting()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ReactTooltip
|
<ReactTooltip
|
||||||
effect='solid'
|
effect='solid'
|
||||||
|
|
@ -184,10 +156,7 @@ class Room extends React.Component
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className='toolarea-wrapper'
|
className={classnames('toolarea-wrapper', { open: toolAreaOpen })}
|
||||||
style={{
|
|
||||||
width : toolAreaOpen ? '20%' : '0%'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{toolAreaOpen ?
|
{toolAreaOpen ?
|
||||||
<ToolArea
|
<ToolArea
|
||||||
|
|
@ -198,6 +167,7 @@ class Room extends React.Component
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Appear>
|
</Appear>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,11 +180,7 @@ Room.propTypes =
|
||||||
toolAreaOpen : PropTypes.bool.isRequired,
|
toolAreaOpen : PropTypes.bool.isRequired,
|
||||||
screenProducer : appPropTypes.Producer,
|
screenProducer : appPropTypes.Producer,
|
||||||
onRoomLinkCopy : PropTypes.func.isRequired,
|
onRoomLinkCopy : PropTypes.func.isRequired,
|
||||||
onShareScreen : PropTypes.func.isRequired,
|
setToolbarsVisible : PropTypes.func.isRequired
|
||||||
onUnShareScreen : PropTypes.func.isRequired,
|
|
||||||
onNeedExtension : PropTypes.func.isRequired,
|
|
||||||
onLeaveMeeting : PropTypes.func.isRequired,
|
|
||||||
onLogin : PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
|
|
@ -242,25 +208,10 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
text : 'Room link copied to the clipboard'
|
text : 'Room link copied to the clipboard'
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
onLeaveMeeting : () =>
|
|
||||||
|
setToolbarsVisible : (visible) =>
|
||||||
{
|
{
|
||||||
dispatch(requestActions.leaveRoom());
|
dispatch(stateActions.setToolbarsVisible(visible));
|
||||||
},
|
|
||||||
onShareScreen : () =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.enableScreenSharing());
|
|
||||||
},
|
|
||||||
onUnShareScreen : () =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.disableScreenSharing());
|
|
||||||
},
|
|
||||||
onNeedExtension : () =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.installExtension());
|
|
||||||
},
|
|
||||||
onLogin : () =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.userLogin());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,21 @@ import * as stateActions from '../redux/stateActions';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Dropdown from 'react-dropdown';
|
import Dropdown from 'react-dropdown';
|
||||||
|
|
||||||
class Settings extends React.Component
|
const modes = [ {
|
||||||
{
|
value : 'democratic',
|
||||||
constructor(props)
|
label : 'Democratic view'
|
||||||
{
|
}, {
|
||||||
super(props);
|
value : 'filmstrip',
|
||||||
}
|
label : 'Filmstrip view'
|
||||||
|
} ];
|
||||||
|
|
||||||
render()
|
const findOption = (options, value) => options.find((option) => option.value === value);
|
||||||
{
|
|
||||||
const {
|
|
||||||
room,
|
|
||||||
me,
|
|
||||||
handleChangeWebcam,
|
|
||||||
handleChangeAudioDevice,
|
|
||||||
onToggleAdvancedMode
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
|
const Settings = ({
|
||||||
|
room, me, onToggleAdvancedMode, handleChangeWebcam,
|
||||||
|
handleChangeAudioDevice, handleChangeMode
|
||||||
|
}) =>
|
||||||
|
{
|
||||||
let webcams;
|
let webcams;
|
||||||
let webcamText;
|
let webcamText;
|
||||||
|
|
||||||
|
|
@ -55,26 +53,36 @@ class Settings extends React.Component
|
||||||
<Dropdown
|
<Dropdown
|
||||||
disabled={!me.canChangeWebcam}
|
disabled={!me.canChangeWebcam}
|
||||||
options={webcams}
|
options={webcams}
|
||||||
onChange={handleChangeWebcam}
|
value={findOption(webcams, me.selectedWebcam)}
|
||||||
|
onChange={(webcam) => handleChangeWebcam(webcam.value)}
|
||||||
placeholder={webcamText}
|
placeholder={webcamText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
disabled={!me.canChangeAudioDevice}
|
disabled={!me.canChangeAudioDevice}
|
||||||
options={audioDevices}
|
options={audioDevices}
|
||||||
onChange={handleChangeAudioDevice}
|
value={findOption(audioDevices, me.selectedAudioDevice)}
|
||||||
|
onChange={(device) => handleChangeAudioDevice(device.value)}
|
||||||
placeholder={audioDevicesText}
|
placeholder={audioDevicesText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
id='room-mode'
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
defaultChecked={room.advancedMode}
|
checked={room.advancedMode}
|
||||||
onChange={onToggleAdvancedMode}
|
onChange={onToggleAdvancedMode}
|
||||||
/>
|
/>
|
||||||
<span>Advanced mode</span>
|
<label htmlFor='room-mode'>Advanced mode</label>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
options={modes}
|
||||||
|
value={findOption(modes, room.mode)}
|
||||||
|
onChange={(mode) => handleChangeMode(mode.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Settings.propTypes =
|
Settings.propTypes =
|
||||||
{
|
{
|
||||||
|
|
@ -82,7 +90,8 @@ Settings.propTypes =
|
||||||
room : appPropTypes.Room.isRequired,
|
room : appPropTypes.Room.isRequired,
|
||||||
handleChangeWebcam : PropTypes.func.isRequired,
|
handleChangeWebcam : PropTypes.func.isRequired,
|
||||||
handleChangeAudioDevice : PropTypes.func.isRequired,
|
handleChangeAudioDevice : PropTypes.func.isRequired,
|
||||||
onToggleAdvancedMode : PropTypes.func.isRequired
|
onToggleAdvancedMode : PropTypes.func.isRequired,
|
||||||
|
handleChangeMode : PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
|
|
@ -93,22 +102,11 @@ const mapStateToProps = (state) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
const mapDispatchToProps = {
|
||||||
{
|
handleChangeWebcam : requestActions.changeWebcam,
|
||||||
return {
|
handleChangeAudioDevice : requestActions.changeAudioDevice,
|
||||||
handleChangeWebcam : (device) =>
|
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
|
||||||
{
|
handleChangeMode : stateActions.setDisplayMode
|
||||||
dispatch(requestActions.changeWebcam(device.value));
|
|
||||||
},
|
|
||||||
handleChangeAudioDevice : (device) =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.changeAudioDevice(device.value));
|
|
||||||
},
|
|
||||||
onToggleAdvancedMode : () =>
|
|
||||||
{
|
|
||||||
dispatch(stateActions.toggleAdvancedMode());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsContainer = connect(
|
const SettingsContainer = connect(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import * as appPropTypes from './appPropTypes';
|
||||||
|
import * as requestActions from '../redux/requestActions';
|
||||||
|
import fscreen from 'fscreen';
|
||||||
|
|
||||||
|
class Sidebar extends Component
|
||||||
|
{
|
||||||
|
state = {
|
||||||
|
fullscreen : false
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggleFullscreen = () =>
|
||||||
|
{
|
||||||
|
if (fscreen.fullscreenElement)
|
||||||
|
{
|
||||||
|
fscreen.exitFullscreen();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fscreen.requestFullscreen(document.documentElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFullscreenChange = () =>
|
||||||
|
{
|
||||||
|
this.setState({
|
||||||
|
fullscreen : fscreen.fullscreenElement !== null
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
if (fscreen.fullscreenEnabled)
|
||||||
|
{
|
||||||
|
fscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount()
|
||||||
|
{
|
||||||
|
if (fscreen.fullscreenEnabled)
|
||||||
|
{
|
||||||
|
fscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
toolbarsVisible, me, screenProducer, onLogin, onShareScreen,
|
||||||
|
onUnShareScreen, onNeedExtension, onLeaveMeeting, onLogout, onToggleHand
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let screenState;
|
||||||
|
let screenTip;
|
||||||
|
|
||||||
|
if (me.needExtension)
|
||||||
|
{
|
||||||
|
screenState = 'need-extension';
|
||||||
|
screenTip = 'Install screen sharing extension';
|
||||||
|
}
|
||||||
|
else if (!me.canShareScreen)
|
||||||
|
{
|
||||||
|
screenState = 'unsupported';
|
||||||
|
screenTip = 'Screen sharing not supported';
|
||||||
|
}
|
||||||
|
else if (screenProducer)
|
||||||
|
{
|
||||||
|
screenState = 'on';
|
||||||
|
screenTip = 'Stop screen sharing';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
screenState = 'off';
|
||||||
|
screenTip = 'Start screen sharing';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames('sidebar room-controls', {
|
||||||
|
'visible' : toolbarsVisible
|
||||||
|
})}
|
||||||
|
data-component='Sidebar'
|
||||||
|
>
|
||||||
|
{fscreen.fullscreenEnabled && (
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'fullscreen', {
|
||||||
|
on : this.state.fullscreen
|
||||||
|
})}
|
||||||
|
onClick={this.handleToggleFullscreen}
|
||||||
|
data-tip='Fullscreen'
|
||||||
|
data-type='dark'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'screen', screenState)}
|
||||||
|
data-tip={screenTip}
|
||||||
|
data-type='dark'
|
||||||
|
onClick={() =>
|
||||||
|
{
|
||||||
|
switch (screenState)
|
||||||
|
{
|
||||||
|
case 'on':
|
||||||
|
{
|
||||||
|
onUnShareScreen();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'off':
|
||||||
|
{
|
||||||
|
onShareScreen();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'need-extension':
|
||||||
|
{
|
||||||
|
onNeedExtension();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{me.loginEnabled && (me.loggedIn ? (
|
||||||
|
<div
|
||||||
|
className='button logout'
|
||||||
|
data-tip='Logout'
|
||||||
|
data-type='dark'
|
||||||
|
onClick={onLogout}
|
||||||
|
>
|
||||||
|
<img src={me.picture || 'resources/images/avatar-empty.jpeg'} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className='button login off'
|
||||||
|
data-tip='Login'
|
||||||
|
data-type='dark'
|
||||||
|
onClick={onLogin}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'raise-hand', {
|
||||||
|
on : me.raiseHand,
|
||||||
|
disabled : me.raiseHandInProgress
|
||||||
|
})}
|
||||||
|
data-tip='Raise hand'
|
||||||
|
data-type='dark'
|
||||||
|
onClick={() => onToggleHand(!me.raiseHand)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'leave-meeting')}
|
||||||
|
data-tip='Leave meeting'
|
||||||
|
data-type='dark'
|
||||||
|
onClick={() => onLeaveMeeting()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sidebar.propTypes = {
|
||||||
|
toolbarsVisible : PropTypes.bool.isRequired,
|
||||||
|
me : appPropTypes.Me.isRequired,
|
||||||
|
onShareScreen : PropTypes.func.isRequired,
|
||||||
|
onUnShareScreen : PropTypes.func.isRequired,
|
||||||
|
onNeedExtension : PropTypes.func.isRequired,
|
||||||
|
onToggleHand : PropTypes.func.isRequired,
|
||||||
|
onLeaveMeeting : PropTypes.func.isRequired,
|
||||||
|
onLogin : PropTypes.func.isRequired,
|
||||||
|
onLogout : PropTypes.func.isRequired,
|
||||||
|
screenProducer : appPropTypes.Producer
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
({
|
||||||
|
toolbarsVisible : state.room.toolbarsVisible,
|
||||||
|
screenProducer : Object.values(state.producers)
|
||||||
|
.find((producer) => producer.source === 'screen'),
|
||||||
|
me : state.me
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
onLeaveMeeting : requestActions.leaveRoom,
|
||||||
|
onShareScreen : requestActions.enableScreenSharing,
|
||||||
|
onUnShareScreen : requestActions.disableScreenSharing,
|
||||||
|
onNeedExtension : requestActions.installExtension,
|
||||||
|
onToggleHand : requestActions.toggleHand,
|
||||||
|
onLogin : requestActions.userLogin,
|
||||||
|
onLogout : requestActions.userLogout
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Sidebar);
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as stateActions from '../../redux/stateActions';
|
import * as toolTabActions from '../../redux/stateActions';
|
||||||
import ParticipantList from '../ParticipantList/ParticipantList';
|
import ParticipantList from '../ParticipantList/ParticipantList';
|
||||||
import Chat from '../Chat/Chat';
|
import Chat from '../Chat/Chat';
|
||||||
import Settings from '../Settings';
|
import Settings from '../Settings';
|
||||||
|
import FileSharing from '../FileSharing';
|
||||||
|
|
||||||
class ToolArea extends React.Component
|
class ToolArea extends React.Component
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +17,9 @@ class ToolArea extends React.Component
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
toolarea,
|
currentToolTab,
|
||||||
|
unreadMessages,
|
||||||
|
unreadFiles,
|
||||||
setToolTab
|
setToolTab
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|
@ -31,14 +34,39 @@ class ToolArea extends React.Component
|
||||||
{
|
{
|
||||||
setToolTab('chat');
|
setToolTab('chat');
|
||||||
}}
|
}}
|
||||||
checked={toolarea.currentToolTab === 'chat'}
|
checked={currentToolTab === 'chat'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='tab-chat'>Chat</label>
|
<label htmlFor='tab-chat'>
|
||||||
|
Chat
|
||||||
|
|
||||||
|
{unreadMessages > 0 && (
|
||||||
|
<span className='badge'>{unreadMessages}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
<div className='tab'>
|
<div className='tab'>
|
||||||
<Chat />
|
<Chat />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='tabs'
|
||||||
|
id='tab-files'
|
||||||
|
onChange={() => setToolTab('files')}
|
||||||
|
checked={currentToolTab === 'files'}
|
||||||
|
/>
|
||||||
|
<label htmlFor='tab-files'>
|
||||||
|
Files
|
||||||
|
|
||||||
|
{unreadFiles > 0 && (
|
||||||
|
<span className='badge'>{unreadFiles}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='tab'>
|
||||||
|
<FileSharing />
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type='radio'
|
type='radio'
|
||||||
name='tabs'
|
name='tabs'
|
||||||
|
|
@ -47,7 +75,7 @@ class ToolArea extends React.Component
|
||||||
{
|
{
|
||||||
setToolTab('users');
|
setToolTab('users');
|
||||||
}}
|
}}
|
||||||
checked={toolarea.currentToolTab === 'users'}
|
checked={currentToolTab === 'users'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='tab-users'>Users</label>
|
<label htmlFor='tab-users'>Users</label>
|
||||||
|
|
||||||
|
|
@ -63,7 +91,7 @@ class ToolArea extends React.Component
|
||||||
{
|
{
|
||||||
setToolTab('settings');
|
setToolTab('settings');
|
||||||
}}
|
}}
|
||||||
checked={toolarea.currentToolTab === 'settings'}
|
checked={currentToolTab === 'settings'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='tab-settings'>Settings</label>
|
<label htmlFor='tab-settings'>Settings</label>
|
||||||
|
|
||||||
|
|
@ -79,25 +107,20 @@ class ToolArea extends React.Component
|
||||||
ToolArea.propTypes =
|
ToolArea.propTypes =
|
||||||
{
|
{
|
||||||
advancedMode : PropTypes.bool,
|
advancedMode : PropTypes.bool,
|
||||||
toolarea : PropTypes.object.isRequired,
|
currentToolTab : PropTypes.string.isRequired,
|
||||||
setToolTab : PropTypes.func.isRequired
|
setToolTab : PropTypes.func.isRequired,
|
||||||
|
unreadMessages : PropTypes.number.isRequired,
|
||||||
|
unreadFiles : PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) => ({
|
||||||
{
|
currentToolTab : state.toolarea.currentToolTab,
|
||||||
return {
|
unreadMessages : state.toolarea.unreadMessages,
|
||||||
toolarea : state.toolarea
|
unreadFiles : state.toolarea.unreadFiles
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
const mapDispatchToProps = {
|
||||||
{
|
setToolTab : toolTabActions.setToolTab
|
||||||
return {
|
|
||||||
setToolTab : (toolTab) =>
|
|
||||||
{
|
|
||||||
dispatch(stateActions.setToolTab(toolTab));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToolAreaContainer = connect(
|
const ToolAreaContainer = connect(
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,28 @@ class ToolAreaButton extends React.Component
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
toolAreaOpen,
|
toolAreaOpen,
|
||||||
toggleToolArea
|
toggleToolArea,
|
||||||
|
unread
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component='ToolAreaButton'>
|
<div data-component='ToolAreaButton' className={classnames({ on: toolAreaOpen })}>
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'toolarea-button', {
|
className={classnames('button toolarea-button room-controls', {
|
||||||
on : toolAreaOpen
|
on : toolAreaOpen,
|
||||||
|
visible : this.props.visible
|
||||||
})}
|
})}
|
||||||
data-tip='Toggle tool area'
|
data-tip='Toggle tool area'
|
||||||
data-type='dark'
|
data-type='dark'
|
||||||
data-for='globaltip'
|
data-for='globaltip'
|
||||||
onClick={() => toggleToolArea()}
|
onClick={() => toggleToolArea()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!toolAreaOpen && unread > 0 && (
|
||||||
|
<span className={classnames('badge', { long: unread >= 10 })}>
|
||||||
|
{unread}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -32,13 +40,17 @@ class ToolAreaButton extends React.Component
|
||||||
ToolAreaButton.propTypes =
|
ToolAreaButton.propTypes =
|
||||||
{
|
{
|
||||||
toolAreaOpen : PropTypes.bool.isRequired,
|
toolAreaOpen : PropTypes.bool.isRequired,
|
||||||
toggleToolArea : PropTypes.func.isRequired
|
toggleToolArea : PropTypes.func.isRequired,
|
||||||
|
unread : PropTypes.number.isRequired,
|
||||||
|
visible : PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
toolAreaOpen : state.toolarea.toolAreaOpen
|
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||||
|
visible : state.room.toolbarsVisible,
|
||||||
|
unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,6 @@ import UrlParse from 'url-parse';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import {
|
|
||||||
applyMiddleware as applyReduxMiddleware,
|
|
||||||
createStore as createReduxStore,
|
|
||||||
compose as composeRedux
|
|
||||||
} from 'redux';
|
|
||||||
import thunk from 'redux-thunk';
|
|
||||||
import { createLogger as createReduxLogger } from 'redux-logger';
|
|
||||||
import { getDeviceInfo } from 'mediasoup-client';
|
import { getDeviceInfo } from 'mediasoup-client';
|
||||||
import randomString from 'random-string';
|
import randomString from 'random-string';
|
||||||
import Logger from './Logger';
|
import Logger from './Logger';
|
||||||
|
|
@ -17,48 +10,11 @@ import * as utils from './utils';
|
||||||
import * as cookiesManager from './cookiesManager';
|
import * as cookiesManager from './cookiesManager';
|
||||||
import * as requestActions from './redux/requestActions';
|
import * as requestActions from './redux/requestActions';
|
||||||
import * as stateActions from './redux/stateActions';
|
import * as stateActions from './redux/stateActions';
|
||||||
import reducers from './redux/reducers';
|
|
||||||
import roomClientMiddleware from './redux/roomClientMiddleware';
|
|
||||||
import Room from './components/Room';
|
import Room from './components/Room';
|
||||||
import { loginEnabled } from '../config';
|
import { loginEnabled } from '../config';
|
||||||
|
import { store } from './store';
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
const reduxMiddlewares =
|
|
||||||
[
|
|
||||||
thunk,
|
|
||||||
roomClientMiddleware
|
|
||||||
];
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development')
|
|
||||||
{
|
|
||||||
const reduxLogger = createReduxLogger(
|
|
||||||
{
|
|
||||||
duration : true,
|
|
||||||
timestamp : false,
|
|
||||||
level : 'log',
|
|
||||||
logErrors : true
|
|
||||||
});
|
|
||||||
|
|
||||||
reduxMiddlewares.push(reduxLogger);
|
|
||||||
}
|
|
||||||
|
|
||||||
const composeEnhancers =
|
|
||||||
typeof window === 'object' &&
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
|
||||||
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
|
||||||
}) : composeRedux;
|
|
||||||
|
|
||||||
const enhancer = composeEnhancers(
|
|
||||||
applyReduxMiddleware(...reduxMiddlewares)
|
|
||||||
// other store enhancers if any
|
|
||||||
);
|
|
||||||
|
|
||||||
const store = createReduxStore(
|
|
||||||
reducers,
|
|
||||||
undefined,
|
|
||||||
enhancer
|
|
||||||
);
|
|
||||||
|
|
||||||
domready(() =>
|
domready(() =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ import
|
||||||
createNewMessage
|
createNewMessage
|
||||||
} from './helper';
|
} from './helper';
|
||||||
|
|
||||||
const initialState = [];
|
const chatmessages = (state = [], action) =>
|
||||||
|
|
||||||
const chatmessages = (state = initialState, action) =>
|
|
||||||
{
|
{
|
||||||
switch (action.type)
|
switch (action.type)
|
||||||
{
|
{
|
||||||
|
|
@ -13,7 +11,7 @@ const chatmessages = (state = initialState, action) =>
|
||||||
{
|
{
|
||||||
const { text } = action.payload;
|
const { text } = action.payload;
|
||||||
|
|
||||||
const message = createNewMessage(text, 'client', 'Me');
|
const message = createNewMessage(text, 'client', 'Me', undefined);
|
||||||
|
|
||||||
return [ ...state, message ];
|
return [ ...state, message ];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,15 @@ const consumers = (state = initialState, action) =>
|
||||||
return { ...state, [consumerId]: newConsumer };
|
return { ...state, [consumerId]: newConsumer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_CONSUMER_VOLUME':
|
||||||
|
{
|
||||||
|
const { consumerId, volume } = action.payload;
|
||||||
|
const consumer = state[consumerId];
|
||||||
|
const newConsumer = { ...consumer, volume };
|
||||||
|
|
||||||
|
return { ...state, [consumerId]: newConsumer };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_CONSUMER_RESUMED':
|
case 'SET_CONSUMER_RESUMED':
|
||||||
{
|
{
|
||||||
const { consumerId, originator } = action.payload;
|
const { consumerId, originator } = action.payload;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
export function createNewMessage(text, sender, name)
|
export function createNewMessage(text, sender, name, picture)
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
type : 'message',
|
type : 'message',
|
||||||
text,
|
text,
|
||||||
time : Date.now(),
|
time : Date.now(),
|
||||||
name,
|
name,
|
||||||
sender
|
sender,
|
||||||
|
picture
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import notifications from './notifications';
|
||||||
import chatmessages from './chatmessages';
|
import chatmessages from './chatmessages';
|
||||||
import chatbehavior from './chatbehavior';
|
import chatbehavior from './chatbehavior';
|
||||||
import toolarea from './toolarea';
|
import toolarea from './toolarea';
|
||||||
|
import sharing from './sharing';
|
||||||
|
|
||||||
const reducers = combineReducers(
|
const reducers = combineReducers(
|
||||||
{
|
{
|
||||||
|
|
@ -19,7 +20,8 @@ const reducers = combineReducers(
|
||||||
notifications,
|
notifications,
|
||||||
chatmessages,
|
chatmessages,
|
||||||
chatbehavior,
|
chatbehavior,
|
||||||
toolarea
|
toolarea,
|
||||||
|
sharing
|
||||||
});
|
});
|
||||||
|
|
||||||
export default reducers;
|
export default reducers;
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,16 @@ const initialState =
|
||||||
webcamInProgress : false,
|
webcamInProgress : false,
|
||||||
audioInProgress : false,
|
audioInProgress : false,
|
||||||
screenShareInProgress : false,
|
screenShareInProgress : false,
|
||||||
loginInProgress : false,
|
|
||||||
loginEnabled : false,
|
loginEnabled : false,
|
||||||
audioOnly : false,
|
audioOnly : false,
|
||||||
audioOnlyInProgress : false,
|
audioOnlyInProgress : false,
|
||||||
raiseHand : false,
|
raiseHand : false,
|
||||||
raiseHandInProgress : false,
|
raiseHandInProgress : false,
|
||||||
restartIceInProgress : false
|
restartIceInProgress : false,
|
||||||
|
picture : null,
|
||||||
|
selectedWebcam : null,
|
||||||
|
selectedAudioDevice : null,
|
||||||
|
loggedIn : false
|
||||||
};
|
};
|
||||||
|
|
||||||
const me = (state = initialState, action) =>
|
const me = (state = initialState, action) =>
|
||||||
|
|
@ -48,6 +51,22 @@ const me = (state = initialState, action) =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'LOGGED_IN':
|
||||||
|
return { ...state, loggedIn: true };
|
||||||
|
|
||||||
|
case 'USER_LOGOUT':
|
||||||
|
return { ...state, loggedIn: false };
|
||||||
|
|
||||||
|
case 'CHANGE_WEBCAM':
|
||||||
|
{
|
||||||
|
return { ...state, selectedWebcam: action.payload.deviceId };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CHANGE_AUDIO_DEVICE':
|
||||||
|
{
|
||||||
|
return { ...state, selectedAudioDevice: action.payload.deviceId };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_MEDIA_CAPABILITIES':
|
case 'SET_MEDIA_CAPABILITIES':
|
||||||
{
|
{
|
||||||
const { canSendMic, canSendWebcam } = action.payload;
|
const { canSendMic, canSendWebcam } = action.payload;
|
||||||
|
|
@ -111,13 +130,6 @@ const me = (state = initialState, action) =>
|
||||||
return { ...state, screenShareInProgress: flag };
|
return { ...state, screenShareInProgress: flag };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_LOGIN_IN_PROGRESS':
|
|
||||||
{
|
|
||||||
const { flag } = action.payload;
|
|
||||||
|
|
||||||
return { ...state, loginInProgress: flag };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_DISPLAY_NAME':
|
case 'SET_DISPLAY_NAME':
|
||||||
{
|
{
|
||||||
let { displayName } = action.payload;
|
let { displayName } = action.payload;
|
||||||
|
|
@ -164,6 +176,11 @@ const me = (state = initialState, action) =>
|
||||||
return { ...state, restartIceInProgress: flag };
|
return { ...state, restartIceInProgress: flag };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_PICTURE':
|
||||||
|
{
|
||||||
|
return { ...state, picture: action.payload.picture };
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,126 +1,93 @@
|
||||||
const initialState = {};
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
const peers = (state = initialState, action) =>
|
const peer = (state = {}, action) =>
|
||||||
|
{
|
||||||
|
switch (action.type)
|
||||||
|
{
|
||||||
|
case 'ADD_PEER':
|
||||||
|
return action.payload.peer;
|
||||||
|
|
||||||
|
case 'SET_PEER_DISPLAY_NAME':
|
||||||
|
return { ...state, displayName: action.payload.displayName };
|
||||||
|
|
||||||
|
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||||
|
return { ...state, peerVideoInProgress: action.payload.flag };
|
||||||
|
|
||||||
|
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||||
|
return { ...state, peerAudioInProgress: action.payload.flag };
|
||||||
|
|
||||||
|
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||||
|
return { ...state, peerScreenInProgress: action.payload.flag };
|
||||||
|
|
||||||
|
case 'SET_PEER_RAISE_HAND_STATE':
|
||||||
|
return { ...state, raiseHandState: action.payload.raiseHandState };
|
||||||
|
|
||||||
|
case 'ADD_CONSUMER':
|
||||||
|
{
|
||||||
|
const consumers = [ ...state.consumers, action.payload.consumer.id ];
|
||||||
|
|
||||||
|
return { ...state, consumers };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'REMOVE_CONSUMER':
|
||||||
|
{
|
||||||
|
const consumers = state.consumers.filter((consumer) =>
|
||||||
|
consumer !== action.payload.consumerId);
|
||||||
|
|
||||||
|
return { ...state, consumers };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_PEER_PICTURE':
|
||||||
|
{
|
||||||
|
return { ...state, picture: action.payload.picture };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const peers = (state = {}, action) =>
|
||||||
{
|
{
|
||||||
switch (action.type)
|
switch (action.type)
|
||||||
{
|
{
|
||||||
case 'ADD_PEER':
|
case 'ADD_PEER':
|
||||||
{
|
{
|
||||||
const { peer } = action.payload;
|
return { ...state, [action.payload.peer.name]: peer(undefined, action) };
|
||||||
|
|
||||||
return { ...state, [peer.name]: peer };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'REMOVE_PEER':
|
case 'REMOVE_PEER':
|
||||||
{
|
{
|
||||||
const { peerName } = action.payload;
|
return omit(state, [ action.payload.peerName ]);
|
||||||
const newState = { ...state };
|
|
||||||
|
|
||||||
delete newState[peerName];
|
|
||||||
|
|
||||||
return newState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_PEER_DISPLAY_NAME':
|
case 'SET_PEER_DISPLAY_NAME':
|
||||||
{
|
|
||||||
const { displayName, peerName } = action.payload;
|
|
||||||
const peer = state[peerName];
|
|
||||||
|
|
||||||
if (!peer)
|
|
||||||
throw new Error('no Peer found');
|
|
||||||
|
|
||||||
const newPeer = { ...peer, displayName };
|
|
||||||
|
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||||
{
|
|
||||||
const { peerName, flag } = action.payload;
|
|
||||||
const peer = state[peerName];
|
|
||||||
|
|
||||||
if (!peer)
|
|
||||||
throw new Error('no Peer found');
|
|
||||||
|
|
||||||
const newPeer = { ...peer, peerVideoInProgress: flag };
|
|
||||||
|
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||||
{
|
|
||||||
const { peerName, flag } = action.payload;
|
|
||||||
const peer = state[peerName];
|
|
||||||
|
|
||||||
if (!peer)
|
|
||||||
throw new Error('no Peer found');
|
|
||||||
|
|
||||||
const newPeer = { ...peer, peerAudioInProgress: flag };
|
|
||||||
|
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||||
{
|
|
||||||
const { peerName, flag } = action.payload;
|
|
||||||
const peer = state[peerName];
|
|
||||||
|
|
||||||
if (!peer)
|
|
||||||
throw new Error('no Peer found');
|
|
||||||
|
|
||||||
const newPeer = { ...peer, peerScreenInProgress: flag };
|
|
||||||
|
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_PEER_RAISE_HAND_STATE':
|
case 'SET_PEER_RAISE_HAND_STATE':
|
||||||
{
|
case 'SET_PEER_PICTURE':
|
||||||
const { peerName, raiseHandState } = action.payload;
|
|
||||||
const peer = state[peerName];
|
|
||||||
|
|
||||||
if (!peer)
|
|
||||||
throw new Error('no Peer found');
|
|
||||||
|
|
||||||
const newPeer = { ...peer, raiseHandState };
|
|
||||||
|
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'ADD_CONSUMER':
|
case 'ADD_CONSUMER':
|
||||||
{
|
{
|
||||||
const { consumer, peerName } = action.payload;
|
const oldPeer = state[action.payload.peerName];
|
||||||
const peer = state[peerName];
|
|
||||||
|
|
||||||
if (!peer)
|
if (!oldPeer)
|
||||||
throw new Error('no Peer found for new Consumer');
|
{
|
||||||
|
throw new Error('no Peer found');
|
||||||
|
}
|
||||||
|
|
||||||
const newConsumers = [ ...peer.consumers, consumer.id ];
|
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||||
const newPeer = { ...peer, consumers: newConsumers };
|
|
||||||
|
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'REMOVE_CONSUMER':
|
case 'REMOVE_CONSUMER':
|
||||||
{
|
{
|
||||||
const { consumerId, peerName } = action.payload;
|
const oldPeer = state[action.payload.peerName];
|
||||||
const peer = state[peerName];
|
|
||||||
|
|
||||||
// NOTE: This means that the Peer was closed before, so it's ok.
|
// NOTE: This means that the Peer was closed before, so it's ok.
|
||||||
if (!peer)
|
if (!oldPeer)
|
||||||
return state;
|
return state;
|
||||||
|
|
||||||
const idx = peer.consumers.indexOf(consumerId);
|
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||||
|
|
||||||
if (idx === -1)
|
|
||||||
throw new Error('Consumer not found');
|
|
||||||
|
|
||||||
const newConsumers = peer.consumers.slice();
|
|
||||||
|
|
||||||
newConsumers.splice(idx, 1);
|
|
||||||
|
|
||||||
const newPeer = { ...peer, consumers: newConsumers };
|
|
||||||
|
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,15 @@ const producers = (state = initialState, action) =>
|
||||||
return { ...state, [producerId]: newProducer };
|
return { ...state, [producerId]: newProducer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_PRODUCER_VOLUME':
|
||||||
|
{
|
||||||
|
const { producerId, volume } = action.payload;
|
||||||
|
const producer = state[producerId];
|
||||||
|
const newProducer = { ...producer, volume };
|
||||||
|
|
||||||
|
return { ...state, [producerId]: newProducer };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_PRODUCER_RESUMED':
|
case 'SET_PRODUCER_RESUMED':
|
||||||
{
|
{
|
||||||
const { producerId, originator } = action.payload;
|
const { producerId, originator } = action.payload;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ const initialState =
|
||||||
activeSpeakerName : null,
|
activeSpeakerName : null,
|
||||||
showSettings : false,
|
showSettings : false,
|
||||||
advancedMode : false,
|
advancedMode : false,
|
||||||
fullScreenConsumer : null // ConsumerID
|
fullScreenConsumer : null, // ConsumerID
|
||||||
|
toolbarsVisible : true,
|
||||||
|
mode : 'democratic',
|
||||||
|
selectedPeerName : null
|
||||||
};
|
};
|
||||||
|
|
||||||
const room = (state = initialState, action) =>
|
const room = (state = initialState, action) =>
|
||||||
|
|
@ -58,6 +61,28 @@ const room = (state = initialState, action) =>
|
||||||
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
|
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_TOOLBARS_VISIBLE':
|
||||||
|
{
|
||||||
|
const { toolbarsVisible } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, toolbarsVisible };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_DISPLAY_MODE':
|
||||||
|
return { ...state, mode: action.payload.mode };
|
||||||
|
|
||||||
|
case 'SET_SELECTED_PEER':
|
||||||
|
{
|
||||||
|
const { selectedPeerName } = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
|
||||||
|
selectedPeerName : state.selectedPeerName === selectedPeerName ?
|
||||||
|
null : selectedPeerName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
const sharing = (state = [], action) =>
|
||||||
|
{
|
||||||
|
switch (action.type)
|
||||||
|
{
|
||||||
|
case 'SEND_FILE':
|
||||||
|
return [ ...state, { ...action.payload, me: true } ];
|
||||||
|
|
||||||
|
case 'ADD_FILE':
|
||||||
|
return [ ...state, action.payload ];
|
||||||
|
|
||||||
|
case 'ADD_FILE_HISTORY':
|
||||||
|
return [ ...action.payload.fileHistory, ...state ];
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sharing;
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
const initialState =
|
const initialState =
|
||||||
{
|
{
|
||||||
toolAreaOpen : false,
|
toolAreaOpen : false,
|
||||||
currentToolTab : 'chat' // chat, settings, users
|
currentToolTab : 'chat', // chat, settings, users
|
||||||
|
unreadMessages : 0,
|
||||||
|
unreadFiles : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolarea = (state = initialState, action) =>
|
const toolarea = (state = initialState, action) =>
|
||||||
|
|
@ -11,15 +13,39 @@ const toolarea = (state = initialState, action) =>
|
||||||
case 'TOGGLE_TOOL_AREA':
|
case 'TOGGLE_TOOL_AREA':
|
||||||
{
|
{
|
||||||
const toolAreaOpen = !state.toolAreaOpen;
|
const toolAreaOpen = !state.toolAreaOpen;
|
||||||
|
const unreadMessages = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||||
|
const unreadFiles = toolAreaOpen && state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||||
|
|
||||||
return { ...state, toolAreaOpen };
|
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_TOOL_TAB':
|
case 'SET_TOOL_TAB':
|
||||||
{
|
{
|
||||||
const { toolTab } = action.payload;
|
const { toolTab } = action.payload;
|
||||||
|
const unreadMessages = toolTab === 'chat' ? 0 : state.unreadMessages;
|
||||||
|
const unreadFiles = toolTab === 'files' ? 0 : state.unreadFiles;
|
||||||
|
|
||||||
return { ...state, currentToolTab: toolTab };
|
return { ...state, currentToolTab: toolTab, unreadMessages, unreadFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_NEW_RESPONSE_MESSAGE':
|
||||||
|
{
|
||||||
|
if (state.toolAreaOpen && state.currentToolTab === 'chat')
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, unreadMessages: state.unreadMessages + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_FILE':
|
||||||
|
{
|
||||||
|
if (state.toolAreaOpen && state.currentToolTab === 'files')
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, unreadFiles: state.unreadFiles + 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,13 @@ export const userLogin = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const userLogout = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'USER_LOGOUT'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const raiseHand = () =>
|
export const raiseHand = () =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -184,9 +191,21 @@ export const installExtension = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendChatMessage = (text, name) =>
|
export const toggleHand = (enable) =>
|
||||||
{
|
{
|
||||||
const message = createNewMessage(text, 'response', name);
|
if (enable)
|
||||||
|
return {
|
||||||
|
type : 'RAISE_HAND'
|
||||||
|
};
|
||||||
|
else
|
||||||
|
return {
|
||||||
|
type : 'LOWER_HAND'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendChatMessage = (text, name, picture) =>
|
||||||
|
{
|
||||||
|
const message = createNewMessage(text, 'response', name, picture);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type : 'SEND_CHAT_MESSAGE',
|
type : 'SEND_CHAT_MESSAGE',
|
||||||
|
|
@ -194,6 +213,14 @@ export const sendChatMessage = (text, name) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendFile = (file, name, picture) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SEND_FILE',
|
||||||
|
payload : { file, name, picture }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// This returns a redux-thunk action (a function).
|
// This returns a redux-thunk action (a function).
|
||||||
export const notify = ({ type = 'info', text, timeout }) =>
|
export const notify = ({ type = 'info', text, timeout }) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,13 @@ export default ({ dispatch, getState }) => (next) =>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'USER_LOGOUT':
|
||||||
|
{
|
||||||
|
client.logout();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'LOWER_HAND':
|
case 'LOWER_HAND':
|
||||||
{
|
{
|
||||||
client.sendRaiseHandState(false);
|
client.sendRaiseHandState(false);
|
||||||
|
|
@ -224,6 +231,12 @@ export default ({ dispatch, getState }) => (next) =>
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SEND_FILE':
|
||||||
|
{
|
||||||
|
client.sendFile(action.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,12 @@ export const toggleAdvancedMode = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setDisplayMode = (mode) =>
|
||||||
|
({
|
||||||
|
type : 'SET_DISPLAY_MODE',
|
||||||
|
payload : { mode }
|
||||||
|
});
|
||||||
|
|
||||||
export const setAudioOnlyState = (enabled) =>
|
export const setAudioOnlyState = (enabled) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -141,14 +147,6 @@ export const setMyRaiseHandState = (flag) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setLoginInProgress = (flag) =>
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
type : 'SET_LOGIN_IN_PROGRESS',
|
|
||||||
payload : { flag }
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toggleSettings = () =>
|
export const toggleSettings = () =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -331,6 +329,22 @@ export const setConsumerTrack = (consumerId, track) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setConsumerVolume = (consumerId, volume) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_CONSUMER_VOLUME',
|
||||||
|
payload : { consumerId, volume }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setProducerVolume = (producerId, volume) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_PRODUCER_VOLUME',
|
||||||
|
payload : { producerId, volume }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const addNotification = (notification) =>
|
export const addNotification = (notification) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -369,6 +383,11 @@ export const toggleConsumerFullscreen = (consumerId) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setToolbarsVisible = (toolbarsVisible) => ({
|
||||||
|
type : 'SET_TOOLBARS_VISIBLE',
|
||||||
|
payload : { toolbarsVisible }
|
||||||
|
});
|
||||||
|
|
||||||
export const increaseBadge = () =>
|
export const increaseBadge = () =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -391,6 +410,14 @@ export const addUserMessage = (text) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addUserFile = (file) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'ADD_NEW_USER_FILE',
|
||||||
|
payload : { file }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const addResponseMessage = (message) =>
|
export const addResponseMessage = (message) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -413,3 +440,41 @@ export const dropMessages = () =>
|
||||||
type : 'DROP_MESSAGES'
|
type : 'DROP_MESSAGES'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addFile = (payload) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'ADD_FILE',
|
||||||
|
payload
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addFileHistory = (fileHistory) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'ADD_FILE_HISTORY',
|
||||||
|
payload : { fileHistory }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setPicture = (picture) =>
|
||||||
|
({
|
||||||
|
type : 'SET_PICTURE',
|
||||||
|
payload : { picture }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setPeerPicture = (peerName, picture) =>
|
||||||
|
({
|
||||||
|
type : 'SET_PEER_PICTURE',
|
||||||
|
payload : { peerName, picture }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loggedIn = () =>
|
||||||
|
({
|
||||||
|
type : 'LOGGED_IN'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSelectedPeer = (selectedPeerName) => ({
|
||||||
|
type : 'SET_SELECTED_PEER',
|
||||||
|
payload : { selectedPeerName }
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {
|
||||||
|
applyMiddleware,
|
||||||
|
createStore,
|
||||||
|
compose
|
||||||
|
} from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { createLogger } from 'redux-logger';
|
||||||
|
import reducers from './redux/reducers';
|
||||||
|
import roomClientMiddleware from './redux/roomClientMiddleware';
|
||||||
|
|
||||||
|
const reduxMiddlewares =
|
||||||
|
[
|
||||||
|
thunk,
|
||||||
|
roomClientMiddleware
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development')
|
||||||
|
{
|
||||||
|
const reduxLogger = createLogger(
|
||||||
|
{
|
||||||
|
duration : true,
|
||||||
|
timestamp : false,
|
||||||
|
level : 'log',
|
||||||
|
logErrors : true
|
||||||
|
});
|
||||||
|
|
||||||
|
reduxMiddlewares.push(reduxLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeEnhancers =
|
||||||
|
typeof window === 'object' &&
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
||||||
|
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
||||||
|
}) : compose;
|
||||||
|
|
||||||
|
const enhancer = composeEnhancers(
|
||||||
|
applyMiddleware(...reduxMiddlewares)
|
||||||
|
// other store enhancers if any
|
||||||
|
);
|
||||||
|
|
||||||
|
export const store = createStore(
|
||||||
|
reducers,
|
||||||
|
undefined,
|
||||||
|
enhancer
|
||||||
|
);
|
||||||
|
|
@ -43,3 +43,23 @@ export function getBrowserType()
|
||||||
|
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a function which will call the callback function
|
||||||
|
* after the given amount of milliseconds has passed since
|
||||||
|
* the last time the callback function was called.
|
||||||
|
*/
|
||||||
|
export const idle = (callback, delay) =>
|
||||||
|
{
|
||||||
|
let handle;
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
if (handle)
|
||||||
|
{
|
||||||
|
clearTimeout(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = setTimeout(callback, delay);
|
||||||
|
};
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,65 +8,72 @@
|
||||||
"main": "lib/index.jsx",
|
"main": "lib/index.jsx",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.6",
|
||||||
|
"create-torrent": "^3.32.1",
|
||||||
"debug": "^3.1.0",
|
"debug": "^3.1.0",
|
||||||
"domready": "^1.0.8",
|
"domready": "^1.0.8",
|
||||||
"hark": "^1.1.6",
|
"drag-drop": "^4.2.0",
|
||||||
|
"file-saver": "^1.3.8",
|
||||||
|
"fscreen": "^1.0.2",
|
||||||
|
"hark": "^1.2.2",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"marked": "^0.3.17",
|
"magnet-uri": "^5.2.3",
|
||||||
|
"marked": "^0.4.0",
|
||||||
"mediasoup-client": "^2.1.1",
|
"mediasoup-client": "^2.1.1",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.2",
|
||||||
"protoo-client": "^2.0.7",
|
"protoo-client": "^3.0.0",
|
||||||
"random-string": "^0.2.0",
|
"random-string": "^0.2.0",
|
||||||
"react": "^16.2.0",
|
"react": "^16.4.1",
|
||||||
"react-clipboard.js": "^1.1.3",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "^16.4.1",
|
||||||
"react-draggable": "^3.0.5",
|
"react-draggable": "^3.0.5",
|
||||||
"react-dropdown": "^1.5.0",
|
"react-dropdown": "^1.5.0",
|
||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.0.7",
|
||||||
"react-spinner": "^0.2.7",
|
"react-spinner": "^0.2.7",
|
||||||
"react-tooltip": "^3.4.0",
|
"react-tooltip": "^3.6.1",
|
||||||
"react-transition-group": "^2.2.1",
|
"react-transition-group": "^2.4.0",
|
||||||
"redux": "^3.7.2",
|
"redux": "^4.0.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.3.0",
|
||||||
|
"resize-observer-polyfill": "^1.5.0",
|
||||||
"riek": "^1.1.0",
|
"riek": "^1.1.0",
|
||||||
"url-parse": "^1.2.0"
|
"url-parse": "^1.4.1",
|
||||||
|
"webtorrent": "^0.101.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.3",
|
||||||
"babel-plugin-transform-object-assign": "^6.22.0",
|
"babel-eslint": "^8.2.6",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"babel-preset-env": "^1.7.0",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react-app": "^3.1.2",
|
||||||
"babel-preset-stage-0": "^6.24.1",
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
"babelify": "^8.0.0",
|
"babelify": "^8.0.0",
|
||||||
"browser-sync": "^2.23.6",
|
"browser-sync": "^2.24.6",
|
||||||
"browserify": "^16.1.0",
|
"browserify": "^16.2.2",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"envify": "^4.1.0",
|
"envify": "^4.1.0",
|
||||||
"eslint": "^4.17.0",
|
"eslint": "^5.2.0",
|
||||||
"eslint-plugin-import": "^2.8.0",
|
"eslint-plugin-import": "^2.13.0",
|
||||||
"eslint-plugin-react": "^7.6.1",
|
"eslint-plugin-react": "^7.10.0",
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
"gulp-css-base64": "^1.3.4",
|
|
||||||
"gulp-eslint": "^4.0.2",
|
|
||||||
"gulp-change": "^1.0.0",
|
"gulp-change": "^1.0.0",
|
||||||
"gulp-header": "^2.0.1",
|
"gulp-css-base64": "^1.3.4",
|
||||||
|
"gulp-eslint": "^5.0.0",
|
||||||
|
"gulp-header": "^2.0.5",
|
||||||
"gulp-if": "^2.0.2",
|
"gulp-if": "^2.0.2",
|
||||||
"gulp-plumber": "^1.2.0",
|
"gulp-plumber": "^1.2.0",
|
||||||
"gulp-rename": "^1.2.2",
|
"gulp-rename": "^1.4.0",
|
||||||
"gulp-stylus": "^2.7.0",
|
"gulp-stylus": "^2.7.0",
|
||||||
"gulp-touch-cmd": "0.0.1",
|
"gulp-touch-cmd": "0.0.1",
|
||||||
"gulp-uglify": "^3.0.0",
|
"gulp-uglify": "^3.0.0",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
|
"lodash": "^4.17.10",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"nib": "^1.1.2",
|
"nib": "^1.1.2",
|
||||||
"supports-color": "^5.2.0",
|
"supports-color": "^5.4.0",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"vinyl-source-stream": "^2.0.0",
|
"vinyl-source-stream": "^2.0.0",
|
||||||
"watchify": "^3.10.0"
|
"watchify": "^3.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg fill="#FFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFF">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 239 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFF">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 242 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg fill="#FFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 304 B |
|
|
@ -61,7 +61,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component='MessageList'] {
|
[data-component='MessageList'] {
|
||||||
background-color: rgba(#fff, 0.9);
|
background-color: rgba(#000, 0.1);
|
||||||
height: 91vmin;
|
height: 91vmin;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
|
|
@ -71,43 +71,35 @@
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
color: rgba(#000, 1.0)
|
|
||||||
|
|
||||||
> .client {
|
> .client {
|
||||||
background-color: rgba(#fff, 0.9);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 6px;
|
|
||||||
max-width: 215px;
|
|
||||||
text-align: left;
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
> .message-text {
|
|
||||||
font-size: 1.3vmin;
|
|
||||||
color: rgba(#000, 1.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .message-time {
|
> .client, > .response {
|
||||||
font-size: 1vmin;
|
background-color: rgba(#000, 0.1);
|
||||||
color: rgba(#777, 1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .response {
|
|
||||||
background-color: rgba(#fff, 0.9);
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 6px;
|
|
||||||
max-width: 215px;
|
max-width: 215px;
|
||||||
text-align: left;
|
display: flex;
|
||||||
font-size: 1.3vmin;
|
align-items: center;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
> .message-avatar {
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .message-content {
|
||||||
|
padding-left: 6px;
|
||||||
|
|
||||||
> .message-text {
|
> .message-text {
|
||||||
font-size: 1.3vmin;
|
font-size: 1.3vmin;
|
||||||
color: rgba(#000, 1.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .message-time {
|
> .message-time {
|
||||||
font-size: 1vmin;
|
font-size: 1vmin;
|
||||||
color: rgba(#777, 1.0);
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +108,7 @@
|
||||||
[data-component='Sender'] {
|
[data-component='Sender'] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: rgba(#fff, 0.9);
|
background-color: rgba(#000, 0.1);
|
||||||
height: 6vmin;
|
height: 6vmin;
|
||||||
padding: 0.5vmin;
|
padding: 0.5vmin;
|
||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
|
|
@ -125,8 +117,8 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: rgba(#fff, 0.9);
|
background-color: rgba(#000, 0.1);
|
||||||
color: #000;
|
color: #fff;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
font-size: 1.4vmin;
|
font-size: 1.4vmin;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
[data-component='FileSharing'] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> .sharing-toolbar {
|
||||||
|
> .share-file {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
background: #252525;
|
||||||
|
border: 1px solid #151515;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 5px solid #151515;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .shared-files {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
> .file-entry {
|
||||||
|
background-color: rgba(0,0,0,0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file-avatar {
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
> p:not(:first-child) {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file-info {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #252525;
|
||||||
|
border: 1px solid #151515;
|
||||||
|
padding: 0.3rem;
|
||||||
|
border-bottom: 5px solid #151515;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#holding-overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag #holding-overlay {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #FFF;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
z-index: 3000;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
[data-component='Filmstrip'] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> .active-peer-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .active-peer {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1vmin;
|
||||||
|
|
||||||
|
> [data-component='Peer'] {
|
||||||
|
border: 5px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .filmstrip {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(0, 0, 0 , 0.5);
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
height: 20vh;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .filmstrip-content {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .film {
|
||||||
|
height: 18vh;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 1vh;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-right: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .film-content {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
|
||||||
|
> [data-component='Peer'] {
|
||||||
|
max-width: 18vh * (4 / 3);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.screen {
|
||||||
|
max-width: 18vh * (2 * 4 / 3);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
> .film-content {
|
||||||
|
border-color: #FFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
> .film-content {
|
||||||
|
border-color: #377EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 200;
|
z-index: 2000;
|
||||||
|
|
||||||
> .controls {
|
> .controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 201;
|
z-index: 2020;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
|
|
||||||
.incompatible-video {
|
.incompatible-video {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2
|
z-index: 2010;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,17 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: rgba(#2a4b58, 0.9);
|
background-image: url('/resources/images/background.svg');
|
||||||
background-image: url('/resources/images/buddy.svg');
|
background-attachment: fixed;
|
||||||
background-position: bottom;
|
background-position: center;
|
||||||
background-size: auto 85%;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
> .info {
|
> .info {
|
||||||
$backgroundTint = #000;
|
$backgroundTint = #000;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5
|
z-index: 10;
|
||||||
top: 0.6vmin;
|
top: 0.6vmin;
|
||||||
left: 0.6vmin;
|
left: 0.6vmin;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
> .controls {
|
> .controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 20;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -27,6 +27,12 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.4vmin;
|
padding: 0.4vmin;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
> .button {
|
> .button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
[data-component='Notifications'] {
|
[data-component='Notifications'] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9999;
|
z-index: 1010;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 0;
|
top: 45px;
|
||||||
right: 65px;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
transition: right 0.3s;
|
||||||
|
|
||||||
|
&.toolarea-open {
|
||||||
|
right: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
+desktop() {
|
+desktop() {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,75 @@
|
||||||
|
|
||||||
> .list-item {
|
> .list-item {
|
||||||
padding: 0.5vmin;
|
padding: 0.5vmin;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #CBCBCB;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.me {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-bottom-color: #377EFF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component='ListPeer'] {
|
[data-component='ListPeer'] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .indicators {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction:; row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.4vmin;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0.2vmin;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 75%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: rgba(#000, 0.5);
|
||||||
|
transition-property: opacity, background-color;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
|
||||||
|
+desktop() {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+mobile() {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.raise-hand {
|
||||||
|
background-image: url('/resources/images/icon-hand-white.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
> .controls {
|
> .controls {
|
||||||
float: right;
|
float: right;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -110,23 +171,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
padding: 8px 16px;
|
|
||||||
float: left;
|
|
||||||
width: auto;
|
|
||||||
border: none;
|
|
||||||
display: block;
|
|
||||||
outline: 0;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
vertical-align: middle;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .peer-info {
|
> .peer-info {
|
||||||
font-size: 1.4vmin;
|
font-size: 1.4vmin;
|
||||||
float: left;
|
|
||||||
width: auto;
|
|
||||||
border: none;
|
border: none;
|
||||||
display: block;
|
display: flex;
|
||||||
outline: 0;
|
padding: 1vmin;
|
||||||
padding: 0.6vmin;
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
> .view-container {
|
> .view-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
&.webcam {
|
&.webcam {
|
||||||
order: 2;
|
order: 2;
|
||||||
|
|
@ -29,11 +30,62 @@
|
||||||
|
|
||||||
&.screen {
|
&.screen {
|
||||||
order: 1;
|
order: 1;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
> .indicators {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction:; row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.4vmin;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0.2vmin;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 75%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: rgba(#000, 0.5);
|
||||||
|
transition-property: opacity, background-color;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
|
||||||
|
+desktop() {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+mobile() {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.raise-hand {
|
||||||
|
background-image: url('/resources/images/icon-hand-white.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
> .controls {
|
> .controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 20;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -41,6 +93,12 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.4vmin;
|
padding: 0.4vmin;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
> .button {
|
> .button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
@ -137,7 +195,7 @@
|
||||||
|
|
||||||
.incompatible-video {
|
.incompatible-video {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2
|
z-index: 10;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
$backgroundTint = #000;
|
$backgroundTint = #000;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5
|
z-index: 10;
|
||||||
top: 0.6vmin;
|
top: 0.6vmin;
|
||||||
left: 0.6vmin;
|
left: 0.6vmin;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: width 0.3s;
|
|
||||||
|
|
||||||
> .state {
|
> .state {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -95,7 +94,7 @@
|
||||||
> .room-link-wrapper {
|
> .room-link-wrapper {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 10;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
@ -142,7 +141,7 @@
|
||||||
|
|
||||||
> .me-container {
|
> .me-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 110;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
|
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
|
||||||
transition-property: border-color;
|
transition-property: border-color;
|
||||||
|
|
@ -153,126 +152,44 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
+desktop() {
|
+desktop() {
|
||||||
bottom: 20px;
|
top: 20px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
border: 1px solid rgba(#fff, 0.15);
|
border: 1px solid rgba(#fff, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
+mobile() {
|
+mobile() {
|
||||||
bottom: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
border: 1px solid rgba(#fff, 0.25);
|
border: 1px solid rgba(#fff, 0.25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .sidebar {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 101;
|
|
||||||
top: calc(50% - 60px);
|
|
||||||
height: 120px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
+desktop() {
|
|
||||||
left: 20px;
|
|
||||||
width: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
+mobile() {
|
|
||||||
left: 10px;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .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;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.login {
|
|
||||||
&.off {
|
|
||||||
background-image: url('/resources/images/icon_login_white.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.settings {
|
|
||||||
&.off {
|
|
||||||
background-image: url('/resources/images/icon_settings_white.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.on {
|
|
||||||
background-image: url('/resources/images/icon_settings_black.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.screen {
|
|
||||||
&.on {
|
|
||||||
background-image: url('/resources/images/no-share-screen-black.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.off {
|
|
||||||
background-image: url('/resources/images/share-screen-white.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unsupported {
|
|
||||||
background-image: url('/resources/images/no-share-screen-white.svg');
|
|
||||||
background-color: rgba(#d42241, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.need-extension {
|
|
||||||
background-image: url('/resources/images/share-screen-extension.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.raise-hand {
|
|
||||||
background-image: url('/resources/images/icon-hand-white.svg');
|
|
||||||
|
|
||||||
&.on {
|
|
||||||
background-image: url('/resources/images/icon-hand-black.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.leave-meeting {
|
|
||||||
background-image: url('/resources/images/leave-meeting.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .toolarea-wrapper {
|
> .toolarea-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
width: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 20%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #FFF;
|
background-color: rgba(50, 50, 50, 0.9);
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-controls {
|
||||||
|
visibility: hidden;
|
||||||
|
animation: fade-out 0.3s;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
animation: fade-in 0.3s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,7 +246,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1000;
|
z-index: 120;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
$backgroundTint = #000;
|
$backgroundTint = #000;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5
|
z-index: 10;
|
||||||
top: 0.6vmin;
|
top: 0.6vmin;
|
||||||
left: 0.6vmin;
|
left: 0.6vmin;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
[data-component='Sidebar'] {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 500;
|
||||||
|
top: calc(50% - 60px);
|
||||||
|
height: 120px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
+desktop() {
|
||||||
|
left: 20px;
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
+mobile() {
|
||||||
|
left: 10px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .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%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
+desktop() {
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
+mobile() {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-color: rgba(#fff, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.login {
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/icon_login_white.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logout > img {
|
||||||
|
height: 65%;
|
||||||
|
max-width: 65%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.settings {
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/icon_settings_white.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon_settings_black.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fullscreen {
|
||||||
|
background-image: url('/resources/images/icon_fullscreen_white.svg');
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon_fullscreen_exit_white.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.screen {
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/no-share-screen-black.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/share-screen-white.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.need-extension {
|
||||||
|
background-image: url('/resources/images/share-screen-extension.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.raise-hand {
|
||||||
|
background-image: url('/resources/images/icon-hand-white.svg');
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon-hand-black.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.leave-meeting {
|
||||||
|
background-image: url('/resources/images/leave-meeting.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
[data-component='ToolAreaButton'] {
|
[data-component='ToolAreaButton'] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 101;
|
z-index: 1000;
|
||||||
top: 20px;
|
right: 0;
|
||||||
right: 20px;
|
|
||||||
height: 36px;
|
height: 36px;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
|
padding: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
transition: right 0.3s;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
right: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
> .button {
|
> .button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
@ -49,11 +54,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .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%;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
> .tabs {
|
> .tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -63,15 +88,26 @@
|
||||||
> label {
|
> label {
|
||||||
order: 1;
|
order: 1;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1vmin 0 1vmin 0;
|
padding: 1vmin 0 0.8vmin 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: rgba(#000, 0.3);
|
background: rgba(0,0,0,0.3);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: background ease 0.2s;
|
transition: background ease 0.2s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 33.33%;
|
width: 25%;
|
||||||
font-size: 1.3vmin;
|
font-size: 1.3vmin;
|
||||||
height: 3vmin;
|
height: 3vmin;
|
||||||
|
|
||||||
|
> .badge {
|
||||||
|
padding: 0.1vmin 1vmin;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1.2vmin;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #b12525;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-left: 1vmin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .tab {
|
> .tab {
|
||||||
|
|
@ -81,7 +117,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: none;
|
display: none;
|
||||||
padding: 1vmin;
|
padding: 1vmin;
|
||||||
background: #fff;
|
background: rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type="radio"] {
|
> input[type="radio"] {
|
||||||
|
|
@ -89,11 +125,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type="radio"]:checked + label {
|
> input[type="radio"]:checked + label {
|
||||||
background: #fff;
|
background: rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type="radio"]:checked + label + .tab {
|
> input[type="radio"]:checked + label + .tab {
|
||||||
display: block;
|
display: block;
|
||||||
|
background: rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ global-reset();
|
||||||
@import './mixins';
|
@import './mixins';
|
||||||
@import './fonts';
|
@import './fonts';
|
||||||
@import './reset';
|
@import './reset';
|
||||||
|
@import './keyframes';
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
@ -28,14 +29,17 @@ html {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#multiparty-meeting {
|
#multiparty-meeting {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@import './components/Room';
|
@import './components/Room';
|
||||||
|
@import './components/Sidebar';
|
||||||
@import './components/Me';
|
@import './components/Me';
|
||||||
@import './components/Peers';
|
@import './components/Peers';
|
||||||
@import './components/Peer';
|
@import './components/Peer';
|
||||||
|
|
@ -48,7 +52,8 @@ body {
|
||||||
@import './components/ParticipantList';
|
@import './components/ParticipantList';
|
||||||
@import './components/FullScreenView';
|
@import './components/FullScreenView';
|
||||||
@import './components/FullView';
|
@import './components/FullView';
|
||||||
}
|
@import './components/Filmstrip';
|
||||||
|
@import './components/FileSharing';
|
||||||
|
|
||||||
// Hack to detect in JS the current media query
|
// Hack to detect in JS the current media query
|
||||||
#multiparty-meeting-media-query-detector {
|
#multiparty-meeting-media-query-detector {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,15 +3,9 @@ module.exports =
|
||||||
// oAuth2 conf
|
// oAuth2 conf
|
||||||
oauth2 :
|
oauth2 :
|
||||||
{
|
{
|
||||||
client_id : '',
|
clientID : '',
|
||||||
client_secret : '',
|
clientSecret : '',
|
||||||
providerID : '',
|
callbackURL : 'https://mYDomainName:port/auth-callback'
|
||||||
redirect_uri : 'https://mYDomainName:port/auth-callback',
|
|
||||||
authorization_endpoint : '',
|
|
||||||
userinfo_endpoint : '',
|
|
||||||
token_endpoint : '',
|
|
||||||
scopes : { request : [ 'openid', 'userid','profile'] },
|
|
||||||
response_type : 'code'
|
|
||||||
},
|
},
|
||||||
// Listening hostname for `gulp live|open`.
|
// Listening hostname for `gulp live|open`.
|
||||||
domain : 'localhost',
|
domain : 'localhost',
|
||||||
|
|
@ -22,6 +16,15 @@ module.exports =
|
||||||
},
|
},
|
||||||
// Listening port for https server.
|
// Listening port for https server.
|
||||||
listeningPort : 3443,
|
listeningPort : 3443,
|
||||||
|
turnServers : [
|
||||||
|
{
|
||||||
|
urls : [
|
||||||
|
'turn:example.com:443?transport=tcp'
|
||||||
|
],
|
||||||
|
username : 'example',
|
||||||
|
credential : 'example'
|
||||||
|
}
|
||||||
|
],
|
||||||
mediasoup :
|
mediasoup :
|
||||||
{
|
{
|
||||||
// mediasoup Server settings.
|
// mediasoup Server settings.
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ const gulp = require('gulp');
|
||||||
const plumber = require('gulp-plumber');
|
const plumber = require('gulp-plumber');
|
||||||
const eslint = require('gulp-eslint');
|
const eslint = require('gulp-eslint');
|
||||||
|
|
||||||
gulp.task('lint', () =>
|
const LINTING_FILES =
|
||||||
{
|
|
||||||
const src =
|
|
||||||
[
|
[
|
||||||
'gulpfile.js',
|
'gulpfile.js',
|
||||||
'server.js',
|
'server.js',
|
||||||
|
|
@ -25,10 +23,22 @@ gulp.task('lint', () =>
|
||||||
'lib/**/*.js'
|
'lib/**/*.js'
|
||||||
];
|
];
|
||||||
|
|
||||||
return gulp.src(src)
|
gulp.task('lint', () =>
|
||||||
|
{
|
||||||
|
|
||||||
|
return gulp.src(LINTING_FILES)
|
||||||
.pipe(plumber())
|
.pipe(plumber())
|
||||||
.pipe(eslint())
|
.pipe(eslint())
|
||||||
.pipe(eslint.format());
|
.pipe(eslint.format());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('lint-fix', function()
|
||||||
|
{
|
||||||
|
return gulp.src(LINTING_FILES)
|
||||||
|
.pipe(plumber())
|
||||||
|
.pipe(eslint({ fix: true }))
|
||||||
|
.pipe(eslint.format())
|
||||||
|
.pipe(gulp.dest((file) => file.base));
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task('default', gulp.series('lint'));
|
gulp.task('default', gulp.series('lint'));
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const EventEmitter = require('events').EventEmitter;
|
const EventEmitter = require('events').EventEmitter;
|
||||||
const protooServer = require('protoo-server');
|
const protooServer = require('protoo-server');
|
||||||
|
const WebTorrent = require('webtorrent-hybrid');
|
||||||
const Logger = require('./Logger');
|
const Logger = require('./Logger');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
@ -11,6 +12,14 @@ const BITRATE_FACTOR = 0.75;
|
||||||
|
|
||||||
const logger = new Logger('Room');
|
const logger = new Logger('Room');
|
||||||
|
|
||||||
|
const torrentClient = new WebTorrent({
|
||||||
|
tracker : {
|
||||||
|
rtcConfig : {
|
||||||
|
iceServers : config.turnServers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
class Room extends EventEmitter
|
class Room extends EventEmitter
|
||||||
{
|
{
|
||||||
constructor(roomId, mediaServer)
|
constructor(roomId, mediaServer)
|
||||||
|
|
@ -28,6 +37,8 @@ class Room extends EventEmitter
|
||||||
|
|
||||||
this._chatHistory = [];
|
this._chatHistory = [];
|
||||||
|
|
||||||
|
this._fileHistory = [];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Protoo Room instance.
|
// Protoo Room instance.
|
||||||
|
|
@ -228,6 +239,18 @@ class Room extends EventEmitter
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'change-profile-picture':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
this._protooRoom.spread('profile-picture-changed', {
|
||||||
|
peerName : protooPeer.id,
|
||||||
|
picture : request.data.picture
|
||||||
|
}, [ protooPeer ]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'chat-message':
|
case 'chat-message':
|
||||||
{
|
{
|
||||||
accept();
|
accept();
|
||||||
|
|
@ -260,6 +283,37 @@ class Room extends EventEmitter
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'send-file':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const fileData = request.data.file;
|
||||||
|
|
||||||
|
this._fileHistory.push(fileData);
|
||||||
|
|
||||||
|
if (!torrentClient.get(fileData.file.magnet))
|
||||||
|
{
|
||||||
|
torrentClient.add(fileData.file.magnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._protooRoom.spread('file-receive', {
|
||||||
|
file : fileData
|
||||||
|
}, [ protooPeer ]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'file-history':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
protooPeer.send('file-history-receive', {
|
||||||
|
fileHistory : this._fileHistory
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'raisehand-message':
|
case 'raisehand-message':
|
||||||
{
|
{
|
||||||
accept();
|
accept();
|
||||||
|
|
@ -267,7 +321,7 @@ class Room extends EventEmitter
|
||||||
const { raiseHandState } = request.data;
|
const { raiseHandState } = request.data;
|
||||||
const { mediaPeer } = protooPeer.data;
|
const { mediaPeer } = protooPeer.data;
|
||||||
|
|
||||||
mediaPeer.appData.raiseHand = request.data.raiseHandState;
|
mediaPeer.appData.raiseHandState = request.data.raiseHandState;
|
||||||
// Spread to others via protoo.
|
// Spread to others via protoo.
|
||||||
this._protooRoom.spread(
|
this._protooRoom.spread(
|
||||||
'raisehand-message',
|
'raisehand-message',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
const mediasoup = require('mediasoup');
|
||||||
|
const readline = require('readline');
|
||||||
|
const colors = require('colors/safe');
|
||||||
|
const repl = require('repl');
|
||||||
|
const homer = require('./lib/homer');
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
// mediasoup server.
|
||||||
|
const mediaServer = mediasoup.Server(
|
||||||
|
{
|
||||||
|
numWorkers : 1,
|
||||||
|
logLevel : config.mediasoup.logLevel,
|
||||||
|
logTags : config.mediasoup.logTags,
|
||||||
|
rtcIPv4 : config.mediasoup.rtcIPv4,
|
||||||
|
rtcIPv6 : config.mediasoup.rtcIPv6,
|
||||||
|
rtcAnnouncedIPv4 : config.mediasoup.rtcAnnouncedIPv4,
|
||||||
|
rtcAnnouncedIPv6 : config.mediasoup.rtcAnnouncedIPv6,
|
||||||
|
rtcMinPort : config.mediasoup.rtcMinPort,
|
||||||
|
rtcMaxPort : config.mediasoup.rtcMaxPort
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do Homer stuff.
|
||||||
|
if (process.env.MEDIASOUP_HOMER_OUTPUT)
|
||||||
|
homer(mediaServer);
|
||||||
|
|
||||||
|
global.SERVER = mediaServer;
|
||||||
|
|
||||||
|
mediaServer.on('newroom', (room) =>
|
||||||
|
{
|
||||||
|
global.ROOM = room;
|
||||||
|
|
||||||
|
room.on('newpeer', (peer) =>
|
||||||
|
{
|
||||||
|
global.PEER = peer;
|
||||||
|
|
||||||
|
if (peer.consumers.length > 0)
|
||||||
|
global.CONSUMER = peer.consumers[peer.consumers.length - 1];
|
||||||
|
|
||||||
|
peer.on('newtransport', (transport) =>
|
||||||
|
{
|
||||||
|
global.TRANSPORT = transport;
|
||||||
|
});
|
||||||
|
|
||||||
|
peer.on('newproducer', (producer) =>
|
||||||
|
{
|
||||||
|
global.PRODUCER = producer;
|
||||||
|
});
|
||||||
|
|
||||||
|
peer.on('newconsumer', (consumer) =>
|
||||||
|
{
|
||||||
|
global.CONSUMER = consumer;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for keyboard input.
|
||||||
|
|
||||||
|
let cmd;
|
||||||
|
let terminal;
|
||||||
|
|
||||||
|
openCommandConsole();
|
||||||
|
|
||||||
|
function openCommandConsole()
|
||||||
|
{
|
||||||
|
stdinLog('[opening Readline Command Console...]');
|
||||||
|
|
||||||
|
closeCommandConsole();
|
||||||
|
closeTerminal();
|
||||||
|
|
||||||
|
cmd = readline.createInterface(
|
||||||
|
{
|
||||||
|
input : process.stdin,
|
||||||
|
output : process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('SIGINT', () =>
|
||||||
|
{
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
readStdin();
|
||||||
|
|
||||||
|
function readStdin()
|
||||||
|
{
|
||||||
|
cmd.question('cmd> ', (answer) =>
|
||||||
|
{
|
||||||
|
switch (answer)
|
||||||
|
{
|
||||||
|
case '':
|
||||||
|
{
|
||||||
|
readStdin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'h':
|
||||||
|
case 'help':
|
||||||
|
{
|
||||||
|
stdinLog('');
|
||||||
|
stdinLog('available commands:');
|
||||||
|
stdinLog('- h, help : show this message');
|
||||||
|
stdinLog('- sd, serverdump : execute server.dump()');
|
||||||
|
stdinLog('- rd, roomdump : execute room.dump() for the latest created mediasoup Room');
|
||||||
|
stdinLog('- pd, peerdump : execute peer.dump() for the latest created mediasoup Peer');
|
||||||
|
stdinLog('- td, transportdump : execute transport.dump() for the latest created mediasoup Transport');
|
||||||
|
stdinLog('- prd, producerdump : execute producer.dump() for the latest created mediasoup Producer');
|
||||||
|
stdinLog('- cd, consumerdump : execute consumer.dump() for the latest created mediasoup Consumer');
|
||||||
|
stdinLog('- t, terminal : open REPL Terminal');
|
||||||
|
stdinLog('');
|
||||||
|
readStdin();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'sd':
|
||||||
|
case 'serverdump':
|
||||||
|
{
|
||||||
|
mediaServer.dump()
|
||||||
|
.then((data) =>
|
||||||
|
{
|
||||||
|
stdinLog(`server.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||||
|
readStdin();
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
stdinError(`mediaServer.dump() failed: ${error}`);
|
||||||
|
readStdin();
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'rd':
|
||||||
|
case 'roomdump':
|
||||||
|
{
|
||||||
|
if (!global.ROOM)
|
||||||
|
{
|
||||||
|
readStdin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.ROOM.dump()
|
||||||
|
.then((data) =>
|
||||||
|
{
|
||||||
|
stdinLog(`room.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||||
|
readStdin();
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
stdinError(`room.dump() failed: ${error}`);
|
||||||
|
readStdin();
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pd':
|
||||||
|
case 'peerdump':
|
||||||
|
{
|
||||||
|
if (!global.PEER)
|
||||||
|
{
|
||||||
|
readStdin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.PEER.dump()
|
||||||
|
.then((data) =>
|
||||||
|
{
|
||||||
|
stdinLog(`peer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||||
|
readStdin();
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
stdinError(`peer.dump() failed: ${error}`);
|
||||||
|
readStdin();
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'td':
|
||||||
|
case 'transportdump':
|
||||||
|
{
|
||||||
|
if (!global.TRANSPORT)
|
||||||
|
{
|
||||||
|
readStdin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.TRANSPORT.dump()
|
||||||
|
.then((data) =>
|
||||||
|
{
|
||||||
|
stdinLog(`transport.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||||
|
readStdin();
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
stdinError(`transport.dump() failed: ${error}`);
|
||||||
|
readStdin();
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'prd':
|
||||||
|
case 'producerdump':
|
||||||
|
{
|
||||||
|
if (!global.PRODUCER)
|
||||||
|
{
|
||||||
|
readStdin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.PRODUCER.dump()
|
||||||
|
.then((data) =>
|
||||||
|
{
|
||||||
|
stdinLog(`producer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||||
|
readStdin();
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
stdinError(`producer.dump() failed: ${error}`);
|
||||||
|
readStdin();
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'cd':
|
||||||
|
case 'consumerdump':
|
||||||
|
{
|
||||||
|
if (!global.CONSUMER)
|
||||||
|
{
|
||||||
|
readStdin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.CONSUMER.dump()
|
||||||
|
.then((data) =>
|
||||||
|
{
|
||||||
|
stdinLog(`consumer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||||
|
readStdin();
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
stdinError(`consumer.dump() failed: ${error}`);
|
||||||
|
readStdin();
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 't':
|
||||||
|
case 'terminal':
|
||||||
|
{
|
||||||
|
openTerminal();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
stdinError(`unknown command: ${answer}`);
|
||||||
|
stdinLog('press \'h\' or \'help\' to get the list of available commands');
|
||||||
|
|
||||||
|
readStdin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTerminal()
|
||||||
|
{
|
||||||
|
stdinLog('[opening REPL Terminal...]');
|
||||||
|
|
||||||
|
closeCommandConsole();
|
||||||
|
closeTerminal();
|
||||||
|
|
||||||
|
terminal = repl.start(
|
||||||
|
{
|
||||||
|
prompt : 'terminal> ',
|
||||||
|
useColors : true,
|
||||||
|
useGlobal : true,
|
||||||
|
ignoreUndefined : false
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal.on('exit', () => openCommandConsole());
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCommandConsole()
|
||||||
|
{
|
||||||
|
if (cmd)
|
||||||
|
{
|
||||||
|
cmd.close();
|
||||||
|
cmd = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTerminal()
|
||||||
|
{
|
||||||
|
if (terminal)
|
||||||
|
{
|
||||||
|
terminal.removeAllListeners('exit');
|
||||||
|
terminal.close();
|
||||||
|
terminal = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stdinLog(msg)
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(colors.green(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stdinError(msg)
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(colors.red.bold('ERROR: ') + colors.red(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = mediaServer;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,11 +7,14 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"base-64": "^0.1.0",
|
||||||
"colors": "^1.1.2",
|
"colors": "^1.1.2",
|
||||||
"debug": "^3.1.0",
|
"debug": "^3.1.0",
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.3",
|
||||||
"mediasoup": "^2.1.0",
|
"mediasoup": "^2.1.0",
|
||||||
"protoo-server": "^2.0.7"
|
"passport-dataporten": "^1.3.0",
|
||||||
|
"protoo-server": "^2.0.7",
|
||||||
|
"webtorrent-hybrid": "^1.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
|
|
|
||||||
196
server/router.js
196
server/router.js
|
|
@ -1,196 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const EventEmitter = require( 'events' );
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const path = require('path');
|
|
||||||
const url = require('url');
|
|
||||||
const httpHelpers = require('./http-helpers');
|
|
||||||
const fs = require('fs');
|
|
||||||
const config = require('./config');
|
|
||||||
const utils = require('./util');
|
|
||||||
const querystring = require('querystring');
|
|
||||||
const https = require('https')
|
|
||||||
const Logger = require('./lib/Logger');
|
|
||||||
|
|
||||||
const logger = new Logger();
|
|
||||||
|
|
||||||
let authRequests = {}; // ongoing auth requests :
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
state:
|
|
||||||
{
|
|
||||||
peerName:'peerName'
|
|
||||||
code:'oauth2 code',
|
|
||||||
roomId: 'romid',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const actions = {
|
|
||||||
'GET': function(req, res) {
|
|
||||||
var parsedUrl = url.parse(req.url,true);
|
|
||||||
if ( parsedUrl.pathname === '/auth-callback' )
|
|
||||||
{
|
|
||||||
if ( typeof(authRequests[parsedUrl.query.state]) != 'undefined' )
|
|
||||||
{
|
|
||||||
console.log('got authorization code for access token: ',parsedUrl.query,authRequests[parsedUrl.query.state]);
|
|
||||||
const auth = "Basic " + new Buffer(config.oauth2.client_id + ":" + config.oauth2.client_secret).toString("base64");
|
|
||||||
const postUrl = url.parse(config.oauth2.token_endpoint);
|
|
||||||
let postData = querystring.stringify({
|
|
||||||
"grant_type":"authorization_code",
|
|
||||||
"code":parsedUrl.query.code,
|
|
||||||
"redirect_uri":config.oauth2.redirect_uri
|
|
||||||
});
|
|
||||||
|
|
||||||
let request = https.request( {
|
|
||||||
host : postUrl.hostname,
|
|
||||||
path : postUrl.pathname,
|
|
||||||
port : postUrl.port,
|
|
||||||
method : 'POST',
|
|
||||||
headers :
|
|
||||||
{
|
|
||||||
'Content-Type' : 'application/x-www-form-urlencoded',
|
|
||||||
'Authorization' : auth,
|
|
||||||
'Content-Length': Buffer.byteLength(postData)
|
|
||||||
}
|
|
||||||
}, function(res)
|
|
||||||
{
|
|
||||||
res.setEncoding("utf8");
|
|
||||||
let body = "";
|
|
||||||
res.on("data", data => {
|
|
||||||
body += data;
|
|
||||||
});
|
|
||||||
res.on("end", () => {
|
|
||||||
if ( res.statusCode == 200 )
|
|
||||||
{
|
|
||||||
console.log('We\'ve got an access token!', body);
|
|
||||||
body = JSON.parse(body);
|
|
||||||
authRequests[parsedUrl.query.state].access_token =
|
|
||||||
body.access_token;
|
|
||||||
const auth = "Bearer " + body.access_token;
|
|
||||||
const getUrl = url.parse(config.oauth2.userinfo_endpoint);
|
|
||||||
let request = https.request( {
|
|
||||||
host : getUrl.hostname,
|
|
||||||
path : getUrl.pathname,
|
|
||||||
port : getUrl.port,
|
|
||||||
method : 'GET',
|
|
||||||
headers :
|
|
||||||
{
|
|
||||||
'Authorization' : auth,
|
|
||||||
}
|
|
||||||
}, function(res)
|
|
||||||
{
|
|
||||||
res.setEncoding("utf8");
|
|
||||||
let body = '';
|
|
||||||
res.on("data", data => {
|
|
||||||
body += data;
|
|
||||||
});
|
|
||||||
res.on("end", () => {
|
|
||||||
// we don't need this any longer:
|
|
||||||
delete authRequests[parsedUrl.query.state].access_token;
|
|
||||||
|
|
||||||
body = JSON.parse(body);
|
|
||||||
console.log(body);
|
|
||||||
if ( res.statusCode == 200 )
|
|
||||||
{
|
|
||||||
authRequests[parsedUrl.query.state].verified = true;
|
|
||||||
if ( typeof(body.sub) != 'undefined')
|
|
||||||
{
|
|
||||||
authRequests[parsedUrl.query.state].sub = body.sub;
|
|
||||||
}
|
|
||||||
if ( typeof(body.name) != 'undefined')
|
|
||||||
{
|
|
||||||
authRequests[parsedUrl.query.state].name = body.name;
|
|
||||||
}
|
|
||||||
if ( typeof(body.picture) != 'undefined')
|
|
||||||
{
|
|
||||||
authRequests[parsedUrl.query.state].picture = body.picture;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{
|
|
||||||
authRequests[parsedUrl.query.state].verified = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eventEmitter.emit('auth',
|
|
||||||
authRequests[parsedUrl.query.state]);
|
|
||||||
|
|
||||||
delete authRequests[parsedUrl.query.state];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
request.write(' ');
|
|
||||||
request.end;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log('access_token denied',body);
|
|
||||||
authRequests[parsedUrl.query.state].verified = false;
|
|
||||||
delete authRequests[parsedUrl.query.state].access_token;
|
|
||||||
eventEmitter.emit('auth',
|
|
||||||
authRequests[parsedUrl.query.state]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
request.write(postData);
|
|
||||||
request.end;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.warn('Got authorization_code for unseen state:', parsedUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (parsedUrl.pathname === '/login') {
|
|
||||||
const state = utils.random(10);
|
|
||||||
httpHelpers.redirector(res, config.oauth2.authorization_endpoint
|
|
||||||
+ '?client_id=' + config.oauth2.client_id
|
|
||||||
+ '&redirect_uri=' + config.oauth2.redirect_uri
|
|
||||||
+ '&state=' + state
|
|
||||||
+ '&scopes=' + config.oauth2.scopes.request.join('+')
|
|
||||||
+ '&response_type=' + config.oauth2.response_type);
|
|
||||||
authRequests[state] =
|
|
||||||
{
|
|
||||||
'roomId' : parsedUrl.query.roomId,
|
|
||||||
'peerName' : parsedUrl.query.peerName
|
|
||||||
};
|
|
||||||
console.log('Started authorization process: ', parsedUrl.query);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log('requested url:', parsedUrl.pathname);
|
|
||||||
var resolvedBase = path.resolve('./public');
|
|
||||||
var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, '');
|
|
||||||
var fileLoc = path.join(resolvedBase, safeSuffix);
|
|
||||||
var headers = {};
|
|
||||||
|
|
||||||
var stream = fs.createReadStream(fileLoc);
|
|
||||||
|
|
||||||
// Handle non-existent file -> delivering index.html
|
|
||||||
stream.on('error', function(error) {
|
|
||||||
stream = fs.createReadStream(path.resolve('./public/index.html'));
|
|
||||||
res.statusCode = 200;
|
|
||||||
stream.pipe(res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// File exists, stream it to user
|
|
||||||
if (parsedUrl.pathname.indexOf('svg') === parsedUrl.pathname.length -3) {headers = {'Content-Type': 'image/svg+xml'}};
|
|
||||||
res.writeHead(200, headers);
|
|
||||||
stream.pipe(res);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'POST': function(req, res) {
|
|
||||||
httpHelpers.prepareResponse(req, function(data) {
|
|
||||||
// Do something with the data that was just collected by the helper
|
|
||||||
// e.g., validate and save to db
|
|
||||||
// either redirect or respond
|
|
||||||
// should be based on result of the operation performed in response to the POST request intent
|
|
||||||
// e.g., if user wants to save, and save fails, throw error
|
|
||||||
httpHelpers.redirector(res, /* redirect path , optional status code - defaults to 302 */);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = eventEmitter;
|
|
||||||
|
|
||||||
module.exports.handleRequest = function(req, res) {
|
|
||||||
var action = actions[req.method];
|
|
||||||
action ? action(req, res) : httpHelpers.send404(res);
|
|
||||||
};
|
|
||||||
414
server/server.js
414
server/server.js
|
|
@ -5,6 +5,16 @@
|
||||||
process.title = 'multiparty-meeting-server';
|
process.title = 'multiparty-meeting-server';
|
||||||
|
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
const fs = require('fs');
|
||||||
|
const https = require('https');
|
||||||
|
const express = require('express');
|
||||||
|
const url = require('url');
|
||||||
|
const protooServer = require('protoo-server');
|
||||||
|
const Logger = require('./lib/Logger');
|
||||||
|
const Room = require('./lib/Room');
|
||||||
|
const Dataporten = require('passport-dataporten');
|
||||||
|
const utils = require('./util');
|
||||||
|
const base64 = require('base-64');
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log('- process.env.DEBUG:', process.env.DEBUG);
|
console.log('- process.env.DEBUG:', process.env.DEBUG);
|
||||||
|
|
@ -12,100 +22,83 @@ console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
|
||||||
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
|
|
||||||
const fs = require('fs');
|
// Start the mediasoup server.
|
||||||
const https = require('https');
|
const mediaServer = require('./mediasoup');
|
||||||
const router = require('./router');
|
|
||||||
const url = require('url');
|
|
||||||
const path = require('path');
|
|
||||||
const protooServer = require('protoo-server');
|
|
||||||
const mediasoup = require('mediasoup');
|
|
||||||
const readline = require('readline');
|
|
||||||
const colors = require('colors/safe');
|
|
||||||
const repl = require('repl');
|
|
||||||
const Logger = require('./lib/Logger');
|
|
||||||
const Room = require('./lib/Room');
|
|
||||||
const homer = require('./lib/homer');
|
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
|
|
||||||
// Map of Room instances indexed by roomId.
|
// Map of Room instances indexed by roomId.
|
||||||
const rooms = new Map();
|
const rooms = new Map();
|
||||||
|
|
||||||
// mediasoup server.
|
// TLS server configuration.
|
||||||
const mediaServer = mediasoup.Server(
|
|
||||||
{
|
|
||||||
numWorkers : 1,
|
|
||||||
logLevel : config.mediasoup.logLevel,
|
|
||||||
logTags : config.mediasoup.logTags,
|
|
||||||
rtcIPv4 : config.mediasoup.rtcIPv4,
|
|
||||||
rtcIPv6 : config.mediasoup.rtcIPv6,
|
|
||||||
rtcAnnouncedIPv4 : config.mediasoup.rtcAnnouncedIPv4,
|
|
||||||
rtcAnnouncedIPv6 : config.mediasoup.rtcAnnouncedIPv6,
|
|
||||||
rtcMinPort : config.mediasoup.rtcMinPort,
|
|
||||||
rtcMaxPort : config.mediasoup.rtcMaxPort
|
|
||||||
});
|
|
||||||
|
|
||||||
// Do Homer stuff.
|
|
||||||
if (process.env.MEDIASOUP_HOMER_OUTPUT)
|
|
||||||
homer(mediaServer);
|
|
||||||
|
|
||||||
global.SERVER = mediaServer;
|
|
||||||
|
|
||||||
mediaServer.on('newroom', (room) =>
|
|
||||||
{
|
|
||||||
global.ROOM = room;
|
|
||||||
|
|
||||||
room.on('newpeer', (peer) =>
|
|
||||||
{
|
|
||||||
global.PEER = peer;
|
|
||||||
|
|
||||||
if (peer.consumers.length > 0)
|
|
||||||
global.CONSUMER = peer.consumers[peer.consumers.length - 1];
|
|
||||||
|
|
||||||
peer.on('newtransport', (transport) =>
|
|
||||||
{
|
|
||||||
global.TRANSPORT = transport;
|
|
||||||
});
|
|
||||||
|
|
||||||
peer.on('newproducer', (producer) =>
|
|
||||||
{
|
|
||||||
global.PRODUCER = producer;
|
|
||||||
});
|
|
||||||
|
|
||||||
peer.on('newconsumer', (consumer) =>
|
|
||||||
{
|
|
||||||
global.CONSUMER = consumer;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// HTTPS server
|
|
||||||
const tls =
|
const tls =
|
||||||
{
|
{
|
||||||
cert : fs.readFileSync(config.tls.cert),
|
cert : fs.readFileSync(config.tls.cert),
|
||||||
key : fs.readFileSync(config.tls.key)
|
key : fs.readFileSync(config.tls.key)
|
||||||
};
|
};
|
||||||
|
|
||||||
const httpsServer = https.createServer(tls, router.handleRequest);
|
const app = express();
|
||||||
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
|
|
||||||
|
const dataporten = new Dataporten.Setup(config.oauth2);
|
||||||
|
|
||||||
|
app.use(dataporten.passport.initialize());
|
||||||
|
app.use(dataporten.passport.session());
|
||||||
|
|
||||||
|
app.get('/login', (req, res, next) =>
|
||||||
{
|
{
|
||||||
logger.info('Server running, port: ',config.listeningPort);
|
dataporten.passport.authenticate('dataporten', {
|
||||||
|
state : base64.encode(JSON.stringify({
|
||||||
|
roomId : req.query.roomId,
|
||||||
|
peerName : req.query.peerName,
|
||||||
|
code : utils.random(10)
|
||||||
|
}))
|
||||||
|
|
||||||
|
})(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.on('auth',function(event){
|
dataporten.setupLogout(app, '/logout');
|
||||||
console.log('router: Got an event: ',event)
|
|
||||||
if ( rooms.has(event.roomId) )
|
|
||||||
{
|
|
||||||
const room = rooms.get(event.roomId)._protooRoom;
|
|
||||||
if ( room.hasPeer(event.peerName) )
|
|
||||||
{
|
|
||||||
const peer = room.getPeer(event.peerName);
|
|
||||||
peer.send('auth', event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Protoo WebSocket server listens to same webserver so everythink is available
|
app.get(
|
||||||
|
'/auth-callback',
|
||||||
|
|
||||||
|
dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),
|
||||||
|
|
||||||
|
(req, res) =>
|
||||||
|
{
|
||||||
|
const state = JSON.parse(base64.decode(req.query.state));
|
||||||
|
|
||||||
|
if (rooms.has(state.roomId))
|
||||||
|
{
|
||||||
|
const room = rooms.get(state.roomId)._protooRoom;
|
||||||
|
|
||||||
|
if (room.hasPeer(state.peerName))
|
||||||
|
{
|
||||||
|
const peer = room.getPeer(state.peerName);
|
||||||
|
|
||||||
|
peer.send('auth', {
|
||||||
|
name : req.user.data.displayName,
|
||||||
|
picture : req.user.data.photos[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send('');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Serve all files in the public folder as static files.
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));
|
||||||
|
|
||||||
|
const httpsServer = https.createServer(tls, app);
|
||||||
|
|
||||||
|
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
|
||||||
|
{
|
||||||
|
logger.info('Server running on port: ', config.listeningPort);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Protoo WebSocket server listens to same webserver so everything is available
|
||||||
// via same port
|
// via same port
|
||||||
const webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
const webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
||||||
{
|
{
|
||||||
|
|
@ -179,268 +172,3 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
|
||||||
|
|
||||||
room.handleConnection(peerName, transport);
|
room.handleConnection(peerName, transport);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for keyboard input.
|
|
||||||
|
|
||||||
let cmd;
|
|
||||||
let terminal;
|
|
||||||
|
|
||||||
openCommandConsole();
|
|
||||||
|
|
||||||
function openCommandConsole()
|
|
||||||
{
|
|
||||||
stdinLog('[opening Readline Command Console...]');
|
|
||||||
|
|
||||||
closeCommandConsole();
|
|
||||||
closeTerminal();
|
|
||||||
|
|
||||||
cmd = readline.createInterface(
|
|
||||||
{
|
|
||||||
input : process.stdin,
|
|
||||||
output : process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
cmd.on('SIGINT', () =>
|
|
||||||
{
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
readStdin();
|
|
||||||
|
|
||||||
function readStdin()
|
|
||||||
{
|
|
||||||
cmd.question('cmd> ', (answer) =>
|
|
||||||
{
|
|
||||||
switch (answer)
|
|
||||||
{
|
|
||||||
case '':
|
|
||||||
{
|
|
||||||
readStdin();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'h':
|
|
||||||
case 'help':
|
|
||||||
{
|
|
||||||
stdinLog('');
|
|
||||||
stdinLog('available commands:');
|
|
||||||
stdinLog('- h, help : show this message');
|
|
||||||
stdinLog('- sd, serverdump : execute server.dump()');
|
|
||||||
stdinLog('- rd, roomdump : execute room.dump() for the latest created mediasoup Room');
|
|
||||||
stdinLog('- pd, peerdump : execute peer.dump() for the latest created mediasoup Peer');
|
|
||||||
stdinLog('- td, transportdump : execute transport.dump() for the latest created mediasoup Transport');
|
|
||||||
stdinLog('- prd, producerdump : execute producer.dump() for the latest created mediasoup Producer');
|
|
||||||
stdinLog('- cd, consumerdump : execute consumer.dump() for the latest created mediasoup Consumer');
|
|
||||||
stdinLog('- t, terminal : open REPL Terminal');
|
|
||||||
stdinLog('');
|
|
||||||
readStdin();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'sd':
|
|
||||||
case 'serverdump':
|
|
||||||
{
|
|
||||||
mediaServer.dump()
|
|
||||||
.then((data) =>
|
|
||||||
{
|
|
||||||
stdinLog(`server.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
|
||||||
readStdin();
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
{
|
|
||||||
stdinError(`mediaServer.dump() failed: ${error}`);
|
|
||||||
readStdin();
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'rd':
|
|
||||||
case 'roomdump':
|
|
||||||
{
|
|
||||||
if (!global.ROOM)
|
|
||||||
{
|
|
||||||
readStdin();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.ROOM.dump()
|
|
||||||
.then((data) =>
|
|
||||||
{
|
|
||||||
stdinLog(`room.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
|
||||||
readStdin();
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
{
|
|
||||||
stdinError(`room.dump() failed: ${error}`);
|
|
||||||
readStdin();
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'pd':
|
|
||||||
case 'peerdump':
|
|
||||||
{
|
|
||||||
if (!global.PEER)
|
|
||||||
{
|
|
||||||
readStdin();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.PEER.dump()
|
|
||||||
.then((data) =>
|
|
||||||
{
|
|
||||||
stdinLog(`peer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
|
||||||
readStdin();
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
{
|
|
||||||
stdinError(`peer.dump() failed: ${error}`);
|
|
||||||
readStdin();
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'td':
|
|
||||||
case 'transportdump':
|
|
||||||
{
|
|
||||||
if (!global.TRANSPORT)
|
|
||||||
{
|
|
||||||
readStdin();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.TRANSPORT.dump()
|
|
||||||
.then((data) =>
|
|
||||||
{
|
|
||||||
stdinLog(`transport.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
|
||||||
readStdin();
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
{
|
|
||||||
stdinError(`transport.dump() failed: ${error}`);
|
|
||||||
readStdin();
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'prd':
|
|
||||||
case 'producerdump':
|
|
||||||
{
|
|
||||||
if (!global.PRODUCER)
|
|
||||||
{
|
|
||||||
readStdin();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.PRODUCER.dump()
|
|
||||||
.then((data) =>
|
|
||||||
{
|
|
||||||
stdinLog(`producer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
|
||||||
readStdin();
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
{
|
|
||||||
stdinError(`producer.dump() failed: ${error}`);
|
|
||||||
readStdin();
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'cd':
|
|
||||||
case 'consumerdump':
|
|
||||||
{
|
|
||||||
if (!global.CONSUMER)
|
|
||||||
{
|
|
||||||
readStdin();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.CONSUMER.dump()
|
|
||||||
.then((data) =>
|
|
||||||
{
|
|
||||||
stdinLog(`consumer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
|
||||||
readStdin();
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
{
|
|
||||||
stdinError(`consumer.dump() failed: ${error}`);
|
|
||||||
readStdin();
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 't':
|
|
||||||
case 'terminal':
|
|
||||||
{
|
|
||||||
openTerminal();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
stdinError(`unknown command: ${answer}`);
|
|
||||||
stdinLog('press \'h\' or \'help\' to get the list of available commands');
|
|
||||||
|
|
||||||
readStdin();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openTerminal()
|
|
||||||
{
|
|
||||||
stdinLog('[opening REPL Terminal...]');
|
|
||||||
|
|
||||||
closeCommandConsole();
|
|
||||||
closeTerminal();
|
|
||||||
|
|
||||||
terminal = repl.start(
|
|
||||||
{
|
|
||||||
prompt : 'terminal> ',
|
|
||||||
useColors : true,
|
|
||||||
useGlobal : true,
|
|
||||||
ignoreUndefined : false
|
|
||||||
});
|
|
||||||
|
|
||||||
terminal.on('exit', () => openCommandConsole());
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeCommandConsole()
|
|
||||||
{
|
|
||||||
if (cmd)
|
|
||||||
{
|
|
||||||
cmd.close();
|
|
||||||
cmd = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTerminal()
|
|
||||||
{
|
|
||||||
if (terminal)
|
|
||||||
{
|
|
||||||
terminal.removeAllListeners('exit');
|
|
||||||
terminal.close();
|
|
||||||
terminal = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stdinLog(msg)
|
|
||||||
{
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(colors.green(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
function stdinError(msg)
|
|
||||||
{
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(colors.red.bold('ERROR: ') + colors.red(msg));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue