Merge branch 'develop'
|
|
@ -1,9 +1,10 @@
|
|||
node_modules/
|
||||
|
||||
/app/config.*
|
||||
!/app/config.example.js
|
||||
/server/config.*
|
||||
!/server/config.example.js
|
||||
/app/build/
|
||||
/app/public/config.js
|
||||
/app/public/images/logo.*
|
||||
/server/config/
|
||||
!/server/config/config.example.js
|
||||
/server/public/
|
||||
/server/certs/
|
||||
!/server/certs/mediasoup-demo.localhost.*
|
||||
|
|
|
|||
38
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# multiparty-meeting
|
||||
|
||||
A WebRTC meeting service using [mediasoup](https://mediasoup.org) as its backend.
|
||||
A WebRTC meeting service using [mediasoup](https://mediasoup.org).
|
||||
|
||||
Try it online at https://letsmeet.no. You can add /roomname to the URL for specifying a room.
|
||||
|
||||
|
|
@ -11,9 +11,12 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci
|
|||
* File sharing
|
||||
* Different video layouts
|
||||
|
||||
There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To test it, call: roomname@letsmeet.no.
|
||||
There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To try it, call: roomname@letsmeet.no.
|
||||
|
||||
## Installation
|
||||
## Docker
|
||||
If you want the automatic approach, you can find a docker image [here](https://hub.docker.com/r/misi/mm/).
|
||||
|
||||
## Manual installation
|
||||
|
||||
* Clone the project:
|
||||
|
||||
|
|
@ -28,10 +31,10 @@ $ cd multiparty-meeting
|
|||
$ cp server/config.example.js server/config.js
|
||||
```
|
||||
|
||||
* Copy `app/config.example.js` to `app/config.js` :
|
||||
* Copy `app/public/config.example.js` to `app/public/config.js` :
|
||||
|
||||
```bash
|
||||
$ cp app/config.example.js app/config.js
|
||||
$ cp app/public/config.example.js app/public/config.js
|
||||
```
|
||||
|
||||
* Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc).
|
||||
|
|
@ -41,16 +44,9 @@ $ cp app/config.example.js app/config.js
|
|||
```bash
|
||||
$ cd app
|
||||
$ npm install
|
||||
$ export NODE_ENV=production
|
||||
$ gulp dist
|
||||
```
|
||||
This will build the client application and copy everythink to `server/public` from where the server can host client code to browser requests. (no apache/NGINX needed)
|
||||
|
||||
* Globally install `gulp-cli` NPM module (may need `sudo`):
|
||||
|
||||
```bash
|
||||
$ npm install -g gulp-cli
|
||||
$ npm run build
|
||||
```
|
||||
This will build the client application and copy everythink to `server/public` from where the server can host client code to browser requests.
|
||||
|
||||
* Set up the server:
|
||||
|
||||
|
|
@ -65,7 +61,8 @@ $ npm install
|
|||
* Run the Node.js server application in a terminal:
|
||||
|
||||
```bash
|
||||
$ node server.js
|
||||
$ cd server
|
||||
$ npm start
|
||||
```
|
||||
* test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname`
|
||||
|
||||
|
|
@ -92,29 +89,24 @@ $ systemctl enable multiparty-meeting
|
|||
## Ports and firewall
|
||||
|
||||
* 3443/tcp (default https webserver and signaling - adjustable in `server/config.js`)
|
||||
* 3000/tcp (default `gulp live` port for developing with live browser reload, not needed in production enviroments - adjustable in app/gulpfile.js)
|
||||
* 4443/tcp (default `npm start` port for developing with live browser reload, not needed in production enviroments - adjustable in app/package.json)
|
||||
* 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`)
|
||||
|
||||
* If you want your service running at standard ports 80/443 you should:
|
||||
* Configure a forwarding rule with iptables from port 443 to your configured service port (default 3443)
|
||||
|
||||
|
||||
## TURN configuration
|
||||
|
||||
* You need an addtional [TURN](https://github.com/coturn/coturn)-server for clients located behind restrictive firewalls! Add your server and credentials to `app/config.js`
|
||||
|
||||
## Author
|
||||
## Authors
|
||||
|
||||
* Håvar Aambø Fosstveit
|
||||
* Stefan Otto
|
||||
* Mészáros Mihály
|
||||
|
||||
|
||||
This is heavily based on the [work](https://github.com/versatica/mediasoup-demo) done by:
|
||||
This started as a fork of the [work](https://github.com/versatica/mediasoup-demo) done by:
|
||||
* Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)]
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
|
|
|||
26
app/.babelrc
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"plugins":
|
||||
[
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"jsx-control-statements",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-runtime"
|
||||
],
|
||||
"presets":
|
||||
[
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": [
|
||||
"chrome >= 67",
|
||||
"edge >= 17",
|
||||
"firefox >= 60",
|
||||
"safari >= 12"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/react"
|
||||
]
|
||||
}
|
||||
231
app/.eslintrc.js
|
|
@ -1,231 +0,0 @@
|
|||
module.exports =
|
||||
{
|
||||
env:
|
||||
{
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
plugins:
|
||||
[
|
||||
'import',
|
||||
'react',
|
||||
'jsx-control-statements'
|
||||
],
|
||||
extends:
|
||||
[
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsx-control-statements/recommended'
|
||||
],
|
||||
settings:
|
||||
{
|
||||
react:
|
||||
{
|
||||
pragma: 'React',
|
||||
version: '16'
|
||||
}
|
||||
},
|
||||
parser: "babel-eslint",
|
||||
parserOptions:
|
||||
{
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures:
|
||||
{
|
||||
impliedStrict: true,
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
rules:
|
||||
{
|
||||
'array-bracket-spacing': [ 2, 'always',
|
||||
{
|
||||
objectsInArrays: true,
|
||||
arraysInArrays: true
|
||||
}],
|
||||
'arrow-parens': [ 2, 'always' ],
|
||||
'arrow-spacing': 2,
|
||||
'block-spacing': [ 2, 'always' ],
|
||||
'brace-style': [ 2, 'allman', { allowSingleLine: true } ],
|
||||
'camelcase': 2,
|
||||
'comma-dangle': 2,
|
||||
'comma-spacing': [ 2, { before: false, after: true } ],
|
||||
'comma-style': 2,
|
||||
'computed-property-spacing': 2,
|
||||
'constructor-super': 2,
|
||||
'func-call-spacing': 2,
|
||||
'generator-star-spacing': 2,
|
||||
'guard-for-in': 2,
|
||||
'indent': [ 2, 'tab', { 'SwitchCase': 1 } ],
|
||||
'key-spacing': [ 2,
|
||||
{
|
||||
singleLine:
|
||||
{
|
||||
beforeColon: false,
|
||||
afterColon: true
|
||||
},
|
||||
multiLine:
|
||||
{
|
||||
beforeColon: true,
|
||||
afterColon: true,
|
||||
align: 'colon'
|
||||
}
|
||||
}],
|
||||
'keyword-spacing': 2,
|
||||
'linebreak-style': [ 2, 'unix' ],
|
||||
'lines-around-comment': [ 2,
|
||||
{
|
||||
allowBlockStart: true,
|
||||
allowObjectStart: true,
|
||||
beforeBlockComment: true,
|
||||
beforeLineComment: false
|
||||
}],
|
||||
'max-len': [ 2, 90,
|
||||
{
|
||||
tabWidth: 2,
|
||||
comments: 110,
|
||||
ignoreUrls: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreRegExpLiterals: true
|
||||
}],
|
||||
'newline-after-var': 2,
|
||||
'newline-before-return': 2,
|
||||
'newline-per-chained-call': 2,
|
||||
'no-alert': 2,
|
||||
'no-caller': 2,
|
||||
'no-case-declarations': 2,
|
||||
'no-catch-shadow': 2,
|
||||
'no-class-assign': 2,
|
||||
'no-confusing-arrow': 2,
|
||||
'no-console': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-debugger': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-div-regex': 2,
|
||||
'no-empty': [ 2, { allowEmptyCatch: true } ],
|
||||
'no-empty-pattern': 2,
|
||||
'no-else-return': 0,
|
||||
'no-eval': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-ex-assign': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-extra-boolean-cast': 2,
|
||||
'no-extra-label': 2,
|
||||
'no-extra-semi': 2,
|
||||
'no-fallthrough': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-global-assign': 2,
|
||||
'no-implicit-coercion': 2,
|
||||
'no-implicit-globals': 2,
|
||||
'no-inner-declarations': 2,
|
||||
'no-invalid-regexp': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-lonely-if': 2,
|
||||
'no-mixed-operators': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multi-spaces': 2,
|
||||
'no-multi-str': 2,
|
||||
'no-multiple-empty-lines': [ 2, { max: 1, maxEOF: 0, maxBOF: 0 } ],
|
||||
'no-native-reassign': 2,
|
||||
'no-negated-in-lhs': 2,
|
||||
'no-new': 2,
|
||||
'no-new-func': 2,
|
||||
'no-new-wrappers': 2,
|
||||
'no-obj-calls': 2,
|
||||
'no-proto': 2,
|
||||
'no-prototype-builtins': 0,
|
||||
'no-redeclare': 2,
|
||||
'no-regex-spaces': 2,
|
||||
'no-restricted-imports': 2,
|
||||
'no-return-assign': 2,
|
||||
'no-self-assign': 2,
|
||||
'no-self-compare': 2,
|
||||
'no-sequences': 2,
|
||||
'no-shadow': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'no-spaced-func': 2,
|
||||
'no-sparse-arrays': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-throw-literal': 2,
|
||||
'no-undef': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unmodified-loop-condition': 2,
|
||||
'no-unreachable': 2,
|
||||
'no-unused-vars': [ 1, { vars: 'all', args: 'after-used' }],
|
||||
'no-use-before-define': [ 2, { functions: false } ],
|
||||
'no-useless-call': 2,
|
||||
'no-useless-computed-key': 2,
|
||||
'no-useless-concat': 2,
|
||||
'no-useless-rename': 2,
|
||||
'no-var': 2,
|
||||
'no-whitespace-before-property': 2,
|
||||
'object-curly-newline': 0,
|
||||
'object-curly-spacing': [ 2, 'always' ],
|
||||
'object-property-newline': [ 2, { allowMultiplePropertiesPerLine: true } ],
|
||||
'prefer-const': 2,
|
||||
'prefer-rest-params': 2,
|
||||
'prefer-spread': 2,
|
||||
'prefer-template': 2,
|
||||
'quotes': [ 2, 'single', { avoidEscape: true } ],
|
||||
'semi': [ 2, 'always' ],
|
||||
'semi-spacing': 2,
|
||||
'space-before-blocks': 2,
|
||||
'space-before-function-paren': [ 2, { anonymous: 'never', named: 'never', 'asyncArrow': 'always'}],
|
||||
'space-in-parens': [ 2, 'never' ],
|
||||
'spaced-comment': [ 2, 'always' ],
|
||||
'strict': 2,
|
||||
'valid-typeof': 2,
|
||||
'eol-last': 0,
|
||||
'yoda': 2,
|
||||
// eslint-plugin-import options.
|
||||
'import/extensions': 2,
|
||||
'import/no-duplicates': 2,
|
||||
// eslint-plugin-react options.
|
||||
'jsx-quotes': [ 2, 'prefer-single' ],
|
||||
'react/display-name': [ 2, { ignoreTranspilerName: false } ],
|
||||
'react/forbid-prop-types': 0,
|
||||
'react/jsx-boolean-value': 2,
|
||||
'react/jsx-closing-bracket-location': 2,
|
||||
'react/jsx-curly-spacing': 2,
|
||||
'react/jsx-equals-spacing': 2,
|
||||
'react/jsx-handler-names': 2,
|
||||
'react/jsx-indent-props': [ 2, 'tab' ],
|
||||
'react/jsx-indent': [ 2, 'tab' ],
|
||||
'react/jsx-key': 2,
|
||||
'react/jsx-max-props-per-line': 0,
|
||||
'react/jsx-no-bind': 0,
|
||||
'react/jsx-no-duplicate-props': 2,
|
||||
'react/jsx-no-literals': 0,
|
||||
'react/jsx-no-undef': 0,
|
||||
'react/jsx-pascal-case': 2,
|
||||
'react/jsx-sort-prop-types': 0,
|
||||
'react/jsx-sort-props': 0,
|
||||
'react/jsx-uses-react': 2,
|
||||
'react/jsx-uses-vars': 2,
|
||||
'react/no-danger': 2,
|
||||
'react/no-deprecated': 2,
|
||||
'react/no-did-mount-set-state': 2,
|
||||
'react/no-did-update-set-state': 2,
|
||||
'react/no-direct-mutation-state': 2,
|
||||
'react/no-is-mounted': 2,
|
||||
'react/no-multi-comp': 0,
|
||||
'react/no-set-state': 0,
|
||||
'react/no-string-refs': 0,
|
||||
'react/no-unknown-property': 2,
|
||||
'react/prefer-es6-class': 2,
|
||||
'react/prop-types': [ 2, { skipUndeclared: true } ],
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 0,
|
||||
'react/jsx-wrap-multilines': [ 2,
|
||||
{
|
||||
declaration: false,
|
||||
assignment: false,
|
||||
return: true
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* <%= pkg.name %> v<%= pkg.version %>
|
||||
* <%= pkg.description %>
|
||||
* Copyright: 2017-<%= currentYear %> <%= pkg.author %>
|
||||
* License: <%= pkg.license %>
|
||||
*/
|
||||
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Multiparty Meeting</title>
|
||||
</head>
|
||||
<style>
|
||||
body{
|
||||
margin:auto;
|
||||
padding:0.5vmin;
|
||||
text-align:center;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 40%;
|
||||
width: 90%;
|
||||
transform: translate(-50%, 0%);
|
||||
background-image: url('/resources/images/background.svg');
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
input:hover {opacity:0.9;}
|
||||
input[type=text]{
|
||||
font-size: 1.5em;
|
||||
padding: 1.5vmin;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
border: 0;
|
||||
color: #fff;
|
||||
margin: 0.8vmin;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
button:hover {background-color: #28bd7b;}
|
||||
button{
|
||||
font-size: 1.5em;
|
||||
padding: 1.5vmin;
|
||||
margin: 0.8vmin;
|
||||
background-color: #38cd8b;
|
||||
border-radius: 1.8vmin;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
}
|
||||
img{
|
||||
height: 15vmin;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a>
|
||||
<img src='/resources/images/logo.svg'></img><br>
|
||||
</a>
|
||||
<input id="room" type="text" onkeypress="checkEnter(event)" value="" placeholder="your room name">
|
||||
<button onclick = "start(location.href)">Go to room</button>
|
||||
</body>
|
||||
<script>
|
||||
var room = document.getElementById("room");
|
||||
var stateObj = { foo: "bar" };
|
||||
room.addEventListener("input", function(e) {
|
||||
console.log(e.charCode);
|
||||
history.replaceState(stateObj, "Multiparty Meeting", "/"+room.value);
|
||||
},true);
|
||||
room.focus();
|
||||
function start(target){
|
||||
location.href;history.replaceState(stateObj, "Multiparty Meeting", "/");
|
||||
window.location = target;
|
||||
}
|
||||
function checkEnter(event){
|
||||
var x = event.charCode || event.keyCode;
|
||||
if (x == 13 ) {
|
||||
start(location.href);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
module.exports =
|
||||
{
|
||||
chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi',
|
||||
loginEnabled : false,
|
||||
turnServers : [
|
||||
{
|
||||
urls : [
|
||||
'turn:example.com:443?transport=tcp'
|
||||
],
|
||||
username : 'example',
|
||||
credential : 'example'
|
||||
}
|
||||
],
|
||||
requestTimeout : 10000,
|
||||
transportOptions :
|
||||
{
|
||||
tcp : true
|
||||
}
|
||||
};
|
||||
288
app/gulpfile.js
|
|
@ -1,288 +0,0 @@
|
|||
/**
|
||||
* Tasks:
|
||||
*
|
||||
* gulp dist
|
||||
* Generates the browser app in development mode (unless NODE_ENV is set
|
||||
* to 'production').
|
||||
*
|
||||
* gulp live
|
||||
* Generates the browser app in development mode (unless NODE_ENV is set
|
||||
* to 'production'), opens it and watches for changes in the source code.
|
||||
*
|
||||
* gulp
|
||||
* Alias for `gulp live`.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const gulpif = require('gulp-if');
|
||||
const gutil = require('gulp-util');
|
||||
const plumber = require('gulp-plumber');
|
||||
const rename = require('gulp-rename');
|
||||
const change = require('gulp-change');
|
||||
const header = require('gulp-header');
|
||||
const touch = require('gulp-touch-cmd');
|
||||
const browserify = require('browserify');
|
||||
const watchify = require('watchify');
|
||||
const envify = require('envify/custom');
|
||||
const uglify = require('gulp-uglify-es').default;
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const del = require('del');
|
||||
const mkdirp = require('mkdirp');
|
||||
const ncp = require('ncp');
|
||||
const eslint = require('gulp-eslint');
|
||||
const stylus = require('gulp-stylus');
|
||||
const cssBase64 = require('gulp-css-base64');
|
||||
const nib = require('nib');
|
||||
const browserSync = require('browser-sync');
|
||||
|
||||
const PKG = require('./package.json');
|
||||
const BANNER = fs.readFileSync('banner.txt').toString();
|
||||
const BANNER_OPTIONS =
|
||||
{
|
||||
pkg : PKG,
|
||||
currentYear : (new Date()).getFullYear()
|
||||
};
|
||||
const OUTPUT_DIR = '../server/public';
|
||||
const appOptions = require('./config');
|
||||
|
||||
// Set Node 'development' environment (unless externally set).
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
gutil.log(`NODE_ENV: ${process.env.NODE_ENV}`);
|
||||
|
||||
function logError(error)
|
||||
{
|
||||
gutil.log(gutil.colors.red(error.stack));
|
||||
}
|
||||
|
||||
function bundle(options)
|
||||
{
|
||||
options = options || {};
|
||||
|
||||
const watch = Boolean(options.watch);
|
||||
|
||||
let bundler = browserify(
|
||||
{
|
||||
entries : PKG.main,
|
||||
extensions : [ '.js', '.jsx' ],
|
||||
// required for sourcemaps (must be false otherwise).
|
||||
debug : process.env.NODE_ENV === 'development',
|
||||
// required for watchify.
|
||||
cache : {},
|
||||
// required for watchify.
|
||||
packageCache : {},
|
||||
// required to be true only for watchify.
|
||||
fullPaths : watch
|
||||
})
|
||||
.transform('babelify')
|
||||
.transform(envify(
|
||||
{
|
||||
NODE_ENV : process.env.NODE_ENV,
|
||||
_ : 'purge'
|
||||
}));
|
||||
|
||||
if (watch)
|
||||
{
|
||||
bundler = watchify(bundler);
|
||||
|
||||
bundler.on('update', () =>
|
||||
{
|
||||
const start = Date.now();
|
||||
|
||||
gutil.log('bundling...');
|
||||
rebundle();
|
||||
gutil.log('bundle took %sms', (Date.now() - start));
|
||||
});
|
||||
}
|
||||
|
||||
function rebundle()
|
||||
{
|
||||
return bundler.bundle()
|
||||
.on('error', logError)
|
||||
.pipe(plumber())
|
||||
.pipe(source(`${PKG.name}.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(rename(`${PKG.name}.js`))
|
||||
.pipe(gulpif(process.env.NODE_ENV === 'production',
|
||||
uglify()
|
||||
))
|
||||
.pipe(header(BANNER, BANNER_OPTIONS))
|
||||
.pipe(gulp.dest(OUTPUT_DIR));
|
||||
}
|
||||
|
||||
return rebundle();
|
||||
}
|
||||
|
||||
function changeHTML(content)
|
||||
{
|
||||
return content.replace(/chromeExtension/g, appOptions.chromeExtension);
|
||||
}
|
||||
|
||||
gulp.task('clean', () => del(OUTPUT_DIR, { force: true }));
|
||||
|
||||
const LINTING_FILES = [
|
||||
'gulpfile.js',
|
||||
'lib/**/*.js',
|
||||
'lib/**/*.jsx'
|
||||
];
|
||||
|
||||
gulp.task('lint', () =>
|
||||
{
|
||||
return gulp.src(LINTING_FILES)
|
||||
.pipe(plumber())
|
||||
.pipe(eslint())
|
||||
.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', () =>
|
||||
{
|
||||
return gulp.src('stylus/index.styl')
|
||||
.pipe(plumber())
|
||||
.pipe(stylus(
|
||||
{
|
||||
use : nib(),
|
||||
compress : process.env.NODE_ENV === 'production'
|
||||
}))
|
||||
.on('error', logError)
|
||||
.pipe(cssBase64(
|
||||
{
|
||||
baseDir : '.',
|
||||
maxWeightResource : 50000 // So big ttf fonts are not included, nice.
|
||||
}))
|
||||
.pipe(rename(`${PKG.name}.css`))
|
||||
.pipe(gulp.dest(OUTPUT_DIR))
|
||||
.pipe(touch());
|
||||
});
|
||||
|
||||
gulp.task('html', () =>
|
||||
{
|
||||
return gulp.src('*.html')
|
||||
.pipe(change(changeHTML))
|
||||
.pipe(gulp.dest(OUTPUT_DIR));
|
||||
});
|
||||
|
||||
gulp.task('resources', (done) =>
|
||||
{
|
||||
const dst = path.join(OUTPUT_DIR, 'resources');
|
||||
|
||||
mkdirp.sync(dst);
|
||||
ncp('resources', dst, { stopOnErr: true }, (error) =>
|
||||
{
|
||||
if (error && error[0].code !== 'ENOENT')
|
||||
throw new Error(`resources copy failed: ${error}`);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('bundle', () =>
|
||||
{
|
||||
return bundle({ watch: false });
|
||||
});
|
||||
|
||||
gulp.task('bundle:watch', () =>
|
||||
{
|
||||
return bundle({ watch: true });
|
||||
});
|
||||
|
||||
gulp.task('livebrowser', (done) =>
|
||||
{
|
||||
const config = require('../server/config');
|
||||
|
||||
browserSync(
|
||||
{
|
||||
open : 'external',
|
||||
host : config.domain,
|
||||
port : 3000,
|
||||
server :
|
||||
{
|
||||
baseDir : OUTPUT_DIR
|
||||
},
|
||||
https : config.tls,
|
||||
ghostMode : false,
|
||||
files : path.join(OUTPUT_DIR, '**', '*')
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('browser', (done) =>
|
||||
{
|
||||
const config = require('../server/config');
|
||||
|
||||
browserSync(
|
||||
{
|
||||
open : 'external',
|
||||
host : config.domain,
|
||||
port : 3000,
|
||||
server :
|
||||
{
|
||||
baseDir : OUTPUT_DIR
|
||||
},
|
||||
https : config.tls,
|
||||
ghostMode : false
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('watch', (done) =>
|
||||
{
|
||||
// Watch changes in HTML.
|
||||
gulp.watch([ '*.html' ], gulp.series(
|
||||
'html'
|
||||
));
|
||||
|
||||
// Watch changes in Stylus files.
|
||||
gulp.watch([ 'stylus/**/*.styl' ], gulp.series(
|
||||
'css'
|
||||
));
|
||||
|
||||
// Watch changes in resources.
|
||||
gulp.watch([ 'resources/**/*' ], gulp.series(
|
||||
'resources', 'css'
|
||||
));
|
||||
|
||||
// Watch changes in JS files.
|
||||
gulp.watch([ 'gulpfile.js', 'lib/**/*.js', 'lib/**/*.jsx' ], gulp.series(
|
||||
'lint'
|
||||
));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('dist', gulp.series(
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle',
|
||||
'html',
|
||||
'css',
|
||||
'resources'
|
||||
));
|
||||
|
||||
gulp.task('live', gulp.series(
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle:watch',
|
||||
'html',
|
||||
'css',
|
||||
'resources',
|
||||
'watch',
|
||||
'livebrowser'
|
||||
));
|
||||
|
||||
gulp.task('open', gulp.series('browser'));
|
||||
|
||||
gulp.task('default', gulp.series('live'));
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Multiparty Meeting</title>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
|
||||
<meta name='description' content='multiparty meeting - Cutting Edge WebRTC Video Conferencing'>
|
||||
|
||||
<link rel='stylesheet' href='/multiparty-meeting.css'>
|
||||
<link rel="chrome-webstore-item" href="chromeExtension">
|
||||
|
||||
<script src='/resources/js/antiglobal.js'></script>
|
||||
<script>
|
||||
window.localStorage.setItem('debug', '* -engine* -socket* -RIE* *WARN* *ERROR*');
|
||||
|
||||
if (window.antiglobal)
|
||||
{
|
||||
window.antiglobal('__multipartyMeetingScreenShareExtensionAvailable__', '___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__');
|
||||
setInterval(window.antiglobal, 180000);
|
||||
}
|
||||
</script>
|
||||
<script async src='/multiparty-meeting.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='multiparty-meeting'></div>
|
||||
<div id='multiparty-meeting-media-query-detector'></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
|
||||
class HiddenPeers extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
this.state = { className: '' };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps)
|
||||
{
|
||||
const { hiddenPeersCount } = this.props;
|
||||
|
||||
if (hiddenPeersCount !== prevProps.hiddenPeersCount)
|
||||
{
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ className: 'pulse' }, () =>
|
||||
{
|
||||
if (this.timeout)
|
||||
{
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
this.timeout = setTimeout(() =>
|
||||
{
|
||||
this.setState({ className: '' });
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
hiddenPeersCount,
|
||||
openUsersTab
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='HiddenPeers'>
|
||||
<div className={classnames('view-container', this.state.className)} onClick={() => openUsersTab()}>
|
||||
<p>+{hiddenPeersCount} <br /> participant
|
||||
{(hiddenPeersCount === 1) ? null : 's'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HiddenPeers.propTypes =
|
||||
{
|
||||
hiddenPeersCount : PropTypes.number,
|
||||
openUsersTab : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
openUsersTab : () =>
|
||||
{
|
||||
dispatch(stateActions.openToolArea());
|
||||
dispatch(stateActions.setToolTab('users'));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const HiddenPeersContainer = connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(HiddenPeers);
|
||||
|
||||
export default HiddenPeersContainer;
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import PeerView from '../VideoContainers/PeerView';
|
||||
import ScreenView from '../VideoContainers/ScreenView';
|
||||
|
||||
class Me extends React.Component
|
||||
{
|
||||
state = {
|
||||
controlsVisible : false
|
||||
};
|
||||
|
||||
handleMouseOver = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : true
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOut = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : false
|
||||
});
|
||||
};
|
||||
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._mounted = false;
|
||||
this._rootNode = null;
|
||||
this._tooltip = true;
|
||||
|
||||
// TODO: Issue when using react-tooltip in Edge:
|
||||
// https://github.com/wwayne/react-tooltip/issues/328
|
||||
if (getDeviceInfo().flag === 'msedge')
|
||||
this._tooltip = false;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
connected,
|
||||
me,
|
||||
advancedMode,
|
||||
micProducer,
|
||||
webcamProducer,
|
||||
screenProducer
|
||||
} = this.props;
|
||||
|
||||
let micState;
|
||||
|
||||
if (!me.canSendMic)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
|
||||
micState = 'on';
|
||||
else
|
||||
micState = 'off';
|
||||
|
||||
let webcamState;
|
||||
|
||||
if (!me.canSendWebcam)
|
||||
webcamState = 'unsupported';
|
||||
else if (webcamProducer)
|
||||
webcamState = 'on';
|
||||
else
|
||||
webcamState = 'off';
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamProducer) &&
|
||||
!webcamProducer.locallyPaused &&
|
||||
!webcamProducer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenProducer) &&
|
||||
!screenProducer.locallyPaused &&
|
||||
!screenProducer.remotelyPaused
|
||||
);
|
||||
|
||||
let tip;
|
||||
|
||||
if (!me.displayNameSet)
|
||||
tip = 'Click on your name to change it';
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='Me'
|
||||
ref={(node) => (this._rootNode = node)}
|
||||
data-tip={tip}
|
||||
data-tip-disable={!tip}
|
||||
data-type='dark'
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
<div className={classnames('view-container', 'webcam')}>
|
||||
<If condition={connected}>
|
||||
<div className={classnames('controls', 'visible')}>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘m‘'
|
||||
data-type='dark'
|
||||
data-place='bottom'
|
||||
data-for='me'
|
||||
className={classnames('button', 'mic', micState, {
|
||||
disabled : me.audioInProgress,
|
||||
visible : micState == 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
micState === 'on' ?
|
||||
roomClient.muteMic() :
|
||||
roomClient.unmuteMic();
|
||||
}}
|
||||
/>
|
||||
<ReactTooltip
|
||||
id='me'
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
className={classnames('button', 'webcam', webcamState, {
|
||||
disabled : me.webcamInProgress,
|
||||
visible : webcamState == 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
webcamState === 'on' ?
|
||||
roomClient.disableWebcam() :
|
||||
roomClient.enableWebcam();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<PeerView
|
||||
isMe
|
||||
advancedMode={advancedMode}
|
||||
peer={me}
|
||||
audioTrack={micProducer ? micProducer.track : null}
|
||||
volume={micProducer ? micProducer.volume : null}
|
||||
videoTrack={webcamProducer ? webcamProducer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
audioCodec={micProducer ? micProducer.codec : null}
|
||||
videoCodec={webcamProducer ? webcamProducer.codec : null}
|
||||
onChangeDisplayName={(displayName) =>
|
||||
{
|
||||
roomClient.changeDisplayName(displayName);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<If condition={screenProducer}>
|
||||
<div className={classnames('view-container', 'screen')}>
|
||||
<ScreenView
|
||||
isMe
|
||||
advancedMode={advancedMode}
|
||||
screenTrack={screenProducer ? screenProducer.track : null}
|
||||
screenVisible={screenVisible}
|
||||
screenCodec={screenProducer ? screenProducer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this._mounted = true;
|
||||
|
||||
if (this._tooltip)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (!this._mounted || this.props.me.displayNameSet)
|
||||
return;
|
||||
|
||||
ReactTooltip.show(this._rootNode);
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
if (this._tooltip)
|
||||
{
|
||||
if (nextProps.me.displayNameSet)
|
||||
ReactTooltip.hide(this._rootNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Me.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
connected : PropTypes.bool.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
micProducer : appPropTypes.Producer,
|
||||
webcamProducer : appPropTypes.Producer,
|
||||
screenProducer : appPropTypes.Producer
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const producersArray = Object.values(state.producers);
|
||||
const micProducer =
|
||||
producersArray.find((producer) => producer.source === 'mic');
|
||||
const webcamProducer =
|
||||
producersArray.find((producer) => producer.source === 'webcam');
|
||||
const screenProducer =
|
||||
producersArray.find((producer) => producer.source === 'screen');
|
||||
|
||||
return {
|
||||
connected : state.room.state === 'connected',
|
||||
me : state.me,
|
||||
micProducer : micProducer,
|
||||
webcamProducer : webcamProducer,
|
||||
screenProducer : screenProducer
|
||||
};
|
||||
};
|
||||
|
||||
const MeContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(Me));
|
||||
|
||||
export default MeContainer;
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import PeerView from '../VideoContainers/PeerView';
|
||||
import ScreenView from '../VideoContainers/ScreenView';
|
||||
|
||||
class Peer extends Component
|
||||
{
|
||||
state = {
|
||||
controlsVisible : false
|
||||
};
|
||||
|
||||
handleMouseOver = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : true
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOut = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : false
|
||||
});
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
advancedMode,
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
toggleConsumerFullscreen,
|
||||
toggleConsumerWindow,
|
||||
style,
|
||||
windowConsumer
|
||||
} = this.props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamConsumer) &&
|
||||
!webcamConsumer.locallyPaused &&
|
||||
!webcamConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
!screenConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
let videoProfile;
|
||||
|
||||
if (webcamConsumer)
|
||||
videoProfile = webcamConsumer.profile;
|
||||
|
||||
let screenProfile;
|
||||
|
||||
if (screenConsumer)
|
||||
screenProfile = screenConsumer.profile;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='Peer'
|
||||
className={classnames({
|
||||
screen : screenConsumer
|
||||
})}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
<If condition={videoVisible && !webcamConsumer.supported}>
|
||||
<div className='incompatible-video'>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={!videoVisible}>
|
||||
<div className='paused-video'>
|
||||
<p>this video is paused</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div className={classnames('view-container', 'webcam')} style={style}>
|
||||
<div className='indicators'>
|
||||
<If condition={peer.raiseHandState}>
|
||||
<div className={
|
||||
classnames(
|
||||
'icon', 'raise-hand', {
|
||||
on : peer.raiseHandState,
|
||||
off : !peer.raiseHandState
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('controls', {
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('button', 'mic', {
|
||||
on : micEnabled,
|
||||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'newwindow', {
|
||||
disabled : !videoVisible ||
|
||||
(windowConsumer === webcamConsumer.id)
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerWindow(webcamConsumer);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', {
|
||||
disabled : !videoVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(webcamConsumer);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PeerView
|
||||
advancedMode={advancedMode}
|
||||
peer={peer}
|
||||
volume={micConsumer ? micConsumer.volume : null}
|
||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
videoProfile={videoProfile}
|
||||
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<If condition={screenConsumer}>
|
||||
<div className={classnames('view-container', 'screen')} style={style}>
|
||||
<div
|
||||
className={classnames('controls', {
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('button', 'newwindow', {
|
||||
disabled : !screenVisible ||
|
||||
(windowConsumer === screenConsumer.id)
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerWindow(screenConsumer);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', {
|
||||
disabled : !screenVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(screenConsumer);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ScreenView
|
||||
advancedMode={advancedMode}
|
||||
screenTrack={screenConsumer ? screenConsumer.track : null}
|
||||
screenVisible={screenVisible}
|
||||
screenProfile={screenProfile}
|
||||
screenCodec={screenConsumer ? screenConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Peer.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer,
|
||||
windowConsumer : PropTypes.number,
|
||||
streamDimensions : PropTypes.object,
|
||||
style : PropTypes.object,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
const webcamConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
const screenConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'screen');
|
||||
|
||||
return {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
windowConsumer : state.room.windowConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerFullscreen : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||
},
|
||||
toggleConsumerWindow : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerWindow(consumer.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const PeerContainer = withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Peer));
|
||||
|
||||
export default PeerContainer;
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
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 { withRoomContext } from '../../RoomContext';
|
||||
import FullScreen from '../FullScreen';
|
||||
|
||||
class Sidebar extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.fullscreen = new FullScreen(document);
|
||||
this.state = {
|
||||
fullscreen : false
|
||||
};
|
||||
}
|
||||
|
||||
handleToggleFullscreen = () =>
|
||||
{
|
||||
if (this.fullscreen.fullscreenElement)
|
||||
{
|
||||
this.fullscreen.exitFullscreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fullscreen.requestFullscreen(document.documentElement);
|
||||
}
|
||||
};
|
||||
|
||||
handleFullscreenChange = () =>
|
||||
{
|
||||
this.setState({
|
||||
fullscreen : this.fullscreen.fullscreenElement !== null
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
if (this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
if (this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
toolbarsVisible,
|
||||
me,
|
||||
screenProducer
|
||||
} = 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'
|
||||
>
|
||||
<If condition={this.fullscreen.fullscreenEnabled}>
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', {
|
||||
on : this.state.fullscreen
|
||||
})}
|
||||
onClick={this.handleToggleFullscreen}
|
||||
data-tip='Fullscreen'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
/>
|
||||
</If>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'screen', screenState)}
|
||||
data-tip={screenTip}
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() =>
|
||||
{
|
||||
switch (screenState)
|
||||
{
|
||||
case 'on':
|
||||
{
|
||||
roomClient.disableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'off':
|
||||
{
|
||||
roomClient.enableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'need-extension':
|
||||
{
|
||||
roomClient.installExtension();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<If condition={me.loginEnabled}>
|
||||
<Choose>
|
||||
<When condition={me.loggedIn}>
|
||||
<div
|
||||
className='button logout'
|
||||
data-tip='Logout'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.logout()}
|
||||
>
|
||||
<img src={me.picture || 'resources/images/avatar-empty.jpeg'} />
|
||||
</div>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<div
|
||||
className='button login off'
|
||||
data-tip='Login'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.login()}
|
||||
/>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</If>
|
||||
<div
|
||||
className={classnames('button', 'raise-hand', {
|
||||
on : me.raiseHand,
|
||||
disabled : me.raiseHandInProgress
|
||||
})}
|
||||
data-tip='Raise hand'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.sendRaiseHandState(!me.raiseHand)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'leave-meeting')}
|
||||
data-tip='Leave meeting'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.close()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Sidebar.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
toolbarsVisible : PropTypes.bool.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
screenProducer : appPropTypes.Producer
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
toolbarsVisible : state.room.toolbarsVisible,
|
||||
screenProducer : Object.values(state.producers)
|
||||
.find((producer) => producer.source === 'screen'),
|
||||
me : state.me
|
||||
});
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(Sidebar));
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { Appear } from '../transitions';
|
||||
import Peer from '../Containers/Peer';
|
||||
import HiddenPeers from '../Containers/HiddenPeers';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
const RATIO = 1.334;
|
||||
|
||||
class Peers extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
peerWidth : 400,
|
||||
peerHeight : 300
|
||||
};
|
||||
|
||||
this.peersRef = React.createRef();
|
||||
}
|
||||
|
||||
updateDimensions = debounce(() =>
|
||||
{
|
||||
if (!this.peersRef.current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const n = this.props.boxes;
|
||||
|
||||
if (n === 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.peersRef.current.clientWidth;
|
||||
const height = this.peersRef.current.clientHeight;
|
||||
|
||||
let x, y, space;
|
||||
|
||||
for (let rows = 1; rows < 100; rows = rows + 1)
|
||||
{
|
||||
x = width / Math.ceil(n / rows);
|
||||
y = x / RATIO;
|
||||
if (height < (y * rows))
|
||||
{
|
||||
y = height / rows;
|
||||
x = RATIO * y;
|
||||
break;
|
||||
}
|
||||
space = height - (y * (rows));
|
||||
if (space < y)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x))
|
||||
{
|
||||
this.setState({
|
||||
peerWidth : 0.9 * x,
|
||||
peerHeight : 0.9 * y
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
window.addEventListener('resize', this.updateDimensions);
|
||||
const observer = new ResizeObserver(this.updateDimensions);
|
||||
|
||||
observer.observe(this.peersRef.current);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
window.removeEventListener('resize', this.updateDimensions);
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
this.updateDimensions();
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
activeSpeakerName,
|
||||
peers,
|
||||
spotlights,
|
||||
spotlightsLength
|
||||
} = this.props;
|
||||
|
||||
const style =
|
||||
{
|
||||
'width' : this.state.peerWidth,
|
||||
'height' : this.state.peerHeight
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-component='Peers' ref={this.peersRef}>
|
||||
{ Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
|
||||
{
|
||||
return (
|
||||
<Appear key={peerName} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'selected' : this.props.selectedPeerName === peerName,
|
||||
'active-speaker' : peerName === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<div className='peer-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peerName}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<div className='hidden-peer-container'>
|
||||
<If condition={spotlightsLength < Object.keys(peers).length}>
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length - spotlightsLength}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Peers.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.object.isRequired,
|
||||
boxes : PropTypes.number,
|
||||
activeSpeakerName : PropTypes.string,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const spotlights = state.room.spotlights;
|
||||
const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
|
||||
const boxes = spotlightsLength + Object.values(state.consumers)
|
||||
.filter((consumer) => consumer.source === 'screen').length;
|
||||
|
||||
return {
|
||||
peers : state.peers,
|
||||
boxes,
|
||||
activeSpeakerName : state.room.activeSpeakerName,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights,
|
||||
spotlightsLength
|
||||
};
|
||||
};
|
||||
|
||||
const PeersContainer = connect(
|
||||
mapStateToProps
|
||||
)(Peers);
|
||||
|
||||
export default PeersContainer;
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as stateActions from '../redux/stateActions';
|
||||
import { Appear } from './transitions';
|
||||
|
||||
const Notifications = ({ notifications, onClick, toolAreaOpen }) =>
|
||||
{
|
||||
return (
|
||||
<div
|
||||
data-component='Notifications'
|
||||
className={classnames({
|
||||
'toolarea-open' : toolAreaOpen
|
||||
})}
|
||||
>
|
||||
{
|
||||
notifications.map((notification) =>
|
||||
{
|
||||
return (
|
||||
<Appear key={notification.id} duration={250}>
|
||||
<div
|
||||
className={classnames('notification', notification.type)}
|
||||
onClick={() => onClick(notification.id)}
|
||||
>
|
||||
<div className='icon' />
|
||||
<p className='text'>{notification.text}</p>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Notifications.propTypes =
|
||||
{
|
||||
notifications : PropTypes.arrayOf(appPropTypes.Notification).isRequired,
|
||||
onClick : PropTypes.func.isRequired,
|
||||
toolAreaOpen : PropTypes.bool
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const { notifications } = state;
|
||||
|
||||
return {
|
||||
notifications,
|
||||
toolAreaOpen : state.toolarea.toolAreaOpen
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onClick : (notificationId) =>
|
||||
{
|
||||
dispatch(stateActions.removeNotification(notificationId));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const NotificationsContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Notifications);
|
||||
|
||||
export default NotificationsContainer;
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import PeerAudio from './PeerAudio';
|
||||
|
||||
const AudioPeer = ({ micConsumer }) =>
|
||||
{
|
||||
return (
|
||||
<PeerAudio
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AudioPeer.propTypes =
|
||||
{
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
name : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
|
||||
return {
|
||||
micConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const AudioPeerContainer = connect(
|
||||
mapStateToProps
|
||||
)(AudioPeer);
|
||||
|
||||
export default AudioPeerContainer;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import AudioPeer from './AudioPeer';
|
||||
|
||||
const AudioPeers = ({ peers }) =>
|
||||
{
|
||||
return (
|
||||
<div data-component='AudioPeers'>
|
||||
{
|
||||
peers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
<AudioPeer
|
||||
key={peer.name}
|
||||
name={peer.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AudioPeers.propTypes =
|
||||
{
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const peers = Object.values(state.peers);
|
||||
|
||||
return {
|
||||
peers
|
||||
};
|
||||
};
|
||||
|
||||
const AudioPeersContainer = connect(
|
||||
mapStateToProps
|
||||
)(AudioPeers);
|
||||
|
||||
export default AudioPeersContainer;
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import CookieConsent from 'react-cookie-consent';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import * as stateActions from '../redux/stateActions';
|
||||
import { Appear } from './transitions';
|
||||
import Me from './Containers/Me';
|
||||
import Peers from './Layouts/Peers';
|
||||
import AudioPeers from './PeerAudio/AudioPeers';
|
||||
import Notifications from './Notifications';
|
||||
import ToolArea from './ToolArea/ToolArea';
|
||||
import FullScreenView from './VideoContainers/FullScreenView';
|
||||
import VideoWindow from './VideoWindow/VideoWindow';
|
||||
import Draggable from 'react-draggable';
|
||||
import { idle } from '../utils';
|
||||
import Sidebar from './Controls/Sidebar';
|
||||
import Filmstrip from './Layouts/Filmstrip';
|
||||
|
||||
// Hide toolbars after 10 seconds of inactivity.
|
||||
const TIMEOUT = 10 * 1000;
|
||||
|
||||
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()
|
||||
{
|
||||
const {
|
||||
room,
|
||||
amActiveSpeaker,
|
||||
onRoomLinkCopy
|
||||
} = this.props;
|
||||
|
||||
const View = {
|
||||
filmstrip : Filmstrip,
|
||||
democratic : Peers
|
||||
}[room.mode];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<CookieConsent>
|
||||
This website uses cookies to enhance the user experience.
|
||||
</CookieConsent>
|
||||
|
||||
<FullScreenView advancedMode={room.advancedMode} />
|
||||
|
||||
<VideoWindow advancedMode={room.advancedMode} />
|
||||
|
||||
<div className='room-wrapper'>
|
||||
<div data-component='Logo' />
|
||||
<AudioPeers />
|
||||
|
||||
<Notifications />
|
||||
|
||||
<If condition={room.advancedMode}>
|
||||
<div className='state' data-tip='Server status'>
|
||||
<div className={classnames('icon', room.state)} />
|
||||
<p className={classnames('text', room.state)}>{room.state}</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div
|
||||
className={classnames('room-link-wrapper room-controls', {
|
||||
'visible' : this.props.room.toolbarsVisible
|
||||
})}
|
||||
>
|
||||
<div className='room-link'>
|
||||
<CopyToClipboard
|
||||
text={room.url}
|
||||
onCopy={onRoomLinkCopy}
|
||||
>
|
||||
<a
|
||||
className='link'
|
||||
href={room.url}
|
||||
target='_blank'
|
||||
data-tip='Click to copy room link'
|
||||
rel='noopener noreferrer'
|
||||
onClick={(event) =>
|
||||
{
|
||||
// If this is a 'Open in new window/tab' don't prevent
|
||||
// click default action.
|
||||
if (
|
||||
event.ctrlKey || event.shiftKey || event.metaKey ||
|
||||
// Middle click (IE > 9 and everyone else).
|
||||
(event.button && event.button === 1)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
invitation link
|
||||
</a>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<View advancedMode={room.advancedMode} />
|
||||
|
||||
<Draggable handle='.me-container' bounds='body' cancel='.display-name'>
|
||||
<div
|
||||
className={classnames('me-container', {
|
||||
'active-speaker' : amActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Me
|
||||
advancedMode={room.advancedMode}
|
||||
/>
|
||||
</div>
|
||||
</Draggable>
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToolArea />
|
||||
</div>
|
||||
</Appear>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Room.propTypes =
|
||||
{
|
||||
room : appPropTypes.Room.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
amActiveSpeaker : PropTypes.bool.isRequired,
|
||||
toolAreaOpen : PropTypes.bool.isRequired,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
onRoomLinkCopy : PropTypes.func.isRequired,
|
||||
setToolbarsVisible : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const producersArray = Object.values(state.producers);
|
||||
const screenProducer =
|
||||
producersArray.find((producer) => producer.source === 'screen');
|
||||
|
||||
return {
|
||||
room : state.room,
|
||||
me : state.me,
|
||||
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||
amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
|
||||
screenProducer : screenProducer
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onRoomLinkCopy : () =>
|
||||
{
|
||||
dispatch(requestActions.notify(
|
||||
{
|
||||
text : 'Room link copied to the clipboard'
|
||||
}));
|
||||
},
|
||||
|
||||
setToolbarsVisible : (visible) =>
|
||||
{
|
||||
dispatch(stateActions.setToolbarsVisible(visible));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const RoomContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Room);
|
||||
|
||||
export default RoomContainer;
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import MessageList from './MessageList';
|
||||
|
||||
class Chat extends Component
|
||||
{
|
||||
createNewMessage(text, sender, name, picture)
|
||||
{
|
||||
return {
|
||||
type : 'message',
|
||||
text,
|
||||
time : Date.now(),
|
||||
name,
|
||||
sender,
|
||||
picture
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
senderPlaceHolder,
|
||||
autofocus,
|
||||
displayName,
|
||||
picture
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='Chat'>
|
||||
<MessageList />
|
||||
<form
|
||||
data-component='Sender'
|
||||
onSubmit={(e) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
const userInput = e.target.message.value;
|
||||
|
||||
if (userInput)
|
||||
{
|
||||
const message = this.createNewMessage(userInput, 'response', displayName, picture);
|
||||
|
||||
roomClient.sendChatMessage(message);
|
||||
}
|
||||
e.target.message.value = '';
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type='text'
|
||||
className='new-message'
|
||||
name='message'
|
||||
placeholder={senderPlaceHolder}
|
||||
autoFocus={autofocus}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
className='send'
|
||||
value='Send'
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chat.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
senderPlaceHolder : PropTypes.string,
|
||||
autofocus : PropTypes.bool,
|
||||
displayName : PropTypes.string,
|
||||
picture : PropTypes.string
|
||||
};
|
||||
|
||||
Chat.defaultProps =
|
||||
{
|
||||
senderPlaceHolder : 'Type a message...',
|
||||
autofocus : false,
|
||||
displayName : null
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
displayName : state.me.displayName,
|
||||
picture : state.me.picture
|
||||
};
|
||||
};
|
||||
|
||||
const ChatContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(Chat));
|
||||
|
||||
export default ChatContainer;
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import marked from 'marked';
|
||||
import { connect } from 'react-redux';
|
||||
import scrollToBottom from '../scrollToBottom';
|
||||
|
||||
const linkRenderer = new marked.Renderer();
|
||||
|
||||
linkRenderer.link = (href, title, text) =>
|
||||
{
|
||||
title = title ? title : href;
|
||||
text = text ? text : href;
|
||||
|
||||
return (`<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`);
|
||||
};
|
||||
|
||||
class MessageList extends Component
|
||||
{
|
||||
getTimeString(time)
|
||||
{
|
||||
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
chatmessages
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='MessageList' id='messages'>
|
||||
<Choose>
|
||||
<When condition={chatmessages.length > 0}>
|
||||
{
|
||||
chatmessages.map((message, i) =>
|
||||
{
|
||||
const messageTime = new Date(message.time);
|
||||
|
||||
const picture = (message.sender === 'response' ?
|
||||
message.picture : this.props.myPicture) || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<div className='message' key={i}>
|
||||
<div className={message.sender}>
|
||||
<img className='message-avatar' src={picture} />
|
||||
|
||||
<div className='message-content'>
|
||||
<div
|
||||
className='message-text'
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html : marked.parse(
|
||||
message.text,
|
||||
{ sanitize: true, renderer: linkRenderer }
|
||||
) }}
|
||||
/>
|
||||
|
||||
<span className='message-time'>
|
||||
{message.name} - {this.getTimeString(messageTime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</When>
|
||||
<Otherwise>
|
||||
<div className='empty'>
|
||||
<p>No one has said anything yet...</p>
|
||||
</div>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageList.propTypes =
|
||||
{
|
||||
chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
myPicture : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
chatmessages : state.chatmessages,
|
||||
myPicture : state.me.picture
|
||||
};
|
||||
};
|
||||
|
||||
const MessageListContainer = compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(MessageList);
|
||||
|
||||
export default MessageListContainer;
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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>
|
||||
);
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import magnet from 'magnet-uri';
|
||||
|
||||
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
class File extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
torrentSupport,
|
||||
file
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='file-entry'>
|
||||
<img className='file-avatar' src={file.picture || DEFAULT_PICTURE} />
|
||||
|
||||
<div className='file-content'>
|
||||
<Choose>
|
||||
<When condition={file.me}>
|
||||
<p>You shared a file.</p>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>{file.displayName} shared a file.</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<If condition={!file.active && !file.files}>
|
||||
<div className='file-info'>
|
||||
<Choose>
|
||||
<When condition={torrentSupport}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.handleDownload(file.magnetUri);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/download-icon.svg' />
|
||||
</span>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>
|
||||
Your browser does not support downloading files using WebTorrent.
|
||||
</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
<p>{magnet.decode(file.magnetUri).dn}</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={file.timeout}>
|
||||
<Fragment>
|
||||
<p>
|
||||
If this process takes a long time, there might not be anyone seeding
|
||||
this torrent. Try asking someone to reupload the file that you want.
|
||||
</p>
|
||||
</Fragment>
|
||||
</If>
|
||||
|
||||
<If condition={file.active}>
|
||||
<progress value={file.progress} />
|
||||
</If>
|
||||
|
||||
<If condition={file.files}>
|
||||
<Fragment>
|
||||
<p>File finished downloading.</p>
|
||||
|
||||
{file.files.map((sharedFile, i) => (
|
||||
<div className='file-info' key={i}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.saveFile(sharedFile);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/save-icon.svg' />
|
||||
</span>
|
||||
|
||||
<p>{sharedFile.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File.propTypes = {
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { magnetUri }) =>
|
||||
{
|
||||
return {
|
||||
file : state.files[magnetUri],
|
||||
torrentSupport : state.room.torrentSupport
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(File));
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import scrollToBottom from '../scrollToBottom';
|
||||
import File from './File';
|
||||
|
||||
class FileList extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
files
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='shared-files'>
|
||||
{ Object.keys(files).map((magnetUri) =>
|
||||
<File key={magnetUri} magnetUri={magnetUri} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileList.propTypes = {
|
||||
files : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
files : state.files
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(FileList);
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import FileList from './FileList';
|
||||
|
||||
class FileSharing extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._fileInput = React.createRef();
|
||||
}
|
||||
|
||||
handleFileChange = async (event) =>
|
||||
{
|
||||
if (event.target.files.length > 0)
|
||||
{
|
||||
this.props.roomClient.shareFiles(event.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = () =>
|
||||
{
|
||||
if (this.props.torrentSupport)
|
||||
{
|
||||
// We want to open the file dialog when we click a button
|
||||
// instead of actually rendering the input element itself.
|
||||
this._fileInput.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
torrentSupport
|
||||
} = this.props;
|
||||
|
||||
const buttonDescription = torrentSupport ?
|
||||
'Share file' : 'File sharing not supported';
|
||||
|
||||
return (
|
||||
<div data-component='FileSharing'>
|
||||
<div className='sharing-toolbar'>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
ref={this._fileInput}
|
||||
type='file'
|
||||
onChange={this.handleFileChange}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<div
|
||||
type='button'
|
||||
onClick={this.handleClick}
|
||||
className={classNames('share-file', {
|
||||
disabled : !torrentSupport
|
||||
})}
|
||||
>
|
||||
<span>{buttonDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileSharing.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
torrentSupport : state.room.torrentSupport,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(FileSharing));
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
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'>
|
||||
<If condition={me.raisedHand}>
|
||||
<div className='icon raise-hand on' />
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
ListMe.propTypes = {
|
||||
me : Me.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
me : state.me
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(ListMe);
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
|
||||
const ListPeer = (props) =>
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
peer,
|
||||
micConsumer,
|
||||
screenConsumer
|
||||
} = props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
!screenConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const picture = peer.picture || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<div data-component='ListPeer'>
|
||||
<img className='avatar' src={picture} />
|
||||
|
||||
<div className='peer-info'>
|
||||
{peer.displayName}
|
||||
</div>
|
||||
<div className='indicators'>
|
||||
<If condition={peer.raiseHandState}>
|
||||
<div className={
|
||||
classnames(
|
||||
'icon', 'raise-hand', {
|
||||
on : peer.raiseHandState,
|
||||
off : !peer.raiseHandState
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
|
||||
</div>
|
||||
<div className='controls'>
|
||||
<If condition={screenConsumer}>
|
||||
<div
|
||||
className={classnames('button', 'screen', {
|
||||
on : screenVisible,
|
||||
off : !screenVisible,
|
||||
disabled : peer.peerScreenInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
screenVisible ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', false);
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
<div
|
||||
className={classnames('button', 'mic', {
|
||||
on : micEnabled,
|
||||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListPeer.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
const webcamConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
const screenConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'screen');
|
||||
|
||||
return {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const ListPeerContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(ListPeer));
|
||||
|
||||
export default ListPeerContainer;
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import ListPeer from './ListPeer';
|
||||
import ListMe from './ListMe';
|
||||
|
||||
const ParticipantList =
|
||||
({
|
||||
roomClient,
|
||||
advancedMode,
|
||||
peers,
|
||||
selectedPeerName,
|
||||
spotlights
|
||||
}) => (
|
||||
<div data-component='ParticipantList'>
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Me:</li>
|
||||
<ListMe />
|
||||
</ul>
|
||||
<br />
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Participants in Spotlight:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return (spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames('list-item', {
|
||||
selected : peer.name === selectedPeerName
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<br />
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Passive Participants:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return !(spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames('list-item', {
|
||||
selected : peer.name === selectedPeerName
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
ParticipantList.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const peersArray = Object.values(state.peers);
|
||||
|
||||
return {
|
||||
peers : peersArray,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights : state.room.spotlights
|
||||
};
|
||||
};
|
||||
|
||||
const ParticipantListContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(ParticipantList));
|
||||
|
||||
export default ParticipantListContainer;
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import * as stateActions from '../../../redux/stateActions';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dropdown from 'react-dropdown';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
const modes = [ {
|
||||
value : 'democratic',
|
||||
label : 'Democratic view'
|
||||
}, {
|
||||
value : 'filmstrip',
|
||||
label : 'Filmstrip view'
|
||||
} ];
|
||||
|
||||
const findOption = (options, value) => options.find((option) => option.value === value);
|
||||
|
||||
const Settings = ({
|
||||
roomClient,
|
||||
room,
|
||||
me,
|
||||
onToggleAdvancedMode,
|
||||
handleChangeMode
|
||||
}) =>
|
||||
{
|
||||
let webcams;
|
||||
|
||||
if (me.webcamDevices)
|
||||
webcams = Array.from(me.webcamDevices.values());
|
||||
else
|
||||
webcams = [];
|
||||
|
||||
let audioDevices;
|
||||
let audioDevicesText;
|
||||
|
||||
if (me.canChangeAudioDevice)
|
||||
audioDevicesText = 'Select audio input device';
|
||||
else
|
||||
audioDevicesText = 'Unable to select audio input device';
|
||||
|
||||
if (me.audioDevices)
|
||||
audioDevices = Array.from(me.audioDevices.values());
|
||||
else
|
||||
audioDevices = [];
|
||||
|
||||
return (
|
||||
<div className='settings'>
|
||||
<Dropdown
|
||||
options={webcams}
|
||||
value={findOption(webcams, me.selectedWebcam)}
|
||||
onChange={(webcam) => roomClient.changeWebcam(webcam.value)}
|
||||
placeholder={'Select camera'}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
disabled={!me.canChangeAudioDevice}
|
||||
options={audioDevices}
|
||||
value={findOption(audioDevices, me.selectedAudioDevice)}
|
||||
onChange={(device) => roomClient.changeAudioDevice(device.value)}
|
||||
placeholder={audioDevicesText}
|
||||
/>
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘a‘'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<input
|
||||
id='room-mode'
|
||||
type='checkbox'
|
||||
checked={room.advancedMode}
|
||||
onChange={onToggleAdvancedMode}
|
||||
/>
|
||||
<label htmlFor='room-mode'>Advanced mode</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tip='keyboard shortcut: type a digit'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<Dropdown
|
||||
options={modes}
|
||||
value={findOption(modes, room.mode)}
|
||||
onChange={(mode) => handleChangeMode(mode.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
onToggleAdvancedMode : PropTypes.func.isRequired,
|
||||
handleChangeMode : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
me : state.me,
|
||||
room : state.room
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
|
||||
handleChangeMode : stateActions.setDisplayMode
|
||||
};
|
||||
|
||||
const SettingsContainer = withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Settings));
|
||||
|
||||
export default SettingsContainer;
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
|
||||
const TabHeader = ({ currentToolTab, setToolTab, id, name, badge }) => (
|
||||
<div
|
||||
className={classNames('tab-header', {
|
||||
checked : currentToolTab === id
|
||||
})}
|
||||
onClick={() => setToolTab(id)}
|
||||
>
|
||||
{name}
|
||||
|
||||
<If condition={badge > 0}>
|
||||
<span className='badge'>{badge}</span>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
|
||||
TabHeader.propTypes = {
|
||||
currentToolTab : PropTypes.string.isRequired,
|
||||
setToolTab : PropTypes.func.isRequired,
|
||||
id : PropTypes.string.isRequired,
|
||||
name : PropTypes.string.isRequired,
|
||||
badge : PropTypes.number
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentToolTab : state.toolarea.currentToolTab
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setToolTab : stateActions.setToolTab
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TabHeader);
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import ParticipantList from './ParticipantList/ParticipantList';
|
||||
import Chat from './Chat/Chat';
|
||||
import Settings from './Settings/Settings';
|
||||
import FileSharing from './FileSharing/FileSharing';
|
||||
import TabHeader from './TabHeader';
|
||||
|
||||
class ToolArea extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
currentToolTab,
|
||||
toolAreaOpen,
|
||||
unreadMessages,
|
||||
unreadFiles,
|
||||
toggleToolArea,
|
||||
unread
|
||||
} = this.props;
|
||||
|
||||
const VisibleTab = {
|
||||
chat : Chat,
|
||||
files : FileSharing,
|
||||
users : ParticipantList,
|
||||
settings : Settings
|
||||
}[currentToolTab];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className={classNames('toolarea-shade', {
|
||||
open : toolAreaOpen
|
||||
})}
|
||||
onClick={toggleToolArea}
|
||||
/>
|
||||
|
||||
<div
|
||||
data-component='ToolArea'
|
||||
className={classNames({
|
||||
open : toolAreaOpen
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className='toolarea-button'
|
||||
onClick={toggleToolArea}
|
||||
>
|
||||
<span className='content'>
|
||||
<div
|
||||
className='toolarea-icon'
|
||||
/>
|
||||
<p>Toolbox</p>
|
||||
</span>
|
||||
<If condition={!toolAreaOpen && unread > 0}>
|
||||
<span className={classNames('badge', { long: unread >= 10 })}>
|
||||
{unread}
|
||||
</span>
|
||||
</If>
|
||||
</div>
|
||||
<div className='tab-headers'>
|
||||
<TabHeader
|
||||
id='chat'
|
||||
name='Chat'
|
||||
badge={unreadMessages}
|
||||
/>
|
||||
|
||||
<TabHeader
|
||||
id='files'
|
||||
name='Files'
|
||||
badge={unreadFiles}
|
||||
/>
|
||||
|
||||
<TabHeader
|
||||
id='users'
|
||||
name='Users'
|
||||
/>
|
||||
|
||||
<TabHeader
|
||||
id='settings'
|
||||
name='Settings'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='tab'>
|
||||
<VisibleTab />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ToolArea.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
currentToolTab : PropTypes.string.isRequired,
|
||||
setToolTab : PropTypes.func.isRequired,
|
||||
unreadMessages : PropTypes.number.isRequired,
|
||||
unreadFiles : PropTypes.number.isRequired,
|
||||
toolAreaOpen : PropTypes.bool,
|
||||
toggleToolArea : PropTypes.func.isRequired,
|
||||
closeToolArea : PropTypes.func.isRequired,
|
||||
unread : PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentToolTab : state.toolarea.currentToolTab,
|
||||
unreadMessages : state.toolarea.unreadMessages,
|
||||
unreadFiles : state.toolarea.unreadFiles,
|
||||
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||
unread : state.toolarea.unreadMessages +
|
||||
state.toolarea.unreadFiles
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setToolTab : stateActions.setToolTab,
|
||||
toggleToolArea : stateActions.toggleToolArea,
|
||||
closeToolArea : stateActions.closeToolArea
|
||||
};
|
||||
|
||||
const ToolAreaContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ToolArea);
|
||||
|
||||
export default ToolAreaContainer;
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import FullView from './FullView';
|
||||
|
||||
const FullScreenView = (props) =>
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
consumer,
|
||||
toggleConsumerFullscreen,
|
||||
toolbarsVisible
|
||||
} = props;
|
||||
|
||||
if (!consumer)
|
||||
return null;
|
||||
|
||||
const consumerVisible = (
|
||||
Boolean(consumer) &&
|
||||
!consumer.locallyPaused &&
|
||||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<div data-component='FullScreenView'>
|
||||
<If condition={consumerVisible && !consumer.supported}>
|
||||
<div className='incompatible-video'>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div className='controls'>
|
||||
<div
|
||||
className={classnames('button', 'exitfullscreen', 'room-controls', {
|
||||
visible : toolbarsVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(consumer);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FullView
|
||||
advancedMode={advancedMode}
|
||||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FullScreenView.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
consumer : appPropTypes.Consumer,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toolbarsVisible : PropTypes.bool
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
consumer : state.consumers[state.room.fullScreenConsumer],
|
||||
toolbarsVisible : state.room.toolbarsVisible
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerFullscreen : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const FullScreenViewContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FullScreenView);
|
||||
|
||||
export default FullScreenViewContainer;
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import EditableInput from '../Controls/EditableInput';
|
||||
|
||||
export default class PeerView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
volume : 0, // Integer from 0 to 10.,
|
||||
videoWidth : null,
|
||||
videoHeight : null
|
||||
};
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._videoResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
peer,
|
||||
volume,
|
||||
advancedMode,
|
||||
videoVisible,
|
||||
videoProfile,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
onChangeDisplayName
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
videoWidth,
|
||||
videoHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='PeerView'>
|
||||
<div className='info'>
|
||||
<If condition={advancedMode}>
|
||||
<div className={classnames('media', { 'is-me': isMe })}>
|
||||
<div className='box'>
|
||||
<If condition={audioCodec}>
|
||||
<p className='codec'>{audioCodec}</p>
|
||||
</If>
|
||||
|
||||
<If condition={videoCodec}>
|
||||
<p className='codec'>{videoCodec} {videoProfile}</p>
|
||||
</If>
|
||||
|
||||
<If condition={(videoVisible && videoWidth !== null)}>
|
||||
<p className='resolution'>{videoWidth}x{videoHeight}</p>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div className={classnames('peer', { 'is-me': isMe })}>
|
||||
<Choose>
|
||||
<When condition={isMe}>
|
||||
<EditableInput
|
||||
value={peer.displayName}
|
||||
propName='displayName'
|
||||
className='display-name editable'
|
||||
classLoading='loading'
|
||||
classInvalid='invalid'
|
||||
shouldBlockWhileLoading
|
||||
editProps={{
|
||||
maxLength : 20,
|
||||
autoCorrect : false,
|
||||
spellCheck : false
|
||||
}}
|
||||
onChange={({ displayName }) => onChangeDisplayName(displayName)}
|
||||
/>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<span className='display-name'>
|
||||
{peer.displayName}
|
||||
</span>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<If condition={advancedMode}>
|
||||
<div className='row'>
|
||||
<span
|
||||
className={classnames('device-icon', peer.device.flag)}
|
||||
/>
|
||||
<span className='device-version'>
|
||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||
</span>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({
|
||||
hidden : !videoVisible,
|
||||
'is-me' : isMe,
|
||||
loading : videoProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={isMe}
|
||||
/>
|
||||
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${volume}`)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack, videoTrack } = this.props;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack, videoTrack } = nextProps;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
|
||||
}
|
||||
|
||||
_setTracks(audioTrack, videoTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
this._videoTrack = videoTrack;
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
this._hideVideoResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (audioTrack || videoTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
if (videoTrack)
|
||||
stream.addTrack(videoTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (videoTrack)
|
||||
this._showVideoResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { videoWidth, videoHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
videoWidth : video.videoWidth,
|
||||
videoHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideVideoResolution()
|
||||
{
|
||||
this.setState({ videoWidth: null, videoHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
PeerView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
peer : PropTypes.oneOfType(
|
||||
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
audioTrack : PropTypes.any,
|
||||
volume : PropTypes.number,
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool.isRequired,
|
||||
videoProfile : PropTypes.string,
|
||||
audioCodec : PropTypes.string,
|
||||
videoCodec : PropTypes.string,
|
||||
onChangeDisplayName : PropTypes.func
|
||||
};
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class ScreenView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
screenWidth : null,
|
||||
screenHeight : null
|
||||
};
|
||||
|
||||
// Latest received screen track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._screenTrack = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._screenResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
advancedMode,
|
||||
screenVisible,
|
||||
screenProfile,
|
||||
screenCodec
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
screenWidth,
|
||||
screenHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='ScreenView'>
|
||||
<div className='info'>
|
||||
<If condition={advancedMode}>
|
||||
<div className={classnames('media', { 'is-me': isMe })}>
|
||||
<If condition={screenVisible}>
|
||||
<div className='box'>
|
||||
<If condition={screenCodec}>
|
||||
<p className='codec'>{screenCodec} {screenProfile}</p>
|
||||
</If>
|
||||
|
||||
<If condition={(screenVisible && screenWidth !== null)}>
|
||||
<p className='resolution'>{screenWidth}x{screenHeight}</p>
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({
|
||||
hidden : !screenVisible,
|
||||
'is-me' : isMe,
|
||||
loading : screenProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={Boolean(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { screenTrack } = this.props;
|
||||
|
||||
this._setTracks(screenTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._screenResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { screenTrack } = nextProps;
|
||||
|
||||
this._setTracks(screenTrack);
|
||||
}
|
||||
|
||||
_setTracks(screenTrack)
|
||||
{
|
||||
if (this._screenTrack === screenTrack)
|
||||
return;
|
||||
|
||||
this._screenTrack = screenTrack;
|
||||
|
||||
clearInterval(this._screenResolutionTimer);
|
||||
this._hideScreenResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (screenTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (screenTrack)
|
||||
stream.addTrack(screenTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (screenTrack)
|
||||
this._showScreenResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_showScreenResolution()
|
||||
{
|
||||
this._screenResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { screenWidth, screenHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === screenWidth && video.videoHeight === screenHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
screenWidth : video.videoWidth,
|
||||
screenHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideScreenResolution()
|
||||
{
|
||||
this.setState({ screenWidth: null, screenHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
ScreenView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
advancedMode : PropTypes.bool,
|
||||
screenTrack : PropTypes.any,
|
||||
screenVisible : PropTypes.bool,
|
||||
screenProfile : PropTypes.string,
|
||||
screenCodec : PropTypes.string
|
||||
};
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
const Appear = ({ duration, children }) => (
|
||||
<CSSTransition
|
||||
in
|
||||
classNames='Appear'
|
||||
timeout={duration || 1000}
|
||||
appear
|
||||
>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
Appear.propTypes =
|
||||
{
|
||||
duration : PropTypes.number,
|
||||
children : PropTypes.any
|
||||
};
|
||||
|
||||
export { Appear };
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import jsCookie from 'js-cookie';
|
||||
|
||||
const USER_COOKIE = 'multiparty-meeting.user';
|
||||
const DEVICES_COOKIE = 'multiparty-meeting.devices';
|
||||
|
||||
export function getUser()
|
||||
{
|
||||
return jsCookie.getJSON(USER_COOKIE);
|
||||
}
|
||||
|
||||
export function setUser({ displayName })
|
||||
{
|
||||
jsCookie.set(USER_COOKIE, { displayName });
|
||||
}
|
||||
|
||||
export function getDevices()
|
||||
{
|
||||
return jsCookie.getJSON(DEVICES_COOKIE);
|
||||
}
|
||||
|
||||
export function setDevices({ webcamEnabled })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
|
||||
}
|
||||
|
||||
export function setAudioDevice({ audioDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { audioDeviceId });
|
||||
}
|
||||
|
||||
export function setVideoDevice({ videoDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { videoDeviceId });
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
import domready from 'domready';
|
||||
import UrlParse from 'url-parse';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import randomString from 'random-string';
|
||||
import Logger from './Logger';
|
||||
import * as utils from './utils';
|
||||
import RoomClient from './RoomClient';
|
||||
import RoomContext from './RoomContext';
|
||||
import * as cookiesManager from './cookiesManager';
|
||||
import * as stateActions from './redux/stateActions';
|
||||
import Room from './components/Room';
|
||||
import { loginEnabled } from '../config';
|
||||
import { store } from './store';
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
let roomClient;
|
||||
|
||||
RoomClient.init({ store });
|
||||
|
||||
domready(() =>
|
||||
{
|
||||
logger.debug('DOM ready');
|
||||
|
||||
// Load stuff and run
|
||||
utils.initialize()
|
||||
.then(run);
|
||||
});
|
||||
|
||||
function run()
|
||||
{
|
||||
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
|
||||
|
||||
const peerName = randomString({ length: 8 }).toLowerCase();
|
||||
const urlParser = new UrlParse(window.location.href, true);
|
||||
let roomId = (urlParser.pathname).substr(1)
|
||||
? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase();
|
||||
const produce = urlParser.query.produce !== 'false';
|
||||
let displayName = urlParser.query.displayName;
|
||||
const isSipEndpoint = urlParser.query.sipEndpoint === 'true';
|
||||
const useSimulcast = urlParser.query.simulcast === 'true';
|
||||
|
||||
if (!roomId)
|
||||
{
|
||||
roomId = randomString({ length: 8 }).toLowerCase();
|
||||
|
||||
urlParser.query.roomId = roomId;
|
||||
window.history.pushState('', '', urlParser.toString());
|
||||
}
|
||||
|
||||
// Get the effective/shareable Room URL.
|
||||
const roomUrlParser = new UrlParse(window.location.href, true);
|
||||
|
||||
for (const key of Object.keys(roomUrlParser.query))
|
||||
{
|
||||
// Don't keep some custom params.
|
||||
switch (key)
|
||||
{
|
||||
case 'roomId':
|
||||
case 'simulcast':
|
||||
break;
|
||||
default:
|
||||
delete roomUrlParser.query[key];
|
||||
}
|
||||
}
|
||||
delete roomUrlParser.hash;
|
||||
|
||||
const roomUrl = roomUrlParser.toString();
|
||||
|
||||
// Get displayName from cookie (if not already given as param).
|
||||
const userCookie = cookiesManager.getUser() || {};
|
||||
let displayNameSet;
|
||||
|
||||
if (!displayName)
|
||||
displayName = userCookie.displayName;
|
||||
|
||||
if (displayName)
|
||||
{
|
||||
displayNameSet = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
displayName = 'Guest';
|
||||
displayNameSet = false;
|
||||
}
|
||||
|
||||
// Get current device.
|
||||
const device = getDeviceInfo();
|
||||
|
||||
// If a SIP endpoint mangle device info.
|
||||
if (isSipEndpoint)
|
||||
{
|
||||
device.flag = 'sipendpoint';
|
||||
device.name = 'SIP Endpoint';
|
||||
device.version = undefined;
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setRoomUrl(roomUrl));
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setMe({ peerName, displayName, displayNameSet, device, loginEnabled }));
|
||||
|
||||
roomClient = new RoomClient(
|
||||
{ roomId, peerName, displayName, device, useSimulcast, produce });
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<RoomContext.Provider value={roomClient}>
|
||||
<Room />
|
||||
</RoomContext.Provider>
|
||||
</Provider>,
|
||||
document.getElementById('multiparty-meeting')
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Debugging stuff.
|
||||
global.CLIENT = roomClient;
|
||||
|
||||
setInterval(() =>
|
||||
{
|
||||
if (!roomClient._room.peers[0])
|
||||
{
|
||||
delete global.CONSUMER;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const peer = roomClient._room.peers[0];
|
||||
|
||||
global.CONSUMER = peer.consumers[peer.consumers.length - 1];
|
||||
}, 2000);
|
||||
|
||||
global.sendSdp = function()
|
||||
{
|
||||
logger.debug('---------- SEND_TRANSPORT LOCAL SDP OFFER:');
|
||||
logger.debug(
|
||||
roomClient._sendTransport._handler._pc.localDescription.sdp);
|
||||
|
||||
logger.debug('---------- SEND_TRANSPORT REMOTE SDP ANSWER:');
|
||||
logger.debug(
|
||||
roomClient._sendTransport._handler._pc.remoteDescription.sdp);
|
||||
};
|
||||
|
||||
global.recvSdp = function()
|
||||
{
|
||||
logger.debug('---------- RECV_TRANSPORT REMOTE SDP OFFER:');
|
||||
logger.debug(
|
||||
roomClient._recvTransport._handler._pc.remoteDescription.sdp);
|
||||
|
||||
logger.debug('---------- RECV_TRANSPORT LOCAL SDP ANSWER:');
|
||||
logger.debug(
|
||||
roomClient._recvTransport._handler._pc.localDescription.sdp);
|
||||
};
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
# APP STATE
|
||||
|
||||
```js
|
||||
{
|
||||
peerWidth : 200,
|
||||
peerHeight : 150,
|
||||
room :
|
||||
{
|
||||
url : 'https://example.io/?&roomId=d0el8y34',
|
||||
state : 'connected', // new/connecting/connected/closed
|
||||
activeSpeakerName : 'alice'
|
||||
},
|
||||
me :
|
||||
{
|
||||
name : 'bob',
|
||||
displayName : 'Bob McFLower',
|
||||
displayNameSet : false, // true if got from cookie or manually set.
|
||||
device : { flag: 'firefox', name: 'Firefox', version: '61' },
|
||||
canSendMic : true,
|
||||
canSendWebcam : true,
|
||||
webcamInProgress : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
restartIceInProgress : false
|
||||
},
|
||||
producers :
|
||||
{
|
||||
1111 :
|
||||
{
|
||||
id : 1111,
|
||||
source : 'mic', // mic/webcam,
|
||||
locallyPaused : true,
|
||||
remotelyPaused : false,
|
||||
track : MediaStreamTrack,
|
||||
codec : 'opus'
|
||||
},
|
||||
1112 :
|
||||
{
|
||||
id : 1112,
|
||||
source : 'webcam', // mic/webcam
|
||||
deviceLabel : 'Macbook Webcam',
|
||||
type : 'front', // front/back
|
||||
locallyPaused : false,
|
||||
remotelyPaused : false,
|
||||
track : MediaStreamTrack,
|
||||
codec : 'vp8',
|
||||
}
|
||||
},
|
||||
peers :
|
||||
{
|
||||
'alice' :
|
||||
{
|
||||
name : 'alice',
|
||||
displayName : 'Alice Thomsom',
|
||||
raiseHandState : false,
|
||||
device : { flag: 'chrome', name: 'Chrome', version: '58' },
|
||||
consumers : [ 5551, 5552 ]
|
||||
}
|
||||
},
|
||||
consumers :
|
||||
{
|
||||
5551 :
|
||||
{
|
||||
id : 5551,
|
||||
peerName : 'alice',
|
||||
source : 'mic', // mic/webcam
|
||||
supported : true,
|
||||
locallyPaused : false,
|
||||
remotelyPaused : false,
|
||||
profile : 'default',
|
||||
track : MediaStreamTrack,
|
||||
codec : 'opus'
|
||||
},
|
||||
5552 :
|
||||
{
|
||||
id : 5552,
|
||||
peerName : 'alice',
|
||||
source : 'webcam',
|
||||
supported : false,
|
||||
locallyPaused : false,
|
||||
remotelyPaused : true,
|
||||
profile : 'medium',
|
||||
track : null,
|
||||
codec : 'h264'
|
||||
}
|
||||
},
|
||||
notifications :
|
||||
[
|
||||
{
|
||||
id : 'qweasdw43we',
|
||||
type : 'info' // info/error
|
||||
text : 'You joined the room'
|
||||
},
|
||||
{
|
||||
id : 'j7sdhkjjkcc',
|
||||
type : 'error'
|
||||
text : 'Could not add webcam'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import {
|
||||
applyMiddleware,
|
||||
createStore,
|
||||
compose
|
||||
} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import reducers from './redux/reducers';
|
||||
|
||||
const reduxMiddlewares =
|
||||
[
|
||||
thunk
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development')
|
||||
{
|
||||
const reduxLogger = createLogger(
|
||||
{
|
||||
// filter VOLUME level actions from log
|
||||
predicate : (getState, action) => ! (action.type == 'SET_PRODUCER_VOLUME'
|
||||
|| action.type == 'SET_CONSUMER_VOLUME'),
|
||||
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
|
||||
);
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
let mediaQueryDetectorElem;
|
||||
|
||||
export function initialize()
|
||||
{
|
||||
// Media query detector stuff.
|
||||
mediaQueryDetectorElem =
|
||||
document.getElementById('multiparty-meeting-media-query-detector');
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export function isDesktop()
|
||||
{
|
||||
return Boolean(mediaQueryDetectorElem.offsetParent);
|
||||
}
|
||||
|
||||
export function isMobile()
|
||||
{
|
||||
return !mediaQueryDetectorElem.offsetParent;
|
||||
}
|
||||
|
||||
export function getBrowserType()
|
||||
{
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
|
||||
// Firefox
|
||||
if (ua.indexOf('firefox') !== -1)
|
||||
{
|
||||
return 'firefox';
|
||||
}
|
||||
|
||||
// Chrome
|
||||
if (ua.indexOf('chrome') !== -1 && ua.indexOf('edge') === -1)
|
||||
{
|
||||
return 'chrome';
|
||||
}
|
||||
|
||||
// MSEdge
|
||||
if (ua.indexOf('edge') !== -1)
|
||||
{
|
||||
return 'edge';
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
};
|
||||
459
app/package.json
|
|
@ -1,82 +1,381 @@
|
|||
{
|
||||
"name": "multiparty-meeting",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting service",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.jsx",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"classnames": "^2.2.6",
|
||||
"create-torrent": "^3.32.1",
|
||||
"debug": "^4.1.0",
|
||||
"domready": "^1.0.8",
|
||||
"drag-drop": "^4.2.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"hark": "^1.2.2",
|
||||
"js-cookie": "^2.2.0",
|
||||
"magnet-uri": "^5.2.3",
|
||||
"marked": "^0.5.1",
|
||||
"mediasoup-client": "^2.3.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^16.5.2",
|
||||
"react-cookie-consent": "^1.9.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-draggable": "^3.0.5",
|
||||
"react-dropdown": "^1.5.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-spinner": "^0.2.7",
|
||||
"react-tooltip": "^3.9.0",
|
||||
"react-transition-group": "^2.5.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"riek": "^1.1.0",
|
||||
"socket.io-client": "^2.1.1",
|
||||
"url-parse": "^1.4.3",
|
||||
"webtorrent": "^0.102.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-plugin-jsx-control-statements": "^3.2.8",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babelify": "^10.0.0",
|
||||
"browser-sync": "^2.26.3",
|
||||
"browserify": "^16.2.3",
|
||||
"del": "^3.0.0",
|
||||
"envify": "^4.1.0",
|
||||
"eslint": "^5.7.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jsx-control-statements": "^2.2.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-change": "^1.0.0",
|
||||
"gulp-css-base64": "^1.3.4",
|
||||
"gulp-eslint": "^5.0.0",
|
||||
"gulp-header": "^2.0.5",
|
||||
"gulp-if": "^2.0.2",
|
||||
"gulp-plumber": "^1.2.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-stylus": "^2.7.0",
|
||||
"gulp-touch-cmd": "0.0.1",
|
||||
"gulp-uglify-es": "^1.0.4",
|
||||
"gulp-util": "^3.0.8",
|
||||
"lodash": "^4.17.10",
|
||||
"mkdirp": "^0.5.1",
|
||||
"ncp": "^2.0.0",
|
||||
"nib": "^1.1.2",
|
||||
"supports-color": "^5.5.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"watchify": "^3.11.0"
|
||||
}
|
||||
"name": "multiparty-meeting",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting service",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^3.9.2",
|
||||
"@material-ui/icons": "^3.0.2",
|
||||
"create-torrent": "^3.33.0",
|
||||
"domready": "^1.0.8",
|
||||
"file-saver": "^2.0.1",
|
||||
"hark": "^1.2.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.6.1",
|
||||
"mediasoup-client": "^2.4.10",
|
||||
"notistack": "^0.5.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^16.8.5",
|
||||
"react-cookie-consent": "^2.2.2",
|
||||
"react-dom": "^16.8.5",
|
||||
"react-draggable": "^3.2.1",
|
||||
"react-redux": "^6.0.1",
|
||||
"react-scripts": "2.1.8",
|
||||
"react-tooltip": "^3.10.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^5.10.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"riek": "^1.1.0",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"source-map-explorer": "^1.8.0",
|
||||
"url-parse": "^1.4.4",
|
||||
"webtorrent": "^0.103.1"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze-main": "source-map-explorer build/static/js/main.*",
|
||||
"analyze-chunk": "source-map-explorer build/static/js/2.*",
|
||||
"start": "HTTPS=true PORT=4443 react-scripts start",
|
||||
"build": "react-scripts build && rm -rf ../server/public/* && cp -r build/* ../server/public/",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"plugins": [
|
||||
"import",
|
||||
"react"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "React",
|
||||
"version": "16"
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"array-bracket-spacing": [
|
||||
2,
|
||||
"always",
|
||||
{
|
||||
"objectsInArrays": true,
|
||||
"arraysInArrays": true
|
||||
}
|
||||
],
|
||||
"arrow-parens": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"arrow-spacing": 2,
|
||||
"block-spacing": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"brace-style": [
|
||||
2,
|
||||
"allman",
|
||||
{
|
||||
"allowSingleLine": true
|
||||
}
|
||||
],
|
||||
"camelcase": 2,
|
||||
"comma-dangle": 2,
|
||||
"comma-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"comma-style": 2,
|
||||
"computed-property-spacing": 2,
|
||||
"constructor-super": 2,
|
||||
"func-call-spacing": 2,
|
||||
"generator-star-spacing": 2,
|
||||
"guard-for-in": 2,
|
||||
"indent": [
|
||||
2,
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"key-spacing": [
|
||||
2,
|
||||
{
|
||||
"singleLine": {
|
||||
"beforeColon": false,
|
||||
"afterColon": true
|
||||
},
|
||||
"multiLine": {
|
||||
"beforeColon": true,
|
||||
"afterColon": true,
|
||||
"align": "colon"
|
||||
}
|
||||
}
|
||||
],
|
||||
"keyword-spacing": 2,
|
||||
"linebreak-style": [
|
||||
2,
|
||||
"unix"
|
||||
],
|
||||
"lines-around-comment": [
|
||||
2,
|
||||
{
|
||||
"allowBlockStart": true,
|
||||
"allowObjectStart": true,
|
||||
"beforeBlockComment": true,
|
||||
"beforeLineComment": false
|
||||
}
|
||||
],
|
||||
"max-len": [
|
||||
2,
|
||||
90,
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"comments": 110,
|
||||
"ignoreUrls": true,
|
||||
"ignoreStrings": true,
|
||||
"ignoreTemplateLiterals": true,
|
||||
"ignoreRegExpLiterals": true
|
||||
}
|
||||
],
|
||||
"newline-after-var": 2,
|
||||
"newline-before-return": 2,
|
||||
"newline-per-chained-call": 2,
|
||||
"no-alert": 2,
|
||||
"no-caller": 2,
|
||||
"no-case-declarations": 2,
|
||||
"no-catch-shadow": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-confusing-arrow": ["error", {"allowParens": true}],
|
||||
"no-console": 2,
|
||||
"no-const-assign": 2,
|
||||
"no-debugger": 2,
|
||||
"no-dupe-args": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-div-regex": 2,
|
||||
"no-empty": [
|
||||
2,
|
||||
{
|
||||
"allowEmptyCatch": true
|
||||
}
|
||||
],
|
||||
"no-empty-pattern": 2,
|
||||
"no-else-return": 0,
|
||||
"no-eval": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-ex-assign": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-boolean-cast": 2,
|
||||
"no-extra-label": 2,
|
||||
"no-extra-semi": 2,
|
||||
"no-fallthrough": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-global-assign": 2,
|
||||
"no-implicit-coercion": 2,
|
||||
"no-implicit-globals": 2,
|
||||
"no-inner-declarations": 2,
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-lonely-if": 2,
|
||||
"no-mixed-operators": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-multiple-empty-lines": [
|
||||
2,
|
||||
{
|
||||
"max": 1,
|
||||
"maxEOF": 0,
|
||||
"maxBOF": 0
|
||||
}
|
||||
],
|
||||
"no-native-reassign": 2,
|
||||
"no-negated-in-lhs": 2,
|
||||
"no-new": 2,
|
||||
"no-new-func": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-obj-calls": 2,
|
||||
"no-proto": 2,
|
||||
"no-prototype-builtins": 0,
|
||||
"no-redeclare": 2,
|
||||
"no-regex-spaces": 2,
|
||||
"no-restricted-imports": 2,
|
||||
"no-return-assign": 2,
|
||||
"no-self-assign": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow": 2,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-sparse-arrays": 2,
|
||||
"no-this-before-super": 2,
|
||||
"no-throw-literal": 2,
|
||||
"no-undef": 2,
|
||||
"no-unexpected-multiline": 2,
|
||||
"no-unmodified-loop-condition": 2,
|
||||
"no-unreachable": 2,
|
||||
"no-unused-vars": [
|
||||
1,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "after-used"
|
||||
}
|
||||
],
|
||||
"no-use-before-define": [
|
||||
2,
|
||||
{
|
||||
"functions": false
|
||||
}
|
||||
],
|
||||
"no-useless-call": 2,
|
||||
"no-useless-computed-key": 2,
|
||||
"no-useless-concat": 2,
|
||||
"no-useless-rename": 2,
|
||||
"no-var": 2,
|
||||
"no-whitespace-before-property": 2,
|
||||
"object-curly-newline": 0,
|
||||
"object-curly-spacing": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"object-property-newline": [
|
||||
2,
|
||||
{
|
||||
"allowMultiplePropertiesPerLine": true
|
||||
}
|
||||
],
|
||||
"prefer-const": 2,
|
||||
"prefer-rest-params": 2,
|
||||
"prefer-spread": 2,
|
||||
"prefer-template": 2,
|
||||
"quotes": [
|
||||
2,
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"semi-spacing": 2,
|
||||
"space-before-blocks": 2,
|
||||
"space-before-function-paren": [
|
||||
2,
|
||||
{
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}
|
||||
],
|
||||
"space-in-parens": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"spaced-comment": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"strict": 2,
|
||||
"valid-typeof": 2,
|
||||
"eol-last": 0,
|
||||
"yoda": 2,
|
||||
"import/extensions": 2,
|
||||
"import/no-duplicates": 2,
|
||||
"jsx-quotes": [
|
||||
2,
|
||||
"prefer-single"
|
||||
],
|
||||
"react/display-name": [
|
||||
2,
|
||||
{
|
||||
"ignoreTranspilerName": false
|
||||
}
|
||||
],
|
||||
"react/forbid-prop-types": 0,
|
||||
"react/jsx-boolean-value": 2,
|
||||
"react/jsx-closing-bracket-location": 2,
|
||||
"react/jsx-curly-spacing": 2,
|
||||
"react/jsx-equals-spacing": 2,
|
||||
"react/jsx-handler-names": 2,
|
||||
"react/jsx-indent-props": [
|
||||
2,
|
||||
"tab"
|
||||
],
|
||||
"react/jsx-indent": [
|
||||
2,
|
||||
"tab"
|
||||
],
|
||||
"react/jsx-key": 2,
|
||||
"react/jsx-max-props-per-line": 0,
|
||||
"react/jsx-no-bind": 0,
|
||||
"react/jsx-no-duplicate-props": 2,
|
||||
"react/jsx-no-literals": 0,
|
||||
"react/jsx-no-undef": 0,
|
||||
"react/jsx-pascal-case": 2,
|
||||
"react/jsx-sort-prop-types": 0,
|
||||
"react/jsx-sort-props": 0,
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/no-danger": 2,
|
||||
"react/no-deprecated": 2,
|
||||
"react/no-did-mount-set-state": 2,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-direct-mutation-state": 2,
|
||||
"react/no-is-mounted": 2,
|
||||
"react/no-multi-comp": 0,
|
||||
"react/no-set-state": 0,
|
||||
"react/no-string-refs": 0,
|
||||
"react/no-unknown-property": 2,
|
||||
"react/prefer-es6-class": 2,
|
||||
"react/prop-types": [
|
||||
2,
|
||||
{
|
||||
"skipUndeclared": true
|
||||
}
|
||||
],
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/sort-comp": 0,
|
||||
"react/jsx-wrap-multilines": [
|
||||
2,
|
||||
{
|
||||
"declaration": false,
|
||||
"assignment": false,
|
||||
"return": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie > 0",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>Multiparty Meeting</title>
|
||||
</head>
|
||||
<style>
|
||||
body
|
||||
{
|
||||
margin:auto;
|
||||
padding:0.5vmin;
|
||||
text-align:center;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 40%;
|
||||
width: 90%;
|
||||
transform: translate(-50%, 0%);
|
||||
background-image: url('/images/background.jpg');
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
input:hover { opacity:0.9; }
|
||||
input[type=text]
|
||||
{
|
||||
font-size: 1.5em;
|
||||
padding: 1.5vmin;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
border: 0;
|
||||
color: #fff;
|
||||
margin: 0.8vmin;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
button:hover { background-color: #f5f5f5; }
|
||||
button
|
||||
{
|
||||
font-size: 1.5em;
|
||||
padding: 1.5vmin;
|
||||
margin: 0.8vmin;
|
||||
background-color: #fafafa;
|
||||
border-radius: 1.8vmin;
|
||||
color: #000;
|
||||
border: 0;
|
||||
}
|
||||
img
|
||||
{
|
||||
height: 15vmin;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a>
|
||||
<img src='/images/logo.svg'></img><br />
|
||||
</a>
|
||||
<input id='room' type='text' onkeypress='checkEnter(event)' value='' placeholder='your room name' />
|
||||
<button onclick = 'start(location.href)'>Go to room</button>
|
||||
</body>
|
||||
<script>
|
||||
let room = document.getElementById('room');
|
||||
let stateObj = { foo: 'bar' };
|
||||
|
||||
room.addEventListener('input', (e) =>
|
||||
{
|
||||
console.log(e.charCode);
|
||||
history.replaceState(stateObj, 'Multiparty Meeting', '/'+room.value);
|
||||
}, true);
|
||||
|
||||
room.focus();
|
||||
|
||||
function start(target)
|
||||
{
|
||||
location.href;history.replaceState(stateObj, 'Multiparty Meeting', '/');
|
||||
window.location = target;
|
||||
}
|
||||
|
||||
function checkEnter(event)
|
||||
{
|
||||
let x = event.charCode || event.keyCode;
|
||||
if (x == 13 )
|
||||
{
|
||||
start(location.href);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// eslint-disable-next-line
|
||||
var config =
|
||||
{
|
||||
loginEnabled : false,
|
||||
developmentPort : 3443,
|
||||
turnServers : [
|
||||
{
|
||||
urls : [
|
||||
'turn:turn.example.com:443?transport=tcp'
|
||||
],
|
||||
username : 'example',
|
||||
credential : 'example'
|
||||
}
|
||||
],
|
||||
requestTimeout : 10000,
|
||||
transportOptions :
|
||||
{
|
||||
tcp : true
|
||||
},
|
||||
lastN : 4,
|
||||
mobileLastN : 1,
|
||||
background : 'images/background.jpg',
|
||||
// Add file and uncomment for adding logo to appbar
|
||||
// logo : 'images/logo.svg',
|
||||
title : 'Multiparty meeting',
|
||||
theme :
|
||||
{
|
||||
palette :
|
||||
{
|
||||
primary :
|
||||
{
|
||||
main : '#313131'
|
||||
}
|
||||
},
|
||||
overrides :
|
||||
{
|
||||
MuiAppBar :
|
||||
{
|
||||
colorPrimary :
|
||||
{
|
||||
backgroundColor : '#313131'
|
||||
}
|
||||
},
|
||||
MuiFab :
|
||||
{
|
||||
primary :
|
||||
{
|
||||
backgroundColor : '#5F9B2D',
|
||||
'&:hover' :
|
||||
{
|
||||
backgroundColor : '#518029'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
typography :
|
||||
{
|
||||
useNextVariants : true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta
|
||||
name='viewport'
|
||||
content='width=device-width, initial-scale=1, shrink-to-fit=no'
|
||||
/>
|
||||
<meta name='description' content='multiparty meeting - Simple web meetings'>
|
||||
<meta name='theme-color' content='#000000' />
|
||||
|
||||
<link rel='chrome-webstore-item' href='https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi'>
|
||||
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
|
||||
<link rel='shortcut icon' href='%PUBLIC_URL%/favicon.ico' />
|
||||
<link rel='manifest' href='%PUBLIC_URL%/manifest.json' />
|
||||
|
||||
<title>Multiparty Meeting</title>
|
||||
|
||||
<script src='%PUBLIC_URL%/config.js' type='text/javascript'></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id='multiparty-meeting'></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "Multiparty Meeting",
|
||||
"name": "Multiparty Meeting - Simple web meetings",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 256 256" height="256px" id="Layer_1" version="1.1" viewBox="0 0 256 256" width="256px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M76.8,217.6c0-1.637,0.625-3.274,1.875-4.524L163.75,128L78.675,42.925c-2.5-2.5-2.5-6.55,0-9.05s6.55-2.5,9.05,0 l89.601,89.6c2.5,2.5,2.5,6.551,0,9.051l-89.601,89.6c-2.5,2.5-6.55,2.5-9.05,0C77.425,220.875,76.8,219.237,76.8,217.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 585 B |
|
|
@ -1,76 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.2" width="67.32mm" height="67.32mm" viewBox="4682 4809 6732 6732" preserveAspectRatio="xMidYMid" fill-rule="evenodd" stroke-width="28.222" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" xmlns:ooo="http://xml.openoffice.org/svg/export" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:presentation="http://sun.com/xmlns/staroffice/presentation" xmlns:smil="http://www.w3.org/2001/SMIL20/" xmlns:anim="urn:oasis:names:tc:opendocument:xmlns:animation:1.0" xml:space="preserve">
|
||||
<defs class="ClipPathGroup">
|
||||
<clipPath id="presentation_clip_path" clipPathUnits="userSpaceOnUse">
|
||||
<rect x="4682" y="4809" width="6732" height="6732"/>
|
||||
</clipPath>
|
||||
<clipPath id="presentation_clip_path_shrink" clipPathUnits="userSpaceOnUse">
|
||||
<rect x="4688" y="4815" width="6719" height="6719"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<defs class="TextShapeIndex">
|
||||
<g ooo:slide="id1" ooo:id-list="id3 id4 id5"/>
|
||||
</defs>
|
||||
<defs class="EmbeddedBulletChars">
|
||||
<g id="bullet-char-template(57356)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 580,1141 L 1163,571 580,0 -4,571 580,1141 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(57354)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 8,1128 L 1137,1128 1137,0 8,0 8,1128 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(10146)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 174,0 L 602,739 174,1481 1456,739 174,0 Z M 1358,739 L 309,1346 659,739 1358,739 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(10132)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 2015,739 L 1276,0 717,0 1260,543 174,543 174,936 1260,936 717,1481 1274,1481 2015,739 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(10007)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 0,-2 C -7,14 -16,27 -25,37 L 356,567 C 262,823 215,952 215,954 215,979 228,992 255,992 264,992 276,990 289,987 310,991 331,999 354,1012 L 381,999 492,748 772,1049 836,1024 860,1049 C 881,1039 901,1025 922,1006 886,937 835,863 770,784 769,783 710,716 594,584 L 774,223 C 774,196 753,168 711,139 L 727,119 C 717,90 699,76 672,76 641,76 570,178 457,381 L 164,-76 C 142,-110 111,-127 72,-127 30,-127 9,-110 8,-76 1,-67 -2,-52 -2,-32 -2,-23 -1,-13 0,-2 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(10004)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 285,-33 C 182,-33 111,30 74,156 52,228 41,333 41,471 41,549 55,616 82,672 116,743 169,778 240,778 293,778 328,747 346,684 L 369,508 C 377,444 397,411 428,410 L 1163,1116 C 1174,1127 1196,1133 1229,1133 1271,1133 1292,1118 1292,1087 L 1292,965 C 1292,929 1282,901 1262,881 L 442,47 C 390,-6 338,-33 285,-33 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(9679)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 813,0 C 632,0 489,54 383,161 276,268 223,411 223,592 223,773 276,916 383,1023 489,1130 632,1184 813,1184 992,1184 1136,1130 1245,1023 1353,916 1407,772 1407,592 1407,412 1353,268 1245,161 1136,54 992,0 813,0 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(8226)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 346,457 C 273,457 209,483 155,535 101,586 74,649 74,723 74,796 101,859 155,911 209,963 273,989 346,989 419,989 480,963 531,910 582,859 608,796 608,723 608,648 583,586 532,535 482,483 420,457 346,457 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(8211)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M -4,459 L 1135,459 1135,606 -4,606 -4,459 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template(61548)" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 173,740 C 173,903 231,1043 346,1159 462,1274 601,1332 765,1332 928,1332 1067,1274 1183,1159 1299,1043 1357,903 1357,740 1357,577 1299,437 1183,322 1067,206 928,148 765,148 601,148 462,206 346,322 231,437 173,577 173,740 Z"/>
|
||||
</g>
|
||||
</defs>
|
||||
<defs class="TextEmbeddedBitmaps"/>
|
||||
<g class="SlideGroup">
|
||||
<g>
|
||||
<g id="container-id1">
|
||||
<g id="id1" class="Slide" clip-path="url(#presentation_clip_path)">
|
||||
<g class="Page">
|
||||
<g class="com.sun.star.drawing.LineShape">
|
||||
<g id="id3">
|
||||
<rect class="BoundingBox" stroke="none" fill="none" x="6253" y="6380" width="3525" height="3525"/>
|
||||
<path fill="none" stroke="rgb(204,0,0)" stroke-width="1016" stroke-linejoin="round" stroke-linecap="round" d="M 6761,9395 L 9268,6888"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="com.sun.star.drawing.LineShape">
|
||||
<g id="id4">
|
||||
<rect class="BoundingBox" stroke="none" fill="none" x="6253" y="6398" width="3525" height="3526"/>
|
||||
<path fill="none" stroke="rgb(204,0,0)" stroke-width="1016" stroke-linejoin="round" stroke-linecap="round" d="M 9269,9414 L 6762,6907"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="com.sun.star.drawing.CustomShape">
|
||||
<g id="id5">
|
||||
<rect class="BoundingBox" stroke="none" fill="none" x="4681" y="4808" width="6734" height="6734"/>
|
||||
<path fill="none" stroke="rgb(204,0,0)" stroke-width="508" stroke-linejoin="round" d="M 8047,5063 C 9811,5063 11159,6410 11159,8174 11159,9938 9811,11286 8047,11286 6283,11286 4936,9938 4936,8174 4936,6410 6283,5063 8047,5063 Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 511.626 511.626" style="enable-background:new 0 0 511.626 511.626;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M477.371,127.44c-22.843-28.074-53.871-50.249-93.076-66.523c-39.204-16.272-82.035-24.41-128.478-24.41 c-34.643,0-67.762,4.805-99.357,14.417c-31.595,9.611-58.812,22.602-81.653,38.97c-22.845,16.37-41.018,35.832-54.534,58.385 C6.757,170.833,0,194.484,0,219.228c0,28.549,8.61,55.3,25.837,80.234c17.227,24.931,40.778,45.871,70.664,62.811 c-2.096,7.611-4.57,14.846-7.426,21.693c-2.855,6.852-5.424,12.474-7.708,16.851c-2.286,4.377-5.376,9.233-9.281,14.562 c-3.899,5.328-6.849,9.089-8.848,11.275c-1.997,2.19-5.28,5.812-9.851,10.849c-4.565,5.048-7.517,8.329-8.848,9.855 c-0.193,0.089-0.953,0.952-2.285,2.567c-1.331,1.615-1.999,2.423-1.999,2.423l-1.713,2.566c-0.953,1.431-1.381,2.334-1.287,2.707 c0.096,0.373-0.094,1.331-0.57,2.851c-0.477,1.526-0.428,2.669,0.142,3.433v0.284c0.765,3.429,2.43,6.187,4.998,8.277 c2.568,2.092,5.474,2.95,8.708,2.563c12.375-1.522,23.223-3.606,32.548-6.276c49.87-12.758,93.649-35.782,131.334-69.097 c14.272,1.522,28.072,2.286,41.396,2.286c46.442,0,89.271-8.138,128.479-24.417c39.208-16.272,70.233-38.448,93.072-66.517 c22.843-28.062,34.263-58.663,34.263-91.781C511.626,186.108,500.207,155.509,477.371,127.44z" fill="#FFFFFF"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 265 B |
|
Before Width: | Height: | Size: 861 B |
|
Before Width: | Height: | Size: 365 B |
|
Before Width: | Height: | Size: 946 B |
|
Before Width: | Height: | Size: 632 B |
|
Before Width: | Height: | Size: 917 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 3h-1v5h1V3zm-2 2h-2V4h2V3h-3v3h2v1h-2v1h3V5zm3-2v5h1V6h2V3h-3zm2 2h-1V4h1v1zm0 10.5c-1.25 0-2.45-.2-3.57-.57-.35-.11-.74-.03-1.01.24l-2.2 2.2c-2.83-1.44-5.15-3.75-6.59-6.59l2.2-2.21c.27-.26.35-.65.24-1C8.7 6.45 8.5 5.25 8.5 4c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.5c0-.55-.45-1-1-1z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 487 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 427 B |
|
|
@ -1,4 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 276 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 351 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 352 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 390 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 410 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 228 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 227 B |
|
|
@ -1,4 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 239 B |
|
|
@ -1,4 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 242 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 217 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 232 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 355 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 534 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 553 B |
|
|
@ -1,8 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path
|
||||
d="M23.78700473 7.7610694C23.78700473 7.7610694 19.32738673 2.3867608 19.32738673 2.3867608C19.13984773 2.1607585 18.91713373 2.0882379 18.63693973 2.2176245C18.36703673 2.3422016 18.22715573 2.5841415 18.22715573 2.9368427C18.22715573 2.9368427 18.22715573 5.6735307 18.22715573 5.6735307C16.60026573 6.0262319 15.09099273 6.6909889 13.72054573 7.6906705C12.03598073 8.9196554 10.69300173 10.4408914 9.74082913 12.3174694C9.56878473 12.6565904 9.61472063 13.0060384 9.85793833 13.2765524C10.07145773 13.5142954 10.41981173 13.5447094 10.65379573 13.3611024C10.66553573 13.3469524 10.66553573 13.3469524 10.67727573 13.3469524C10.84110373 13.2202534 11.00363973 13.1043994 11.15725573 13.0083964C11.31983073 12.9069714 11.66067273 12.7122784 12.18733073 12.4444524C12.71395073 12.1762954 13.21736773 11.9365244 13.72058873 11.7532424C14.22400573 11.5696774 14.85607473 11.3852164 15.59343573 11.2311224C16.34257473 11.0749524 17.07997473 10.9913044 17.79417273 10.9913044C17.79417273 10.9913044 18.22715973 10.9913044 18.22715973 10.9913044C18.22715973 10.9913044 18.22715973 13.6574994 18.22715973 13.6574994C18.22715973 14.0102004 18.36723673 14.2509614 18.63694373 14.3767174C18.73010373 14.4203774 18.81250973 14.4332974 18.87092773 14.4332974C19.04645473 14.4332974 19.19826973 14.3622874 19.32739073 14.2076254C19.32739073 14.2076254 23.78708673 8.8613254 23.78708673 8.8613254C24.03351273 8.5660564 24.03300473 8.0574684 23.78700873 7.7610674C23.78700873 7.7610674 23.78700873 7.7610674 23.78700873 7.7610674M19.51465573 11.7673384C19.51465573 11.7673384 19.51465573 10.2720364 19.51465573 10.2720364C19.51465573 9.8630814 19.26897373 9.5103804 18.94108373 9.4962344C18.68362273 9.4537944 18.29731573 9.4396544 17.79417273 9.4396544C16.12032873 9.4396544 14.43478673 9.7782104 12.76090373 10.4694194C14.54011773 8.6497074 16.61200773 7.5365769 18.96455973 7.1263967C19.28071173 7.0713227 19.51473473 6.7454039 19.51473473 6.3364497C19.51473473 6.3364497 19.51473473 4.8128558 19.51473473 4.8128558C19.51473473 4.8128558 22.41761773 8.3111524 22.41761773 8.3111524C22.41761773 8.3111524 19.51465573 11.7673414 19.51465573 11.7673414" />
|
||||
<path
|
||||
d="M17.17360373 20.0049884C17.17360373 20.0615284 17.10341373 20.1461164 17.04499073 20.1461164C17.04499073 20.1461164 1.44180703 20.1461164 1.44180703 20.1461164C1.35991303 20.1461164 1.32469783 20.1036764 1.32469783 20.0049884C1.32469783 20.0049884 1.32469783 6.3082043 1.32469783 6.3082043C1.32469783 6.2095143 1.35991283 6.1670295 1.44180703 6.1670295C1.44180703 6.1670295 14.34107173 6.1670295 14.34107173 6.1670295C14.34107173 6.1670295 14.34107173 4.5871826 14.34107173 4.5871826C14.34107173 4.5871826 1.44180703 4.5871826 1.44180703 4.5871826C0.70440653 4.5871826 0.10735711 5.2784393 0.02542373 6.139068C0.02542373 6.139068 0.02542373 6.3082043 0.02542373 6.3082043C0.02542373 6.3082043 0.02542373 20.0050354 0.02542373 20.0050354C0.02542373 20.0050354 0.02542373 20.1744544 0.02542373 20.1744544C0.10731799 21.0348004 0.70440653 21.7118644 1.44180703 21.7118644C1.44180703 21.7118644 17.04499073 21.7118644 17.04499073 21.7118644C17.44280173 21.7118644 17.78516873 21.5460284 18.05162873 21.2180354C18.32380073 20.8830634 18.46141273 20.4705254 18.46141273 20.0049884C18.46141273 20.0049884 18.46141273 15.3501814 18.46141273 15.3501814C18.46141273 15.3501814 17.17364273 15.3501814 17.17364273 15.3501814C17.17364273 15.3501814 17.17364273 20.0049884 17.17364273 20.0049884C17.17364273 20.0049884 17.17360373 20.0049884 17.17360373 20.0049884" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 214 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 320 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 534 B |
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 96 96"
|
||||
style="enable-background:new 0 0 96 96;"
|
||||
xml:space="preserve">
|
||||
<metadata
|
||||
id="metadata11"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||
<defs
|
||||
id="defs9" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.40677965"
|
||||
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
|
||||
id="path3710"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 335 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 332 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#000000" height="48" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" d="M0 0h20v20H0V0z"/>
|
||||
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 790 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="24" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 746 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 0 24 24" width="48">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 195 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 210 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 265 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 266 B |
|
|
@ -1,4 +0,0 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 354 B |
|
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 435.933 435.933" style="enable-background:new 0 0 435.933 435.933;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M286.482,279.088c0.944,1.249,0.939,2.973-0.023,4.225l-65.723,85.483c-0.658,0.867-1.682,1.368-2.762,1.368 c-1.081,0-2.102-0.501-2.764-1.368l-65.727-85.483c-0.476-0.621-0.722-1.368-0.722-2.12c0-0.741,0.236-1.485,0.705-2.104 c0.941-1.253,2.607-1.724,4.069-1.137l43.516,17.176V194.393c0-1.926,1.556-3.48,3.486-3.48h34.877c1.92,0,3.485,1.554,3.485,3.48 v100.74l43.516-17.177C283.873,277.37,285.54,277.841,286.482,279.088z M196.93,65.927C-28.392,70.252-1.475,181.812,3.69,198.193 c5.532,27.833,26.852,41.76,48.488,33.864l41.166-12.881c22.179-8.096,36.25-38.272,31.408-67.418l-0.917-5.518 c74.849-21.183,139.407-15.746,189.768-1.727l-0.396,2.353c-4.819,29.149,9.231,59.333,31.423,67.423l41.164,17.16 c18.604,6.791,36.965-6.287,45.271-27.171c0.107,0.09,0.173,0.15,0.173,0.15s1.139-2.812,2.288-7.625 c0.093-0.375,0.163-0.786,0.262-1.155c0.191-0.881,0.383-1.792,0.575-2.785c0.115-0.561,0.246-1.092,0.353-1.658l-0.041-0.035 C440.692,155.657,434.471,61.393,196.93,65.927z" fill="#D80027"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#000000;}
|
||||
</style>
|
||||
<g id="XMLID_2_">
|
||||
<path id="XMLID_8_" class="st0" d="M69.1,61.3l4.6,4.6h1.8v-4.6H69.1z M70.9,56.7l0-22.9c0-2.5-2.1-4.6-4.6-4.6H37l12,12
|
||||
c0.4-0.1,0.8-0.2,1.3-0.2v-4.9l9.2,8.5L55.8,48l12.7,12.7C69.9,59.9,70.9,58.4,70.9,56.7z M26,23.9L23,26.8l3.5,3.5
|
||||
c-0.9,0.8-1.5,2-1.5,3.4v22.9c0,2.5,2,4.6,4.6,4.6h-9.2v4.6H62l6.2,6.2l2.9-2.9L26,23.9z M36.5,54.4c0.7-3.4,2.1-6.8,4.7-9.3
|
||||
l3.6,3.6C41.4,49.6,38.7,51.4,36.5,54.4z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 733 B |
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="XMLID_2_">
|
||||
<path id="XMLID_8_" class="st0" d="M69.1,61.3l4.6,4.6h1.8v-4.6H69.1z M70.9,56.7l0-22.9c0-2.5-2.1-4.6-4.6-4.6H37l12,12
|
||||
c0.4-0.1,0.8-0.2,1.3-0.2v-4.9l9.2,8.5L55.8,48l12.7,12.7C69.9,59.9,70.9,58.4,70.9,56.7z M26,23.9L23,26.8l3.5,3.5
|
||||
c-0.9,0.8-1.5,2-1.5,3.4v22.9c0,2.5,2,4.6,4.6,4.6h-9.2v4.6H62l6.2,6.2l2.9-2.9L26,23.9z M36.5,54.4c0.7-3.4,2.1-6.8,4.7-9.3
|
||||
l3.6,3.6C41.4,49.6,38.7,51.4,36.5,54.4z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 733 B |
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="tiny" x="0px" y="0px" width="480px" height="480px" viewBox="0 0 480 480" xml:space="preserve">
|
||||
<g id="all_friends">
|
||||
<path d="M185.161,285.818c-9.453-7.317-17-18.963-20.438-28.691c-0.406-0.371-0.949-0.846-1.373-1.23 c-5.297-4.638-15.156-13.284-15.156-32.656c0-9.143,1.748-15.652,5.514-20.48c0.518-0.651,1.174-1.407,1.984-2.188v-19.394 c0-23.428,16.861-54.488,53.471-65.856c-6.391-14.644-22.232-30.543-53.656-30.543c-43.455,0-57.205,30.422-57.205,45.851 c0,8.313,0,22.479,0,23.514c0,1.049-6.188-2.826-6.188,11.153c0,15.946,10.752,17.047,12.658,23.061 c3.375,10.691,13.877,22.314,20.826,22.314c0,0,0.531,2.209,0.531,6.278c0,3.296-0.078,3.24-2.402,3.24 c-37.77,0-63.721,39.909-63.721,52.653c0,16.292,0,34.201,0,34.201h80.076C153.303,297.144,168.805,289.123,185.161,285.818z"/>
|
||||
<path d="M356.256,220.19c-2.312,0-2.408,0.056-2.408-3.24c0-4.069,0.531-6.278,0.531-6.278c6.955,0,17.469-11.623,20.844-22.314 c1.906-6.014,12.658-7.114,12.658-23.061c0-13.979-6.174-10.104-6.174-11.153c0-1.035,0-15.2,0-23.514 c0-15.429-13.781-45.851-57.232-45.851c-31.486,0-47.342,15.985-53.701,30.659c36.389,11.459,53.154,42.386,53.154,65.74v19.397 c4.125,3.957,7.486,10.812,7.486,22.664c0,19.377-9.859,28.022-15.158,32.661c-0.439,0.384-0.971,0.854-1.375,1.229 c-3.422,9.733-10.986,21.37-20.439,28.688c16.379,3.313,31.877,11.334,45.078,21.227h80.488c0,0,0-17.909,0-34.201 C420.008,260.1,394.04,220.19,356.256,220.19z"/>
|
||||
<path d="M278.368,298.86c-2.814,0-2.926-8.948-2.926-12.952c0-4.927,0.643-7.619,0.643-7.619c8.439,0,21.189-14.091,25.283-27.064 c2.312-7.296,15.355-8.634,15.355-27.984c0-16.956-7.498-12.257-7.498-13.525c0-1.26,0-18.445,0-28.536 c0-18.712-16.703-55.618-69.422-55.618c-52.705,0-69.406,36.906-69.406,55.618c0,10.091,0,27.276,0,28.536 c0,1.269-7.516-3.431-7.516,13.525c0,19.351,13.047,20.688,15.359,27.984c4.107,12.974,16.844,27.064,25.283,27.064 c0,0,0.639,2.692,0.639,7.619c0,4.004-0.096,12.952-2.922,12.952c-45.811,0-86.328,48.422-86.328,63.89c0,19.765,0,32.467,0,32.467 h124.891h124.893c0,0,0-12.702,0-32.467C364.696,347.282,324.178,298.86,278.368,298.86z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |